msm/init/msm
2012-06-28 12:02:44 +01:00

2648 lines
85 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
### 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:
# Description:
### END INIT INFO
# See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for
# more information on debain init.d scripts, which may help you understand
# this script.
### The configuration file
# Get the MSM_CONF environment variable or use the default location
CONF="${MSM_CONF:-/etc/msm.conf}"
### The Minecraft Server Manager version, use "msm version" to check yours.
VERSION="0.3.2 Beta"
### Config variables the user should not need/want to change
# Jar group file which contains the download target URL
declare -r JARGROUP_TARGET="target.txt"
# Jar group directory name to download new jars to, is deleted afterwards
declare -r JARGROUP_DOWNLOAD_DIR="downloads"
# The server configuration file name
declare -r SERVER_CONF_NAME="server.properties"
# The start of a regex to find a log line
declare -r LOG_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\]"
### Script State Variables
# "true" whilst the script is counting down a delay to stop the server
declare STOP_COUNTDOWN
# "true" whilst the script is counting down a delay to restart the server
declare RESTART_COUNTDOWN
### 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
error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"."
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 "$@" 1>&2
}
# 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;;
esac
echo "${2:-"Unknown Error"}" 1>&2
exit "${code:-$1}"
}
# 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() {
if [[ "$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)$"
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\" or \"all\"."
else
return 0
fi
else
error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and unscores."
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
get_latest_file() {
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)
echo "$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() {
if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then
as_user "${SERVER_USER_NAME[${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() {
as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\""
}
# Toggles a worlds ramdisk state
# $1: The ID of the world
world_toggle_ramdisk_state() {
if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then
echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\""
echo "Done."
echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... "
as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\""
echo "Done."
else
echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "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() {
echo -n "Backing up world \"${WORLD_NAME[$1]}\"... "
file_name="$(date "+%F-%H-%M-%S").zip"
local server_id="${WORLD_SERVER_ID[$1]}"
local containing_dir="$(dirname "${WORLD_PATH[$1]}")"
local dir_name="$(basename "${WORLD_PATH[$1]}")"
as_user "${SERVER_USER_NAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\""
echo "Done."
}
# Activates a world
# $1: The ID of the world
world_activate() {
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... "
local new_path="${WORLD_ACTIVE_PATH[$1]}"
as_user "${SERVER_USER_NAME[${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() {
if server_is_running "${WORLD_SERVER_ID[$1]}"; then
exit_error 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="${WORLD_INACTIVE_PATH[$1]}"
as_user "${SERVER_USER_NAME[${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
exit_error DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found."
fi
fi
fi
}
### 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() {
for ((i=0; i<$num_servers; i++)); do
if [[ "${SERVER_NAME[$i]}" == "$1" ]]; then
echo "$i"
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() {
if [ -d "${SERVER_WORLD_STORAGE[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE[$1]}/$2" ]; then
# If the directory exists
local start="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$start; i<$max; i++)); do
if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then
echo "$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() {
if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null
then
return 0
else
return 1
fi
}
# Creates symbolic links in the server directory (SERVER_STORAGE_PATH) for each
# of the Minecraft worlds located in the world storage directory.
# $1: The id of the server for which links should be ensured
server_ensure_links() {
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
if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then
# Remove the symbolic link if it exists
as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
continue
fi
# -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 direcotry 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
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_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the RAM version of the world
as_user "${SERVER_USER_NAME[$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
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_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the disk version of the world
as_user "${SERVER_USER_NAME[$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() {
# Only proceed if there is a ramdisk path set in config
if [ ! -z "$RAMDISK_STORAGE_PATH" ]; 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
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() {
if [ ! -z "$RAMDISK_STORAGE_PATH" ]; 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
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 line or lines in the log to wait for
# returns: When the line is found
SERVER_LOG_get_line() {
# Make sure there is a server log to check
as_user "${SERVER_USER_NAME[$1]}" "touch ${SERVER_LOG[$1]}"
while read line; do
line_time="$(log_line_get_time "$line")"
# If the entry is old enough
if [[ "$line_time" -ge "$2" ]]; then
for search_line in "${@:3}"; do
local regex="${LOG_REGEX} ${search_line}"
# and matches the regular expression
if [[ "$line" =~ $regex ]]; then
echo "${line:0:${#line}-3}"
return 0
fi
done
fi
done < <(as_user "${SERVER_USER_NAME[$1]}" "tail --pid=$$ --follow --lines=100 --sleep-interval=0.1 \"${SERVER_LOG[$1]}\"")
}
# The same as SERVER_LOG_get_line, but does not print the line to stdout
# when found.
SERVER_LOG_wait_for_line() {
SERVER_LOG_get_line "$@" > /dev/null
}
# 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() {
as_user "${SERVER_USER_NAME[$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 line or lines of text in the log to wait for
# stdout: The full entry found in the logs
server_eval_and_get_line() {
time_now="$(now)"
server_eval "$1" "$2"
SERVER_LOG_get_line "$1" "$time_now" "${@:3}"
}
# The same as server_eval_and_get_line, but does not print anything to stdout
server_eval_and_wait() {
server_eval_and_get_line "$@" > /dev/null
}
# Gets the process ID for a server if running, otherwise it outputs nothing
# $1: The ID of the server
server_pid() {
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 fairyl 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() {
case "$2" in
active)
as_user "${SERVER_USER_NAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE[$1]}\""
SERVER_ACTIVE[$1]="true"
;;
inactive)
as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE[$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() {
if [[ -d "${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 "${JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0)
done < <(find "${JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
}
# Creates a new jargroup
# $1: The name for the jargroup
jargroup_create() {
if is_valid_name "$1"; then
if [[ ! -d "$JAR_STORAGE_PATH/$1" ]]; then
printf "Creating jar group... "
local error="$(as_user_stderr "$USERNAME" "mkdir -p \"$JAR_STORAGE_PATH/$1\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
error="$(as_user "$USERNAME" "echo \"$2\" > \"$JAR_STORAGE_PATH/$1/$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
}
# 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
if [[ -d "$JAR_STORAGE_PATH/$1" ]]; then
if [[ -f "$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET" ]]; then
printf "Downloading latest version... "
# Try and make
local error="$(as_user_stderr "$USERNAME" "mkdir -p '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
as_user "$USERNAME" "wget --quiet --trust-server-names --no-check-certificate --input-file='$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET' --directory-prefix='$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'"
echo "Done."
local num_files="$(as_user "$USERNAME" "ls -1 '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR' | wc -l")"
if [[ "$num_files" == 1 ]]; then
# There was 1 file downloaded
local file_name="$(ls -1 "$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR")"
local new_name="$(date +%F-%H-%M-%S)-$file_name"
local most_recent_jar="$(get_latest_file "$JAR_STORAGE_PATH/$1")"
if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$JAR_STORAGE_PATH/$1/$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 "$USERNAME" "mv '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR/$file_name' '$JAR_STORAGE_PATH/$1/$new_name'"
if [[ ! -z "$most_recent_jar" ]]; then
echo "Downloaded version was different to previous latest. Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"."
else
echo "Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"."
fi
else
echo "Existing version \"$JAR_STORAGE_PATH/$1/$new_name\" 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 "$USERNAME" "rm -fr '$JAR_STORAGE_PATH/$1/$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
if [[ -d "$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 "$USERNAME" "rm -rf \"$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
if [[ -d "$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 "$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 "$USERNAME" "mv '$JAR_STORAGE_PATH/$1' '$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
# Echos a list of servers in the SERVER_STORAGE_PATH
server_list() {
if [ -d "$SERVER_STORAGE_PATH" ]; then
ls -1 "$SERVER_STORAGE_PATH"
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
if [[ -d "$SERVER_STORAGE_PATH/$1" ]]; then
error_exit DUPLICATE_NAME "A server with that name already exists."
else
printf "Creating server directory... "
as_user "$USERNAME" "mkdir -p '$SERVER_STORAGE_PATH/$1'"
as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_WHITELIST'"
as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_IPS'"
as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_PLAYERS'"
as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_OPS'"
as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_PROPERTIES'"
echo "Done."
# Now that the new server has been created, we must call init again
# to ensure it is recognised.
init
# TODO: Handle server default setup stuff better than just using
# the "minecraft" jar group. And make it configurable.
if [ -d "$JAR_STORAGE_PATH/minecraft" ]; then
server_set_jar "$(server_get_id "$1")" "minecraft"
fi
fi
fi
}
# Deletes an existing server
# $1: The server name to delete
server_delete() {
if is_valid_name "$1"; then
if [[ -d "$SERVER_STORAGE_PATH/$1" ]]; then
printf "Are you sure you want to delete this server and its worlds (note: backups are preserved) [y/N]: "
read answer
if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then
# TODO: stop the server if running first
as_user "$USERNAME" "rm -rf '$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
if [ -d "$SERVER_STORAGE_PATH/$1" ]; then
# If the server name is valid and exists
local existing_id="$(server_get_id "$1")"
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 "$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 "$USERNAME" "mv '$SERVER_STORAGE_PATH/$1' '$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() {
if server_is_running "$1"; then
echo "Server \"${SERVER_NAME[$1]}\" is already running!"
else
server_ensure_links "$1"
server_worlds_to_ram "$1"
local time_now="$(now)"
printf "Starting server... "
as_user "${SERVER_USER_NAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}"
SERVER_LOG_wait_for_line "$1" "$time_now" "${SERVER_CONFIRM_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... "
# Send the "save-all" command and wait for it to finish
server_eval_and_wait "$1" "save-all" "${SERVER_CONFIRM_SAVE_ALL[$1]}"
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... "
# Send the "save-off" command and wait for it to finish
server_eval_and_wait "$1" "save-off" "${SERVER_CONFIRM_SAVE_OFF[$1]}"
echo "Done."
# Writes any in-memory data manged 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... "
# Send the "save-on" command and wait for it to finish
server_eval_and_wait "$1" "save-on" "${SERVER_CONFIRM_SAVE_ON[$1]}"
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() {
if server_is_running "$1"; then
# Change the state of the script
STOP_COUNTDOWN[$1]="true"
server_eval "$1" "say ${SERVER_STOP_MESSAGE[$1]}"
echo "Issued the warning \"${SERVER_STOP_MESSAGE[$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() {
# 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_RESTART_MESSAGE[$1]}"
echo "Issued the warning \"${SERVER_RESTART_MESSAGE[$1]}\" to players."
echo -n "Restarting... "
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"
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() {
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
while [[ "$i" -lt "$max" ]]; do
if "${WORLD_INRAM[$i]}"; then
echo "[RAM] ${WORLD_NAME[$i]}"
else
echo "[DSK] ${WORLD_NAME[$i]}"
fi
i="$(( $i + 1 ))"
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:
while [[ "$i" -lt "$max" ]]; do
world_backup "$i"
i="$(( $i + 1 ))"
done
}
# Moves a servers log into another file, leaving the original log file empty
# $1: The ID of the server
SERVER_LOG_roll() {
# Moves and Gzips the logfile, a big log file slows down the
# server A LOT (what was notch thinking?)
printf "Rolling server logs... "
if [ -e "${SERVER_LOG[$1]}" ]; then
file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log"
as_user "${SERVER_USER_NAME[$1]}" "mkdir -p \"${SERVER_LOG_archive_path[$1]}\" && cp \"${SERVER_LOG[$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_USER_NAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG[$1]}\""
as_user "${SERVER_USER_NAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_archive_path[$1]}\\\"\" > \"${SERVER_LOG[$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() {
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_USER_NAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$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() {
if [ -d "$JAR_STORAGE_PATH/$2" ]; then
if [ -z "$3" ]; then
# If a specific jar file is not mentioned
# Download the latest version
jargroup_getlatest "$2"
local jar="$(get_latest_file "$JAR_STORAGE_PATH/$2")"
else
# If a specific jar IS mentioned use that
local jar="$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_USER[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR[$1]}\""
echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"."
fi
else
error_exit NAME_NOT_FOUND "There is no jargorup 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
local line="$(server_eval_and_get_line "$1" "list" "Connected players:")"
# Cuts the start off the line, and the last three (invisible)
# characters from the end.
local players="${line:46}"
if [ -z "$players" ]; then
echo "No players are connected."
else
echo "$players"
fi
else
echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected."
fi
}
### 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 effects
# 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"
if [[ "${SERVER_STOP_DELAY[$i]}" -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_STOP_MESSAGE[$server]}";;
restart) server_eval "$server" "say ${SERVER_RESTART_MESSAGE[$server]}";;
esac
# Send message to stdout
echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:"
case "$1" in
stop) echo " Issued the warning \"${SERVER_STOP_MESSAGE[$server]}\" to players.";;
restart) echo " Issued the warning \"${SERVER_RESTART_MESSAGE[$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 server, to see if its their time to
# stop. If so issue the stop command, and don't hang.
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
was_running[$server]="true"
any_running="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
}
### Command Functions
# Starts all servers
command_start() {
# Required start option, for debian init.d scripts
for ((server=0; server<${num_servers}; server++)); do
# 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:"
command_stop
echo "Starting servers:"
command_start
}
# Restarts all servers without delay
command_restart_now() {
echo "Stopping servers:"
command_stop_now
echo "Starting servers:"
command_start
}
# Displays the MSM version
command_version() {
echo "Minecraft Server Manager $VERSION"
}
# 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_changetarget() {
jargroup_settarget "$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 Mangement 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
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> 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"
}
# 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 inram 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 inram worlds back to disk for an individual server
# $1: The server ID
command_server_worlds_todisk() {
server_save_off "$1"
server_save_all "$1"
server_worlds_to_disk "$1"
server_save_on "$1"
}
# 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_eval "$1" "say ${SERVER_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_eval "$1" "say ${SERVER_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_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_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_eval "$1" "whitelist on"
echo "Whitelist enabled"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Turns a server's whitelist protection off
# $1: The server ID
command_server_whitelist_off() {
if server_is_running "$1"; then
server_eval "$1" "whitelist off"
echo "Whitelist disabled"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Adds a player name to a server's whitelist
# $1: The server ID
# $2: The player name
command_server_whitelist_add() {
# TODO: Support whitelisting multiple players (see blacklist player add)
if server_is_running "$1"; then
server_eval "$1" "whitelist add $2"
echo "Added \"$2\" to the whitelist."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Removes a player name from a server's whitelist
# $1: The server ID
# $2: The player name
command_server_whitelist_remove() {
# TODO: Support multiple player names
if server_is_running "$1"; then
server_eval "$1" "whitelist remove $2"
echo "Removed \"$2\" from the whitelist."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Displays a list of whitelisted players for an individual server
# $1: The server ID
command_server_whitelist_list() {
if [ -f "${SERVER_WHITELIST[$1]}" ]; then
local players="$(cat "${SERVER_WHITELIST[$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() {
for player in "${@:2}"; do
server_eval "$1" "ban $player"
done
if [[ $# -gt 2 ]]; then
echo -n "Blacklisted the following players: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "Blacklisted \"$2\"."
fi
}
# Removes player names from a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_remove() {
for player in "${@:2}"; do
server_eval "$1" "pardon $player"
done
if [[ $# -gt 2 ]]; then
echo -n "Removed the following players from the blacklist: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "Removed \"$2\" from the blacklist."
fi
}
# Adds ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_add() {
for address in "${@:2}"; do
server_eval "$1" "ban-ip $address"
done
if [[ $# -gt 2 ]]; then
echo -n "Blacklisted the following ip addresses: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $address"
done
echo "."
else
echo "Blacklisted \"$2\"."
fi
}
# Removes ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_remove() {
for address in "${@:2}"; do
server_eval "$1" "pardon-ip $address"
done
if [[ $# -gt 2 ]]; then
echo -n "Removed the following ip addresses from the blacklist: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $address"
done
echo "."
else
echo "Removed \"$2\" from the ip blacklist."
fi
}
# Displays a server's banned player names and ip addresses
# $1: The server ID
command_server_blacklist_list() {
local players
local ips
if [ -f "${SERVER_BANNED_PLAYERS[$1]}" ]; then
players="$(cat "${SERVER_BANNED_PLAYERS[$1]}")"
fi
if [ -f "${SERVER_BANNED_IPS[$1]}" ]; then
ips="$(cat "${SERVER_BANNED_IPS[$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() {
# TODO: Support multiple player names
if server_is_running "$1"; then
server_eval "$1" "op $2"
echo "The player \"$2\" is now an operator for the server."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
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
server_eval "$1" "deop $2"
echo "The player \"$2\" is no longer an operator for the server."
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Displays a list of operators for an individual server
# $1: The server ID
command_server_operator_list() {
# TODO: Protect against non-existent files
local players="$(cat "${SERVER_OPS[$1]}")"
if [ -z "$players" ]; then
echo "No players are operators."
else
echo "$players"
fi
}
# Sets the game mode for
# $1: The server ID
# $2: The game mode
# $3: The player name
command_server_gamemode() {
# TODO: Support multiple player names
if server_is_running "$1"; then
local mode line regex
case "$2" in
creative|1) mode=1;;
survival|0) mode=0;;
*) error_exit INVALID_ARGUMENT "Invalid gamemode type \"$2\" options are \"survival\", \"creative\", \"0\" or \"1\".";;
esac
line="$(server_eval_and_get_line "$1" "gamemode $3 $mode" "${SERVER_CONFIRM_GAMEMODE[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}")"
regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Changed game mode of \"$3\" to \"$2\"."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$3\" was not found to be logged on."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$3\" was already in mode \"$2\"."
fi
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() {
# TODO: Support multiple player names
if server_is_running "$1"; then
local line regex
line="$(server_eval_and_get_line "$1" "kick $2" "${SERVER_CONFIRM_KICK[$1]}" "${SERVER_CONFIRM_KICK_FAIL[$1]}")"
regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Kicked \"$2\" from game."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK_FAIL[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$2\" is not connected."
fi
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 concatinated with spaces
command_server_say() {
if server_is_running "$1"; then
server_eval "$1" "say ${*:2}"
echo "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
local line regex
line="$(server_eval_and_get_line "$1" "time set $2" "${SERVER_CONFIRM_TIME_SET[$1]}" "${SERVER_CONFIRM_TIME_SET_FAIL[$1]}")"
regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Set time to \"$2\"."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET_FAIL[$1]}"
if [[ "$line" =~ $regex ]]; then
error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time."
fi
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
local line regex
line="$(server_eval_and_get_line "$1" "time add $2" "${SERVER_CONFIRM_TIME_ADD[$1]}" "${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}")"
regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Added \"$2\" to time."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}"
if [[ "$line" =~ $regex ]]; then
error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time."
fi
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
local line regex
line="$(server_eval_and_get_line "$1" "toggledownfall" "${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" "${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}")"
regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:36}"
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:34}"
fi
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 concatinated 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 concatinated with spaces
command_server_cmdlog() {
if server_is_running "$1"; then
server_eval "$1" "${*:2}"
echo "Now watching logs (press Ctrl+C to exit):"
as_user "${SERVER_USER_NAME[$1]}" "tail --pid=$$ --follow --lines=0 --sleep-interval=0.1 ${SERVER_LOG[$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
as_user "${SERVER_USER_NAME[$1]}" "screen -r ${SERVER_SCREEN_NAME[$1]}"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
### Main Functions
# Initialises a server's world
# $1: The server id
# $2: The world id to use
# $3: The name of the world
server_world_init() {
WORLD_SERVER_ID[$2]="$1"
WORLD_NAME[$2]="$3"
WORLD_ACTIVE_PATH[$2]="${SERVER_WORLD_STORAGE[$1]}/${WORLD_NAME[$2]}"
WORLD_INACTIVE_PATH[$2]="${SERVER_WORLD_STORAGE_INACTIVE[$1]}/${WORLD_NAME[$2]}"
# Set the status of this world (active/inactive)
if [ -d "${WORLD_ACTIVE_PATH[$2]}" ]; then
WORLD_STATUS[$2]="active"
WORLD_PATH[$2]="${WORLD_ACTIVE_PATH[$2]}"
else
if [ -d "${WORLD_INACTIVE_PATH[$2]}" ]; then
WORLD_STATUS[$2]="inactive"
WORLD_PATH[$2]="${WORLD_INACTIVE_PATH[$2]}"
else
WORLD_STATUS[$2]="unknown"
error_exit NAME_NOT_FOUND "World cannot be found in either \"${WORLD_ACTIVE_PATH[$2]}\" or \"${WORLD_INACTIVE_PATH[$2]}\"."
fi
fi
# TODO: Allow the inram flag location to be overridable.
WORLD_FLAG_INRAM[$2]="${WORLD_PATH[$2]}/inram"
WORLD_LINK[$2]="${SERVER_PATH[$1]}/${WORLD_NAME[$2]}"
WORLD_BACKUP_PATH[$2]="$WORLD_ARCHIVE_PATH/${SERVER_NAME[$1]}/${WORLD_NAME[$2]}"
# If the ramdisk path is set, get the path for this world
if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then
WORLD_RAMDISK_PATH[$2]="${RAMDISK_STORAGE_PATH}/${SERVER_NAME[$1]}/${WORLD_NAME[$2]}"
fi
# Detect whether this world should be in ram
if [[ -e "${WORLD_FLAG_INRAM[$2]}" ]]; then
WORLD_INRAM[$2]="true"
else
WORLD_INRAM[$2]="false"
fi
}
# Load a config file for a server
# $1: The id of the server to laod
server_load_config() {
local name value
if [[ -f "${SERVER_CONF[$1]}" ]]; then
while read line; do
# ignore comment lines
echo "$line" | grep "^#" >/dev/null 2>&1 && continue
# if not empty, set the property using declare
if [ ! -z "$line" ]; then
name="$(echo $line | awk -F '=' '{print $1}')"
value="$(echo $line | awk -F '=' '{print $2}')"
fi
case "$name" in
# Minecraft settings:
allow-flight) SERVER_PROPERTIES_ALLOW_FLIGHT="$value";;
allow-nether) SERVER_PROPERTIES_ALLOW_NETHER="$value";;
difficulty) SERVER_PROPERTIES_DIFFICULTY="$value";;
enable-query) SERVER_PROPERTIES_ENABLE_QUERY="$value";;
enable-rcon) SERVER_PROPERTIES_ENABLE_RCON="$value";;
gamemode) SERVER_PROPERTIES_GAMEMODE="$value";;
generate-structures) SERVER_PROPERTIES_GENERATE_STRUCTURES="$value";;
level-name) SERVER_PROPERTIES_LEVEL_NAME="$value";;
level-seed) SERVER_PROPERTIES_LEVEL_SEED="$value";;
level-type) SERVER_PROPERTIES_LEVEL_TYPE="$value";;
max-build-height) SERVER_PROPERTIES_MAX_BUILD_HEIGHT="$value";;
max-players) SERVER_PROPERTIES_MAX_PLAYERS="$value";;
motd) SERVER_PROPERTIES_MOTD="$value";;
texture-pack) SERVER_PROPERTIES_TEXTURE_PACK="$value";;
online-mode) SERVER_PROPERTIES_ONLINE_MODE="$value";;
pvp) SERVER_PROPERTIES_PVP="$value";;
query.port) SERVER_PROPERTIES_QUERY_PORT="$value";;
rcon.password) SERVER_PROPERTIES_RCON_PASSWORD="$value";;
rcon.port) SERVER_PROPERTIES_RCON_PORT="$value";;
server-ip) SERVER_PROPERTIES_SERVER_IP="$value";;
server-port) SERVER_PROPERTIES_SERVER_PORT="$value";;
spawn-animals) SERVER_PROPERTIES_SPAWN_ANIMALS="$value";;
spawn-monsters) SERVER_PROPERTIES_SPAWN_MONSTERS="$value";;
spawn-npcs) SERVER_PROPERTIES_SPAWN_NPCS="$value";;
view-distance) SERVER_PROPERTIES_VIEW_DISTANCE="$value";;
white-list) SERVER_PROPERTIES_WHITE_LIST="$value";;
# MSM setttings:
msm-server-user) SERVER_USER_NAME[$1]="$value";;
msm-screen-name) SERVER_SCREEN_NAME[$1]="$value";;
msm-world-storage-path) SERVER_WORLD_STORAGE[$1]="${SERVER_PATH[$1]}/$value";;
msm-world-storage-inactive-path) SERVER_WORLD_STORAGE_INACTIVE[$1]="${SERVER_PATH[$1]}/$value";;
msm-log) SERVER_LOG[$1]="${SERVER_PATH[$1]}/$value";;
msm-whitelist) SERVER_WHITELIST[$1]="${SERVER_PATH[$1]}/$value";;
msm-banned-players) SERVER_BANNED_PLAYERS[$1]="${SERVER_PATH[$1]}/$value";;
msm-banned-ips) SERVER_BANNED_IPS[$1]="${SERVER_PATH[$1]}/$value";;
msm-ops) SERVER_OPS[$1]="${SERVER_PATH[$1]}/$value";;
msm-jar) SERVER_JAR[$1]="${SERVER_PATH[$1]}/$value";;
msm-ram) SERVER_RAM[$1]="$value";;
msm-invocation) SERVER_INVOCATION[$1]="$value";;
msm-stop-delay) SERVER_STOP_DELAY[$1]="$value";;
msm-restart-delay) SERVER_RESTART_DELAY[$1]="$value";;
msm-stop-message) SERVER_STOP_MESSAGE[$1]="$value";;
msm-stop-abort) SERVER_STOP_ABORT[$1]="$value";;
msm-restart-message) SERVER_RESTART_MESSAGE[$1]="$value";;
msm-restart-abort) SERVER_RESTART_ABORT[$1]="$value";;
msm-world-backup-started) SERVER_WORLD_BACKUP_STARTED[$1]="$value";;
msm-world-backup-finished) SERVER_WORLD_BACKUP_FINISHED[$1]="$value";;
msm-complete-backup-started) SERVER_COMPLETE_BACKUP_STARTED[$1]="$value";;
msm-complete-backup-finished) SERVER_COMPLETE_BACKUP_FINISHED[$1]="$value";;
msm-complete-backup-follow-symlinks) SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]="$value";;
msm-confirm-save-on) SERVER_CONFIRM_SAVE_ON[$1]="$value";;
msm-confirm-save-off) SERVER_CONFIRM_SAVE_OFF[$1]="$value";;
msm-confirm-save-all) SERVER_CONFIRM_SAVE_ALL[$1]="$value";;
msm-confirm-start) SERVER_CONFIRM_START[$1]="$value";;
msm-confirm-whitelist-list) SERVER_CONFIRM_WHITELIST_LIST[$1]="$value";;
msm-confirm-kick) SERVER_CONFIRM_CONFIRM_KICK[$1]="$value";;
msm-confirm-kick-fail) SERVER_CONFIRM_KICK_FAIL[$1]="$value";;
msm-confirm-time-set) SERVER_CONFIRM_TIME_SET[$1]="$value";;
msm-confirm-time-set-fail) SERVER_CONFIRM_TIME_SET_FAIL[$1]="$value";;
msm-confirm-time-add) SERVER_CONFIRM_TIME_ADD[$1]="$value";;
msm-confirm-time-add-fail) SERVER_CONFIRM_TIME_ADD_FAIL[$1]="$value";;
msm-confirm-toggledownfall) SERVER_CONFIRM_TOGGLEDOWNFALL[$1]="$value";;
msm-confirm-toggledownfall-fail) SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]="$value";;
msm-confirm-gamemode) SERVER_CONFIRM_GAMEMODE[$1]="$value";;
msm-confirm-gamemode-fail-no-user) SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]="$value";;
msm-confirm-gamemode-fail-no-change) SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]="$value";;
esac
done < "${SERVER_CONF[$1]}"
fi
}
# Initialise a server's variables
# $1: The id to use for this server
# $2: The name of the server
server_init() {
SERVER_NAME[$1]="$2"
SERVER_PATH[$1]="$SERVER_STORAGE_PATH/$2"
SERVER_CONF[$1]="${SERVER_PATH[$1]}/$SERVER_CONF_NAME"
SERVER_FLAG_ACTIVE[$1]="${SERVER_PATH[$1]}/active"
SERVER_BACKUP_PATH[$1]="$BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}"
SERVER_LOG_archive_path[$1]="$LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}"
if [[ -e "${SERVER_FLAG_ACTIVE[$1]}" ]]; then
SERVER_ACTIVE[$1]="true"
else
SERVER_ACTIVE[$1]="false"
fi
# Setup defaults
# Note: screen_name will at this stage have the {SERVER_NAME} tag in it
# which needs to be replaced.
# Invocation may also the {RAM} and {JAR} tags.
SERVER_USER_NAME[$1]="$DEFAULT_SERVER_USER"
SERVER_SCREEN_NAME[$1]="${DEFAULT_SCREEN_NAME//\{SERVER_NAME\}/${SERVER_NAME[$1]}}" # Replace tags now, they cannot change
SERVER_WORLD_STORAGE[$1]="${SERVER_PATH[$1]}/$DEFAULT_WORLD_STORAGE_PATH"
SERVER_WORLD_STORAGE_INACTIVE[$1]="${SERVER_PATH[$1]}/$DEFAULT_WORLD_STORAGE_INACTIVE_PATH"
SERVER_LOG[$1]="${SERVER_PATH[$1]}/$DEFAULT_LOG"
SERVER_WHITELIST[$1]="${SERVER_PATH[$1]}/$DEFAULT_WHITELIST"
SERVER_BANNED_PLAYERS[$1]="${SERVER_PATH[$1]}/$DEFAULT_BANNED_PLAYERS"
SERVER_BANNED_IPS[$1]="${SERVER_PATH[$1]}/$DEFAULT_BANNED_IPS"
SERVER_OPS[$1]="${SERVER_PATH[$1]}/$DEFAULT_OPS"
SERVER_JAR[$1]="${SERVER_PATH[$1]}/$DEFAULT_JAR"
SERVER_RAM[$1]="$DEFAULT_RAM"
SERVER_INVOCATION[$1]="$DEFAULT_INVOCATION" # Don't replace tags yet, they may change
SERVER_STOP_DELAY[$1]="$DEFAULT_STOP_DELAY"
SERVER_RESTART_DELAY[$1]="$DEFAULT_RESTART_DELAY"
SERVER_STOP_MESSAGE[$1]="$DEFAULT_STOP_MESSAGE"
SERVER_STOP_ABORT[$1]="$DEFAULT_STOP_ABORT"
SERVER_RESTART_MESSAGE[$1]="$DEFAULT_RESTART_MESSAGE"
SERVER_RESTART_ABORT[$1]="$DEFAULT_RESTART_ABORT"
SERVER_WORLD_BACKUP_STARTED[$1]="$DEFAULT_WORLD_BACKUP_STARTED"
SERVER_WORLD_BACKUP_FINISHED[$1]="$DEFAULT_WORLD_BACKUP_FINISHED"
SERVER_COMPLETE_BACKUP_STARTED[$1]="$DEFAULT_COMPLETE_BACKUP_STARTED"
SERVER_COMPLETE_BACKUP_FINISHED[$1]="$DEFAULT_COMPLETE_BACKUP_FINISHED"
SERVER_CONFIRM_SAVE_ON[$1]="$DEFAULT_CONFIRM_SAVE_ON"
SERVER_CONFIRM_SAVE_OFF[$1]="$DEFAULT_CONFIRM_SAVE_OFF"
SERVER_CONFIRM_SAVE_ALL[$1]="$DEFAULT_CONFIRM_SAVE_ALL"
SERVER_CONFIRM_START[$1]="$DEFAULT_CONFIRM_START"
SERVER_CONFIRM_KICK[$1]="$DEFAULT_CONFIRM_KICK"
SERVER_CONFIRM_KICK_FAIL[$1]="$DEFAULT_CONFIRM_KICK_FAIL"
SERVER_CONFIRM_TIME_SET[$1]="$DEFAULT_CONFIRM_TIME_SET"
SERVER_CONFIRM_TIME_SET_FAIL[$1]="$DEFAULT_CONFIRM_TIME_SET_FAIL"
SERVER_CONFIRM_TIME_ADD[$1]="$DEFAULT_CONFIRM_TIME_ADD"
SERVER_CONFIRM_TIME_ADD_FAIL[$1]="$DEFAULT_CONFIRM_TIME_ADD_FAIL"
SERVER_CONFIRM_TOGGLEDOWNFALL[$1]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL"
SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL"
SERVER_CONFIRM_GAMEMODE[$1]="$DEFAULT_CONFIRM_GAMEMODE"
SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER"
SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE"
SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]="$DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS"
# Load config overrides from server config file if present
server_load_config "$1"
# Replace tags in delay messages
SERVER_STOP_MESSAGE[$1]="${SERVER_STOP_MESSAGE[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}"
SERVER_RESTART_MESSAGE[$1]="${SERVER_RESTART_MESSAGE[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}"
# Replace tags in server invocation
SERVER_INVOCATION[$1]="${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}"
SERVER_INVOCATION[$1]="${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR[$1]}}"
# Load worlds if there is a world storage directory present
SERVER_WORLD_OFFSET[$1]=0
SERVER_NUM_WORLDS[$1]=0
# Start world id's for this server's worlds at the end of the array
local id="$num_worlds"
# Record the index at which worlds for this server start
SERVER_WORLD_OFFSET[$1]="$id"
if [[ -d "${SERVER_WORLD_STORAGE[$1]}" ]]; then
# Load active worlds
while IFS= read -r -d $'\0' path; do
local name="$(basename "$path")"
server_world_init "$1" "$id" "$name"
# Build the server_worlds comma separated list
if [[ "$id" == "${SERVER_WORLD_OFFSET[$1]}" ]]; then
SERVER_WORLDS[$1]="$name"
else
SERVER_WORLDS[$1]="${SERVER_WORLDS[$1]}, $name"
fi
id="$(($id+1))"
num_worlds="$id"
done < <(find "${SERVER_WORLD_STORAGE[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE[$1]}" ]]; then
# Load inactive worlds
while IFS= read -r -d $'\0' path; do
local name="$(basename "$path")"
server_world_init "$1" "$id" "$name"
# Build the server_worlds_inactive comma separated list
if [[ "$id" == "${SERVER_WORLD_OFFSET[$1]}" ]]; then
SERVER_WORLDS[$1]="$name"
else
SERVER_WORLDS[$1]="${SERVER_WORLDS[$1]}, $name"
fi
id="$(($id+1))"
num_worlds="$id"
done < <(find "${SERVER_WORLD_STORAGE_INACTIVE[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
# Record the number of worlds this server has
SERVER_NUM_WORLDS[$1]="$(( $id - ${SERVER_WORLD_OFFSET[$1]} ))"
}
# Asserts that a variable has been set in the config file
# $1: The name of the variable to test
assert_is_set_in_config() {
[[ ! ${!1} && ${!1-_} ]] && {
error_exit CONF_ERROR "$1 must be set in $CONF"
}
}
# Ensures all non-optional settings have been set
init_ensure_settings() {
assert_is_set_in_config USERNAME
assert_is_set_in_config SERVER_STORAGE_PATH
assert_is_set_in_config JAR_STORAGE_PATH
assert_is_set_in_config RAMDISK_STORAGE_PATH
assert_is_set_in_config WORLD_ARCHIVE_PATH
assert_is_set_in_config LOG_ARCHIVE_PATH
assert_is_set_in_config BACKUP_ARCHIVE_PATH
assert_is_set_in_config DEFAULT_SERVER_USER
assert_is_set_in_config DEFAULT_SCREEN_NAME
assert_is_set_in_config DEFAULT_WORLD_STORAGE_PATH
assert_is_set_in_config DEFAULT_WORLD_STORAGE_INACTIVE_PATH
assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS
assert_is_set_in_config DEFAULT_LOG
assert_is_set_in_config DEFAULT_PROPERTIES
assert_is_set_in_config DEFAULT_WHITELIST
assert_is_set_in_config DEFAULT_BANNED_PLAYERS
assert_is_set_in_config DEFAULT_BANNED_IPS
assert_is_set_in_config DEFAULT_OPS
assert_is_set_in_config DEFAULT_JAR
assert_is_set_in_config DEFAULT_RAM
assert_is_set_in_config DEFAULT_INVOCATION
assert_is_set_in_config DEFAULT_STOP_DELAY
assert_is_set_in_config DEFAULT_RESTART_DELAY
assert_is_set_in_config DEFAULT_STOP_MESSAGE
assert_is_set_in_config DEFAULT_STOP_ABORT
assert_is_set_in_config DEFAULT_RESTART_MESSAGE
assert_is_set_in_config DEFAULT_RESTART_ABORT
assert_is_set_in_config DEFAULT_WORLD_BACKUP_STARTED
assert_is_set_in_config DEFAULT_WORLD_BACKUP_FINISHED
assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_STARTED
assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_FINISHED
assert_is_set_in_config DEFAULT_CONFIRM_SAVE_ON
assert_is_set_in_config DEFAULT_CONFIRM_SAVE_OFF
assert_is_set_in_config DEFAULT_CONFIRM_SAVE_ALL
assert_is_set_in_config DEFAULT_CONFIRM_START
assert_is_set_in_config DEFAULT_CONFIRM_KICK
assert_is_set_in_config DEFAULT_CONFIRM_KICK_FAIL
assert_is_set_in_config DEFAULT_CONFIRM_TIME_SET
assert_is_set_in_config DEFAULT_CONFIRM_TIME_SET_FAIL
assert_is_set_in_config DEFAULT_CONFIRM_TIME_ADD
assert_is_set_in_config DEFAULT_CONFIRM_TIME_ADD_FAIL
assert_is_set_in_config DEFAULT_CONFIRM_TOGGLEDOWNFALL
assert_is_set_in_config DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL
assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE
assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER
assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE
}
init() {
# Sourcing $CONF will override the previous defaults, with new values
source "$CONF"
init_ensure_settings
num_worlds=0
num_servers=0
if [ -d "$SERVER_STORAGE_PATH" ]; then
local id=0
while IFS= read -r -d $'\0' path; do
local name="$(basename "$path")"
server_init "$id" "$name"
id="$(($id+1))"
num_servers="$id"
done < <(find "$SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0)
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_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_RESTART_ABORT[$i]}"
echo "Server \"${SERVER_NAME[$i]}\" restart was aborted."
fi
done
exit
}
COMMAND_COUNT=0
# 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 derrived
# 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
regex="${regex}.+ "
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 varibales 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() {
for ((i=0; i<$COMMAND_COUNT; i++)); do
if [[ "$*" =~ ${COMMAND_REGEX[$i]} ]]; 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[$i]}; do
# Whether a positional argument is a varibale 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
# commadn 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 "<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 "$SERVER_STORAGE_PATH/$specified_name" ]; then
sid="$(server_get_id "$specified_name")"
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
exit_error 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
exit_error 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
if [ -d "${SERVER_WORLD_ACTIVE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_INACTIVE_PATH[$sid]}/$specified_name" ]; then
wid="$(server_world_get_id "$sid" "$specified_name")"
fi
fi
if [ -z "$wid" ]; 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 placeholders 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[$i]} "${replaced_args[@]}"
done
# Prevent the default singular call later on.
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[$i]} "${replaced_args[@]}"
done
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[$i]} "${replaced_args[@]}"
done
return
fi
# Otherwise it's a simple single call of the handler.
${COMMAND_HANDLER[$i]} "${args[@]}"
return
fi
done
echo "No such command. See $0 help"
}
# The main function which starts the script
main() {
# Initialises variables that represent system state
init
# Trap interrupts to the script by calling the interrupt function
trap interrupt EXIT
# 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
#
# <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 apposed 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 "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 changetarget <name> <string>" "command_jargroup_changetarget"
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> <name>" "command_server_jar"
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 <string>" "command_server_whitelist_add"
register_command "<name:server> whitelist|wl remove <string>" "command_server_whitelist_remove"
register_command "<name:server> whitelist|wl list" "command_server_whitelist_list"
register_command "<name:server> blacklist|bl player add <string>" "command_server_blacklist_player_add"
register_command "<name:server> blacklist|bl player remove <string>" "command_server_blacklist_player_remove"
register_command "<name:server> blacklist|bl ip add <string>" "command_server_blacklist_ip_add"
register_command "<name:server> blacklist|bl ip remove <string>" "command_server_blacklist_ip_remove"
register_command "<name:server> blacklist|bl list" "command_server_blacklist_list"
register_command "<name:server> operator|op add <string>" "command_server_operator_add"
register_command "<name:server> operator|op remove <string>" "command_server_operator_remove"
register_command "<name:server> operator|op list" "command_server_operator_list"
register_command "<name:server> gamemode|gm survival|creative|0|1 <string>" "command_server_gamemode"
register_command "<name:server> kick <string>" "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> 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" "command_server_cmd"
register_command "<name:server> cmdlog" "command_server_cmdlog"
register_command "<name:server> console" "command_server_console"
# This function call matches the user input to a registered command
# signature, and then calls that commands handler function with positional
# arguments containing any variable strings.
call_command "$@"
}
### Start point
main "$@"
exit 0