msm/init/msm
2012-06-04 21:40:33 +01:00

2262 lines
68 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.2.4 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 flag which indicates the world should be put in RAM
declare -r WORLD_FLAG_INRAM="inram"
# The flag which indicates the server is active
declare -r SERVER_FLAG_ACTIVE="active"
# 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
echoerr "This command must be executed as the user \"$1\" or \"root\"."
exit 1
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
}
# 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.
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
echoerr "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\" or \"all\"."
return 1
else
return 0
fi
else
echoerr "Invalid name \"$1\": A name may only contain letters, numbers, dashes and unscores."
return 1
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 '$WORLD_FLAG_INRAM' \"${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 '$WORLD_FLAG_INRAM' \"${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
echoerr "Error: Directory \"${world_inactive_path[$1]}\" could not be found."
return 1
fi
fi
}
# Deactivates a world
# $1: The ID of the world
world_deactivate() {
if server_is_running "${world_server_id[$1]}"; then
echoerr "Error: Worlds cannot be deactivated whilst the server is running."
return 1
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
echoerr "Error: Directory \"${world_active_path[$1]}\" could not be found."
return 1
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() {
local i=0
while [[ "$i" -lt "$num_servers" ]]; do
if [[ "${server_name[$i]}" == "$1" ]]; then
echo "$i"
return 0
fi
i="$(( $i + 1 ))"
done
return 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
return 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"
return 0
fi
done
fi
done < <(as_user "${server_user_name[$1]}" "tail --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"
;;
*)
return 1
;;
esac
}
### Jar Group Functions
# Lists the jar files grouped by jar groups.
jargroup_list() {
for group in $(ls -1 "$JAR_STORAGE_PATH"); do
echo "${group}:"
for jar in $(ls -1r "$JAR_STORAGE_PATH/$group"); do
if [[ "$jar" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then
echo " $jar"
fi
done
done
}
# Creates a new jargroup
# $1: The name for the jargroup
jargroup_create() {
if is_valid_name "$1"; then
if [[ ! -e "$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."
echo "Reason: $error"
return 1
fi
error="$(as_user "$USERNAME" "echo \"$2\" > \"$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
echo "Reason: $error"
return 1
fi
echo "Done."
# Download the latest version now
jargroup_getlatest "$1"
else
echo "A jar group with that name already exists."
return 1
fi
else
return 1
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 [[ -e "$JAR_STORAGE_PATH/$1" ]]; then
if [[ -e "$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."
echo "Reason: $error"
return 1
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 [[ ! -e "$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
[[ -e "$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
echo "Target URL not found, use $0 jargroup seturl <download-url>"
return 1
fi
else
echo "There is no jar group with the name \"$1\"."
return 1
fi
else
return 1
fi
}
# Deletes an existing jargroup
# $1: The name of the existing jargroup
jargroup_delete() {
if is_valid_name "$1"; then
if [[ -e "$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
echo "There is no jar group with the name \"$1\"."
return 1
fi
else
return 1
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 [[ -e "$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
echo "Could not be renamed, there is already a jar group with the name \"$2\"."
return 1
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
else
return 1
fi
else
echo "There is no jar group with the name \"$1\"."
return 1
fi
else
return 1
fi
}
### Server Functions
# Echos a list of servers in the SERVER_STORAGE_PATH
server_list() {
ls -1 "$SERVER_STORAGE_PATH"
}
# Creates a new server
# $1: The server name to create
server_create() {
if is_valid_name "$1"; then
if [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then
echo "A server with that name already exists."
return 1
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
server_set_jar "$(server_get_id "$1")" "minecraft"
fi
else
return 1
fi
}
# Deletes an existing server
# $2: The server name to delete
server_delete() {
if is_valid_name "$1"; then
if [[ -e "$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
echo "There is no server with the name \"$1\"."
return 1
fi
else
return 1
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 server_is_running "$(server_get_id "$1")"; then
echoerr "Error: Can only rename a stopped server."
return 1
else
if [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then
# If the server name is valid and exists
# TODO: Check that the server is not running first, return if it is
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
echoerr "Error: Could not be renamed, there is already a server with the name \"$2\"."
return 1
else
as_user "$USERNAME" "mv '$SERVER_STORAGE_PATH/$1' '$SERVER_STORAGE_PATH/$2'"
echo "Renamed server \"$1\" to \"$2\"."
fi
else
return 1
fi
else
echoerr "Error: There is no server with the name \"$1\"."
return 1
fi
fi
else
return 1
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[$id]="true"
server_eval "$id" "say ${server_stop_message[$id]}"
echo "Issued the warning \"${server_stop_message[$id]}\" to players."
echo -n "Shutting down... "
for ((i="${server_stop_delay[$id]}"; 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[$id]="true"
server_eval "$id" "say ${server_restart_message[$id]}"
echo "Issued the warning \"${server_restart_message[$id]}\" to players."
echo -n "Restarting... "
for ((i="${server_stop_delay[$id]}"; 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
echoerr "Failed."
return 1
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 [ -e "$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
echo "There is no jar named \"$3\" in jargroup \"$2\"."
return 1
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
echo "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[$id]}\" 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
}
### 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"
echoerr "Error: World cannot be found in either \"${world_active_path[$2]}\" or \"${world_inactive_path[$2]}\"."
return 1
fi
fi
world_link[$2]="${server_path[$1]}/${world_name[$2]}"
world_flag_inram[$2]="${world_path[$2]}/$WORLD_FLAG_INRAM"
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_path[$2]}/$WORLD_FLAG_INRAM" ]]; 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
SERVER_USER) server_user_name[$1]="$value";;
SCREEN_NAME) server_screen_name[$1]="$value";;
WORLD_STORAGE_PATH) server_world_storage[$1]="${server_path[$1]}/$value";;
WORLD_STORAGE_INACTIVE_PATH) server_world_storage_inactive[$1]="${server_path[$1]}/$value";;
LOG) server_log[$1]="${server_path[$1]}/$value";;
PROPERTIES) server_properties[$1]="$value";;
WHITELIST) server_whitelist[$1]="$value";;
BANNED_PLAYERS) server_banned_players[$1]="$value";;
BANNED_IPS) server_banned_ips[$1]="$value";;
OPS) server_ops[$1]="$value";;
JAR) server_jar[$1]="${server_path[$1]}/$value";;
RAM) server_ram[$1]="$value";;
INVOCATION) server_invocation[$1]="$value";;
STOP_DELAY) server_stop_delay[$1]="$value";;
RESTART_DELAY) server_restart_delay[$1]="$value";;
STOP_MESSAGE) server_stop_message[$1]="$value";;
STOP_ABORT) server_stop_abort[$1]="$value";;
RESTART_MESSAGE) server_restart_message[$1]="$value";;
RESTART_ABORT) server_restart_abort[$1]="$value";;
WORLD_BACKUP_STARTED) server_world_backup_started[$1]="$value";;
WORLD_BACKUP_FINISHED) server_world_backup_finished[$1]="$value";;
COMPLETE_BACKUP_STARTED) server_complete_backup_started[$1]="$value";;
COMPLETE_BACKUP_FINISHED) server_complete_backup_finished[$1]="$value";;
CONFIRM_SAVE_ON) server_confirm_save_on[$1]="$value";;
CONFIRM_SAVE_OFF) server_confirm_save_off[$1]="$value";;
CONFIRM_SAVE_ALL) server_confrim_save_all[$1]="$value";;
CONFIRM_START) server_confrim_start[$1]="$value";;
CONFIRM_WHITELIST_LIST) server_confirm_whitelist_list[$1]="$value";;
CONFIRM_KICK) server_confirm_kick[$1]="$value";;
CONFIRM_KICK_FAIL) server_confirm_kick_fail[$1]="$value";;
CONFIRM_TIME_SET) server_confirm_time_set[$1]="$value";;
CONFIRM_TIME_SET_FAIL) server_confirm_time_set_fail[$1]="$value";;
CONFIRM_TIME_ADD) server_confirm_time_add[$1]="$value";;
CONFIRM_TIME_ADD_FAIL) server_confirm_time_add_fail[$1]="$value";;
CONFIRM_TOGGLEDOWNFALL) server_confirm_toggledownfall[$1]="$value";;
CONFIRM_TOGGLEDOWNFALL_FAIL) server_confirm_toggledownfall_fail[$1]="$value";;
CONFIRM_GAMEMODE) server_confirm_gamemode[$1]="$value";;
CONFIRM_GAMEMODE_FAIL_NO_USER) server_confirm_gamemode_fail_no_user[$1]="$value";;
CONFIRM_GAMEMODE_FAIL_NO_CHANGE) server_confirm_gamemode_fail_no_change[$1]="$value";;
COMPLETE_BACKUP_FOLLOW_SYMLINKS) server_complete_backup_follow_symlinks[$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]}/$DEFAULT_SERVER_CONF"
server_flag_active[$1]="${server_path[$1]}/$SERVER_FLAG_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_properties[$1]="${server_path[$1]}/$DEFAULT_PROPERTIES"
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
# $2: The message to print to stderr if the variable is unset
assert_is_set_in_config() {
[[ ! ${!1} && ${!1-_} ]] && {
echoerr "Error: $1 must be set in $CONF"
exit 1
}
}
# 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_CONF
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
}
# The main function which starts the script
main() {
# Initialise variables that represent system state
init
# Trap interrupts to the script by calling the interrupt function
trap interrupt EXIT
case "$1" in
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
;;
stop)
if [ "$2" != "now" ]; then
# If the the now flag is not specified
manager_stop_all_servers "stop"
else
# If the now flag is specified
manager_stop_all_servers_now
fi
;;
restart)
# Required restart option, for debian init.d scripts
# Stop any running servers, now if specified
echo "Stopping servers:"
main "stop" "$2"
echo "Starting servers:"
main "start"
;;
version)
echo "Minecraft Server Manager $VERSION"
;;
server)
case "$2" in
list)
# Lists the existing servers
server_list
;;
create)
if [ -z "$3" ]; then
# If a server name is not provided
echo "Invalid command."
else
# Create a new server
server_create "$3"
fi
;;
delete)
if [ -z "$3" ]; then
# If a server name is not provided
echo "Invalid command."
else
# Delete an existing server, with confirmation
server_delete "$3"
fi
;;
rename)
if [ -z "$3" ] || [ -z "$4" ]; then
# If a server name is not provided
echo "Invalid command."
else
# Rename an existing server
server_rename "$3" "$4"
fi
;;
*)
# "server" is not a valid command
echo "Invalid command."
;;
esac
;;
jargroup)
case "$2" in
list)
# Lists the jars grouped by jargroup
jargroup_list
;;
create)
if [ -z "$3" ] || [ -z "$4" ]; then
echo "Invlaid command."
else
jargroup_create "$3" "$4"
fi
;;
delete)
if [ -z "$3" ]; then
echo "Invalid command."
else
jargroup_delete "$3"
fi
;;
rename)
if [ -z "$3" ] || [ -z "$4" ]; then
echo "Invalid command."
else
jargroup_rename "$3" "$4"
fi
;;
changetarget)
if [ -z "$3" ] || [ -z "$4" ]; then
echo "Invalid command."
else
jargroup_settarget "$3" "$4"
fi
;;
getlatest)
if [ -z "$3" ]; then
echo "Invalid command."
else
jargroup_getlatest "$3"
fi
;;
*)
# "jargroup" is not a valid command
echo "Invalid command."
;;
esac
;;
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 installed8"
;;
"")
echo "No such command see: $0 help"
;;
*)
if [[ "$1" == "all" ]] || [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then
local id="$(server_get_id "$1")"
case "$2" in
start)
server_set_active "$id" "active"
server_start "$id"
;;
stop)
server_set_active "$id" "inactive"
if [[ "$3" != "now" ]]; then
server_stop "$id"
else
server_stop_now "$id"
fi
;;
restart)
server_set_active "$1" "active"
if [[ "$3" != "now" ]]; then
server_restart "$id"
else
server_restart_now "$id"
fi
;;
status)
if server_is_running "$id"; then
echo "Server \"${server_name[$id]}\" is running."
else
echo "Server \"${server_name[$id]}\" is stopped."
fi
;;
connected)
server_connected "$id"
;;
worlds)
case "$3" in
list)
server_worlds_list "$id"
;;
load)
server_ensure_links "$id"
;;
ram)
if [ -z "$4" ]; then
echo "Invalid command."
else
world_id="$(server_world_get_id "$id" "$4")"
if [ ! -z "$world_id" ]; then
world_toggle_ramdisk_state "$world_id"
else
echo "Server \"${server_name[$id]}\" has no world with that name."
fi
fi
;;
todisk)
server_save_off "$id"
server_save_all "$id"
server_worlds_to_disk "$id"
server_save_on "$id"
;;
backup)
if server_is_running "$id"; then
server_eval "$id" "say ${server_world_backup_started[$id]}"
server_save_off "$id"
server_save_all "$id"
fi
server_worlds_to_disk "$id"
server_worlds_backup "$id"
if server_is_running "$id"; then
server_save_on "$id"
server_eval "$id" "say ${server_world_backup_finished[$id]}"
fi
echo "Backup took $SECONDS seconds".
;;
on)
if [ -z "$4" ]; then
echo "Invalid command."
else
world_activate "$(server_world_get_id "$id" "$4")"
fi
;;
off)
if [ -z "$4" ]; then
echo "Invalid command."
else
world_deactivate "$(server_world_get_id "$id" "$4")"
fi
;;
*)
echo "Invalid command."
;;
esac
;;
logroll)
server_log_roll "$id"
;;
backup)
if server_is_running "$id"; then
server_eval "$id" "say ${server_complete_backup_started[$id]}"
server_save_off "$id"
server_save_all "$id"
fi
server_worlds_to_disk "$id"
server_backup "$id"
if server_is_running "$id"; then
server_save_on "$id"
server_eval "$id" "say ${server_complete_backup_finished[$id]}"
fi
echo "Backup took $SECONDS seconds".
;;
jar)
if [ -z "$3" ]; then
echo "Invalid command."
else
server_set_jar "$id" "$3" "$4"
fi
;;
whitelist|wl)
case "$3" in
on)
if server_is_running "$id"; then
server_eval "$id" "whitelist on"
echo "Whitelist enabled"
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
off)
if server_is_running "$id"; then
server_eval "$id" "whitelist off"
echo "Whitelist disabled"
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
add)
if [ -z "$4" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
server_eval "$id" "whitelist add $4"
echo "Added \"$4\" to the whitelist."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
remove)
if [ -z "$4" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
server_eval "$id" "whitelist remove $4"
echo "Removed \"$4\" from the whitelist."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
list)
local players="$(cat "${server_whitelist[$id]}")"
if [ -z "$players" ]; then
echo "No players are whitelisted."
else
echo "$players"
fi
;;
*)
echo "Invalid command."
;;
esac
;;
blacklist|bl)
case "$3" in
player)
if [ -z "$5" ]; then
echo "Invalid command."
else
case "$4" in
add)
for player in ${*:5}; do
server_eval "$id" "ban $player"
done
if [[ $# -gt 5 ]]; then
echo -n "Blacklisted the following players: "
echo -n "$5"
for player in ${*:6}; do
echo -n ", $player"
done
echo "."
else
echo "Blacklisted \"$5\"."
fi
;;
remove)
for player in ${*:5}; do
server_eval "$id" "pardon $player"
done
if [[ $# -gt 5 ]]; then
echo -n "Removed the following players from the blacklist: "
echo -n "$5"
for player in ${*:6}; do
echo -n ", $player"
done
echo "."
else
echo "Removed \"$5\" from the blacklist."
fi
;;
*)
echo "Invalid command."
;;
esac
fi
;;
ip)
case "$4" in
add)
for address in ${*:5}; do
server_eval "$id" "ban-ip $address"
done
if [[ $# > 5 ]]; then
echo -n "Blacklisted the following ip addresses: "
echo -n "$5"
for player in ${*:6}; do
echo -n ", $address"
done
echo "."
else
echo "Blacklisted \"$5\"."
fi
;;
remove)
for address in ${*:5}; do
server_eval "$id" "pardon-ip $address"
done
if [[ $# > 5 ]]; then
echo -n "Removed the following ip addresses from the blacklist: "
echo -n "$5"
for player in ${*:6}; do
echo -n ", $address"
done
echo "."
else
echo "Removed \"$5\" from the ip blacklist."
fi
;;
*)
echo "Invalid command."
;;
esac
;;
list)
local players="$(cat "${server_banned_players[$id]}")"
local ips="$(cat "${server_banned_ips[$id]}")"
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
;;
*)
echo "Invalid command."
;;
esac
;;
operator|op)
case "$3" in
add)
if server_is_running "$id"; then
server_eval "$id" "op $4"
echo "The player \"$4\" is now an operator for the server."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
remove)
if server_is_running "$id"; then
server_eval "$id" "deop $4"
echo "The player \"$4\" is no longer an operator for the server."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
list)
local players="$(cat "${server_ops[$id]}")"
if [ -z "$players" ]; then
echo "No players are operators."
else
echo "$players"
fi
;;
*)
echo "Invalid command"
;;
esac
;;
gamemode|gm)
case "$3" in
survival|creative|0|1)
if [ -z "$4" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
case "$3" in
creative|1) local mode=1;;
survival|0) local mode=0;;
*) echoerr "Invalid mode"; exit 1;;
esac
local line="$(server_eval_and_get_line "$id" "gamemode $4 $mode" "${server_confirm_gamemode[$id]}" "${server_confirm_gamemode_fail_no_user[$id]}" "${server_confirm_gamemode_fail_no_change[$id]}")"
local regex="${LOG_REGEX} ${server_confirm_gamemode[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Changed game mode of \"$4\" to \"$3\"."
fi
local regex="${LOG_REGEX} ${server_confirm_gamemode_fail_no_user[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$4\" was not found to be logged on."
fi
local regex="${LOG_REGEX} ${server_confirm_gamemode_fail_no_change[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$4\" was already in mode \"$3\"."
fi
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
*)
echo "Invalid command."
;;
esac
;;
kick)
if [ -z "$3" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
local line="$(server_eval_and_get_line "$id" "kick $3" "${server_confirm_kick[$id]}" "${server_confirm_kick_fail[$id]}")"
local regex="${LOG_REGEX} ${server_confirm_kick[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Kicked \"$3\" from game."
fi
local regex="${LOG_REGEX} ${server_confirm_kick_fail[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$3\" is not connected."
fi
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
say)
if [ -z "$3" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
server_eval "$id" "say ${*:3}"
echo "Message sent to players."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
time)
case "$3" in
set)
if [ -z "$4" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
local line="$(server_eval_and_get_line "$id" "time set $4" "${server_confirm_time_set[$id]}" "${server_confirm_time_set_fail[$id]}")"
local regex="${LOG_REGEX} ${server_confirm_time_set[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Set time to \"$4\"."
fi
local regex="${LOG_REGEX} ${server_confirm_time_set_fail[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Unable to convert \"$4\" to a time."
fi
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
add)
if [ -z "$4" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
local line="$(server_eval_and_get_line "$id" "time add $4" "${server_confirm_time_add[$id]}" "${server_confirm_time_add_fail[$id]}")"
local regex="${LOG_REGEX} ${server_confirm_time_add[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Added \"$4\" to time."
fi
local regex="${LOG_REGEX} ${server_confirm_time_add_fail[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "Unable to convert \"$4\" to a time."
fi
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
*)
echo "Invalid command."
;;
esac
;;
toggledownfall|tdf)
if server_is_running "$id"; then
local line="$(server_eval_and_get_line "$id" "toggledownfall $3" "${server_confirm_toggledownfall[$id]}" "${server_confirm_toggledownfall_fail[$id]}")"
local regex="${LOG_REGEX} ${server_confirm_toggledownfall[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:36:(-3)}"
fi
local regex="${LOG_REGEX} ${server_confirm_toggledownfall_fail[$id]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:34:(-3)}"
fi
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
save)
case "$3" in
on)
server_save_on "$id"
;;
off)
server_save_off "$id"
;;
all)
server_save_all "$id"
;;
*)
echo "Invalid command."
;;
esac
;;
cmd)
if [ -z "$3" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
server_eval "$id" "${*:3}"
echo "Command sent."
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
cmdlog)
if [ -z "$3" ]; then
echo "Invalid command."
else
if server_is_running "$id"; then
server_eval "$id" "${*:3}"
echo "Now watching logs (press Ctrl+C to exit):"
as_user "${server_user_name[$id]}" "tail --follow --lines=0 --sleep-interval=0.1 ${server_log[$id]}"
else
echo "Server \"${server_name[$id]}\" is not running."
fi
fi
;;
console)
if server_is_running "$id"; then
as_user "${server_user_name[$1]}" "screen -r ${server_screen_name[$1]}"
else
echo "Server \"${server_name[$id]}\" is not running."
fi
;;
*)
echo "Invalid command."
;;
esac
else
echo "No server with that name."
fi
;;
esac
}
### Start point
main "$@"
exit 0