msm/init/msm
kbmorris 03f36ec777 Reverted spellings identified as correct in comments
Reverted upper-case to uppercase; lower-case to lowercase;
and time-stamp to timestamp.
2014-03-05 21:07:41 -05:00

4083 lines
122 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Minecraft Server Manager
# ========================
#
# A single init script for managing multiple Minecraft servers.
# Created by Marcus Whybrow
#
# http://marcuswhybrow.net/minecraft-server-manager/
#
### BEGIN INIT INFO
# Provides: msm
# Required-Start: $local_fs $remote_fs
# Required-Stop: $local_fs $remote_fs
# Should-Start: $network
# Should-Stop: $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: MSM: Minecraft init script
# Description: Minecraft Server Manager, an init script for Minecraft/Bukkit servers
### END INIT INFO
# See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for
# more information on debian init.d scripts, which may help you understand
# this script.
# The Minecraft Server Manager version, use "msm version" to check yours.
VERSION="0.8.16"
# Source, if it exists, the msm profile.d script
if [ -f "/etc/profile.d/msm.sh" ]; then
source "/etc/profile.d/msm.sh"
fi
# $1: The file to follow links for
follow_links() {
unset RETURN
local file="$1"
while [[ -L "$file" ]]; do
file="$(readlink "$file")"
done
RETURN="$file"
}
# Get real script file location
follow_links "$0"; SCRIPT="$RETURN"
# Get the MSM_CONF environment variable or use the default location
CONF="${MSM_CONF:-/etc/msm.conf}"
# Get the MSM_BASH_COMPLETION environment variable or use default location
COMPLETION="${MSM_BASH_COMPLETION:-/etc/bash_completion.d/msm}"
follow_links "$COMPLETION"; COMPLETION="$RETURN"
### Config variables the user should not need/want to change
# The start of a regex to find a log line
LOG_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\]"
# Lazy allocation status
ALLOCATED_SERVERS="false"
ALLOCATED_WORLDS="false"
# Global totals
NUM_WORLDS=0
NUM_SERVERS=0
COMMAND_COUNT=0
SETTING_COUNT=0
SERVER_SETTING_COUNT=0
VERSIONS_COUNT=0
### Utility Functions
# Executes the command "$2" as user "$1"
# $1: The user to execute the command as
# $2: The command to execute
as_user() {
local user="$(whoami)"
if [ "$user" == "$1" ]; then
bash -c "$2"
else
if [ "$user" == "root" ]; then
su - "$1" -s /bin/bash -c "$2"
else
if [[ "$1" == "root" ]]; then
error_exit INVALID_USER "This command must be executed as the user \"$1\"."
else
error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"."
fi
fi
fi
}
# Executes the command "$1" as SERVER_USER but returns stderr instead
as_user_stderr() {
as_user "$@" > /dev/null 2>&1
}
# Echo to stderr
echoerr() {
echo -e "$@" 1>&2
}
COLOUR_PURPLE="\e[1;35m"
COLOUR_RED="\e[1;31m"
COLOUR_CYAN="\e[1;36m"
COLOUR_GREEN="\e[1;32m"
COLOUR_RESET="\e[0m"
# Creates a coloured warning line
# $1 The warning to echo
msm_warning() {
echoerr "${COLOUR_PURPLE}[MSM Warning: ${1}]${COLOUR_RESET}"
}
msm_error() {
echoerr "${COLOUR_RED}[MSM Error: ${1}]${COLOUR_RESET}"
}
msm_info() {
echo -e "${COLOUR_CYAN}[MSM Info: ${1}]${COLOUR_RESET}"
}
msm_success() {
echo -e "${COLOUR_CYAN}[MSM: ${1}]${COLOUR_RESET}"
}
# Echoes the first non-empty string in the arguments list
# $1->: Candidate strings for echoing
echo_fallback() {
for arg in "$@"; do
[ -z "$arg" ] && continue
echo "$arg" && break
done
}
# $1: The string to echo if present
echo_if() {
[ ! -z "$1" ] && echo "$1"
}
# Exit's the script
error_exit() {
case "$1" in
INVALID_USER) code=64;;
INVALID_COMMAND) code=65;;
INVALID_ARGUMENT) code=66;;
SERVER_STOPPED) code=67;;
SERVER_RUNNING) code=68;;
NAME_NOT_FOUND) code=69;;
FILE_NOT_FOUND) code=70;;
DUPLICATE_NAME) code=71;;
LOGS_NOT_ROLLED) code=72;;
CONF_ERROR) code=73;;
FATAL_ERROR) code=74;;
JAVA_NOT_INSTALLED) code=75;;
esac
echo "${2:-"Unknown Error"}" 1>&2
exit "${code:-$1}"
}
# Tests the bash version installed
# $1: The bash version required
is_bash_version() {
if [[ "$BASH_VERSION" =~ ^$1 ]]; then
return 0
fi
return 1
}
# Converts a string to be ready for use as a global
# variable name.
# $1: The string to convert
# RETURN: The name in uppercase and with underscores
to_global_name() {
unset RETURN
# Translate to uppercase, and replace dashes with underscores
local result="$1"
if is_bash_version 4; then
# Much faster than the `tr` command
result="${result//-/_}"
result="${result//./_}"
result="${result^^}" # to uppercase
else
result="$(echo "$result" | tr '[\-\.a-z]' '[\_\_A-Z]')"
fi
RETURN="$result"
}
# Converts a global BASH variable name to a server.properties file
# variable name.
# $1: The string to convert
# RETURN: The name in lowercase and with dashes
to_properties_name() {
unset RETURN
# Translate to uppercase, and replace dashes with underscores
local result="$1"
if is_bash_version 4; then
# Much faster than the `tr` command
result="${result//_/-}"
result="${result,,}" # to lowercase
else
result="$(echo "$result" | tr '[\_A-Z]' '[\-a-z]')"
fi
RETURN="$result"
}
# A custom basename function which is faster
# than opening a subshell
# $1: The path to get the basename of
# RETURN: The basename of the path
quick_basename() {
unset RETURN
if [[ "$1" =~ \/([^\/]*)$ ]]; then
RETURN="${BASH_REMATCH[1]}"
fi
}
# A function used to print debug messages to stdout. Prevents messages from
# appearing unless in debug mode, and allows debug statements to be easily
# distinguished from necessary echo statements.
# $1: The message to output
debug() {
manager_property DEBUG
if [[ "$SETTINGS_DEBUG" == "true" ]]; then
echoerr "$1"
fi
}
# Determines whether "$1" is a valid name for a server or jar group directory
# It must only contain upper or lower case letters, digits, dashes or
# underscores.
# It must also not be one of a list of reserved names.
# $1: The name to check
is_valid_name() {
local valid="^[a-zA-Z0-9\_\-]+$"
local invalid="^(start|stop|restart|version|server|jargroup|all|config|update|help|\-\-.*)$"
if [[ "$1" =~ $valid ]]; then
if [[ "$1" =~ $invalid ]]; then
error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\", \"all\", \"config\", \"update\" or \"help\" or start with two dashes (--)."
else
return 0
fi
else
error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and underscores."
fi
}
# Gets the latest jar from a jar group, based upon the date and time encoded
# in the file name.
# $1: The directory to search
# RETURN: The latest file
get_latest_file() {
unset RETURN
local best_time=0
local best_file=""
while IFS= read -r -d $'\0' file; do
# Remove the path, leaving just the file name
local date_time="$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}')"
# Get the time in seconds since 1970 from file name
local seconds="$(date -d "$date_time" "+%s" 2> /dev/null)"
# If that is newer than the current best, override variables
if [[ "$seconds" -gt "$best_time" ]]; then
best_time="$seconds"
best_file="$file"
fi
done < <(find "$1" -maxdepth 1 -type f -print0)
RETURN="$best_file"
}
# Returns the current time as a UNIX timestamp (in seconds since 1970)
now() {
date +%s
}
### Log Utility Functions
# Gets the UNIX timestamp for a server log line
# $1: A server log line
# returns: Time in seconds since 1970-01-01 00:00:00 UTC
log_line_get_time() {
time_string="$(echo "$1" | awk '{print $1 " " $2}')"
date -d "$time_string" "+%s" 2> /dev/null
}
### World Utility Functions
### -----------------------
# Moves a world to RAM
# $1: the ID of the world to move
world_to_ram() {
manager_property RAMDISK_STORAGE_ENABLED
manager_property RAMDISK_STORAGE_PATH
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" RAMDISK_PATH
world_property "$1" FLAG_INRAM
world_property "$1" PATH
if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"${WORLD_RAMDISK_PATH[$1]}\" && rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_PATH[$1]}/\" \"${WORLD_RAMDISK_PATH[$1]}\""
fi
}
# Moves a world in RAM to disk
# $1: the ID of the world to move
world_to_disk() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" FLAG_INRAM
world_property "$1" RAMDISK_PATH
world_property "$1" PATH
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\""
}
# Toggles a worlds ram disk state
# $1: The ID of the world
world_toggle_ramdisk_state() {
world_property "$1" FLAG_INRAM
world_property "$1" RAMDISK_PATH
local sid="${WORLD_SERVER_ID[$1]}"
server_property "$sid" USERNAME
if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then
echo -n "Synchronising world \"${WORLD_NAME[$1]}\" to disk... "
world_to_disk "$1"
echo "Done."
echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USERNAME[$sid]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\""
echo "Done."
echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... "
as_user "${SERVER_USERNAME[$sid]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\""
echo "Done."
else
echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USERNAME[$sid]}" "touch \"${WORLD_FLAG_INRAM[$1]}\""
echo "Done."
echo -n "Copying world to RAM... "
world_to_ram "$1"
echo "Done."
fi
echo "Changes will only take effect after server is restarted."
}
# Backs up a world
# $1: The ID of the world
world_backup() {
manager_property WORLD_ARCHIVE_ENABLED
manager_property RDIFF_BACKUP_ENABLED
manager_property RSYNC_BACKUP_ENABLED
local server_id="${WORLD_SERVER_ID[$1]}"
local containing_dir="$(dirname "${WORLD_PATH[$1]}")"
local dir_name="$(basename "${WORLD_PATH[$1]}")"
world_property "$1" PATH
world_property "$1" BACKUP_PATH
echo -n "Entering in backup function ... "
if [[ "$SETTINGS_WORLD_ARCHIVE_ENABLED" == "true" ]]; then
echo -n "Backing up world \"${WORLD_NAME[$1]}\"... "
file_name="$(date "+%F-%H-%M-%S").zip"
server_property "$server_id" USERNAME
as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\""
echo "Done."
fi
if [[ "$SETTINGS_RDIFF_BACKUP_ENABLED" == "true" ]]; then
echo -n "rdiff-backup world \"${WORLD_NAME[$1]}\"... "
server_property "$server_id" USERNAME
as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RDIFF_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup \"${dir_name}\" \"${RDIFF_BACKUP_PATH[$1]}\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup --remove-older-than \"$SETTINGS_RDIFF_BACKUP_ROTATION\"D --force \"${RDIFF_BACKUP_PATH[$1]}\""
echo "Done."
fi
if [[ "$SETTINGS_RSYNC_BACKUP_ENABLED" == "true" ]]; then
echo -n "rsync-backup world \"${WORLD_NAME[$1]}\"... "
file_name="$(date "+%F-%H-%M-%S")"
server_property "$server_id" USERNAME
as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RSYNC_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && rsync -aH --link-dest=\"${RSYNC_BACKUP_PATH[$1]}/latest\" \"${dir_name}\" \"${RSYNC_BACKUP_PATH[$1]}/${file_name}\" && rm -f \"${RSYNC_BACKUP_PATH[$1]}/latest\" && ln -s \"${file_name}\" \"${RSYNC_BACKUP_PATH[$1]}/latest\""
echo "Done."
fi
}
# Activates a world
# $1: The ID of the world
world_activate() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_PATH
world_property "$1" INACTIVE_PATH
world_property "$1" ACTIVE_PATH
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... "
local new_path="${SERVER_WORLD_STORAGE_PATH[${WORLD_SERVER_ID[$1]}]}"
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_INACTIVE_PATH[$1]}\" \"$new_path\""
echo "Done."
else
if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
echo "World \"${WORLD_NAME[$1]}\" is already activate."
else
error_exit DIR_NOT_FOUND "Directory \"${WORLD_INACTIVE_PATH[$1]}\" could not be found."
fi
fi
}
# Deactivates a world
# $1: The ID of the world
world_deactivate() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_INACTIVE_PATH
world_property "$1" ACTIVE_PATH
world_property "$1" INACTIVE_PATH
world_property "$1" PATH
if server_is_running "${WORLD_SERVER_ID[$1]}"; then
error_exit 68 "Worlds cannot be deactivated whilst the server is running."
else
if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
echo -n "Moving world \"${WORLD_NAME[$1]}\" to the inactive worldstorage directory... "
local new_path="${SERVER_WORLD_STORAGE_INACTIVE_PATH[${WORLD_SERVER_ID[$1]}]}"
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_PATH[$1]}\" \"$new_path\""
echo "Done."
else
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
echo "World \"${WORLD_NAME[$1]}\" is already deactivate."
else
error_exit DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found."
fi
fi
fi
}
# Get the value of a world property
# $1: The world ID
# $2: The property name
world_property() {
# Get the current value
eval local value=\"\${WORLD_$2[$1]}\"
# If it is empty, then set it
if [ -z "$value" ]; then
local sid="${WORLD_SERVER_ID[$1]}"
case "$2" in
NAME|PATH)
# Defined at allocation
return 0
;;
ACTIVE_PATH)
server_property "$sid" WORLD_STORAGE_PATH
WORLD_ACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
INACTIVE_PATH)
server_property "$sid" WORLD_STORAGE_INACTIVE_PATH
WORLD_INACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
STATUS)
world_property "$1" ACTIVE_PATH
if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
WORLD_STATUS[$1]="active"
else
world_property "$1" INACTIVE_PATH
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
WORLD_STATUS[$1]="inactive"
else
WORLD_STATUS[$1]="unknown"
fi
fi
;;
FLAG_INRAM)
world_property "$1" PATH
server_property "$sid" WORLDS_FLAG_INRAM
WORLD_FLAG_INRAM[$1]="${WORLD_PATH[$1]}/${SERVER_WORLDS_FLAG_INRAM[$sid]}"
;;
LINK)
server_property "$sid" PATH
WORLD_LINK[$1]="${SERVER_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
BACKUP_PATH)
manager_property WORLD_ARCHIVE_PATH
manager_property WORLD_RDIFF_PATH
manager_property WORLD_RSYNC_PATH
WORLD_BACKUP_PATH[$1]="$SETTINGS_WORLD_ARCHIVE_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
RDIFF_BACKUP_PATH[$1]="$SETTINGS_WORLD_RDIFF_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
RSYNC_BACKUP_PATH[$1]="$SETTINGS_WORLD_RSYNC_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
;;
RAMDISK_PATH)
manager_property RAMDISK_STORAGE_ENABLED
# If the ram disk path is set, get the path for this world
if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
manager_property RAMDISK_STORAGE_PATH
WORLD_RAMDISK_PATH[$1]="${SETTINGS_RAMDISK_STORAGE_PATH}/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
fi
;;
INRAM)
world_property "$1" FLAG_INRAM
# Detect whether this world should be in ram
if [[ -e "${WORLD_FLAG_INRAM[$1]}" ]]; then
WORLD_INRAM[$1]="true"
else
WORLD_INRAM[$1]="false"
fi
;;
esac
fi
}
# $1: The world ID
world_dirty_properties() {
local index
# Removes properties for all servers if an index
# is not specified
if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
index="[$1]"
else
index=""
fi
unset WORLD_NAME$index
unset WORLD_PATH$index
unset WORLD_ACTIVE_PATH$index
unset WORLD_INACTIVE_PATH$index
unset WORLD_STATUS$index
unset WORLD_FLAG_INRAM$index
unset WORLD_LINK$index
unset WORLD_BACKUP_PATH$index
unset RDIFF_BACKUP_PATH$index
unset RSYNC_BACKUP_PATH$index
unset WORLD_RAMDISK_PATH$index
unset WORLD_INRAM$index
}
### Server Utility Functions
### ------------------------
# Returns the ID for a server.
# An ID is given to a server when loaded into memory, and can be used to lookup
# config information for that server
# $1: The name of the server
server_get_id() {
unset RETURN
for ((server=0; server<$NUM_SERVERS; server++)); do
if [[ "${SERVER_NAME[$server]}" == "$1" ]]; then
RETURN="$server"
return 0
fi
done
error_exit NAME_NOT_FOUND "Could not find id for server name \"$1\"."
}
# Returns the ID of a server's world.
# $1: The ID of the server
# $2: The name of the world
server_world_get_id() {
server_property "$1" WORLD_STORAGE_PATH
server_property "$1" WORLD_STORAGE_INACTIVE_PATH
unset RETURN
if [ -d "${SERVER_WORLD_STORAGE_PATH[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}/$2" ]; then
# If the directory exists
local start="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$start; i<$max; i++)); do
if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then
RETURN="$i"
return 0
fi
done
fi
error_exit NAME_NOT_FOUND "Could not find id for world \"$2\" for server \"${SERVER_NAME[$1]}\"."
}
# Returns 0 if the server $1 is running and 1 if not
# $1: The ID of the server
server_is_running() {
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null
then
return 0
else
return 1
fi
}
# Ensures the server has a jar file where it is expected to be
# $1: The id of the server
server_ensure_jar() {
server_property "$1" JAR_PATH
if [ -f "${SERVER_JAR_PATH[$1]}" ]; then
return 0
fi
error_exit FILE_NOT_FOUND "Could not find jar for server \"${SERVER_NAME[$1]}\": Expected \"${SERVER_JAR_PATH[$1]}\"."
}
# Read a value from the server configuration file
# $1: The id of the server
# $2: The setting name to read
server_read_config() {
unset RETURN
# Convert name into uppercase with underscores
# msm-setting => SERVER_SETTING
# setting => SERVER_PROPERTIES_SETTING
if [[ "$2" =~ ^msm\-(.*)$ ]]; then
to_global_name "${BASH_REMATCH[1]}"
else
to_global_name "PROPERTIES_$2"
fi
local name="$RETURN"
# Display the value of that setting
unset RETURN
server_property "$1" "$name"
eval RETURN=\"\${SERVER_$name[$1]}\"
}
# Creates symbolic links in the server directory (SETTINGS_SERVER_STORAGE_PATH) for each
# of the Minecraft worlds located in the worldstorage directory.
# $1: The id of the server for which links should be ensured
server_ensure_links() {
server_property "$1" USERNAME
server_property "$1" WORLD_STORAGE_PATH
# Ensure a directory for level-name exists in worldstorage.
# This allows a symlink to be created, and prevents new worlds
# being generated outside of worldstorage.
command_server_config "$1" "level-name"
as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_WORLD_STORAGE_PATH[$1]}/$RETURN\""
server_worlds_allocate "$1"
echo -n "Maintaining world symbolic links... "
local start="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"
local output="false"
for ((i=$start; i<$max; i++)); do
world_property "$i" STATUS
world_property "$i" LINK
if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
continue
fi
world_property "$i" INRAM
# -L checks for the path being a link rather than a file
# ! -a, since it is within double square brackets means: the negation of
# the existence of the file. In other words: true if does not exist
if [[ -L "${WORLD_LINK[$i]}" || ! -a "${WORLD_LINK[$i]}" ]]; then
# If there is a symbolic link in the server directory to this world,
# or there is not a directory in the server directory containing this world.
# Get the original file path the symbolic link is pointing to
# If there is no link, link_target will contain nothing
link_target="$(readlink "${WORLD_LINK[$i]}")"
if "${WORLD_INRAM[$i]}"; then
# If this world is marked as loaded into RAM
world_property "$i" RAMDISK_PATH
if [ "${link_target}" != "${WORLD_RAMDISK_PATH[$i]}" ]; then
# If the symbolic link does not point to the RAM version of the world
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the RAM version of the world
as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_RAMDISK_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
fi
else
# Otherwise the world is not loaded into RAM, and is just on disk
world_property "$i" PATH
if [ "${link_target}" != "${WORLD_PATH[$i]}" ]; then
# If the symbolic link does not point to the disk version of the world
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the disk version of the world
as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
fi
fi
else
echoerr -en "\n Error: Could not create link for world \"${WORLD_NAME[$i]}\". The file \"${WORLD_LINK[$i]}\" already exists, and should not be overwritten automatically. Either remove this file, or rename \"${WORLD_NAME[$i]}\"."
output="true"
fi
done
if [[ "$output" == "true" ]]; then
echo -e "\nDone."
else
echo "Done."
fi
}
# Moves a servers worlds into RAM
# $1: The ID of the server
server_worlds_to_ram() {
manager_property RAMDISK_STORAGE_ENABLED
# Only proceed if there is a ram disk path set in config
if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
echo -n "Synchronising flagged worlds on disk to RAM... "
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
while [[ "$i" -lt "$max" ]]; do
world_property "$i" INRAM
world_property "$i" LINK
if "${WORLD_INRAM[$i]}" && [ -L "${WORLD_LINK[$i]}" ]; then
world_to_ram "$i"
fi
i="$(( $i + 1 ))"
done
echo "Done."
fi
}
# Moves a servers "in RAM" worlds back to disk
# $1: The ID of the server
server_worlds_to_disk() {
manager_property RAMDISK_STORAGE_ENABLED
if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
echo -n "Synchronising worlds in RAM to disk... "
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
while [[ "$i" -lt "$max" ]]; do
world_property "$i" RAMDISK_PATH
if [ -d "${WORLD_RAMDISK_PATH[$i]}" ]; then
world_to_disk "$i"
fi
i="$(( $i + 1 ))"
done
echo "Done."
fi
}
# Watches a server's log for a specific line
# $1: The ID for the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3: The regex that matches log lines
# $4: A timeout in seconds
# returns: When the line is found
server_log_get_line() {
server_property "$1" USERNAME
server_property "$1" LOG_PATH
unset RETURN
# Make sure there is a server log to check
as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}"
local regex="${LOG_REGEX} ($3)"
local timeout_deadline=$(( $(now) + $4 ))
# Read log, break if nothing is read in $4 seconds
while read -t $4 line; do
line_time="$(log_line_get_time "$line")"
# If the time is after the timeout deadline, break
[[ "$(now)" -gt "$timeout_deadline" ]] && break
# If the entry is old enough
if [[ "$line_time" -ge "$2" ]] && [[ "$line" =~ $regex ]]; then
# Return the line
RETURN="${BASH_REMATCH[1]}"
return 0
fi
done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=20 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"")
}
# The same as server_log_get_line, but prints a dot instead of the log line
# to stdout, and returns when line is found.
# $1: the ID of the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3: The regex that matches log lines
# $4: A timeout in seconds
# returns: When the line is found
server_log_dots_for_lines() {
server_property "$1" USERNAME
server_property "$1" LOG_PATH
# Make sure there is a server log to check
as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}"
local regex="${LOG_REGEX} ($3)"
local timeout_deadline=$(( $(now) + $4 ))
# Read log, break if nothing is read in $4 seconds
while read -t $4 line; do
line_time="$(log_line_get_time "$line")"
# If the time is after the timeout deadline, break
[[ "$(now)" -gt "$timeout_deadline" ]] && break
# If the entry is old enough
if [[ "$line_time" -ge "$2" ]]; then
# Print a dot for this line
echo -n '.'
# and if it matches the regular expression, return
if [[ "$line" =~ $regex ]]; then
return 0
fi
fi
done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"")
}
# Sends as string to a server for execution
# $1: The ID of the server
# $2: The line of text to enter into the server console
server_eval() {
server_property "$1" USERNAME
server_property "$1" SCREEN_NAME
as_user "${SERVER_USERNAME[$1]}" "screen -p 0 -S ${SERVER_SCREEN_NAME[$1]} -X eval 'stuff \"$2\"\015'"
}
# The same as server_eval, but also waits for a log entry before returning
# $1: The ID of the server
# $2: A line of text to enter into the server console
# $3: The regex that matches log lines
# $4: A timeout in seconds
# RETURN: The full entry found in the logs
server_eval_and_get_line() {
unset RETURN
time_now="$(now)"
server_eval "$1" "$2"
server_log_get_line "$1" "$time_now" "$3" "$4"
RETURN="$RETURN"
}
# The same as server_eval_and_get_line, but does not set RETURN
server_eval_and_wait() {
server_eval_and_get_line "$@"
unset RETURN # Do not return anything
}
# Executes a "version correct" command in a server's console.
# If the command has output to watch for, then wait until that
# output is found and return it, or until the timeout for that
# command
# $1: The ID of the server
# $2: The name of the command
# $3->: Command arguments in the form "argname=argvalue"
# $RETURN: The output found, if any
server_command() {
unset RETURN
# Load variables
eval server_property $1 CONSOLE_COMMAND_OUTPUT_$2
eval server_property $1 CONSOLE_COMMAND_PATTERN_$2
eval server_property $1 CONSOLE_COMMAND_TIMEOUT_$2
eval local output_regex=\"\${SERVER_CONSOLE_COMMAND_OUTPUT_$2[$1]}\"
eval local pattern=\"\${SERVER_CONSOLE_COMMAND_PATTERN_$2[$1]}\"
# Replace arguments in pattern
for arg in "${@:3}"; do
if [[ "$arg" =~ (.*)=(.*) ]]; then
pattern="${pattern//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}"
output_regex="${output_regex//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}"
fi
done
# If there is no output to watch for, execute the command immediately
# and return immediately
if [ -z "$output_regex" ]; then
server_eval "$1" "$pattern"
unset RETURN
else
# Otherwise execute the command and wait for the specified output
# or the timeout
eval local timeout=\"\${SERVER_CONSOLE_COMMAND_TIMEOUT_$2[$1]}\"
server_eval_and_get_line "$1" "$pattern" "$output_regex" "$timeout"
RETURN="$RETURN"
fi
}
# Gets the process ID for a server if running, otherwise it outputs nothing
# $1: The ID of the server
server_pid() {
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" | awk '{print $1}'
}
# Waits for a server to stop by polling 10 times a second
# This approach is fairly intensive, so only use when you are expecting the
# server to stop soon
# $1: The ID of the server to wait for
server_wait_for_stop() {
local pid="$(server_pid "$1")"
# if the process is still running, wait for it to stop
if [ ! -z "$pid" ]; then
while ps -p "$pid" > /dev/null; do
sleep 0.1
done
fi
}
# Sets a server's active/inactive state
# $1: The ID of the server
# $2: A string containing "active" or "inactive"
server_set_active() {
server_property "$1" USERNAME
server_property "$1" FLAG_ACTIVE_PATH
case "$2" in
active)
as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
SERVER_ACTIVE[$1]="true"
;;
inactive)
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
SERVER_ACTIVE[$1]="false"
;;
*)
error_exit INVALID_ARGUMENT "Invalid argument."
;;
esac
}
### Jar Group Functions
### -------------------
# Lists the jar files grouped by jar groups.
jargroup_list() {
manager_property JAR_STORAGE_PATH
if [[ -d "${SETTINGS_JAR_STORAGE_PATH}" ]]; then
local jargroup_name
local jar_name
while IFS= read -r -d $'\0' jargroup_path; do
jargroup_name="$(basename "${jargroup_path}")"
echo "$jargroup_name"
while IFS= read -r -d $'\0' jar_path; do
jar_name="$(basename "${jar_path}")"
if [[ "$jar_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then
echo " $jar_name"
fi
done < <(find "${SETTINGS_JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0)
done < <(find "${SETTINGS_JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
}
# Creates a new jargroup
# $1: The name for the jargroup
# $2: The URL target for the jargroup
jargroup_create() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
manager_property JARGROUP_TARGET
if [[ ! -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
echo -n "Creating jar group... "
local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p \"$SETTINGS_JAR_STORAGE_PATH/$1\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
error="$(as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
echo "Done."
else
error_exit DUPLICATE_NAME "A jar group with that name already exists."
fi
fi
}
# Changes an existing jargroups target URL
# $1: The jargroup name to change the url of
# $2: The new target URL to set
jargroup_changeurl() {
manager_property JAR_STORAGE_PATH
manager_property USERNAME
manager_property JARGROUP_TARGET
echo -n "Changing target URL... "
local target="$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET"
if [ -e "${target}" ]; then
as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"${target}\""
echo "Done."
else
echo "Failed."
error_exit FILE_NOT_FOUND "Could not find URL target file \"${target}\""
fi
}
# Downloads the latest version for a jargroup, using the target URL for that
# group. Saves the download with the date and time encoded in the start of the
# file name, in the jar group directory in question. Removes the file if there
# is no difference between it and the current version.
# $1: The jargroup name to download the latest version for
jargroup_getlatest() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property JARGROUP_TARGET
manager_property USERNAME
manager_property JARGROUP_DOWNLOAD_DIR
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
if [[ -f "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET" ]]; then
printf "Downloading latest version... "
# Try and make
local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
# test wget for --trust-server-names option
local wget_opts="--trust-server-names"
wget $wget_opts >/dev/null 2>&1
if [[ $? != 1 ]]; then
wget_opts=""
fi
# If target contains the word 'minecraft' check JSON version file for correct filename
local target="$(as_user "$SETTINGS_USERNAME" "cat $SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET")"
if [[ "$target" == "minecraft" ]]; then
printf "Checking minecraft version JSON... "
local versions_url="http://s3.amazonaws.com/Minecraft.Download/versions/versions.json"
local versions_file="/tmp/minecraft_versions.json"
as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate -O '$versions_file' '$versions_url'"
local latest_version=$(as_user "$SETTINGS_USERNAME" "cat '$versions_file' | egrep '^[[:space:]]*\"release\"' | cut -d':' -f2 | egrep -o '[[:digit:].]*'")
if [[ -n "$latest_version" ]]; then
local jar_url="https://s3.amazonaws.com/Minecraft.Download/versions/$latest_version/minecraft_server.$latest_version.jar"
fi
fi
if [[ -n "$jar_url" ]]; then
as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' '$jar_url'"
else
as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --input-file='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET' --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
fi
echo "Done."
local num_files="$(as_user "$SETTINGS_USERNAME" "ls -1 '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' | wc -l")"
if [[ "$num_files" == 1 ]]; then
# There was 1 file downloaded
local file_name="$(ls -1 "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR")"
local new_name="$(date +%F-%H-%M-%S)-$file_name"
get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$1"
local most_recent_jar="$RETURN"
if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then
# There is not a previous version to do a comparison against, or
# The previous version is different:
# Add it to the group
[[ -f "$most_recent_jar" ]]
local was_previous="$?"
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name' '$SETTINGS_JAR_STORAGE_PATH/$1/$new_name'"
if [[ ! -z "$most_recent_jar" ]]; then
echo "Downloaded version was different to previous latest. Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
else
echo "Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
fi
else
echo "Existing version \"$most_recent_jar\" was already up to date."
fi
elif [[ "$num_files" == 0 ]]; then
# No file was downloaded
echo "Failed. No files were downloaded."
else
# Multiple files were
echo "Error. URL downloads multiple files."
fi
# Clean up the temp download folder
as_user "$SETTINGS_USERNAME" "rm -fr '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
else
error_exit FILE_NOT_FOUND "Target URL not found, use $0 jargroup seturl <download-url>"
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
# Deletes an existing jargroup
# $1: The name of the existing jargroup
jargroup_delete() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
printf "Are you sure you want to delete this jar group [y/N]: "
read answer
if [[ "$answer" =~ ^y|Y|yes$ ]]; then
as_user "$SETTINGS_USERNAME" "rm -rf \"$SETTINGS_JAR_STORAGE_PATH/$1\""
echo "Jar group deleted."
else
echo "Jar group was NOT deleted."
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
# Renames an existing jargroup
# $1: The name of the existing jargroup
# $2: The new name
jargroup_rename() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
# If the jar group name is valid,
# and there is no other jar group with the name $1
if is_valid_name "$2"; then
if [[ -e "$SETTINGS_JAR_STORAGE_PATH/$2" ]]; then
error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"."
else
# TODO: Update any symbolic links which point to a jar in this directory
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1' '$SETTINGS_JAR_STORAGE_PATH/$2'"
echo "Renamed jar group \"$1\" to \"$2\"."
fi
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
### Server Functions
### ----------------
# Echoes a list of servers in the SETTINGS_SERVER_STORAGE_PATH
server_list() {
if [ "$NUM_SERVERS" -gt 0 ]; then
for ((server=0; server<$NUM_SERVERS; server++)); do
server_property "$server" ACTIVE
if "${SERVER_ACTIVE[$server]}"; then
echo -n "[ ACTIVE ] "
else
echo -n "[INACTIVE] "
fi
echo -n "\"${SERVER_NAME[$server]}\" "
if "${SERVER_ACTIVE[$server]}"; then
if server_is_running "$server"; then
echo "is running. Everything is OK."
else
echo "is stopped. Server is down!"
fi
else
if server_is_running "$server"; then
echo "is running. It should not be running!"
else
echo "is stopped. Everything is OK."
fi
fi
done
else
echo "[There are no servers]"
fi
}
# Creates a new server
# $1: The server name to create
server_create() {
if is_valid_name "$1"; then
manager_property USERNAME
manager_property SERVER_STORAGE_PATH
manager_property DEFAULT_WHITELIST_PATH
manager_property DEFAULT_BANNED_IPS_PATH
manager_property DEFAULT_BANNED_PLAYERS_PATH
manager_property DEFAULT_OPS_PATH
manager_property DEFAULT_OPS_LIST
manager_property SERVER_PROPERTIES
manager_property DEFAULT_WORLD_STORAGE_PATH
manager_property JAR_STORAGE_PATH
if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
error_exit DUPLICATE_NAME "A server with that name already exists."
else
printf "Creating server directory... "
as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WHITELIST_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_IPS_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_PLAYERS_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'"
# Set default ops users as appropriate
if [ ! -z "$SETTINGS_DEFAULT_OPS_LIST" ]; then
IFS=","; for default_ops_user in $SETTINGS_DEFAULT_OPS_LIST; do
as_user "$SETTINGS_USERNAME" "echo $default_ops_user | tr -d ' ' >> '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'"
done
fi
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES'"
as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH'"
as_user "$SETTINGS_USERNAME" "echo \"MSM requires all your worlds be moved into this directory.\" > '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH/readme.txt'"
echo "Done."
# Creates a server stub in memory, enough to use server_properties for.
SERVER_NAME[$NUM_SERVERS]="$1"
SERVER_PATH[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1"
SERVER_CONF[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES"
NUM_SERVERS=$(($NUM_SERVERS+1))
# TODO: Dirty all server variables, or don't allow further in script access
# TODO: Handle server default setup stuff better than just using
# the "minecraft" jar group. And make it configurable.
if [ -d "$SETTINGS_JAR_STORAGE_PATH/minecraft" ]; then
server_get_id "$1"
server_set_jar "$RETURN" "minecraft"
fi
fi
fi
}
# Deletes an existing server
# $1: The server name to delete
server_delete() {
if is_valid_name "$1"; then
manager_property SERVER_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
printf "Are you sure you want to delete server \"$1\" and its worlds? (note: backups are preserved) [y/N]: "
read answer
if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then
server_get_id "$1"
local existing_id="$RETURN"
if server_is_running "$existing_id"; then
echo "Server \"$1\" is running."
server_stop_now "$existing_id"
fi
as_user "$SETTINGS_USERNAME" "rm -rf '$SETTINGS_SERVER_STORAGE_PATH/$1'"
echo "Server deleted."
else
echo "Server was NOT deleted."
fi
else
error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
fi
fi
}
# Renames an existing server
# $1: The server name to change
# $2: The new name for the server
server_rename() {
if is_valid_name "$1"; then
manager_property SERVER_STORAGE_PATH
manager_property USERNAME
if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]; then
# If the server name is valid and exists
server_get_id "$1"
local existing_id="$RETURN"
if server_is_running "$existing_id"; then
error_exit SERVER_RUNNING "Can only rename a stopped server."
else
if is_valid_name "$2"; then
# If the server name is valid
if [[ -e "$SETTINGS_SERVER_STORAGE_PATH/$2" ]]; then
# and there is not already a server with the name $2
error_exit DUPLICATE_NAME "Could not be renamed, there is already a server with the name \"$2\"."
else
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_SERVER_STORAGE_PATH/$1' '$SETTINGS_SERVER_STORAGE_PATH/$2'"
echo "Renamed server \"$1\" to \"$2\"."
fi
fi
fi
else
error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
fi
fi
}
# Starts a single server
# $1: The ID of the server
server_start() {
server_property "$1" USERNAME
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
server_property "$1" CONSOLE_EVENT_START
if server_is_running "$1"; then
echo "Server \"${SERVER_NAME[$1]}\" is already running!"
else
if ! which java > /dev/null; then
error_exit JAVA_NOT_INSTALLED "Could not start server as Java is not installed."
fi
server_ensure_jar "$1"
server_ensure_links "$1"
server_worlds_to_ram "$1"
local time_now="$(now)"
printf "Starting server..."
# This is the important line! Let's start this server!
as_user "${SERVER_USERNAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}"
# Wait for the server to fully start
server_log_dots_for_lines "$1" "$time_now" "${SERVER_CONSOLE_EVENT_OUTPUT_START[$1]}" "${SERVER_CONSOLE_EVENT_TIMEOUT_START[$1]}"
echo " Done."
fi
}
# Sends the "save-all" command to a server
# $1: The ID of the server
server_save_all() {
if server_is_running "$1"; then
echo -n "Forcing save... "
server_command "$1" SAVE_ALL
echo "Done."
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sends the "save-off" command to a server
# $1: The ID of the server
server_save_off() {
if server_is_running "$1"; then
echo -n "Disabling level saving... "
server_command "$1" SAVE_OFF
echo "Done."
# Writes any in-memory data managed by the kernel to disk
sync
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sends the "save-on" command to a server
# $1: The ID of the server
server_save_on() {
if server_is_running "$1"; then
echo -n "Enabling level saving... "
server_command "$1" SAVE_ON
echo "Done."
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Stops a single server after a delay
# $1: The ID of the server
server_stop() {
server_property "$1" MESSAGE_STOP
server_property "$1" STOP_DELAY
if server_is_running "$1"; then
# Change the state of the script
STOP_COUNTDOWN[$1]="true"
server_eval "$1" "say ${SERVER_MESSAGE_STOP[$1]}"
echo "Issued the warning \"${SERVER_MESSAGE_STOP[$1]}\" to players."
echo -n "Shutting down... "
for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do
tput sc # Save cursor position
echo -n "in $i seconds."
sleep 1
tput rc # Restore cursor to position of last `sc'
tput el # Clear to end of line
done
echo -e "Now."
server_stop_now "$1"
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Stops a single server right now
# $1: The ID of the server
server_stop_now() {
if server_is_running "$1"; then
server_save_all "$1"
echo -n "Stopping the server... "
server_eval "$1" "stop"
STOP_COUNTDOWN[$1]="false"
RESTART_COUNTDOWN[$1]="false"
server_wait_for_stop "$1"
echo "Done."
# Synchronise all worlds in RAM to disk
server_worlds_to_disk "$1"
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Restarts a single server after a delay
# $1: The ID of the server
server_restart() {
server_property "$1" MESSAGE_RESTART
server_property "$1" RESTART_DELAY
# Restarts the server if it is already running
if server_is_running "$1"; then
# Change the state of the script
RESTART_COUNTDOWN[$1]="true"
server_eval "$1" "say ${SERVER_MESSAGE_RESTART[$1]}"
echo "Issued the warning \"${SERVER_MESSAGE_RESTART[$1]}\" to players."
echo -n "Restarting... "
for ((i="${SERVER_RESTART_DELAY[$1]}"; i>0; i--)); do
tput sc # Save cursor position
echo -n "in $i seconds."
sleep 1
tput rc # Restore cursor to position of last `sc'
tput el # Clear to end of line
done
echo -e "Now."
server_stop_now "$1"
fi
server_start "$1"
}
# Restarts a single server right away
# $1: The ID of the server
server_restart_now() {
# Restarts the server if it is already running
if server_is_running "$1"; then
server_stop_now "$1"
fi
server_start "$1"
}
# List the worlds available for a server
# $1: The ID of the server
server_worlds_list() {
if [[ "${SERVER_NUM_WORLDS[$1]}" -eq 0 ]]; then
echo "There are no worlds in worldstorage."
return 0
fi
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$i; i<$max; i++)); do
world_property "$i" INRAM
if "${WORLD_INRAM[$i]}"; then
echo "RAM ${WORLD_NAME[$i]}"
else
echo " ${WORLD_NAME[$i]}"
fi
done
}
# Backs up the worlds for a server
# $1: The ID of the server
server_worlds_backup() {
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$i; i<$max; i++)); do
world_property "$i" STATUS
if [[ "${WORLD_STATUS[$i]}" == "active" ]]; then
world_backup "$i"
fi
done
}
# Moves a servers log into another file, leaving the original log file empty
# $1: The ID of the server
server_log_roll() {
server_property "$1" LOG_PATH
server_property "$1" USERNAME
server_property "$1" LOG_ARCHIVE_PATH
# Moves and Gzips the logfile, a big log file slows down the
# server A LOT
# Creates the server log if not already present. Prevents errors.
as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_LOG_PATH[$1]}\""
local log_lines="$(cat "${SERVER_LOG_PATH[$1]}" | wc -l )"
if [ "$log_lines" -le '1' ]; then
echo "No new log entries to roll. No change made."
return 0
fi
echo -n "Rolling server logs... "
if [ -e "${SERVER_LOG_PATH[$1]}" ]; then
file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log"
as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_LOG_ARCHIVE_PATH[$1]}\" && cp \"${SERVER_LOG_PATH[$1]}\" \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\" && gzip \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\""
if [ -e "${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}.gz" ]; then
as_user "${SERVER_USERNAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG_PATH[$1]}\""
as_user "${SERVER_USERNAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_ARCHIVE_PATH[$1]}\\\"\" > \"${SERVER_LOG_PATH[$1]}\""
else
echo "Failed."
error_exit LOGS_NOT_ROLLED "Logs were not rolled."
fi
fi
echo "Done."
}
# Backups a server's directory
# $1: The ID of the server
server_backup() {
manager_property SERVER_STORAGE_PATH
server_property "$1" COMPLETE_BACKUP_FOLLOW_SYMLINKS
server_property "$1" BACKUP_PATH
server_property "$1" USERNAME
echo -n "Backing up the entire server directory... "
zip_flags="-rq"
# Add the "y" flag if symbolic links should not be followed
if [ "${SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]}" != "true" ]; then
zip_flags="${zip_flags}y"
fi
# Zip up the server directory
file_name="${SERVER_BACKUP_PATH[$1]}/$(date "+%F-%H-%M-%S").zip"
as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$SETTINGS_SERVER_STORAGE_PATH\" && zip ${zip_flags} \"${file_name}\" \"${SERVER_NAME[$1]}\""
echo "Done."
}
# Sets a server's jar file
# $1: The ID of the server
# $2: The name of the jar group
# $3: Optionally, a specific jar to use.
server_set_jar() {
manager_property JAR_STORAGE_PATH
server_property "$1" JAR_PATH
server_property "$1" USERNAME
if [ -d "$SETTINGS_JAR_STORAGE_PATH/$2" ]; then
if [ -z "$3" ]; then
# If a specific jar file is not mentioned
# Download the latest version
jargroup_getlatest "$2"
get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$2"
local jar="$RETURN"
else
# If a specific jar IS mentioned use that
local jar="$SETTINGS_JAR_STORAGE_PATH/$2/$3"
if [[ ! -e "$jar" ]]; then
error_exit NAME_NOT_FOUND "There is no jar named \"$3\" in jargroup \"$2\"."
fi
fi
if [[ ! -z "$jar" ]]; then
as_user "${SERVER_USERNAME[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR_PATH[$1]}\""
echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"."
fi
else
error_exit NAME_NOT_FOUND "There is no jargroup named \"$2\"."
fi
}
# Lists the players currently connected to a server
# $1: The ID of the server
server_connected() {
if server_is_running "$1"; then
server_command "$1" CONNECTED
echo_fallback "$RETURN" "No players are connected."
else
echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected."
fi
}
# Sets the value of a server property
# $1: The ID of the server
# $2: The name of the server property
# $3: The value for the property
server_set_property() {
eval SERVER_$2[$1]=\"$3\"
}
# Get the value of a server property
# $1: The ID of the server
# $2: The name of the server property
server_property() {
# Do nothing if we want to load a property handled
# by a versioning file that is already loaded.
if [[ "$2" =~ ^CONSOLE_ ]] && [ "${SERVER_VERSIONING_LOADED[$1]}" == "true" ]; then
return 0
fi
eval local value=\"\${SERVER_$2[$1]}\"
if [ -z "$value" ]; then
# If the value is empty it has not been loaded yet
# These properties are not overridable
case "$2" in
NAME|PATH)
# Defined at allocation
return 0
;;
CONF)
manager_property SERVER_PROPERTIES
server_set_property "$1" "$2" "${SERVER_PATH[$1]}/$SETTINGS_SERVER_PROPERTIES"
return 0
;;
VERSION_CONF)
manager_property VERSIONING_STORAGE_PATH
server_property "$1" VERSION
get_closest_version "${SERVER_VERSION[$1]}"
local version="$RETURN"
if [[ "$version" == "unknown" ]]; then
# Use the latest Minecraft version if there is no explicit setting
if [[ -z "${VERSIONS_NEWEST_MINECRAFT_PATH}" ]]; then
msm_warning "No version set for server, and no default found. Please use 'msm update' to download defaults"
else
msm_info "Assuming 'minecraft/${VERSIONS_NEWEST_MINECRAFT_VERSION}' for this server. You should override this value by adding 'msm-version=minecraft/x.x.x' to '${SERVER_CONF[$1]}' to make this message go away"
SERVER_VERSION_CONF[$1]="${VERSIONS_NEWEST_MINECRAFT_PATH}"
fi
else
SERVER_VERSION_CONF[$1]="${SETTINGS_VERSIONING_STORAGE_PATH}/${version}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
fi
return 0
;;
BACKUP_PATH)
manager_property BACKUP_ARCHIVE_PATH
server_set_property "$1" "$2" "$SETTINGS_BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}"
return 0
;;
LOG_ARCHIVE_PATH)
manager_property LOG_ARCHIVE_PATH
server_set_property "$1" "$2" "$SETTINGS_LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}"
return 0
;;
ACTIVE)
server_property "$1" FLAG_ACTIVE_PATH
if [[ -e "${SERVER_FLAG_ACTIVE_PATH[$1]}" ]]; then
server_set_property "$1" "$2" "true"
else
server_set_property "$1" "$2" "false"
fi
return 0
;;
esac
# If its a command lookup, load from versioning files
if [[ "$2" =~ ^CONSOLE_ ]]; then
server_property "$1" VERSION_CONF
if [[ -f "${SERVER_VERSION_CONF[$1]}" ]]; then
VERSIONING_SERVER_ID="$1"
source "${SERVER_VERSION_CONF[$1]}"
unset VERSIONING_SERVER_ID
SERVER_VERSIONING_LOADED[$1]="true"
fi
return 0
fi
# If not a non-overridable load from conf
to_properties_name "$2"
local name="$RETURN"
if [[ "$name" =~ ^properties\-(.*)$ ]]; then
name="${BASH_REMATCH[1]}"
else
name="msm-$name"
fi
server_property "$1" CONF
local from_conf="$(sed -rn "s/^$name=('|\"|)(.*)\1/\2/ip" "${SERVER_CONF[$1]}" | tail -n 1)"
if [ ! -z "$from_conf" ]; then
# If the value is found in the server conf file (server.properties)
# then set that as value for the property
eval SERVER_$2[$1]=\"$from_conf\"
else
# Otherwise use the default value
manager_property "DEFAULT_$2"
eval SERVER_$2[$1]=\"\$SETTINGS_DEFAULT_$2\"
fi
### Post-changes to variables after loading
# If it is a path make that path absolute
if [[ "$2" =~ _PATH$ ]]; then
server_property "$1" PATH
eval SERVER_$2[$1]=\"${SERVER_PATH[$1]}/\${SERVER_$2[$1]}\"
fi
# Replace any place holders in a property we just loaded
case "$2" in
SCREEN_NAME)
server_set_property "$1" "$2" "${SERVER_SCREEN_NAME[$1]//\{SERVER_NAME\}/${SERVER_NAME[$1]}}"
;;
MESSAGE_STOP)
server_property "$1" STOP_DELAY
server_set_property "$1" "$2" "${SERVER_MESSAGE_STOP[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}"
;;
MESSAGE_RESTART)
server_property "$1" RESTART_DELAY
server_set_property "$1" "$2" "${SERVER_MESSAGE_RESTART[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}"
;;
INVOCATION)
server_property "$1" RAM
server_property "$1" JAR_PATH
server_set_property "$1" "$2" "${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}"
server_set_property "$1" "$2" "${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR_PATH[$1]}}"
;;
esac
fi
}
# $1: The server ID
server_dirty_properties() {
local index
# Removes properties for all servers if an index
# is not specified
if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
index="[$1]"
else
index=""
fi
for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
eval unset SERVER_${SERVER_SETTING_NAME[$i]}$index
done
unset SERVER_CONF$index
unset SERVER_BACKUP_PATH$index
unset SERVER_LOG_ARCHIVE_PATH$index
unset SERVER_ACTIVE$index
}
### Manager Functions
### -----------------
# Stops all running servers after a servers specified delay
# $1: String containing "stop" or "restart". Represents whether the stop is
# with a mind to stop the server, or just to restart it. And affects
# the message issued to players on a server.
manager_stop_all_servers() {
# An array of true/false for each server
local was_running
# False if no servers were running at all
local any_running="false"
# For all running servers issue the stop warning
local max_countdown=0
for ((server=0; server<${NUM_SERVERS}; server++)); do
if server_is_running "$server"; then
any_running="true"
was_running[$server]="true"
STOP_COUNTDOWN[$server]="true"
server_property "$server" STOP_DELAY
server_property "$server" MESSAGE_STOP
server_property "$server" MESSAGE_RESTART
if [[ "${SERVER_STOP_DELAY[$server]}" -gt "$max_countdown" ]]; then
max_countdown="${SERVER_STOP_DELAY[$server]}"
fi
# Send a warning message to the server
case "$1" in
stop) server_eval "$server" "say ${SERVER_MESSAGE_STOP[$server]}";;
restart) server_eval "$server" "say ${SERVER_MESSAGE_RESTART[$server]}";;
esac
# Send message to stdout
echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:"
case "$1" in
stop) echo " Issued the warning \"${SERVER_MESSAGE_STOP[$server]}\" to players.";;
restart) echo " Issued the warning \"${SERVER_MESSAGE_RESTART[$server]}\" to players.";;
esac
case "${SERVER_STOP_DELAY[$server]}" in
0) echo " Stopping without delay.";;
1) echo " Stopping after 1 second.";;
*) echo " Stopping after ${SERVER_STOP_DELAY[$server]} seconds.";;
esac
else
echo "Server \"${SERVER_NAME[$server]}\" was NOT running."
was_running[$server]="false"
fi
done
if "$any_running"; then
# Wait for the maximum possible delay, stopping servers
# at the correct times
echo -n "All servers will have been issued the stop command... "
for ((tick="${max_countdown}"; tick>=0; tick--)); do
tput sc # Save cursor position
if [[ "$tick" -le 1 ]]; then
echo -n "in $tick second."
else
echo -n "in $tick seconds."
fi
# Each second check all servers, to see if it's their time to
# stop. If so issue the stop command, and don't wait.
for ((server=0; server<${NUM_SERVERS}; server++)); do
if server_is_running "$server"; then
stop_tick="$(( ${max_countdown} - ${SERVER_STOP_DELAY[$server]} ))"
if [[ "$stop_tick" == "$tick" ]]; then
server_eval "$server" "stop"
STOP_COUNTDOWN[$server]="false"
fi
fi
done
if [[ "$tick" > 0 ]]; then
sleep 1
fi
tput rc # Restore cursor to position of last `sc'
tput el # Clear to end of line
done
# Start a new line
echo "Now."
# Finally check all servers have stopped
for ((server=0; server<${NUM_SERVERS}; server++)); do
if "${was_running[$server]}"; then
echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... "
server_wait_for_stop "$server"
echo "Done."
fi
done
else
echo "No servers were running."
fi
}
# Stops all running servers without delay
manager_stop_all_servers_now() {
# An array of true/false for each server
local was_running
# False if no servers were running at all
local any_running="false"
# Stop all servers at the same time
for ((server=0; server<${NUM_SERVERS}; server++)); do
if server_is_running "$server"; then
any_running="true"
was_running[$server]="true"
echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping."
server_eval "$server" "stop"
else
echo "Server \"${SERVER_NAME[$server]}\" was NOT running."
was_running[$server]="false"
fi
done
if "$any_running"; then
# Ensure all the servers have stopped
for ((server=0; server<${NUM_SERVERS}; server++)); do
if "${was_running[$server]}"; then
echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... "
server_wait_for_stop "$server"
echo "Done."
fi
done
else
echo "No servers were running."
fi
}
# Get the value of a global manager property
# $1: The name of the property
manager_property() {
local from_conf="$(sed -rn "s/^$1=('|\"|)(.*)\1/\2/ip" "$CONF" | tail -n 1)"
# If this property has not yet been loaded, load it:
eval local loaded=\"\$LOADED_$1\"
if [ ! -z "$loaded" ] && ! "$loaded"; then
if [ ! -z "$from_conf" ]; then
# Override the default value
eval SETTINGS_$1=\"$from_conf\"
fi
# State that this property has now been loaded
eval LOADED_$1=\"true\"
fi
}
manager_dirty_properties() {
for ((i=0; i<$SETTING_COUNT; i++)); do
eval LOADED_${SETTING_NAME[$i]}=\"false\"
done
}
manager_dirty_all() {
manager_dirty_properties
server_dirty_properties
world_dirty_properties
}
### Command Handler Functions
### -------------------------
# Starts all servers
command_start() {
# Required start option, for debian init.d scripts
for ((server=0; server<${NUM_SERVERS}; server++)); do
server_property "$server" ACTIVE
# Only starts active servers
if "${SERVER_ACTIVE[$server]}"; then
if server_is_running "$server"; then
echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" already started."
else
echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" starting:"
server_start "$server"
fi
else
if server_is_running "$server"; then
echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" already started. It should not be running! Use \"$0 ${SERVER_NAME[$server]} stop\" to stop this server."
else
echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" leaving stopped, as this server is inactive."
fi
fi
done
}
# Stops all servers after a delay
command_stop() {
manager_stop_all_servers "stop"
}
# Stops all servers without delay
command_stop_now() {
manager_stop_all_servers_now
}
# Restarts all servers
command_restart() {
echo "Stopping servers:"
manager_stop_all_servers "restart"
echo "Starting servers:"
command_start
}
# Restarts all servers without delay
command_restart_now() {
echo "Stopping servers:"
manager_stop_all_servers_now
echo "Starting servers:"
command_start
}
# Displays the MSM version
command_version() {
local version="$VERSION"
if [ "${version:0:1}" -eq 0 ]; then
version="$version Beta"
fi
echo "Minecraft Server Manager $version"
}
# Displays config values used by MSM
command_config() {
for ((i=0; i<$SETTING_COUNT; i++)); do
manager_property "${SETTING_NAME[$i]}"
echo -n "${SETTING_NAME[$i]}=\""
eval echo -n \"\$SETTINGS_${SETTING_NAME[$i]}\"
echo '"'
done
}
# Downloads latest versions of all MSM files
command_update() {
echo -n "Checking for updates to version ${VERSION}..."
local any_files_updated="false"
# Check flags, semi-colon ';' delimits flags for example
# COMMAND_FLAGS could contain ";--noinput;--quiet;-q;-ni;"
if [[ "$COMMAND_FLAGS" =~ \;--noinput\; ]]; then
local noinput="true"
fi
manager_property UPDATE_URL
manager_property USERNAME
# Create the temp download directory
local output_dir="/tmp/msmupdate"
# Clean up the temp directory created for downloads
cleanup() {
as_user "$SETTINGS_USERNAME" "rm -rf \"${output_dir}\""
}
# Remove the directory if it exists already
cleanup
# $1: The file name to download
download_file() {
local dir_name="$(dirname "${output_dir}/${1}")"
as_user "${SETTINGS_USERNAME}" "mkdir -p \"${dir_name}\""
as_user "${SETTINGS_USERNAME}" "wget --quiet --no-check-certificate ${SETTINGS_UPDATE_URL}/$1 -O ${output_dir}/$1"
}
# $1: The newly download file (relative to download dir)
# $2: The current file that may be overwritten
# $RETURN: The "current file" path if it should be overwritten
# since it is different to the new version
compare_file() {
unset RETURN
local new_file
# Make relative URLs absolute, using the download dir
if [[ "$1" =~ ^/ ]]; then
new_file="$1"
else
new_file="${output_dir}/$1"
fi
# If the new file path is wrong return
[ ! -e "$new_file" ] && return 1
if [ -e "$2" ]; then
if diff -q "$new_file" "$2" >/dev/null 2>/dev/null; then
return 1
else
RETURN="$2"
fi
fi
}
# Download the latest MSM script and check its version number
download_file "init/msm"
local latest_version="$(sed -rn "s/^VERSION=('|\"|)(.*)\1/\2/ip" "${output_dir}/init/msm" | tail -n 1)"
# Download the other files if that version is different (implicitly better) to the current version
if [[ "$VERSION" == "$latest_version" ]]; then
echo " Already at latest version."
else
echo " $latest_version is available."
fi
### BEGIN Fancy warnings
echo -n "Checking if any files need to be updated..."
download_file "bash_completion/msm"
download_file "versioning/versions.txt"
# Downloads all versioning files in the latest MSM version
download_upstream_versions() {
manager_property VERSIONING_FILE_EXTENSION
while read line; do
if [[ "$line" =~ ^([^#]{1}.*)$ ]]; then
download_file "versioning/${BASH_REMATCH[1]}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
fi
done < "${output_dir}/versioning/versions.txt"
}
# $returns: 0 if at least one file needs updating, 1 otherwise
files_need_updating() {
compare_file "bash_completion/msm" "$COMPLETION"
[ ! -z "$RETURN" ] && return 0
compare_file "init/msm" "$SCRIPT"
[ ! -z "$RETURN" ] && return 0
manager_property VERSIONING_STORAGE_PATH
local version_name regex
regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
while IFS= read -r -d $'\0' path; do
if [[ "$path" =~ $regex ]]; then
version_name="${BASH_REMATCH[1]}"
version_name_without_ext="${BASH_REMATCH[2]}"
compare_file "versioning/$version_name" "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
[ ! -z "$RETURN" ] && return 0
fi
done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
return 1
}
files_need_creating() {
[ ! -e "$COMPLETION" ] && return 0
[ ! -e "$SCRIPT" ] && return 0
manager_property VERSIONING_STORAGE_PATH
local version_name
while IFS= read -r -d $'\0' path; do
if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then
version_name_without_ext="${BASH_REMATCH[1]}"
[ ! -e "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" ] && return 0
fi
done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
return 1
}
download_upstream_versions
local updating="false"
local creating="false"
files_need_updating && updating="true"
files_need_creating && creating="true"
if [[ "$updating" == "false" ]] && [[ "$creating" == "false" ]]; then
echo " No. We're all done."
return 0
else
echo " Done."
fi
if [[ "$updating" == "true" ]]; then
echo "Updating will overwrite the following files:"
compare_file "init/msm" "$SCRIPT"
[ ! -z "$RETURN" ] && echo " > The main MSM script: $SCRIPT"
compare_file "bash_completion/msm" "$COMPLETION"
[ ! -z "$RETURN" ] && echo " > The bash completion script: $COMPLETION"
manager_property VERSIONING_STORAGE_PATH
local version_name version_path regex
regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
while IFS= read -r -d $'\0' path; do
if [[ "$path" =~ $regex ]]; then
version_name="${BASH_REMATCH[1]}"
version_name_without_ext="${BASH_REMATCH[2]}"
version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
compare_file "versioning/$version_name" "$version_path"
[ ! -z "$RETURN" ] && echo " > Version file: $version_path"
fi
done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
fi
if [[ "$creating" == "true" ]]; then
echo "Updating will create the following files:"
[ ! -e "$SCRIPT" ] && echo " > The main MSM script: $SCRIPT"
[ ! -e "$COMPLETION" ] && echo " > The bash completion script: $COMPLETION"
manager_property VERSIONING_STORAGE_PATH
local version_name version_path
while IFS= read -r -d $'\0' path; do
if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then
version_name="${BASH_REMATCH[1]}"
version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
[ ! -e "$version_path" ] && echo " > Version file: $version_path"
fi
done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
fi
### END Fancy warnings
if [[ ! "$noinput" ]]; then
echo -n "Do you want to continue [y/N]: "
read answer
else
answer="y"
fi
if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then
echo "Updating MSM to ${latest_version}:"
# Overwrite bash completion file
local created="false"
compare_file "bash_completion/msm" "$COMPLETION"
if [ ! -z "$RETURN" ] || [ ! -e "$COMPLETION" ]; then
[ ! -e "$COMPLETION" ] && created="true"
any_files_updated="true"
local dir="$(dirname "$COMPLETION")"
as_user "root" "mkdir -p \"${dir}\""
as_user "root" "mv -f \"${output_dir}/bash_completion/msm\" \"$COMPLETION\""
source "$COMPLETION"
if "$created"; then
echo " > Created: $COMPLETION"
else
echo " > Updated: $COMPLETION"
fi
fi
# Overwrite the MSM script itself
created="false"
compare_file "init/msm" "$SCRIPT"
if [ ! -z "$RETURN" ] || [ ! -e "$SCRIPT" ]; then
[ ! -e "$SCRIPT" ] && created="true"
any_files_updated="true"
dir="$(dirname "$SCRIPT")"
as_user "root" "mkdir -p \"${dir}\""
as_user "root" "mv -f \"${output_dir}/init/msm\" \"$SCRIPT\""
as_user "root" "chmod +x \"$SCRIPT\""
if "$created"; then
echo " > Created: $SCRIPT"
else
echo " > Updated: $SCRIPT"
fi
fi
# Overwrite the versioning files
manager_property VERSIONING_STORAGE_PATH
local version_name version_path regex
regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
while IFS= read -r -d $'\0' path; do
created="false"
if [[ "$path" =~ $regex ]]; then
version_name="${BASH_REMATCH[1]}"
version_name_without_ext="${BASH_REMATCH[2]}"
version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
compare_file "${output_dir}/versioning/$version_name" "$version_path"
if [ ! -z "$RETURN" ] || [ ! -e "$version_path" ]; then
[ ! -e "$version_path" ] && created="true"
any_files_updated="true"
dir="$(dirname ${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name})"
as_user "root" "mkdir -p \"${dir}\""
as_user "root" "mv -f \"$path\" \"$version_path\""
as_user "root" "chmod +x \"$version_path\""
as_user "root" "chown ${SETTINGS_USERNAME}:${SETTINGS_USERNAME} \"$version_path\""
if "$created"; then
echo " > Created: $version_path"
else
echo " > Updated: $version_path"
fi
fi
fi
done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
echo "Done."
else
echo "MSM was not updated."
fi
cleanup
# This script will now be replaced. So run the new script's
# update code, in case there are new things to update that
# this version of MSM does not know about yet.
if [[ "$any_files_updated" == "true" ]]; then
$0 update
fi
}
# Displays a list of servers
command_server_list() {
server_list
}
# Creates a new server with name $1
# $1: The new (valid) server name
command_server_create() {
server_create "$1"
}
# Deletes an existing server with name $1
# $1: The name of the existing server
command_server_delete() {
server_delete "$1"
}
# Renames an existing server
# $1: The existing server name
# $2: The new (valid) server name
command_server_rename() {
server_rename "$1" "$2"
}
# Displays a list of all jar's in jar groups
command_jargroup_list() {
jargroup_list
}
# Creates a new jar group
# $1: The new (valid) jar group name
# $2: The URL to use as the jar group target
command_jargroup_create() {
jargroup_create "$1" "$2"
}
# Deletes and existing jar group
# $1: The name of a jar group to delete
command_jargroup_delete() {
jargroup_delete "$1"
}
# Renames an existing jar group
# $1: The name of the existing jar group
# $2: The new (valid) name for the jar group
command_jargroup_rename() {
jargroup_rename "$1" "$2"
}
# Changes a jar group's target url for automatic downloads
# $1: The jar group name
# $2: The new URL to use
command_jargroup_changeurl() {
jargroup_changeurl "$1" "$2"
}
# Downloads the latest jar for a jar group
# $1: The name of the jar group
command_jargroup_getlatest() {
jargroup_getlatest "$1"
}
# Displays a list of possible commands and help strings
command_help() {
# Outputs a list of all commands
echo -e "Usage: $0 command:"
echo -e
echo -e "--Setup Commands------------------------------------------------"
echo -e " server list List servers"
echo -e " server create <name> Creates a new Minecraft server"
echo -e " server delete <name> Deletes an existing Minecraft server"
echo -e " server rename <name> <new-name> Renames an existing Minecraft server"
echo -e
echo -e "--Server Management Commands------------------------------------"
echo -e " <server> start Starts a server"
echo -e " <server> stop [now] Stops a server after warning players, or right now"
echo -e " <server> restart [now] Restarts a server after warning players, or right now"
echo -e " <server> status Show the running/stopped status of a server"
echo -e " <server> connected List a servers connected players"
echo -e " <server> worlds list Lists the worlds a server has"
echo -e " <server> worlds load Creates links to worlds in storage for a server"
echo -e " <server> worlds ram <world> Toggles a world's \"in RAM\" status"
echo -e " <server> worlds todisk Synchronises any \"in RAM\" worlds to disk a server has"
echo -e " <server> worlds backup Makes a backup of all worlds a server has"
echo -e " <server> worlds on|off <world> Activate or deactivate a world, inactive worlds are not backed up"
echo -e " <server> logroll Move a server log to a gziped archive, to reduce lag"
echo -e " <server> backup Makes a backup of an entire server directory"
echo -e " <server> jar <jargroup> [<file>] Sets a server's jar file"
echo -e " <server> console Connects to the interactive console. Access may be limited"
echo -e " <server> config [<setting> <value>] Lists server settings, or sets a specific setting."
echo -e
echo -e "--Server Pass Through Commands----------------------------------"
echo -e " <server> wl on|off Enables/disables server whitelist checking"
echo -e " <server> wl add|remove <player> Add/remove a player to/from a server's whitelist"
echo -e " <server> wl list List the players whitelisted for a server"
echo -e " <server> bl player add|remove <player> Ban/pardon a player from/for a server"
echo -e " <server> bl ip add|remove <ip address> Ban/pardon an IP address from/for a server"
echo -e " <server> bl list Lists the banned players and IP address for a server"
echo -e " <server> op add|remove <player> Add/remove operator status for a player on a server"
echo -e " <server> op list Lists the operator players for a server"
echo -e " <server> gm survival|creative <player> Change the game mode for a player on a server"
echo -e " <server> kick <player> Forcibly disconnect a player from a server"
echo -e " <server> say <message> Broadcast a (pink) message to all players on a server"
echo -e " <server> time set|add <number> Set/increment time on a server (0-24000)"
echo -e " <server> toggledownfall Toggles rain and snow on a server"
echo -e " <server> give <player> <item> [amount] [data] Gives an entity to a player"
echo -e " <server> xp <player> <amount> Gives XP to, or takes away (when negative) XP from, a player"
echo -e " <server> save on|off Enable/disable writing world changes to file"
echo -e " <server> save all Force the writing of all non-saved world changes to file"
echo -e " <server> cmd <command> Send a command string to the server and return"
echo -e " <server> cmdlog <command> Same as 'cmd' but shows log output afterwards (Ctrl+C to exit)"
echo -e
echo -e "--Jar Commands--------------------------------------------------"
echo -e " jargroup list List the stored jar files."
echo -e " jargroup create <name> <download-url> Create a new jar group, with a URL for new downloads"
echo -e " jargroup delete <name> Delete a jar group"
echo -e " jargroup rename <name> <new-name> Rename a jar group"
echo -e " jargroup changeurl <name> <download-url> Change the download URL for a jar group"
echo -e " jargroup getlatest <name> Download the latest jar file for a jar group"
echo -e
echo -e "--Global Commands-----------------------------------------------"
echo -e " start Starts all active servers"
echo -e " stop [now] Stops all running servers"
echo -e " restart [now] Restarts all active servers"
echo -e " version Prints the Minecraft Server Manager version installed"
echo -e " config Displays a list of the config values used by MSM"
echo -e " update [--noinput] Replaces MSM files with the latest recommended versions"
}
# Starts an individual server
# $1: The server ID
command_server_start() {
server_set_active "$1" "active"
server_start "$1"
}
# Stops an individual server after a delay
# $1: The server ID
command_server_stop() {
server_set_active "$1" "inactive"
server_stop "$1"
}
# Stops an individual server without delay
# $1: The server ID
command_server_stop_now() {
server_set_active "$1" "inactive"
server_stop_now "$1"
}
# Restarts an individual server after a delay
# $1: The server ID
command_server_restart() {
server_set_active "$1" "active"
server_restart "$1"
}
# Restarts an individual server without delay
# $1: The server ID
command_server_restart_now() {
server_set_active "$1" "active"
server_restart_now "$1"
}
# Displays the running/stopped status of an individual server
# $1: The server ID
command_server_status() {
if server_is_running "$1"; then
echo "Server \"${SERVER_NAME[$1]}\" is running."
else
echo "Server \"${SERVER_NAME[$1]}\" is stopped."
fi
}
# Displays a list of connected players for an individual server
# $1: The server ID
command_server_connected() {
server_connected "$1"
}
# Displays a list of worlds for an individual server
# $1: The server ID
command_server_worlds_list() {
server_worlds_list "$1"
}
# Creates symlinks for all active worlds so they can be used by the Minecraft
# server when running
# $1: The server ID
command_server_worlds_load() {
server_ensure_links "$1"
}
# Toggles a world's in ram status
# $1: The server ID
# $2: The world ID
command_server_worlds_ram() {
if server_is_running "$1"; then
error_exit SERVER_RUNNING "Server \"${SERVER_NAME[$1]}\" is running. Please stop the server before altering a worlds in-ram status."
else
world_toggle_ramdisk_state "$2"
fi
}
# Synchronises all in ram worlds back to disk for an individual server
# $1: The server ID
command_server_worlds_todisk() {
if server_is_running "$1"; then
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
if server_is_running "$1"; then
server_save_on "$1"
fi
}
# Makes a backup of all worlds for an individual server
# $1: The server ID
command_server_worlds_backup() {
if server_is_running "$1"; then
server_property "$1" MESSAGE_WORLD_BACKUP_STARTED
server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_STARTED[$1]}"
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
server_worlds_backup "$1"
if server_is_running "$1"; then
server_save_on "$1"
server_property "$1" MESSAGE_WORLD_BACKUP_FINISHED
server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_FINISHED[$1]}"
fi
echo "Backup took $SECONDS seconds".
}
# Enables a world to be used by its server
# $1: The server ID
# $2: The world ID
command_server_worlds_on() {
world_activate "$2"
}
# Disables a world from being used by its server, also prevents it from being
# backed up with the other worlds.
# $1: The server ID
# $2: The world ID
command_server_worlds_off() {
world_deactivate "$2"
}
# Moves an individual server's log text to another file, leaving it empty
# $1: The server ID
command_server_logroll() {
server_log_roll "$1"
}
# Makes a backup of an entire server directory
# $1: The server ID
command_server_backup() {
if server_is_running "$1"; then
server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_STARTED[$1]}"
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
server_backup "$1"
if server_is_running "$1"; then
server_save_on "$1"
server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_FINISHED[$1]}"
fi
echo "Backup took $SECONDS seconds".
}
# Sets an individual server's jar file to use when starting up
# $1: The server ID
# $2: The jar group name
# $3: Optionally a specific jar file name which exists within that jargroup, if
# not provided the latest version will be used.
command_server_jar() {
server_set_jar "$1" "$2" "$3"
}
# Turns a server's whitelist protection on
# $1: The server ID
command_server_whitelist_on() {
if server_is_running "$1"; then
server_command "$1" WHITELIST_ON
echo_fallback "$RETURN" "Whitelist enabled."
else
command_server_config "$1" "white-list" "true"
fi
}
# Turns a server's whitelist protection off
# $1: The server ID
command_server_whitelist_off() {
if server_is_running "$1"; then
server_command "$1" WHITELIST_OFF
echo_fallback "$RETURN" "Whitelist disabled."
else
command_server_config "$1" "white-list" "false"
fi
}
# Adds a player name to a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_add() {
# TODO: Support whitelisting multiple players (see blacklist player add)
if server_is_running "$1"; then
# Whitelist players
for player in "${@:2}"; do
server_command "$1" WHITELIST_ADD player="$player"
echo_fallback "$RETURN" "Player $player is now whitelisted."
done
else
server_property "$1" WHITELIST_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_WHITELIST_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_WHITELIST_PATH[$1]}"
echo_fallback "$RETURN" "Player $player is now whitelisted."
fi
done
fi
}
# Removes a player name from a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_remove() {
# TODO: Support multiple player names
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" WHITELIST_REMOVE player="$player"
echo_fallback "$RETURN" "Player $player is no longer whitelisted."
done
else
server_property "$1" WHITELIST_PATH
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_WHITELIST_PATH[$1]}"
echo_fallback "$RETURN" "Player $player is no longer whitelisted."
done
fi
}
# Displays a list of whitelisted players for an individual server
# $1: The server ID
command_server_whitelist_list() {
server_property "$1" WHITELIST_PATH
if [ -f "${SERVER_WHITELIST_PATH[$1]}" ]; then
local players="$(cat "${SERVER_WHITELIST_PATH[$1]}")"
if [ -z "$players" ]; then
echo "No players are whitelisted."
else
echo "$players"
fi
else
echo "No players are whitelisted."
fi
}
# Adds player names to a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_add() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" BLACKLIST_PLAYER_ADD player="$player"
echo_fallback "$RETURN" "Player $player is now blacklisted."
done
else
server_property "$1" BANNED_PLAYERS_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_BANNED_PLAYERS_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_BANNED_PLAYERS_PATH[$1]}"
echo "Player $player is now blacklisted."
fi
done
fi
}
# Removes player names from a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_remove() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" BLACKLIST_PLAYER_REMOVE player="$player"
echo_fallback "$RETURN" "Player $player is no longer blacklisted."
done
else
server_property "$1" BANNED_PLAYERS_PATH
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
echo "Player $player is no longer blacklisted."
done
fi
}
# Adds ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_add() {
if server_is_running "$1"; then
for address in "${@:2}"; do
server_command "$1" BLACKLIST_IP_ADD address="$address"
echo_fallback "$RETURN" "IP address $address is now blacklisted."
done
else
server_property "$1" BANNED_IPS_PATH
for address in "${@:2}"; do
if ! grep "^$address\$" "${SERVER_BANNED_IPS_PATH[$1]}" >/dev/null; then
echo "$address" >> "${SERVER_BANNED_IPS_PATH[$1]}"
echo "IP address $address is now blacklisted."
fi
done
fi
}
# Removes ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_remove() {
if server_is_running "$1"; then
for address in "${@:2}"; do
server_command "$1" BLACKLIST_IP_REMOVE address="$address"
echo_fallback "$RETURN" "IP address $address is no longer blacklisted."
done
else
server_property "$1" BANNED_PLAYERS_PATH
for address in "${@:2}"; do
sed -ri "/^$address\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
echo "IP address $address is no longer blacklisted."
done
fi
}
# Displays a server's banned player names and ip addresses
# $1: The server ID
command_server_blacklist_list() {
server_property "$1" BANNED_PLAYERS_PATH
server_property "$1" BANNED_IPS_PATH
local players
local ips
if [ -f "${SERVER_BANNED_PLAYERS_PATH[$1]}" ]; then
players="$(cat "${SERVER_BANNED_PLAYERS_PATH[$1]}")"
fi
if [ -f "${SERVER_BANNED_IPS_PATH[$1]}" ]; then
ips="$(cat "${SERVER_BANNED_IPS_PATH[$1]}")"
fi
if [[ -z "$players" && -z "$ips" ]]; then
echo "The blacklist is empty."
else
if [[ ! -z "$players" ]]; then
echo "Players:"
for name in $players; do
echo " $name"
done
fi
if [[ ! -z "$ips" ]]; then
echo "IP Addresses:"
for address in $ips; do
echo " $address"
done
fi
fi
}
# Adds a player name to a server's list of operators
# $1: The server ID
# $2->: The player name
command_server_operator_add() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" OP_ADD player="$player"
echo_fallback "$RETURN" "Player $player is now an operator."
done
else
server_property "$1" OPS_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_OPS_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_OPS_PATH[$1]}"
fi
done
fi
if [[ $# -gt 2 ]]; then
echo -n "The following players are now operators: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "\"$2\" is now an operator."
fi
}
# Removes a player name to a server's list of operators
# $1: The server ID
# $2: The player name
command_server_operator_remove() {
# TODO: Support multiple player names
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" OP_REMOVE player="$player"
echo_fallback "$RETURN" "Player $player is no longer an operator."
done
else
server_property "$1" OPS_PATH
for player in "${@:2}"; do
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_OPS_PATH[$1]}"
done
done
fi
if [[ $# -gt 2 ]]; then
echo -n "The following players are no longer operators: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "\"$2\" is no longer an operator."
fi
}
# Displays a list of operators for an individual server
# $1: The server ID
command_server_operator_list() {
server_property "$1" OPS_PATH
if [ -f "${SERVER_OPS_PATH[$1]}" ]; then
local players="$(cat "${SERVER_OPS_PATH[$1]}")"
if [ ! -z "$players" ]; then
echo "$players"
return 0
fi
fi
echo "No players are operators."
}
# Sets the game mode for
# $1: The server ID
# $2: The game mode
# $3->: The player name
command_server_gamemode() {
if server_is_running "$1"; then
for player in "${@:3}"; do
server_command "$1" GAMEMODE player="$player" mode="$2"
echo_fallback "$RETURN" "No output found. It may have worked."
done
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Kicks a connected player from a server
# $1: The server ID
# $2->: The player name
command_server_kick() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_command "$1" KICK player="$player"
echo_fallback "$RETURN" "Player $player has been kicked."
done
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Broadcasts a message to all connected players for a server
# $1: The server ID
# $2->: Words of the message, will be concatenated with spaces
command_server_say() {
if server_is_running "$1"; then
server_command "$1" SAY message="${*:2}"
echo_fallback "$RETURN" "Message sent to players."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sets the time on an individual server
# $1: The server ID
# $2: The time
command_server_time_set() {
if server_is_running "$1"; then
server_command "$1" TIME_SET time="$2"
echo_fallback "$RETURN" "Time set to $2."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Increments the time on an individual server
# $1: The server ID
# $2: The time to add
command_server_time_add() {
if server_is_running "$1"; then
server_command "$1" TIME_ADD time="$2"
echo_fallback "$RETURN" "Time increased by $2."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Toggles the downfall of rain and snow on an individual server
# $1: The server ID
command_server_toggledownfall() {
if server_is_running "$1"; then
server_command "$1" TOGGLEDOWNFALL
echo_fallback "$RETURN" "Downfall toggled."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Gives entities to players in game
# $1: The server ID
# $2: The player name
# $3: The entity id/name
# $4: The amount to give
# $5: The entity damage value
command_server_give() {
if server_is_running "$1"; then
server_command "$1" GIVE player="$2" item="$3" amount="$4" damage="$5"
local amount="x1"
[ ! -z "$4" ] && amount="x$4"
echo_fallback "$RETURN" "Given item $3 ${amount} to $2."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Gives XP to a player in game
# $1: The server ID
# $2: The player name
# $3: The amount of XP to give (can be negative)
command_server_xp() {
if server_is_running "$1"; then
server_command "$1" XP player="$2" amount="$3"
echo_fallback "$RETURN" "Given $3 experience to $2."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Turns world saving on for an individual server
# $1: The server ID
command_server_save_on() {
server_save_on "$1"
}
# Turns world saving off for an individual server
# $1: The server ID
command_server_save_off() {
server_save_off "$1"
}
# Forces the saving of all pending world saves
# $1: The server ID
command_server_save_all() {
server_save_all "$1"
}
# Sends a command string to the server to be executed
# $1: The server ID
# $2->: A command, separate arguments are concatenated with spaces
command_server_cmd() {
if server_is_running "$1"; then
server_eval "$1" "${*:2}"
echo "Command sent."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sends a command string to the server to be executed, and then tails the
# server logs to watch fro results.
# $1: The server ID
# $2->: A command, separate arguments are concatenated with spaces
command_server_cmdlog() {
if server_is_running "$1"; then
server_property "$1" LOG_PATH
server_property "$1" USERNAME
echo "Now watching logs (press Ctrl+C to exit):"
echo "..."
server_eval "$1" "${*:2}"
as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=5 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]}"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Resumes a server's screen session (requires ssh-ed in as server user, using
# the `su` command will not work.)
# $1: The server ID
command_server_console() {
if server_is_running "$1"; then
server_property "$1" USERNAME
as_user "${SERVER_USERNAME[$1]}" "screen -r ${SERVER_SCREEN_NAME[$1]}"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sets a parameter in the config file if it exists, otherwise inserts the
# parameter.
# $1: The server ID
# $2: Optionally, a setting name
# $3: Optionally, a value to set for $2
command_server_config() {
# If both a setting name and value are given
if [ ! -z "$2" ] && [ ! -z "$3" ]; then
server_property "$1" CONF
if [[ -f "${SERVER_CONF[$1]}" ]]; then
if grep "$2" "${SERVER_CONF[$1]}" >/dev/null; then
sed -i /$2=/s/.*/"$2=$3"/g "${SERVER_CONF[$1]}"
else
echo "$2=$3" >> "${SERVER_CONF[$1]}"
fi
if server_is_running "$1"; then
echo "Changes to config may require a server restart to take effect: sudo $0 ${SERVER_NAME[$1]} restart";
fi
fi
return 0
fi
# If only a setting name is given
if [ ! -z "$2" ]; then
server_read_config "$1" "$2"
echo "$RETURN"
fi
# If no parameter name is given
if [ -z "$2" ]; then
# List all parameters
for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
server_property "$1" "${SERVER_SETTING_NAME[$i]}"
to_properties_name "${SERVER_SETTING_NAME[$i]}"
eval echo "msm-$RETURN=\\\"\${SERVER_${SERVER_SETTING_NAME[$i]}[$1]}\\\""
done
fi
}
### Register Functions
### ------------------
# Registers a setting that can be defined in /etc/msm.conf
# $1: Setting name to register
# $2: Optionally a default value for this setting
register_setting() {
# Create the default version of the variable
eval SETTINGS_$1=\"$2\"
# State that the variable has not yet been loaded
eval LOADED_$1=\"false\"
# Keep track of the setting name in a list
SETTING_NAME[$SETTING_COUNT]="$1"
SETTING_COUNT=$(( $SETTING_COUNT + 1 ))
}
# Registers a setting that can be defined for each server
# $1: Server setting name to register
# $2: Optionally a default value
register_server_setting() {
register_setting "DEFAULT_$1" "$2"
SERVER_SETTING_NAME[$SERVER_SETTING_COUNT]="$1"
SERVER_SETTING_COUNT=$(( $SERVER_SETTING_COUNT + 1 ))
}
# Register possible settings
register_settings() {
register_setting DEBUG "false"
register_setting USERNAME "minecraft"
register_setting SERVER_STORAGE_PATH "/opt/msm/servers"
register_setting JAR_STORAGE_PATH "/opt/msm/jars"
register_setting VERSIONING_STORAGE_PATH "/opt/msm/versioning"
register_setting VERSIONING_FILE_EXTENSION "sh"
register_setting RAMDISK_STORAGE_ENABLED "true"
register_setting RAMDISK_STORAGE_PATH "/dev/shm/msm"
register_setting WORLD_ARCHIVE_ENABLED "true"
register_setting WORLD_RDIFF_PATH "/opt/msm/rdiff-backup/worlds"
register_setting RDIFF_BACKUP_ENABLED "false"
register_setting RDIFF_BACKUP_NICE "19"
register_setting RDIFF_BACKUP_ROTATION "7"
register_setting UPDATE_URL "https://raw.github.com/marcuswhybrow/minecraft-server-manager/latest"
register_setting WORLD_ARCHIVE_PATH "/opt/msm/archives/worlds"
register_setting LOG_ARCHIVE_PATH "/opt/msm/archives/logs"
register_setting BACKUP_ARCHIVE_PATH "/opt/msm/archives/backups"
register_setting RSYNC_BACKUP_ENABLED "false"
register_setting WORLD_RSYNC_PATH "/opt/msm/rsync/worlds"
register_setting JARGROUP_TARGET "target.txt"
register_setting JARGROUP_DOWNLOAD_DIR "downloads"
register_setting SERVER_PROPERTIES "server.properties"
register_server_setting USERNAME "minecraft"
register_server_setting SCREEN_NAME "msm-{SERVER_NAME}"
register_server_setting VERSION "unknown"
register_server_setting WORLD_STORAGE_PATH "worldstorage"
register_server_setting WORLD_STORAGE_INACTIVE_PATH "worldstorage_inactive"
register_server_setting LOG_PATH "server.log"
register_server_setting WHITELIST_PATH "white-list.txt"
register_server_setting BANNED_PLAYERS_PATH "banned-players.txt"
register_server_setting BANNED_IPS_PATH "banned-ips.txt"
register_server_setting OPS_PATH "ops.txt"
register_server_setting OPS_LIST ""
register_server_setting JAR_PATH "server.jar"
register_server_setting FLAG_ACTIVE_PATH "active"
register_server_setting COMPLETE_BACKUP_FOLLOW_SYMLINKS "false"
register_server_setting WORLDS_FLAG_INRAM "inram"
register_server_setting RAM "1024"
register_server_setting INVOCATION "java -Xms{RAM}M -Xmx{RAM}M -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -jar {JAR} nogui"
register_server_setting STOP_DELAY "10"
register_server_setting RESTART_DELAY "10"
# Message that are displayed in-game by the server
register_server_setting MESSAGE_STOP "SERVER SHUTTING DOWN IN {DELAY} SECONDS!"
register_server_setting MESSAGE_STOP_ABORT "Server shut down aborted."
register_server_setting MESSAGE_RESTART "SERVER REBOOT IN {DELAY} SECONDS!"
register_server_setting MESSAGE_RESTART_ABORT "Server reboot aborted."
register_server_setting MESSAGE_WORLD_BACKUP_STARTED "Backing up world."
register_server_setting MESSAGE_WORLD_BACKUP_FINISHED "Backup complete."
register_server_setting MESSAGE_COMPLETE_BACKUP_STARTED "Backing up entire server."
register_server_setting MESSAGE_COMPLETE_BACKUP_FINISHED "Backup complete."
# No need for defaults, values fall back on versioning file info
register_server_setting CONFIRM_SAVE_ON
register_server_setting CONFIRM_SAVE_OFF
register_server_setting CONFIRM_SAVE_ALL
register_server_setting CONFIRM_START
register_server_setting CONFIRM_KICK
register_server_setting CONFIRM_TIME_SET
register_server_setting CONFIRM_TIME_ADD
register_server_setting CONFIRM_TOGGLEDOWNFALL
register_server_setting CONFIRM_GAMEMODE
register_server_setting CONFIRM_GIVE
register_server_setting CONFIRM_XP
}
# Adds a command to the list, allowing it to be called from the command line.
# $1: The command signature, a coded string describing the structure of the
# command.
# $2: The handler function to call, if this command is identified.
register_command() {
# Here we build a regular expression which will match any user input
# that could be passed to the given handler function. It is derived
# automatically from the given command signature.
local regex="^"
# Iterate over each element in the command signature
for word in $1; do
# Variables are denoted by angle brackets (e.g. "<variable>") and can
# at this stage be accepted as any non-zero string
if [[ "$word" =~ ^\<.*\>$ ]]; then
case "$word" in
"<strings>")
regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* "
;;
"<flags>")
regex="${regex:0:${#regex}-1}( ((--|-)[^ ]+)( (--|-)[^ ]+)*)? "
;;
*)
regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") "
;;
esac
continue
fi
# Sometimes different worlds may be used to call the same command, in
# these cases, the different words may be written contiguously,
# separated by the pipe character (i.e. "|") and any of the options
# provided will be allowed as a match.
if [[ "$word" =~ \| ]]; then
regex="${regex}($word) "
continue
fi
# Anything else found in the command signature will be taken to mean
# a fixed string, which must be provided to match this command.
regex="${regex}$word "
done
if [ ${#regex} -ge 1 ]; then
regex="${regex:0:${#regex}-1}\$"
# Sets the global command variables in order to register this command
COMMAND_SIGNATURE[$COMMAND_COUNT]="$1"
COMMAND_REGEX[$COMMAND_COUNT]="$regex"
COMMAND_HANDLER[$COMMAND_COUNT]="$2"
COMMAND_COUNT=$(( $COMMAND_COUNT + 1 ))
else
error_exit FATAL_ERROR "Fatal error: Sorry about this, would you be so kind as to file a bug at http://git.io/2f_x-A and cite: \"Erroneous command regex '${regex}' for signature '${1}'\""
fi
}
# Match and call a command from user input
# $*: User input
call_command() {
manager_property SERVER_STORAGE_PATH
local args
local space="\ "
for arg in "$@"; do
if [[ "$arg" =~ $space ]]; then
args="$args\"$arg\" "
else
args="$args$arg "
fi
done
if [ ${#args} -ge 1 ]; then
args="${args:0:${#args}-1}"
fi
# Clear any command flags that might exist
# Start it with the delimiter necessary later on
COMMAND_FLAGS=";"
for ((command=0; command<$COMMAND_COUNT; command++)); do
if [[ "$args" =~ ${COMMAND_REGEX[$command]} ]]; then
unset args
local word_offset=1
local args
local arg_offset=0
local sid=-1
local wid=-1
# Helper function to build the argument list
# $1: The argument to push onto the list
push_arg() {
args[$arg_offset]="$1"
arg_offset="$(( $arg_offset + 1 ))"
}
# The following loop builds a set of arguments to pass to the
# matched command handler function. Rather than passing all args
# given to the script, to the handler (which may contain constant
# strings), it only includes variables.
for word in ${COMMAND_SIGNATURE[$command]}; do
# Whether a positional argument is a variable or not is
# determined by the respective element in the command signature
# given when registering.
#
# This case statement handles each possible type of signature
# token, and pushes the respective user input onto the stack of
# arguments.
case "$word" in
# The "<string>" token expects any type of string argument,
# accepting spaces, limited to one argument.
"<string>")
# Do no checks, just push the argument onto the stack
push_arg "${!word_offset}"
;;
# The "<strings>" token must only be placed at the end of a
# command signature, and allows an arbitrary amount of
# arguments to be passed to the command handler function.
"<strings>")
# Put all remaining user input onto the argument stack
for input_arg in "${@:$word_offset}"; do
push_arg "$input_arg"
done
# Break from analysing the rest of the input
break
;;
# The "<flags>" token expects any string without spaces that
# starts with one or two dashes: "--noinput -q" are examples.
# All flags are consumed and stored in the COMMAND_FLAGS
# variable.
"<flags>")
local num_flags=0
for potential_flag in "${@:$word_offset}"; do
if [[ "$potential_flag" =~ ^(\-\-|\-)[^\ ]+$ ]]; then
COMMAND_FLAGS="${COMMAND_FLAGS}${potential_flag};"
num_flags=$(( $num_flags + 1 ))
else
# Stop processing words, since all flags must be
# contiguous
break
fi
done
# We may have consumed more than one "word", the outer
# loop expects us to only take one, so must correct for
# this if we have take two words or more
if [[ "$num_flags" -ge 2 ]]; then
word_offset=$(( $word_offset + $num_flags - 1 ))
fi
;;
# The "<name>" token is similar to "<string>" but adds an
# extra assurance that the string is a valid name, as used
# for creating servers and other things.
"<name>")
# Check the argument is a valid name and then add push it onto the argument stack
local specified_name="${!word_offset}"
if is_valid_name "$specified_name"; then
push_arg "$specified_name"
fi
;;
# The "<name:server>" token improves on "<name>" by also
# checking that the server exists, and passing the argument
# on as the server id, instead of the server name to
# command handler functions.
"<name:server>")
local specified_name="${!word_offset}"
if [[ "$specified_name" == "all" ]]; then
# Do for all servers
sid="server:all"
else
if is_valid_name "$specified_name"; then
if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$specified_name" ]; then
server_get_id "$specified_name"
sid="$RETURN"
fi
fi
if [[ "$sid" -eq "-1" ]]; then
error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"."
fi
fi
push_arg "$sid"
;;
# The "<name:world>" token also improves upon "<name>" by
# ensuring that the world actually exists, and passes the
# argument on to command handlers as the world ID, rather
# than the original world name input by the user.
"<name:world>")
local specified_name="${!word_offset}"
if [[ "$sid" -eq "-1" ]]; then
# Server id not set yet
error_exit 1 "Ill-defined command $*. Please file an issue by opening the following link: https://github.com/marcuswhybrow/minecraft-server-manager/issues"
fi
if [[ "$sid" -eq "-2" ]]; then
if [[ "$specified_name" == "all" ]]; then
wid="world:all"
else
error_exit INVALID_ARGUMENT "When specifying \"all\" servers, \"all\" worlds must be specified also."
fi
fi
if [[ "$sid" -ge "0" ]]; then
if is_valid_name "$specified_name"; then
server_property "$sid" WORLD_STORAGE_PATH
server_property "$sid" WORLD_STORAGE_INACTIVE_PATH
if [ -d "${SERVER_WORLD_STORAGE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/$specified_name" ]; then
server_world_get_id "$sid" "$specified_name"
wid="$RETURN"
fi
fi
if [[ "$wid" -eq "-1" ]]; then
error_exit NAME_NOT_FOUND "There is no world with the name \"$specified_name\"."
fi
push_arg "$wid"
fi
;;
esac
word_offset=$(( $word_offset + 1 ))
done
# The argument list for the call to the command handler has been
# built. But there are several ways to call a handler. Either just
# once, or multiple times based upon if multiple servers or worlds
# were specified.
# This code block calls the handler for all possible servers and
# all possible worlds.
if [[ "$sid" == "server:all" ]] && [[ "$wid" == "world:all" ]]; then
for ((j=0; j<$NUM_WORLDS; j++)); do
# Replace server and world id place holders with actual id's
local replaced_args
for k in ${!args[@]}; do
replaced_args[$k]="${args[$k]//server:all/${WORLD_SERVER_ID[$j]}}"
replaced_args[$k]="${args[$k]//world:all/$j}"
done
# Call the function with the specific replaced args
${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
done
# Prevent the default singular call later on.
unset COMMAND_FLAGS; return
fi
# This calls the handler for all possible servers, and preserves
# all other arguments.
if [[ "$sid" == "server:all" ]]; then
for ((j=0; j<$NUM_SERVERS; j++)); do
local replaced_args
for k in ${!args[@]}; do
replaced_args[$k]="${args[$k]//server\:all/$j}"
done
${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
done
unset COMMAND_FLAGS; return
fi
# This calls the handlers for all possible worlds for a specific
# server.
if [[ "$sid" != "server:all" ]] && [[ "$wid" == "world:all" ]]; then
for ((j=${SERVER_WORLD_OFFSET[$sid]}; j<${SERVER_NUM_WORLDS[$sid]}; j++)); do
local replaced_args
for k in ${!args[@]}; do
replaced_args[$k]="${args[$k]//world:all/$j}"
done
${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
done
unset COMMAND_FLAGS; return
fi
# Otherwise it's a simple single call of the handler.
${COMMAND_HANDLER[$command]} "${args[@]}"
unset COMMAND_FLAGS; return
fi
done
echo "No such command. See $0 help"
}
# Defines every MSM command.
register_commands() {
# The following section registers commands to be available for use. The
# register_command function accepts a command_signature and a
# command_handler_function_name as positional arguments 1 and 2
# respectively.
#
# A command signature consists of multiple elements separated by spaces,
# the available options are as follows:
#
# fixedstring Matches an argument containing the specified
# characters, in this case the characters "fixedstring"
#
# <string> Same as "fixedstring", but is variable and the value
# is passed to the handler function as a positional
# argument
#
# <strings> Same as "<string>", but matches multiple arguments,
# must be final element
#
# <flags> Matches a list of space separated flags, such as
# "--noinput --quiet -p -d". Not passed as a positional
# argument. Instead set as the value of COMMAND_FLAGS.
#
# <name> Same as "<string>", also ensures it's a valid name
# using the is_valid_name function
#
# <name:server> Same as "<name>", also converts value to server id or
# fails if the server does not exist
#
# <name:world> Same as "<name>", also converts value to world id or
# fails if the world does not exist. Must only be
# included after a "<name:server>" element.
#
# Elements listed above encapsulated within angle brackets must be included
# within a signature verbatim, as opposed to the "fixedstring" element
# which is arbitrary.
#
# Variables passed to handler functions are of course positional and there
# position matches the position of that element in the command signature.
register_command "start" "command_start"
register_command "stop" "command_stop"
register_command "stop now" "command_stop_now"
register_command "restart" "command_restart"
register_command "restart now" "command_restart_now"
register_command "version" "command_version"
register_command "config" "command_config"
register_command "update <flags>" "command_update"
register_command "server list" "command_server_list"
register_command "server create <name>" "command_server_create"
register_command "server delete <name>" "command_server_delete"
register_command "server rename <name> <name>" "command_server_rename"
register_command "jargroup list" "command_jargroup_list"
register_command "jargroup create <name> <string>" "command_jargroup_create"
register_command "jargroup delete <name>" "command_jargroup_delete"
register_command "jargroup rename <name> <name>" "command_jargroup_rename"
register_command "jargroup changeurl <name> <string>" "command_jargroup_changeurl"
register_command "jargroup getlatest <name>" "command_jargroup_getlatest"
register_command "help" "command_help"
register_command "<name:server> start" "command_server_start"
register_command "<name:server> stop" "command_server_stop"
register_command "<name:server> stop now" "command_server_stop_now"
register_command "<name:server> restart" "command_server_restart"
register_command "<name:server> restart now" "command_server_restart_now"
register_command "<name:server> status" "command_server_status"
register_command "<name:server> connected" "command_server_connected"
register_command "<name:server> worlds list" "command_server_worlds_list"
register_command "<name:server> worlds load" "command_server_worlds_load"
register_command "<name:server> worlds ram <name:world>" "command_server_worlds_ram"
register_command "<name:server> worlds todisk" "command_server_worlds_todisk"
register_command "<name:server> worlds backup" "command_server_worlds_backup"
register_command "<name:server> worlds on <name:world>" "command_server_worlds_on"
register_command "<name:server> worlds off <name:world>" "command_server_worlds_off"
register_command "<name:server> logroll" "command_server_logroll"
register_command "<name:server> backup" "command_server_backup"
register_command "<name:server> jar <name>" "command_server_jar"
register_command "<name:server> jar <name> <string>" "command_server_jar"
register_command "<name:server> console" "command_server_console"
register_command "<name:server> config" "command_server_config"
register_command "<name:server> config <string>" "command_server_config"
register_command "<name:server> config <string> <string>" "command_server_config"
register_command "<name:server> whitelist|wl on" "command_server_whitelist_on"
register_command "<name:server> whitelist|wl off" "command_server_whitelist_off"
register_command "<name:server> whitelist|wl add <strings>" "command_server_whitelist_add"
register_command "<name:server> whitelist|wl remove <strings>" "command_server_whitelist_remove"
register_command "<name:server> whitelist|wl list" "command_server_whitelist_list"
register_command "<name:server> blacklist|bl player add <strings>" "command_server_blacklist_player_add"
register_command "<name:server> blacklist|bl player remove <strings>" "command_server_blacklist_player_remove"
register_command "<name:server> blacklist|bl ip add <strings>" "command_server_blacklist_ip_add"
register_command "<name:server> blacklist|bl ip remove <strings>" "command_server_blacklist_ip_remove"
register_command "<name:server> blacklist|bl list" "command_server_blacklist_list"
register_command "<name:server> operator|op add <strings>" "command_server_operator_add"
register_command "<name:server> operator|op remove <strings>" "command_server_operator_remove"
register_command "<name:server> operator|op list" "command_server_operator_list"
register_command "<name:server> gamemode|gm <string> <strings>" "command_server_gamemode"
register_command "<name:server> kick <strings>" "command_server_kick"
register_command "<name:server> say <strings>" "command_server_say"
register_command "<name:server> time set <string>" "command_server_time_set"
register_command "<name:server> time add <string>" "command_server_time_add"
register_command "<name:server> toggledownfall|tdf" "command_server_toggledownfall"
register_command "<name:server> give <string> <string>" "command_server_give"
register_command "<name:server> give <string> <string> <string>" "command_server_give"
register_command "<name:server> give <string> <string> <string> <string>" "command_server_give"
register_command "<name:server> xp <string> <string>" "command_server_xp"
register_command "<name:server> save on" "command_server_save_on"
register_command "<name:server> save off" "command_server_save_off"
register_command "<name:server> save all" "command_server_save_all"
register_command "<name:server> cmd <strings>" "command_server_cmd"
register_command "<name:server> cmdlog <strings>" "command_server_cmdlog"
}
# $1: Server path
server_allocate() {
unset RETURN
# Get an ID for this new server
local server_id="$NUM_SERVERS"
# Store the path for this new server
SERVER_PATH[$server_id]="$1"
# Store the name for this server
quick_basename "${SERVER_PATH[$server_id]}"
SERVER_NAME[$server_id]="$RETURN"
NUM_SERVERS=$(( $NUM_SERVERS + 1 ))
RETURN="$server_id"
}
# $1: Server ID
server_worlds_allocate() {
local world_id
# A server's worlds require contiguous ID's
# thus they are loaded one after another all at once.
# $1: Server ID
# $2: World path
world_allocate() {
# Get an ID for this new world
world_id="$NUM_WORLDS"
# Store the path for this new world
WORLD_PATH[$world_id]="$2"
# Store the name for this world
quick_basename "${WORLD_PATH[$world_id]}"
WORLD_NAME[$world_id]="$RETURN"
# Store the server ID this world belongs to
WORLD_SERVER_ID[$world_id]="$1"
NUM_WORLDS=$(( $NUM_WORLDS + 1 ))
}
server_property "$1" WORLD_STORAGE_PATH
server_property "$1" WORLD_STORAGE_INACTIVE_PATH
local world_name
# Record the index at which worlds for this server will start
SERVER_WORLD_OFFSET[$1]="$NUM_WORLDS"
if [[ -d "${SERVER_WORLD_STORAGE_PATH[$1]}" ]]; then
while IFS= read -r -d $'\0' path; do
world_allocate "$1" "$path"
done < <(find "${SERVER_WORLD_STORAGE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" ]]; then
while IFS= read -r -d $'\0' path; do
world_allocate "$1" "$path"
done < <(find "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
# Record the number of worlds this server has
SERVER_NUM_WORLDS[$1]="$(( $NUM_WORLDS - ${SERVER_WORLD_OFFSET[$1]} ))"
}
# Allocates stub variables, in this context a stub is
# enough data to be able to load in more data via
# the *_property functions.
allocate() {
manager_property SERVER_STORAGE_PATH
# Determine server names (but don't load them)
if [ -d "$SETTINGS_SERVER_STORAGE_PATH" ]; then
while IFS= read -r -d $'\0' path; do
server_allocate "$path"
server_worlds_allocate "$RETURN"
done < <(find "$SETTINGS_SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0)
fi
}
# Loads stub data for available versions
load_versions() {
manager_property USERNAME
manager_property VERSIONING_STORAGE_PATH
if [ -e "$SETTINGS_VERSIONING_STORAGE_PATH" ]; then
local newest_minecraft_version="0.0.0"
while IFS= read -r -d $'\0' path; do
local dir="$(dirname "$path")"
local file_name="$(basename "$path")"
local version="${file_name%.*}"
local version_type="$(basename "$dir")"
# Determine the newest minecraft version
if [[ "$version_type" == "minecraft" ]]; then
_newest_version "$version" "$newest_minecraft_version"
newest_minecraft_version="$RETURN"
fi
VERSIONS[$VERSIONS_COUNT]="${version_type}/$version"
VERSIONS_PATH[$VERSIONS_COUNT]="$path"
VERSIONS_COUNT=$(( $VERSIONS_COUNT + 1 ))
done < <(find "$SETTINGS_VERSIONING_STORAGE_PATH" -mindepth 1 -type f -print0)
# Record the latest minecraft version to use as a default
if [[ "$newest_minecraft_version" == "0.0.0" ]]; then
msm_warning "Could not find versioning files, please use 'msm update' to download them"
else
VERSIONS_NEWEST_MINECRAFT_VERSION="${newest_minecraft_version}"
VERSIONS_NEWEST_MINECRAFT_PATH="${SETTINGS_VERSIONING_STORAGE_PATH}/minecraft/${newest_minecraft_version}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
fi
else
msm_warning "Could not find versioning files, please use 'msm update' to download them"
fi
}
# $1: Version one
# $2: Version two
# $RETURN: The greater version
_newest_version() {
unset RETURN
# Compare the major versions [].0.0
component_one=`echo $1 | awk -F'.' '{print $1}'`
component_two=`echo $2 | awk -F'.' '{print $1}'`
if [[ "$component_one" -lt "$component_two" ]]; then
# Give up if the given major version is less than this one's
RETURN="$2"; return 0
fi
# Compare the minor versions 0.[].0
component_one=`echo $1 | awk -F'.' '{print $2}'`
component_two=`echo $2 | awk -F'.' '{print $2}'`
if [[ "$component_one" -lt "$component_two" ]]; then
# Give up if the given minor version is less than this one's
RETURN="$2"; return 0
fi
# Compare the patch versions 0.0.[]
component_one=`echo $1 | awk -F'.' '{print $3}'`
component_two=`echo $2 | awk -F'.' '{print $3}'`
if [[ "$component_one" -lt "$component_two" ]]; then
# Give up if the given patch version is less than this one's
RETURN="$2"; return 0
fi
RETURN="$1"
}
# Checks available versions MSM supports and returns the
# closes match.
# $1: Version name preferred
# $RETURN: The closest available version, older or equal
# to the given version $1
get_closest_version() {
unset RETURN
local given_type="${1%/*}"
local given_version="${1##*/}"
local closest_version cv_val
local v v_version v_type v_full v_val given_val
closest_version="0.0.0"
for ((v=0; v<$VERSIONS_COUNT; v++)); do
v_full="${VERSIONS[$v]}"
v_type="${v_full%/*}"
v_version="${v_full##*/}"
if [[ "$given_type" == "$v_type" ]]; then
# If this version type is the same as the given type (i.e. "minecraft")
# Then check the version is before or equal to this version:
_newest_version "$given_version" "$v_version"
if [[ "$RETURN" == "$given_version" ]]; then
# This version is older than or equal to the given version
_newest_version "$closest_version" "$v_version"
if [[ "$RETURN" == "$v_version" ]]; then
# This version is newer than or equal to the closest version
closest_version="$v_version"
fi
fi
fi
done
if [[ "$closest_version" == "0.0.0" ]]; then
RETURN="unknown"
else
RETURN="${given_type}/${closest_version}"
fi
}
# Called if the script is interrupted before exiting naturally
interrupt() {
local exit_message="false"
for ((i=0; $i<$NUM_SERVERS; i++)); do
if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
if [[ "$exit_message" == "false" ]]; then
echo -e "\nInterrupted..."
exit_message="true"
fi
server_eval "$i" "say ${SERVER_MESSAGE_STOP_ABORT[$i]}"
echo "Server \"${SERVER_NAME[$i]}\" shutdown was aborted."
fi
if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
if [[ "$exit_message" == "false" ]]; then
echo -e "\nInterrupted..."
exit_message="true"
fi
server_eval "$i" "say ${SERVER_MESSAGE_RESTART_ABORT[$i]}"
echo "Server \"${SERVER_NAME[$i]}\" restart was aborted."
fi
done
exit
}
### Versioning Functions
### --------------------
# Sources another versioning file
# $1: The name of the versioning file
extends() {
manager_property VERSIONING_STORAGE_PATH
source "${SETTINGS_VERSIONING_STORAGE_PATH}/$1.${SETTINGS_VERSIONING_FILE_EXTENSION}"
}
# Defines a servers console event variables, VERSIONING_SERVER_ID
# must be set before calling this function
# $1: The name of the event
# $2->: The log lines to accept as confirmation
console_event() {
# Build a regex with all lines in
local lines="$2"
for line in "${@:3}"; do
lines="$lines|$line"
done
local event_name event_timeout
if [[ "$1" =~ (.*):(.*) ]]; then
# If there is a colon in the name, use that
# to extract the included delay
event_name="${BASH_REMATCH[1]}"
event_timeout="${BASH_REMATCH[2]}"
else
event_name="$1"
event_timeout="1"
fi
# Set server variable
eval SERVER_CONSOLE_EVENT_OUTPUT_${event_name}[$VERSIONING_SERVER_ID]=\"$lines\"
eval SERVER_CONSOLE_EVENT_TIMEOUT_${event_name}[$VERSIONING_SERVER_ID]=\"$event_timeout\"
}
# Defines a servers console command variables, VERSIONING_SERVER_ID
# must be set before calling this function
# $1: The name of the command
# $2: The command pattern
# $3->: The log lines to accept as confirmation
console_command() {
local command_name command_timeout
if [[ "$1" =~ (.*):(.*) ]]; then
# If there is a colon in the name, use that
# to extract the included delay
command_name="${BASH_REMATCH[1]}"
command_timeout="${BASH_REMATCH[2]}"
else
command_name="$1"
command_timeout="1"
fi
eval SERVER_CONSOLE_COMMAND_PATTERN_${command_name}[$VERSIONING_SERVER_ID]=\"$2\"
# Build a regex with all lines in
local lines="$3"
for line in "${@:4}"; do
lines="$lines|$line"
done
eval SERVER_CONSOLE_COMMAND_OUTPUT_${command_name}[$VERSIONING_SERVER_ID]=\"$lines\"
eval SERVER_CONSOLE_COMMAND_TIMEOUT_${command_name}[$VERSIONING_SERVER_ID]=\"$command_timeout\"
}
### Starting Code
### -------------
# The main function which starts the script
main() {
register_settings
register_commands
load_versions
allocate
# Trap interrupts to the script by calling the interrupt function
trap interrupt EXIT
# This function call matches the user input to a registered command
# signature, and then calls that command's handler function with positional
# arguments containing any "variable" strings.
call_command "$@"
}
### Start point
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
# MSM was called from the command line
main "$@"
exit 0
else
# MSM was sourced from another script.
# Just register settings instead.
register_settings
load_versions
allocate
fi