msm/init/msm
2012-07-24 14:19:37 +01:00

3459 lines
102 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

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

#!/bin/bash
# Minecraft Server Manager
# ========================
#
# A single init script for managing multiple Minecraft servers.
# Created by Marcus Whybrow
#
# http://marcuswhybrow.net/minecraft-server-manager/
#
### BEGIN INIT INFO
# Provides: msm
# Required-Start: $local_fs $remote_fs
# Required-Stop: $local_fs $remote_fs
# Should-Start: $network
# Should-Stop: $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: MSM: Minecraft init script
# Description: Minecraft Server Manager, an init script for Minecraft/Bukkit servers
### END INIT INFO
# See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for
# more information on debain init.d scripts, which may help you understand
# this script.
# Source, if it exists, the msm profile.d script
if [ -f "/etc/profile.d/msm.sh" ]; then
source "/etc/profile.d/msm.sh"
fi
# 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.7.1"
### Config variables the user should not need/want to change
# The start of a regex to find a log line
LOG_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\]"
# Lazy allocation status
ALLOCATED_SERVERS="false"
ALLOCATED_WORLDS="false"
# Global totals
NUM_WORLDS=0
NUM_SERVERS=0
COMMAND_COUNT=0
SETTING_COUNT=0
SERVER_SETTING_COUNT=0
### 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}"
}
# Tests the bash version installed
# $1: The bash version required
is_bash_version() {
if [[ "$BASH_VERSION" =~ ^$1 ]]; then
return 0
fi
return 1
}
# Converts a string to be ready for use as a global
# variable name.
# $1: The string to convert
# RETURN: The name in uppercase and with underscores
to_global_name() {
unset RETURN
# Translate to uppercase, and replace dashes with underscores
local result="$1"
if is_bash_version 4; then
# Much faster than the `tr` command
result="${result//-/_}"
result="${result//./_}"
result="${result^^}" # to uppercase
else
result="$(echo "$result" | tr '[\-\.a-z]' '[\_\_A-Z]')"
fi
RETURN="$result"
}
# Converts a global BASH variable name to a server.properties file
# varibale name.
# $1: The string to convert
# RETURN: The name in lowercase and with dashes
to_properties_name() {
unset RETURN
# Translate to uppercase, and replace dashes with underscores
local result="$1"
if is_bash_version 4; then
# Much faster than the `tr` command
result="${result//_/-}"
result="${result,,}" # to lowercase
else
result="$(echo "$result" | tr '[\_A-Z]' '[\-a-z]')"
fi
RETURN="$result"
}
# A custom basename function which is faster
# than opening a subshell
# $1: The path to get the basename of
# RETURN: The basename of the path
quick_basename() {
unset RETURN
if [[ "$1" =~ \/([^\/]*)$ ]]; then
RETURN="${BASH_REMATCH[1]}"
fi
}
# A function used to print debug messages to stdout. Prevents messages from
# appearing unless in debug mode, and allows debug statements to be easily
# distinguished from necessary echo statements.
# $1: The message to output
debug() {
manager_property DEBUG
if [[ "$SETTINGS_DEBUG" == "true" ]]; then
echoerr "$1"
fi
}
# Determines whether "$1" is a valid name for a server or jar group directory
# It must only contain upper or lower case letters, digits, dashes or
# underscores.
# It must also not be one of a list of reserved names.
# $1: The name to check
is_valid_name() {
local valid="^[a-zA-Z0-9\_\-]+$"
local invalid="^(start|stop|restart|version|server|jargroup|all|config|\-\-.*)$"
if [[ "$1" =~ $valid ]]; then
if [[ "$1" =~ $invalid ]]; then
error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\", \"all\", \"config\" or start with two dashes (--)."
else
return 0
fi
else
error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and 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
# RETURN: The latest file
get_latest_file() {
unset RETURN
local best_time=0
local best_file=""
while IFS= read -r -d $'\0' file; do
# Remove the path, leaving just the file name
local date_time="$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}')"
# Get the time in seconds since 1970 from file name
local seconds="$(date -d "$date_time" "+%s" 2> /dev/null)"
# If that is newer than the current best, override variables
if [[ "$seconds" -gt "$best_time" ]]; then
best_time="$seconds"
best_file="$file"
fi
done < <(find "$1" -maxdepth 1 -type f -print0)
RETURN="$best_file"
}
# Returns the current time as a UNIX timestamp (in seconds since 1970)
now() {
date +%s
}
### Log Utility Functions
# Gets the UNIX timestamp for a server log line
# $1: A server log line
# returns: Time in seconds since 1970-01-01 00:00:00 UTC
log_line_get_time() {
time_string="$(echo "$1" | awk '{print $1 " " $2}')"
date -d "$time_string" "+%s" 2> /dev/null
}
### World Utility Functions
### -----------------------
# Moves a world to RAM
# $1: the ID of the world to move
world_to_ram() {
manager_property RAMDISK_STORAGE_PATH
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" RAMDISK_PATH
world_property "$1" FLAG_INRAM
world_property "$1" PATH
if [ ! -z "$SETTINGS_RAMDISK_STORAGE_PATH" ]; then
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"${WORLD_RAMDISK_PATH[$1]}\" && rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_PATH[$1]}/\" \"${WORLD_RAMDISK_PATH[$1]}\""
fi
}
# Moves a world in RAM to disk
# $1: the ID of the world to move
world_to_disk() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" FLAG_INRAM
world_property "$1" RAMDISK_PATH
world_property "$1" PATH
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\""
}
# Toggles a worlds ramdisk state
# $1: The ID of the world
world_toggle_ramdisk_state() {
world_property "$1" FLAG_INRAM
world_property "$1" RAMDISK_PATH
local sid="${WORLD_SERVER_ID[$1]}"
server_property "$sid" USERNAME
if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then
echo -n "Synchronising world \"${WORLD_NAME[$1]}\" to disk... "
world_to_disk "$1"
echo "Done."
echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USERNAME[$sid]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\""
echo "Done."
echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... "
as_user "${SERVER_USERNAME[$sid]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\""
echo "Done."
else
echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... "
as_user "${SERVER_USERNAME[$sid]}" "touch \"${WORLD_FLAG_INRAM[$1]}\""
echo "Done."
echo -n "Copying world to RAM... "
world_to_ram "$1"
echo "Done."
fi
echo "Changes will only take effect after server is restarted."
}
# Backs up a world
# $1: The ID of the world
world_backup() {
world_property "$1" PATH
world_property "$1" BACKUP_PATH
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]}")"
server_property "$server_id" USERNAME
as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\""
echo "Done."
}
# Activates a world
# $1: The ID of the world
world_activate() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" INACTIVE_PATH
world_property "$1" ACTIVE_PATH
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... "
local new_path="${WORLD_ACTIVE_PATH[$1]}"
as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_INACTIVE_PATH[$1]}\" \"$new_path\""
echo "Done."
else
if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
echo "World \"${WORLD_NAME[$1]}\" is already activate."
else
error_exit DIR_NOT_FOUND "Directory \"${WORLD_INACTIVE_PATH[$1]}\" could not be found."
fi
fi
}
# Deactivates a world
# $1: The ID of the world
world_deactivate() {
server_property "${WORLD_SERVER_ID[$1]}" USERNAME
world_property "$1" ACTIVE_PATH
world_property "$1" INACTIVE_PATH
world_property "$1" PATH
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_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_PATH[$1]}\" \"$new_path\""
echo "Done."
else
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
echo "World \"${WORLD_NAME[$1]}\" is already deactivate."
else
exit_error DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found."
fi
fi
fi
}
# Get the value of a world property
# $1: The world ID
# $2: The property name
world_property() {
# Get the current value
eval local value=\"\${WORLD_$2[$1]}\"
# If it is empty, then set it
if [ -z "$value" ]; then
local sid="${WORLD_SERVER_ID[$1]}"
case "$2" in
NAME|PATH)
# Defined at allocation
return 0
;;
ACTIVE_PATH)
server_property "$sid" WORLD_STORAGE_PATH
WORLD_ACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
INACTIVE_PATH)
server_property "$sid" WORLD_STORAGE_INACTIVE_PATH
WORLD_INACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
STATUS)
world_property "$1" ACTIVE_PATH
if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
WORLD_STATUS[$1]="active"
else
world_property "$1" INACTIVE_PATH
if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
WORLD_STATUS[$1]="inactive"
else
WORLD_STATUS[$1]="unknown"
fi
fi
;;
FLAG_INRAM)
world_property "$1" PATH
server_property "$sid" WORLDS_FLAG_INRAM
WORLD_FLAG_INRAM[$1]="${WORLD_PATH[$1]}/${SERVER_WORLDS_FLAG_INRAM[$sid]}"
;;
LINK)
server_property "$sid" PATH
WORLD_LINK[$1]="${SERVER_PATH[$sid]}/${WORLD_NAME[$1]}"
;;
BACKUP_PATH)
manager_property WORLD_ARCHIVE_PATH
WORLD_BACKUP_PATH[$1]="$SETTINGS_WORLD_ARCHIVE_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
;;
RAMDISK_PATH)
manager_property RAMDISK_STORAGE_PATH
# If the ramdisk path is set, get the path for this world
if [ ! -z "$SETTINGS_RAMDISK_STORAGE_PATH" ]; then
WORLD_RAMDISK_PATH[$1]="${SETTINGS_RAMDISK_STORAGE_PATH}/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
fi
;;
INRAM)
world_property "$1" FLAG_INRAM
# Detect whether this world should be in ram
if [[ -e "${WORLD_FLAG_INRAM[$1]}" ]]; then
WORLD_INRAM[$1]="true"
else
WORLD_INRAM[$1]="false"
fi
;;
esac
fi
}
# $1: The world ID
world_dirty_properties() {
local index
# Removes properties for all servers if an index
# is not specified
if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
index="[$1]"
else
index=""
fi
unset WORLD_NAME$index
unset WORLD_PATH$index
unset WORLD_ACTIVE_PATH$index
unset WORLD_INACTIVE_PATH$index
unset WORLD_STATUS$index
unset WORLD_FLAG_INRAM$index
unset WORLD_LINK$index
unset WORLD_BACKUP_PATH$index
unset WORLD_RAMDISK_PATH$index
unset WORLD_INRAM$index
}
### Server Utility Functions
### ------------------------
# Returns the ID for a server.
# An ID is given to a server when loaded into memory, and can be used to lookup
# config information for that server
# $1: The name of the server
server_get_id() {
unset RETURN
for ((server=0; server<$NUM_SERVERS; server++)); do
if [[ "${SERVER_NAME[$server]}" == "$1" ]]; then
RETURN="$server"
return 0
fi
done
error_exit NAME_NOT_FOUND "Could not find id for server name \"$1\"."
}
# Returns the ID of a server's world.
# $1: The ID of the server
# $2: The name of the world
server_world_get_id() {
server_property "$1" WORLD_STORAGE_PATH
server_property "$1" WORLD_STORAGE_INACTIVE_PATH
unset RETURN
if [ -d "${SERVER_WORLD_STORAGE_PATH[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}/$2" ]; then
# If the directory exists
local start="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$start; i<$max; i++)); do
if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then
RETURN="$i"
return 0
fi
done
fi
error_exit NAME_NOT_FOUND "Could not find id for world \"$2\" for server \"${SERVER_NAME[$1]}\"."
}
# Returns 0 if the server $1 is running and 1 if not
# $1: The ID of the server
server_is_running() {
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null
then
return 0
else
return 1
fi
}
# Ensures the server has a jar file where it is expected to be
# $1: The id of the server
server_ensure_jar() {
server_property "$1" JAR_PATH
if [ -f "${SERVER_JAR_PATH[$1]}" ]; then
return 0
fi
error_exit FILE_NOT_FOUND "Could not find jar for server \"${SERVER_NAME[$1]}\": Expected \"${SERVER_JAR_PATH[$1]}\"."
}
# Creates symbolic links in the server directory (SETTINGS_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() {
server_property "$1" USERNAME
echo -n "Maintaining world symbolic links... "
local start="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"
local output="false"
for ((i=$start; i<$max; i++)); do
world_property "$i" STATUS
world_property "$i" LINK
if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
continue
fi
world_property "$i" INRAM
# -L checks for the path being a link rather than a file
# ! -a, since it is within double square brackets means: the negation of
# the existence of the file. In other words: true if does not exist
if [[ -L "${WORLD_LINK[$i]}" || ! -a "${WORLD_LINK[$i]}" ]]; then
# If there is a symbolic link in the server 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
world_property "$i" RAMDISK_PATH
if [ "${link_target}" != "${WORLD_RAMDISK_PATH[$i]}" ]; then
# If the symbolic link does not point to the RAM version of the world
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the RAM version of the world
as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_RAMDISK_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
fi
else
# Otherwise the world is not loaded into RAM, and is just on disk
world_property "$i" PATH
if [ "${link_target}" != "${WORLD_PATH[$i]}" ]; then
# If the symbolic link does not point to the disk version of the world
# Remove the symbolic link if it exists
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
# Create a new symbolic link pointing to the disk version of the world
as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
fi
fi
else
echoerr -en "\n Error: Could not create link for world \"${WORLD_NAME[$i]}\". The file \"${WORLD_LINK[$i]}\" already exists, and should not be overwritten automatically. Either remove this file, or rename \"${WORLD_NAME[$i]}\"."
output="true"
fi
done
if [[ "$output" == "true" ]]; then
echo -e "\nDone."
else
echo "Done."
fi
}
# Moves a servers worlds into RAM
# $1: The ID of the server
server_worlds_to_ram() {
manager_property RAMDISK_STORAGE_PATH
# Only proceed if there is a ramdisk path set in config
if [ ! -z "$SETTINGS_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
world_property "$i" INRAM
world_property "$i" LINK
if "${WORLD_INRAM[$i]}" && [ -L "${WORLD_LINK[$i]}" ]; then
world_to_ram "$i"
fi
i="$(( $i + 1 ))"
done
echo "Done."
fi
}
# Moves a servers "in RAM" worlds back to disk
# $1: The ID of the server
server_worlds_to_disk() {
manager_property RAMDISK_STORAGE_PATH
if [ ! -z "$SETTINGS_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
world_property "$i" RAMDISK_PATH
if [ -d "${WORLD_RAMDISK_PATH[$i]}" ]; then
world_to_disk "$i"
fi
i="$(( $i + 1 ))"
done
echo "Done."
fi
}
# Watches a server's log for a specific line
# $1: The ID for the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3->: The line or lines in the log to wait for
# returns: When the line is found
server_log_get_line() {
server_property "$1" USERNAME
server_property "$1" LOG_PATH
unset RETURN
# Make sure there is a server log to check
as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}"
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
RETURN="${line}"
return 0
fi
done
fi
done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"")
}
# The same as server_log_get_line, but prints a dot instead of the log line
# to stdout, and retruns when line is found.
# $1: the ID of the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3->: The line or lines in the log to wait for
# returns: When the line is found
server_log_dots_for_lines() {
server_property "$1" USERNAME
server_property "$1" LOG_PATH
# Make sure there is a server log to check
as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}"
while read line; do
line_time="$(log_line_get_time "$line")"
# If the entry is old enough
if [[ "$line_time" -ge "$2" ]]; then
# Print a dot for this line
echo -n '.'
for search_line in "${@:3}"; do
local regex="${LOG_REGEX} ${search_line}"
# and matches the regular expression
if [[ "$line" =~ $regex ]]; then
# Return if this is the line being looked for
return 0
fi
done
fi
done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"")
}
# Sends as string to a server for execution
# $1: The ID of the server
# $2: The line of text to enter into the server console
server_eval() {
server_property "$1" USERNAME
server_property "$1" SCREEN_NAME
as_user "${SERVER_USERNAME[$1]}" "screen -p 0 -S ${SERVER_SCREEN_NAME[$1]} -X eval 'stuff \"$2\"\015'"
}
# The same as server_eval, but also waits for a log entry before returning
# $1: The ID of the server
# $2: A line of text to enter into the server console
# $3->: The line or lines of text in the log to wait for
# RETURN: The full entry found in the logs
server_eval_and_get_line() {
unset RETURN
time_now="$(now)"
server_eval "$1" "$2"
server_log_get_line "$1" "$time_now" "${@:3}"
RETURN="$RETURN"
}
# The same as server_eval_and_get_line, but does not set RETURN
server_eval_and_wait() {
server_eval_and_get_line "$@"
unset RETURN # Do not return anything
}
# Gets the process ID for a server if running, otherwise it outputs nothing
# $1: The ID of the server
server_pid() {
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" | awk '{print $1}'
}
# Waits for a server to stop by polling 10 times a second
# This approach is 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() {
server_property "$1" USERNAME
server_property "$1" FLAG_ACTIVE_PATH
case "$2" in
active)
as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
SERVER_ACTIVE[$1]="true"
;;
inactive)
as_user "${SERVER_USERNAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
SERVER_ACTIVE[$1]="false"
;;
*)
error_exit INVALID_ARGUMENT "Invalid argument."
;;
esac
}
### Jar Group Functions
### -------------------
# Lists the jar files grouped by jar groups.
jargroup_list() {
manager_property JAR_STORAGE_PATH
if [[ -d "${SETTINGS_JAR_STORAGE_PATH}" ]]; then
local jargroup_name
local jar_name
while IFS= read -r -d $'\0' jargroup_path; do
jargroup_name="$(basename "${jargroup_path}")"
echo "$jargroup_name"
while IFS= read -r -d $'\0' jar_path; do
jar_name="$(basename "${jar_path}")"
if [[ "$jar_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then
echo " $jar_name"
fi
done < <(find "${SETTINGS_JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0)
done < <(find "${SETTINGS_JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
}
# Creates a new jargroup
# $1: The name for the jargroup
jargroup_create() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
manager_property JARGROUP_TARGET
if [[ ! -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
printf "Creating jar group... "
local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p \"$SETTINGS_JAR_STORAGE_PATH/$1\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
error="$(as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET\"")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
echo "Done."
else
error_exit DUPLICATE_NAME "A jar group with that name already exists."
fi
fi
}
# Downloads the latest version for a jargroup, using the target URL for that
# group. Saves the download with the date and time encoded in the start of the
# file name, in the jar group directory in question. Removes the file if there
# is no difference between it and the current version.
# $1: The jargroup name to download the latest version for
jargroup_getlatest() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property JARGROUP_TARGET
manager_property USERNAME
manager_property JARGROUP_DOWNLOAD_DIR
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
if [[ -f "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET" ]]; then
printf "Downloading latest version... "
# Try and make
local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'")"
if [[ "$error" != "" ]]; then
echo "Failed."
error_exit FILE_NOT_FOUND "$error"
fi
as_user "$SETTINGS_USERNAME" "wget --quiet --trust-server-names --no-check-certificate --input-file='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET' --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
echo "Done."
local num_files="$(as_user "$SETTINGS_USERNAME" "ls -1 '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' | wc -l")"
if [[ "$num_files" == 1 ]]; then
# There was 1 file downloaded
local file_name="$(ls -1 "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR")"
local new_name="$(date +%F-%H-%M-%S)-$file_name"
get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$1"
local most_recent_jar="$RETURN"
if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then
# There is not a previous version to do a comparison against, or
# The previous version is different:
# Add it to the group
[[ -f "$most_recent_jar" ]]
local was_previous="$?"
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name' '$SETTINGS_JAR_STORAGE_PATH/$1/$new_name'"
if [[ ! -z "$most_recent_jar" ]]; then
echo "Downloaded version was different to previous latest. Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
else
echo "Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
fi
else
echo "Existing version \"$SETTINGS_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 "$SETTINGS_USERNAME" "rm -fr '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
else
error_exit FILE_NOT_FOUND "Target URL not found, use $0 jargroup seturl <download-url>"
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
# Deletes an existing jargroup
# $1: The name of the existing jargroup
jargroup_delete() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
printf "Are you sure you want to delete this jar group [y/N]: "
read answer
if [[ "$answer" =~ ^y|Y|yes$ ]]; then
as_user "$SETTINGS_USERNAME" "rm -rf \"$SETTINGS_JAR_STORAGE_PATH/$1\""
echo "Jar group deleted."
else
echo "Jar group was NOT deleted."
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
# Renames an existing jargroup
# $1: The name of the existing jargroup
# $2: The new name
jargroup_rename() {
if is_valid_name "$1"; then
manager_property JAR_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
# If the jar group name is valid,
# and there is no other jar group with the name $1
if is_valid_name "$2"; then
if [[ -e "$SETTINGS_JAR_STORAGE_PATH/$2" ]]; then
error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"."
else
# TODO: Update any symbolic links which point to a jar in this directory
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1' '$SETTINGS_JAR_STORAGE_PATH/$2'"
echo "Renamed jar group \"$1\" to \"$2\"."
fi
fi
else
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
fi
fi
}
### Server Functions
### ----------------
# Echos a list of servers in the SETTINGS_SERVER_STORAGE_PATH
server_list() {
if [ "$NUM_SERVERS" -gt 0 ]; then
for ((server=0; server<$NUM_SERVERS; server++)); do
server_property "$server" ACTIVE
if "${SERVER_ACTIVE[$server]}"; then
echo -n "[ ACTIVE ] "
else
echo -n "[INACTIVE] "
fi
echo -n "\"${SERVER_NAME[$server]}\" "
if "${SERVER_ACTIVE[$server]}"; then
if server_is_running "$server"; then
echo "is running. Everything is OK."
else
echo "is stopped. Server is down!"
fi
else
if server_is_running "$server"; then
echo "is running. It should not be running!"
else
echo "is stopped. Everything is OK."
fi
fi
done
else
echo "[There are no servers]"
fi
}
# Creates a new server
# $1: The server name to create
server_create() {
if is_valid_name "$1"; then
manager_property USERNAME
manager_property SERVER_STORAGE_PATH
manager_property DEFAULT_WHITELIST_PATH
manager_property DEFAULT_BANNED_IPS_PATH
manager_property DEFAULT_BANNED_PLAYERS_PATH
manager_property DEFAULT_OPS_PATH
manager_property SERVER_PROPERTIES
manager_property DEFAULT_WORLD_STORAGE_PATH
manager_property JAR_STORAGE_PATH
if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
error_exit DUPLICATE_NAME "A server with that name already exists."
else
printf "Creating server directory... "
as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WHITELIST_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_IPS_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_PLAYERS_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'"
as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES'"
as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH'"
as_user "$USERNAME" "echo \"MSM requires all your worlds be moved into this directory.\" > '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH/readme.txt'"
echo "Done."
# Creates a server stub in memory, enough to use server_properties for.
SERVER_NAME[$NUM_SERVERS]="$1"
SERVER_PATH[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1"
SERVER_CONF[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES"
NUM_SERVERS=$(($NUM_SERVERS+1))
# TODO: Dirty all server varibales, or don't allow further in script access
# TODO: Handle server default setup stuff better than just using
# the "minecraft" jar group. And make it configurable.
if [ -d "$SETTINGS_JAR_STORAGE_PATH/minecraft" ]; then
server_get_id "$1"
server_set_jar "$RETURN" "minecraft"
fi
fi
fi
}
# Deletes an existing server
# $1: The server name to delete
server_delete() {
if is_valid_name "$1"; then
manager_property SERVER_STORAGE_PATH
manager_property USERNAME
if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
printf "Are you sure you want to delete 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 "$SETTINGS_USERNAME" "rm -rf '$SETTINGS_SERVER_STORAGE_PATH/$1'"
echo "Server deleted."
else
echo "Server was NOT deleted."
fi
else
error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
fi
fi
}
# Renames an existing server
# $1: The server name to change
# $2: The new name for the server
server_rename() {
if is_valid_name "$1"; then
manager_property SERVER_STORAGE_PATH
manager_property USERNAME
if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]; then
# If the server name is valid and exists
server_get_id "$1"
local existing_id="$RETURN"
if server_is_running "$existing_id"; then
error_exit SERVER_RUNNING "Can only rename a stopped server."
else
if is_valid_name "$2"; then
# If the server name is valid
if [[ -e "$SETTINGS_SERVER_STORAGE_PATH/$2" ]]; then
# and there is not already a server with the name $2
error_exit DUPLICATE_NAME "Could not be renamed, there is already a server with the name \"$2\"."
else
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_SERVER_STORAGE_PATH/$1' '$SETTINGS_SERVER_STORAGE_PATH/$2'"
echo "Renamed server \"$1\" to \"$2\"."
fi
fi
fi
else
error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
fi
fi
}
# Starts a single server
# $1: The ID of the server
server_start() {
server_property "$1" USERNAME
server_property "$1" SCREEN_NAME
server_property "$1" INVOCATION
server_property "$1" CONFIRM_START
if server_is_running "$1"; then
echo "Server \"${SERVER_NAME[$1]}\" is already running!"
else
server_ensure_jar "$1"
server_ensure_links "$1"
server_worlds_to_ram "$1"
local time_now="$(now)"
printf "Starting server..."
as_user "${SERVER_USERNAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}"
server_log_dots_for_lines "$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
server_property "$1" CONFIRM_SAVE_ALL
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
server_property "$1" CONFIRM_SAVE_OFF
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
server_property "$1" CONFIRM_SAVE_ON
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() {
server_property "$1" MESSAGE_STOP
server_property "$1" STOP_DELAY
if server_is_running "$1"; then
# Change the state of the script
STOP_COUNTDOWN[$1]="true"
server_eval "$1" "say ${SERVER_MESSAGE_STOP[$1]}"
echo "Issued the warning \"${SERVER_MESSAGE_STOP[$1]}\" to players."
echo -n "Shutting down... "
for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do
tput sc # Save cursor position
echo -n "in $i seconds."
sleep 1
tput rc # Restore cursor to position of last `sc'
tput el # Clear to end of line
done
echo -e "Now."
server_stop_now "$1"
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Stops a single server right now
# $1: The ID of the server
server_stop_now() {
if server_is_running "$1"; then
server_save_all "$1"
echo -n "Stopping the server... "
server_eval "$1" "stop"
STOP_COUNTDOWN[$1]="false"
RESTART_COUNTDOWN[$1]="false"
server_wait_for_stop "$1"
echo "Done."
# Synchronise all worlds in RAM to disk
server_worlds_to_disk "$1"
else
echo "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Restarts a single server after a delay
# $1: The ID of the server
server_restart() {
server_property "$1" MESSAGE_RESTART
server_property "$1" RESTART_DELAY
# Restarts the server if it is already running
if server_is_running "$1"; then
# Change the state of the script
RESTART_COUNTDOWN[$1]="true"
server_eval "$1" "say ${SERVER_MESSAGE_RESTART[$1]}"
echo "Issued the warning \"${SERVER_MESSAGE_RESTART[$1]}\" to players."
echo -n "Restarting... "
for ((i="${SERVER_RESTART_DELAY[$1]}"; i>0; i--)); do
tput sc # Save cursor position
echo -n "in $i seconds."
sleep 1
tput rc # Restore cursor to position of last `sc'
tput el # Clear to end of line
done
echo -e "Now."
server_stop_now "$1"
fi
server_start "$1"
}
# Restarts a single server right away
# $1: The ID of the server
server_restart_now() {
# Restarts the server if it is already running
if server_is_running "$1"; then
server_stop_now "$1"
fi
server_start "$1"
}
# List the worlds available for a server
# $1: The ID of the server
server_worlds_list() {
if [[ "${SERVER_NUM_WORLDS[$1]}" -eq 0 ]]; then
echo "There are no worlds in world storage."
return 0
fi
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$i; i<$max; i++)); do
world_property "$i" INRAM
if "${WORLD_INRAM[$i]}"; then
echo "RAM ${WORLD_NAME[$i]}"
else
echo " ${WORLD_NAME[$i]}"
fi
done
}
# Backs up the worlds for a server
# $1: The ID of the server
server_worlds_backup() {
local i="${SERVER_WORLD_OFFSET[$1]}"
local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"
# For each of the servers worlds:
for ((i=$i; i<$max; i++)); do
world_property "$i" STATUS
if [[ "${WORLD_STATUS[$i]}" == "active" ]]; then
world_backup "$i"
fi
done
}
# Moves a servers log into another file, leaving the original log file empty
# $1: The ID of the server
server_log_roll() {
server_property "$1" LOG_PATH
server_property "$1" USERNAME
server_property "$1" LOG_ARCHIVE_PATH
# Moves and Gzips the logfile, a big log file slows down the
# server A LOT
# Creates the server log if not already present. Prevents errors.
as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_LOG_PATH[$1]}\""
local log_lines="$(cat "${SERVER_LOG_PATH[$1]}" | wc -l )"
if [ "$log_lines" -le '1' ]; then
echo "No new log enteries to roll. No change made."
return 0
fi
echo -n "Rolling server logs... "
if [ -e "${SERVER_LOG_PATH[$1]}" ]; then
file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log"
as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_LOG_ARCHIVE_PATH[$1]}\" && cp \"${SERVER_LOG_PATH[$1]}\" \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\" && gzip \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\""
if [ -e "${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}.gz" ]; then
as_user "${SERVER_USERNAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG_PATH[$1]}\""
as_user "${SERVER_USERNAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_ARCHIVE_PATH[$1]}\\\"\" > \"${SERVER_LOG_PATH[$1]}\""
else
echo "Failed."
error_exit LOGS_NOT_ROLLED "Logs were not rolled."
fi
fi
echo "Done."
}
# Backups a server's directory
# $1: The ID of the server
server_backup() {
manager_property SERVER_STORAGE_PATH
server_property "$1" COMPLETE_BACKUP_FOLLOW_SYMLINKS
server_property "$1" BACKUP_PATH
echo -n "Backing up the entire server directory... "
zip_flags="-rq"
# Add the "y" flag if symbolic links should not be followed
if [ "${SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]}" != "true" ]; then
zip_flags="${zip_flags}y"
fi
# Zip up the server directory
file_name="${SERVER_BACKUP_PATH[$1]}/$(date "+%F-%H-%M-%S").zip"
as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$SETTINGS_SERVER_STORAGE_PATH\" && zip ${zip_flags} \"${file_name}\" \"${SERVER_NAME[$1]}\""
echo "Done."
}
# Sets a server's jar file
# $1: The ID of the server
# $2: The name of the jar group
# $3: Optionally, a specific jar to use.
server_set_jar() {
manager_property JAR_STORAGE_PATH
server_property "$1" JAR_PATH
server_property "$1" USERNAME
if [ -d "$SETTINGS_JAR_STORAGE_PATH/$2" ]; then
if [ -z "$3" ]; then
# If a specific jar file is not mentioned
# Download the latest version
jargroup_getlatest "$2"
get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$2"
local jar="$RETURN"
else
# If a specific jar IS mentioned use that
local jar="$SETTINGS_JAR_STORAGE_PATH/$2/$3"
if [[ ! -e "$jar" ]]; then
error_exit NAME_NOT_FOUND "There is no jar named \"$3\" in jargroup \"$2\"."
fi
fi
if [[ ! -z "$jar" ]]; then
as_user "${SERVER_USERNAME[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR_PATH[$1]}\""
echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"."
fi
else
error_exit NAME_NOT_FOUND "There is no 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
server_eval_and_get_line "$1" "list" "Connected players:"
local line="$RETURN"
# Cuts the start off the line
local players="${line:46}"
# TODO: Use a reliable method of detecting when no players are online,
# and display a different message.
# This is currently hard as invisible characters make string length
# always non-zero, and also may be ommitted.
echo "$players"
else
echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected."
fi
}
# Sets the valud of a server property
# $1: The ID of the server
# $2: The name of the server property
# $3: The value for the property
server_set_property() {
eval SERVER_$2[$1]=\"$3\"
}
# Get the value of a server property
# $1: The ID of the server
# $2: The name of the server property
server_property() {
eval local value=\"\${SERVER_$2[$1]}\"
if [ -z "$value" ]; then
# If the value is empty it has not been loaded yet
# These properties are not overridable
case "$2" in
NAME|PATH)
# Defined at allocation
return 0
;;
CONF)
manager_property SERVER_PROPERTIES
server_set_property "$1" "$2" "${SERVER_PATH[$1]}/$SETTINGS_SERVER_PROPERTIES"
return 0
;;
BACKUP_PATH)
manager_property BACKUP_ARCHIVE_PATH
server_set_property "$1" "$2" "$SETTINGS_BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}"
return 0
;;
LOG_ARCHIVE_PATH)
manager_property LOG_ARCHIVE_PATH
server_set_property "$1" "$2" "$SETTINGS_LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}"
return 0
;;
ACTIVE)
server_property "$1" FLAG_ACTIVE_PATH
if [[ -e "${SERVER_FLAG_ACTIVE_PATH[$1]}" ]]; then
server_set_property "$1" "$2" "true"
else
server_set_property "$1" "$2" "false"
fi
return 0
;;
esac
# If not a non-overridable load from conf
to_properties_name "$2"
local name="$RETURN"
if [[ "$name" =~ ^properties\-(.*)$ ]]; then
name="${BASH_REMATCH[1]}"
else
name="msm-$name"
fi
server_property "$1" CONF
local from_conf="$(sed -rn "s/^$name=('|\"|)(.*)\1/\2/ip" "${SERVER_CONF[$1]}" | tail -n 1)"
if [ ! -z "$from_conf" ]; then
# If the value is found in the server conf file (server.properties)
# then set that as value for the property
eval SERVER_$2[$1]=\"$from_conf\"
else
# Otherwise use the default value
manager_property "DEFAULT_$2"
eval SERVER_$2[$1]=\"\$SETTINGS_DEFAULT_$2\"
fi
### Post-changes to varibales after loading
# If it is a path
if [[ "$2" =~ _PATH$ ]]; then
server_property "$1" PATH
eval SERVER_$2[$1]=\"${SERVER_PATH[$1]}/\${SERVER_$2[$1]}\"
fi
case "$2" in
SCREEN_NAME)
server_set_property "$1" "$2" "${SERVER_SCREEN_NAME[$1]//\{SERVER_NAME\}/${SERVER_NAME[$1]}}"
;;
MESSAGE_STOP)
server_property "$1" STOP_DELAY
server_set_property "$1" "$2" "${SERVER_MESSAGE_STOP[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}"
;;
MESSAGE_RESTART)
server_property "$1" RESTART_DELAY
server_set_property "$1" "$2" "${SERVER_MESSAGE_RESTART[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}"
;;
INVOCATION)
server_property "$1" RAM
server_property "$1" JAR_PATH
server_set_property "$1" "$2" "${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}"
server_set_property "$1" "$2" "${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR_PATH[$1]}}"
;;
esac
fi
}
# $1: The server ID
server_dirty_properties() {
local index
# Removes properties for all servers if an index
# is not specified
if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
index="[$1]"
else
index=""
fi
for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
eval unset SERVER_${SERVER_SETTING_NAME[$i]}$index
done
unset SERVER_CONF$index
unset SERVER_BACKUP_PATH$index
unset SERVER_LOG_ARCHIVE_PATH$index
unset SERVER_ACTIVE$index
}
### Manager Functions
### -----------------
# Stops all running servers after a servers specified delay
# $1: String containing "stop" or "restart". Represents whether the stop is
# with a mind to stop the server, or just to restart it. And 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[$server]}" -gt "$max_countdown" ]]; then
max_countdown="${SERVER_STOP_DELAY[$server]}"
fi
# Send a warning message to the server
case "$1" in
stop) server_eval "$server" "say ${SERVER_MESSAGE_STOP[$server]}";;
restart) server_eval "$server" "say ${SERVER_MESSAGE_RESTART[$server]}";;
esac
# Send message to stdout
echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:"
case "$1" in
stop) echo " Issued the warning \"${SERVER_MESSAGE_STOP[$server]}\" to players.";;
restart) echo " Issued the warning \"${SERVER_MESSAGE_RESTART[$server]}\" to players.";;
esac
case "${SERVER_STOP_DELAY[$server]}" in
0) echo " Stopping without delay.";;
1) echo " Stopping after 1 second.";;
*) echo " Stopping after ${SERVER_STOP_DELAY[$server]} seconds.";;
esac
else
echo "Server \"${SERVER_NAME[$server]}\" was NOT running."
was_running[$server]="false"
fi
done
if "$any_running"; then
# Wait for the maximum possible delay, stopping servers
# at the correct times
echo -n "All servers will have been issued the stop command... "
for ((tick="${max_countdown}"; tick>=0; tick--)); do
tput sc # Save cursor position
if [[ "$tick" -le 1 ]]; then
echo -n "in $tick second."
else
echo -n "in $tick seconds."
fi
# Each second check all 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
}
# Get the value of a global manager property
# $1: The name of the property
manager_property() {
local from_conf="$(sed -rn "s/^$1=('|\"|)(.*)\1/\2/ip" "$CONF" | tail -n 1)"
# If this property has not yet been loaded, load it:
eval local loaded=\"\$LOADED_$1\"
if [ ! -z "$loaded" ] && ! "$loaded"; then
if [ ! -z "$from_conf" ]; then
# Override the default value
eval SETTINGS_$1=\"$from_conf\"
fi
# State that this property has now been loaded
eval LOADED_$1=\"true\"
fi
}
manager_dirty_properties() {
for ((i=0; i<$SETTING_COUNT; i++)); do
eval LOADED_${SETTING_NAME[$i]}=\"false\"
done
}
manager_dirty_all() {
manager_dirty_properties
server_dirty_properties
world_dirty_properties
}
### Command Handler Functions
### -------------------------
# Starts all servers
command_start() {
# Required start option, for debian init.d scripts
for ((server=0; server<${NUM_SERVERS}; server++)); do
# 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() {
local version="$VERSION"
if [ "${version:0:1}" -eq 0 ]; then
version="$version Beta"
fi
echo "Minecraft Server Manager $version"
}
# Displays config values used by MSM
command_config() {
for ((i=0; i<$SETTING_COUNT; i++)); do
manager_property "${SETTING_NAME[$i]}"
echo -n "${SETTING_NAME[$i]}=\""
eval echo -n \"\$SETTINGS_${SETTING_NAME[$i]}\"
echo '"'
done
}
# 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 " <server> console Connects to the interactive console. Access may be limited"
echo -e " <server> config [<setting> <value>] Lists server settings, or sets a specific setting."
echo -e
echo -e "--Server Pass Through Commands----------------------------------"
echo -e " <server> wl on|off Enables/disables server whitelist checking"
echo -e " <server> wl add|remove <player> Add/remove a player to/from a server's whitelist"
echo -e " <server> wl list List the players whitelisted for a server"
echo -e " <server> bl player add|remove <player> Ban/pardon a player from/for a server"
echo -e " <server> bl ip add|remove <ip address> Ban/pardon an IP address from/for a server"
echo -e " <server> bl list Lists the banned players and IP address for a server"
echo -e " <server> op add|remove <player> Add/remove operator status for a player on a server"
echo -e " <server> op list Lists the operator players for a server"
echo -e " <server> gm survival|creative <player> Change the game mode for a player on a server"
echo -e " <server> kick <player> Forcibly disconnect a player from a server"
echo -e " <server> say <message> Broadcast a (pink) message to all players on a server"
echo -e " <server> time set|add <number> Set/increment time on a server (0-24000)"
echo -e " <server> toggledownfall Toggles rain and snow on a server"
echo -e " <server> give <player> <item> [amount] [data] Gives an entity to a player"
echo -e " <server> xp <player> <amount> Gives XP to, or takes away (when negative) XP from, a player"
echo -e " <server> save on|off Enable/disable writing world changes to file"
echo -e " <server> save all Force the writing of all non-saved world changes to file"
echo -e " <server> cmd <command> Send a command string to the server and return"
echo -e " <server> cmdlog <command> Same as 'cmd' but shows log output afterwards (Ctrl+C to exit)"
echo -e
echo -e "--Jar Commands--------------------------------------------------"
echo -e " jargroup list List the stored jar files."
echo -e " jargroup create <name> <download-url> Create a new jar group, with a URL for new downloads"
echo -e " jargroup delete <name> Delete a jar group"
echo -e " jargroup rename <name> <new-name> Rename a jar group"
echo -e " jargroup changeurl <name> <download-url> Change the download URL for a jar group"
echo -e " jargroup getlatest <name> Download the latest jar file for a jar group"
echo -e
echo -e "--Global Commands-----------------------------------------------"
echo -e " start Starts all active servers"
echo -e " stop [now] Stops all running servers"
echo -e " restart [now] Restarts all active servers"
echo -e " version Prints the Minecraft Server Manager version installed"
echo -e " config Displays a list of the config values used by MSM"
}
# 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() {
if server_is_running "$1"; then
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
if server_is_running "$1"; then
server_save_on "$1"
fi
}
# Makes a backup of all worlds for an individual server
# $1: The server ID
command_server_worlds_backup() {
if server_is_running "$1"; then
server_eval "$1" "say ${SERVER_MESSAGE_WORLD_BACKUP_STARTED[$1]}"
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
server_worlds_backup "$1"
if server_is_running "$1"; then
server_save_on "$1"
server_eval "$1" "say ${SERVER_MESSAGE_WORLD_BACKUP_FINISHED[$1]}"
fi
echo "Backup took $SECONDS seconds".
}
# Enables a world to be used by its server
# $1: The server ID
# $2: The world ID
command_server_worlds_on() {
world_activate "$2"
}
# Disables a world from being used by its server, also prevents it from being
# backed up with the other worlds.
# $1: The server ID
# $2: The world ID
command_server_worlds_off() {
world_deactivate "$2"
}
# Moves an individual server's log text to another file, leaving it empty
# $1: The server ID
command_server_logroll() {
server_log_roll "$1"
}
# Makes a backup of an entire server directory
# $1: The server ID
command_server_backup() {
if server_is_running "$1"; then
server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_STARTED[$1]}"
server_save_off "$1"
server_save_all "$1"
fi
server_worlds_to_disk "$1"
server_backup "$1"
if server_is_running "$1"; then
server_save_on "$1"
server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_FINISHED[$1]}"
fi
echo "Backup took $SECONDS seconds".
}
# Sets an individual server's jar file to use when starting up
# $1: The server ID
# $2: The jar group name
# $3: Optionally a specific jar file name which exists within that jargroup, if
# not provided the latest version will be used.
command_server_jar() {
server_set_jar "$1" "$2" "$3"
}
# Turns a server's whitelist protection on
# $1: The server ID
command_server_whitelist_on() {
if server_is_running "$1"; then
server_eval "$1" "whitelist on"
else
command_server_config "$1" "white-list" "true"
fi
echo "Whitelist enabled"
}
# 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"
else
command_server_config "$1" "white-list" "false"
fi
echo "Whitelist disabled"
}
# Adds a player name to a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_add() {
# TODO: Support whitelisting multiple players (see blacklist player add)
if server_is_running "$1"; then
# Whitelist players
for player in "${@:2}"; do
server_eval "$1" "whitelist add $player"
done
else
server_property "$1" WHITELIST_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_WHITELIST_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_WHITELIST_PATH[$1]}"
fi
done
fi
# Confirmation display
if [[ $# -gt 2 ]]; then
echo -n "Added the following players to the whitelist: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "Added \"$2\" to the whitelist."
fi
}
# Removes a player name from a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_remove() {
# TODO: Support multiple player names
if server_is_running "$1"; then
for player in "${@:2}"; do
server_eval "$1" "whitelist remove $player"
done
else
server_property "$1" WHITELIST_PATH
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_WHITELIST_PATH[$1]}"
done
fi
# Confirmation display
if [[ $# -gt 2 ]]; then
echo -n "Removed the following players from the whitelist: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "Removed \"$2\" from the whitelist."
fi
}
# Displays a list of whitelisted players for an individual server
# $1: The server ID
command_server_whitelist_list() {
server_property "$1" WHITELIST_PATH
if [ -f "${SERVER_WHITELIST_PATH[$1]}" ]; then
local players="$(cat "${SERVER_WHITELIST_PATH[$1]}")"
if [ -z "$players" ]; then
echo "No players are whitelisted."
else
echo "$players"
fi
else
echo "No players are whitelisted."
fi
}
# Adds player names to a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_add() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_eval "$1" "ban $player"
done
else
server_property "$1" BANNED_PLAYERS_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_BANNED_PLAYERS_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_BANNED_PLAYERS_PATH[$1]}"
fi
done
fi
if [[ $# -gt 2 ]]; then
echo -n "Added the following players to the blacklist: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "Added \"$2\" to the blacklist."
fi
}
# Removes player names from a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_remove() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_eval "$1" "pardon $player"
done
else
server_property "$1" BANNED_PLAYERS_PATH
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
done
fi
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() {
if server_is_running "$1"; then
for address in "${@:2}"; do
server_eval "$1" "ban-ip $address"
done
else
server_property "$1" BANNED_IPS_PATH
for address in "${@:2}"; do
if ! grep "^$address\$" "${SERVER_BANNED_IPS_PATH[$1]}" >/dev/null; then
echo "$address" >> "${SERVER_BANNED_IPS_PATH[$1]}"
fi
done
fi
if [[ $# -gt 2 ]]; then
echo -n "Added the following ip addresses to the blacklist: "
echo -n "$2"
for address in "${@:3}"; do
echo -n ", $address"
done
echo "."
else
echo "Added \"$2\" to the blacklist."
fi
}
# Removes ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_remove() {
if server_is_running "$1"; then
for address in "${@:2}"; do
server_eval "$1" "pardon-ip $address"
done
else
server_property "$1" BANNED_PLAYERS_PATH
for address in "${@:2}"; do
sed -ri "/^$address\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
done
fi
if [[ $# -gt 2 ]]; then
echo -n "Removed the following players from the blacklist: "
echo -n "$2"
for address in "${@:3}"; do
echo -n ", $address"
done
echo "."
else
echo "Removed \"$2\" from the blacklist."
fi
}
# Displays a server's banned player names and ip addresses
# $1: The server ID
command_server_blacklist_list() {
server_property "$1" BANNED_PLAYERS_PATH
server_property "$1" BANNED_IPS_PATH
local players
local ips
if [ -f "${SERVER_BANNED_PLAYERS_PATH[$1]}" ]; then
players="$(cat "${SERVER_BANNED_PLAYERS_PATH[$1]}")"
fi
if [ -f "${SERVER_BANNED_IPS_PATH[$1]}" ]; then
ips="$(cat "${SERVER_BANNED_IPS_PATH[$1]}")"
fi
if [[ -z "$players" && -z "$ips" ]]; then
echo "The blacklist is empty."
else
if [[ ! -z "$players" ]]; then
echo "Players:"
for name in $players; do
echo " $name"
done
fi
if [[ ! -z "$ips" ]]; then
echo "IP Addresses:"
for address in $ips; do
echo " $address"
done
fi
fi
}
# Adds a player name to a server's list of operators
# $1: The server ID
# $2->: The player name
command_server_operator_add() {
if server_is_running "$1"; then
for player in "${@:2}"; do
server_eval "$1" "op $player"
done
else
server_property "$1" OPS_PATH
for player in "${@:2}"; do
if ! grep "^$player\$" "${SERVER_OPS_PATH[$1]}" >/dev/null; then
echo "$player" >> "${SERVER_OPS_PATH[$1]}"
fi
done
fi
if [[ $# -gt 2 ]]; then
echo -n "The following players are now operators: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "\"$2\" is now an operator."
fi
}
# Removes a player name to a server's list of operators
# $1: The server ID
# $2: The player name
command_server_operator_remove() {
# TODO: Support multiple player names
if server_is_running "$1"; then
for player in "${@:2}"; do
server_eval "$1" "deop $player"
done
else
server_property "$1" OPS_PATH
for player in "${@:2}"; do
for player in "${@:2}"; do
sed -ri "/^$player\$/d" "${SERVER_OPS_PATH[$1]}"
done
done
fi
if [[ $# -gt 2 ]]; then
echo -n "The following players are no longer operators: "
echo -n "$2"
for player in "${@:3}"; do
echo -n ", $player"
done
echo "."
else
echo "\"$2\" is no longer an operator."
fi
}
# Displays a list of operators for an individual server
# $1: The server ID
command_server_operator_list() {
server_property "$1" OPS_PATH
if [ -f "${SERVER_OPS_PATH[$1]}" ]; then
local players="$(cat "${SERVER_OPS_PATH[$1]}")"
if [ ! -z "$players" ]; then
echo "$players"
return 0
fi
fi
echo "No players are operators."
}
# Sets the game mode for
# $1: The server ID
# $2: The game mode
# $3->: The player name
command_server_gamemode() {
if server_is_running "$1"; then
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
server_property "$1" CONFIRM_GAMEMODE
server_property "$1" CONFIRM_GAMEMODE_FAIL_NO_USER
server_property "$1" CONFIRM_GAMEMODE_FAIL_NO_CHANGE
for player in "${@:3}"; do
server_eval_and_get_line "$1" "gamemode $player $mode" "${SERVER_CONFIRM_GAMEMODE[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}"
line="$RETURN"
regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Changed game mode of \"$player\" to \"$2\"."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$player\" 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 \"$player\" was already in mode \"$2\"."
fi
done
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Kicks a connected player from a server
# $1: The server ID
# $2->: The player name
command_server_kick() {
if server_is_running "$1"; then
local line regex
server_property "$1" CONFIRM_KICK
server_property "$1" CONFIRM_KICK_FAIL
for player in "${@:2}"; do
server_eval_and_get_line "$1" "kick $player" "${SERVER_CONFIRM_KICK[$1]}" "${SERVER_CONFIRM_KICK_FAIL[$1]}"
line="$RETURN"
regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "Kicked \"$player\" from game."
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK_FAIL[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "The player \"$player\" is not connected."
fi
done
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Broadcasts a message to all connected players for a server
# $1: The server ID
# $2->: Words of the message, will be 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
server_property "$1" CONFIRM_TIME_SET
server_property "$1" CONFIRM_TIME_SET_FAIL
server_eval_and_get_line "$1" "time set $2" "${SERVER_CONFIRM_TIME_SET[$1]}" "${SERVER_CONFIRM_TIME_SET_FAIL[$1]}"
line="$RETURN"
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
server_property "$1" CONFIRM_TIME_ADD
server_property "$1" CONFIRM_TIME_ADD_FAIL
server_eval_and_get_line "$1" "time add $2" "${SERVER_CONFIRM_TIME_ADD[$1]}" "${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}"
line="$RETURN"
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
server_property "$1" CONFIRM_TOGGLEDOWNFALL
server_property "$1" CONFIRM_TOGGLEDOWNFALL_FAIL
server_eval_and_get_line "$1" "toggledownfall" "${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" "${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}"
line="$RETURN"
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
}
# Gives entities to players in game
# $1: The server ID
# $2: The player name
# $3: The entity id/name
# $4: The amount to give
# $5: The entity damage value
command_server_give() {
if server_is_running "$1"; then
local line regex
if [[ "$3" =~ ^\-[0-9]+$ ]]; then
error_exit INVALID_ARGUMENT "Item ID \"$3\" must be a positive integer or string."
fi
server_property "$1" CONFIRM_GIVE
server_property "$1" CONFIRM_GIVE_FAIL_NO_USER
server_property "$1" CONFIRM_GIVE_FAIL_NO_ITEM
server_eval_and_get_line "$1" "give $2 $3 $4 $5" "${SERVER_CONFIRM_GIVE[$1]}" "${SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]}"
line="$RETURN"
regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:36}"
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:27}"
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:27}"
fi
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Gives XP to a player in game
# $1: The server ID
# $2: The player name
# $3: The amount of XP to give (can be negative)
command_server_xp() {
if server_is_running "$1"; then
local line regex
server_property "$1" CONFIRM_XP
server_property "$1" CONFIRM_XP_FAIL_NO_USER
server_property "$1" CONFIRM_XP_FAIL_INVALID_AMOUNT
server_eval_and_get_line "$1" "xp $2 $3" "${SERVER_CONFIRM_XP[$1]}" "${SERVER_CONFIRM_XP_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]}"
line="$RETURN"
regex="${LOG_REGEX} ${SERVER_CONFIRM_XP[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:36}"
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_XP_FAIL_NO_USER[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:27}"
fi
regex="${LOG_REGEX} ${SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]}"
if [[ "$line" =~ $regex ]]; then
echo "${line:27}"
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_property "$1" LOG_PATH
server_property "$1" USERNAME
echo "Now watching logs (press Ctrl+C to exit):"
server_eval "$1" "${*:2}"
as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=0 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]}"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Resumes a server's screen session (requires ssh-ed in as server user, using
# the `su` command will not work.)
# $1: The server ID
command_server_console() {
if server_is_running "$1"; then
server_property "$1" USERNAME
as_user "${SERVER_USERNAME[$1]}" "screen -r ${SERVER_SCREEN_NAME[$1]}"
else
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
fi
}
# Sets a parameter in the config file if it exists, otherwise inserts the
# paramerter.
# $1: The server ID
# $2: Optionally, a setting name
# $3: Optionally, a value to set for $2
command_server_config() {
# If both a setting name and value are given
if [ ! -z "$2" ] && [ ! -z "$3" ]; then
server_property "$1" CONF
if [[ -f "${SERVER_CONF[$1]}" ]]; then
if grep "$2" "${SERVER_CONF[$1]}" >/dev/null; then
sed -i /$2=/s/.*/"$2=$3"/g "${SERVER_CONF[$1]}"
else
echo "$2=$3" >> "${SERVER_CONF[$1]}"
fi
if server_is_running "$1"; then
echo "Changes to config may require a server restart to take effect: sudo $0 ${SERVER_NAME[$1]} restart";
fi
fi
return 0
fi
# If only a setting name is given
if [ ! -z "$2" ]; then
# Convert name into upper-case with underscores
# msm-setting => SERVER_SETTING
# setting => SERVER_PROPERTIES_SETTING
if [[ "$2" =~ ^msm\-(.*)$ ]]; then
to_global_name "${BASH_REMATCH[1]}"
else
to_global_name "PROPERTIES_$2"
fi
local name="$RETURN"
# Display the value of that setting
server_property "$1" "$name"
eval echo \"\${SERVER_$name[$1]}\"
fi
# If no paramter name is given
if [ -z "$2" ]; then
# List all parameters
for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
server_property "$1" "${SERVER_SETTING_NAME[$i]}"
to_properties_name "${SERVER_SETTING_NAME[$i]}"
eval echo "msm-$RETURN=\\\"\${SERVER_${SERVER_SETTING_NAME[$i]}[$1]}\\\""
done
fi
}
### Register Functions
### ------------------
# Registers a setting that can be defined in /etc/msm.conf
# $1: Setting name to register
# $2: Optionally a default value for this setting
register_setting() {
# Create the default version of the variable
eval SETTINGS_$1=\"$2\"
# State that the variable has not yet been loaded
eval LOADED_$1=\"false\"
# Keep track of the setting name in a list
SETTING_NAME[$SETTING_COUNT]="$1"
SETTING_COUNT=$(( $SETTING_COUNT + 1 ))
}
# Registers a setting that can be defined for each server
# $1: Server setting name to register
# $2: Optionally a default value
register_server_setting() {
register_setting "DEFAULT_$1" "$2"
SERVER_SETTING_NAME[$SERVER_SETTING_COUNT]="$1"
SERVER_SETTING_COUNT=$(( $SERVER_SETTING_COUNT + 1 ))
}
# Register possible settings
register_settings() {
register_setting DEBUG "false"
register_setting USERNAME "minecraft"
register_setting SERVER_STORAGE_PATH "/opt/msm/servers"
register_setting JAR_STORAGE_PATH "/opt/msm/jars"
register_setting RAMDISK_STORAGE_PATH "/dev/shm/msm"
register_setting WORLD_ARCHIVE_PATH "/opt/msm/archives/worlds"
register_setting LOG_ARCHIVE_PATH "/opt/msm/archives/logs"
register_setting BACKUP_ARCHIVE_PATH "/opt/msm/archives/backups"
register_setting JARGROUP_TARGET "target.txt"
register_setting JARGROUP_DOWNLOAD_DIR "downloads"
register_setting SERVER_PROPERTIES "server.properties"
register_server_setting USERNAME "minecraft"
register_server_setting SCREEN_NAME "msm-{SERVER_NAME}"
register_server_setting WORLD_STORAGE_PATH "worldstorage"
register_server_setting WORLD_STORAGE_INACTIVE_PATH "worldstorage_inactive"
register_server_setting LOG_PATH "server.log"
register_server_setting WHITELIST_PATH "white-list.txt"
register_server_setting BANNED_PLAYERS_PATH "banned-players.txt"
register_server_setting BANNED_IPS_PATH "banned-ips.txt"
register_server_setting OPS_PATH "ops.txt"
register_server_setting JAR_PATH "server.jar"
register_server_setting FLAG_ACTIVE_PATH "active"
register_server_setting COMPLETE_BACKUP_FOLLOW_SYMLINKS "false"
register_server_setting WORLDS_FLAG_INRAM "inram"
register_server_setting RAM "1024"
register_server_setting INVOCATION "java -Xms{RAM}M -Xmx{RAM}M -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -jar {JAR} nogui"
register_server_setting STOP_DELAY "10"
register_server_setting RESTART_DELAY "10"
register_server_setting MESSAGE_STOP "SERVER SHUTTING DOWN IN {DELAY} SECONDS!"
register_server_setting MESSAGE_STOP_ABORT "Server shut down aborted."
register_server_setting MESSAGE_RESTART "SERVER REBOOT IN {DELAY} SECONDS!"
register_server_setting MESSAGE_RESTART_ABORT "Server reboot aborted."
register_server_setting MESSAGE_WORLD_BACKUP_STARTED "Backing up world."
register_server_setting MESSAGE_WORLD_BACKUP_FINISHED "Backup complete."
register_server_setting MESSAGE_COMPLETE_BACKUP_STARTED "Backing up entire server."
register_server_setting MESSAGE_COMPLETE_BACKUP_FINISHED "Backup complete."
register_server_setting CONFIRM_SAVE_ON "CONSOLE: Enabling level saving.."
register_server_setting CONFIRM_SAVE_OFF "CONSOLE: Disabling level saving.."
register_server_setting CONFIRM_SAVE_ALL "CONSOLE: Save complete."
register_server_setting CONFIRM_START "Done"
register_server_setting CONFIRM_KICK "CONSOLE: Kicking "
register_server_setting CONFIRM_KICK_FAIL "Can't find user "
register_server_setting CONFIRM_TIME_SET "CONSOLE: Set time to"
register_server_setting CONFIRM_TIME_SET_FAIL "Unable to convert time value"
register_server_setting CONFIRM_TIME_ADD "CONSOLE: Added .+ to time"
register_server_setting CONFIRM_TIME_ADD_FAIL "Unable to convert time value"
register_server_setting CONFIRM_TOGGLEDOWNFALL "CONSOLE: Toggling downfall on|off for world"
register_server_setting CONFIRM_TOGGLEDOWNFALL_FAIL ".\[31m;1mNo world exists with the name"
register_server_setting CONFIRM_GAMEMODE "CONSOLE: Setting .+ to game mode (1|0)"
register_server_setting CONFIRM_GAMEMODE_FAIL_NO_USER "Can't find user .+"
register_server_setting CONFIRM_GAMEMODE_FAIL_NO_CHANGE ".+ already has game mode (1|0)"
register_server_setting CONFIRM_GIVE "CONSOLE: Giving .+ some .+ (.+)"
register_server_setting CONFIRM_GIVE_FAIL_NO_USER "Can't find user .+"
register_server_setting CONFIRM_GIVE_FAIL_NO_ITEM "There's no item called .+"
register_server_setting CONFIRM_XP "CONSOLE: Giving .+ exp to .+"
register_server_setting CONFIRM_XP_FAIL_NO_USER "Can't find user .+"
register_server_setting CONFIRM_XP_FAIL_INVALID_AMOUNT "Invalid exp count: .+"
}
# 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
if [[ "$word" == "<strings>" ]]; then
regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* "
else
regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") "
fi
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() {
manager_property SERVER_STORAGE_PATH
local args
local space="\ "
for arg in "$@"; do
if [[ "$arg" =~ $space ]]; then
args="$args\"$arg\" "
else
args="$args$arg "
fi
done
if [ ${#args} -ge 1 ]; then
args="${args:0:${#args}-1}"
fi
for ((command=0; command<$COMMAND_COUNT; command++)); do
if [[ "$args" =~ ${COMMAND_REGEX[$command]} ]]; then
unset args
local word_offset=1
local args
local arg_offset=0
local sid=-1
local wid=-1
# Helper function to build the argument list
# $1: The argument to push onto the list
push_arg() {
args[$arg_offset]="$1"
arg_offset="$(( $arg_offset + 1 ))"
}
# The following loop builds a set of arguments to pass to the
# matched command handler function. Rather than passing all args
# given to the script, to the handler (which may contain constant
# strings), it only includes variables.
for word in ${COMMAND_SIGNATURE[$command]}; do
# Whether a positional argument is a 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 "$SETTINGS_SERVER_STORAGE_PATH/$specified_name" ]; then
server_get_id "$specified_name"
sid="$RETURN"
fi
fi
if [[ "$sid" -eq "-1" ]]; then
error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"."
fi
fi
push_arg "$sid"
;;
# The "<name:world>" token also improves upon "<name>" by
# ensuring that the world actually exists, and passes the
# argument on to command handlers as the world ID, rather
# than the original world name input by the user.
"<name:world>")
local specified_name="${!word_offset}"
if [[ "$sid" -eq "-1" ]]; then
# Server id not set yet
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
server_property "$sid" WORLD_STORAGE_PATH
server_property "$sid" WORLD_STORAGE_INACTIVE_PATH
if [ -d "${SERVER_WORLD_STORAGE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/$specified_name" ]; then
server_world_get_id "$sid" "$specified_name"
wid="$RETURN"
fi
fi
if [[ "$wid" -eq "-1" ]]; then
error_exit NAME_NOT_FOUND "There is no world with the name \"$specified_name\"."
fi
push_arg "$wid"
fi
;;
esac
word_offset=$(( $word_offset + 1 ))
done
# The argument list for the call to the command handler has been
# built. But there are several ways to call a handler. Either just
# once, or multiple times based upon if multiple servers or worlds
# were specified.
# This code block calls the handler for all possible servers and
# all possible worlds.
if [[ "$sid" == "server:all" ]] && [[ "$wid" == "world:all" ]]; then
for ((j=0; j<$NUM_WORLDS; j++)); do
# Replace server and world id 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[$command]} "${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[$command]} "${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[$command]} "${replaced_args[@]}"
done
return
fi
# Otherwise it's a simple single call of the handler.
${COMMAND_HANDLER[$command]} "${args[@]}"
return
fi
done
echo "No such command. See $0 help"
}
# Defines every MSM command.
register_commands() {
# The following section registers commands to be available for use. The
# register_command function accepts a command_signature and a
# command_handler_function_name as positional arguments 1 and 2
# respectively.
#
# A command signature consists of multiple elements separated by spaces,
# the available options are as follows:
#
# fixedstring Matches an argument containing the specified
# characters, in this case the characters "fixedstring"
#
# <string> Same as "fixedstring", but is variable and the value
# is passed to the handler function as a positional
# argument
#
# <strings> Same as "<string>", but matches multiple arguments,
# must be final element
#
# <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 "config" "command_config"
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> <string>" "command_server_jar"
register_command "<name:server> console" "command_server_console"
register_command "<name:server> config" "command_server_config"
register_command "<name:server> config <string>" "command_server_config"
register_command "<name:server> config <string> <string>" "command_server_config"
register_command "<name:server> whitelist|wl on" "command_server_whitelist_on"
register_command "<name:server> whitelist|wl off" "command_server_whitelist_off"
register_command "<name:server> whitelist|wl add <strings>" "command_server_whitelist_add"
register_command "<name:server> whitelist|wl remove <strings>" "command_server_whitelist_remove"
register_command "<name:server> whitelist|wl list" "command_server_whitelist_list"
register_command "<name:server> blacklist|bl player add <strings>" "command_server_blacklist_player_add"
register_command "<name:server> blacklist|bl player remove <strings>" "command_server_blacklist_player_remove"
register_command "<name:server> blacklist|bl ip add <strings>" "command_server_blacklist_ip_add"
register_command "<name:server> blacklist|bl ip remove <strings>" "command_server_blacklist_ip_remove"
register_command "<name:server> blacklist|bl list" "command_server_blacklist_list"
register_command "<name:server> operator|op add <strings>" "command_server_operator_add"
register_command "<name:server> operator|op remove <strings>" "command_server_operator_remove"
register_command "<name:server> operator|op list" "command_server_operator_list"
register_command "<name:server> gamemode|gm <string> <strings>" "command_server_gamemode"
register_command "<name:server> kick <strings>" "command_server_kick"
register_command "<name:server> say <strings>" "command_server_say"
register_command "<name:server> time set <string>" "command_server_time_set"
register_command "<name:server> time add <string>" "command_server_time_add"
register_command "<name:server> toggledownfall|tdf" "command_server_toggledownfall"
register_command "<name:server> give <string> <string>" "command_server_give"
register_command "<name:server> give <string> <string> <string>" "command_server_give"
register_command "<name:server> give <string> <string> <string> <string>" "command_server_give"
register_command "<name:server> xp <string> <string>" "command_server_xp"
register_command "<name:server> save on" "command_server_save_on"
register_command "<name:server> save off" "command_server_save_off"
register_command "<name:server> save all" "command_server_save_all"
register_command "<name:server> cmd <strings>" "command_server_cmd"
register_command "<name:server> cmdlog <strings>" "command_server_cmdlog"
}
# $1: Server path
server_allocate() {
unset RETURN
# Get an ID for this new server
local server_id="$NUM_SERVERS"
# Store the path for this new server
SERVER_PATH[$server_id]="$1"
# Store the name for this server
quick_basename "${SERVER_PATH[$server_id]}"
SERVER_NAME[$server_id]="$RETURN"
NUM_SERVERS=$(( $NUM_SERVERS + 1 ))
RETURN="$server_id"
}
# $1: Server ID
server_worlds_allocate() {
local world_id
# A server's worlds require contiguous ID's
# thus they are loaded one after another all at once.
# $1: Server ID
# $2: World path
world_allocate() {
# Get an ID for this new world
world_id="$NUM_WORLDS"
# Store the path for this new world
WORLD_PATH[$world_id]="$2"
# Store the name for this world
quick_basename "${WORLD_PATH[$world_id]}"
WORLD_NAME[$world_id]="$RETURN"
# Store the server ID this world belongs to
WORLD_SERVER_ID[$world_id]="$1"
NUM_WORLDS=$(( $NUM_WORLDS + 1 ))
}
server_property "$1" WORLD_STORAGE_PATH
server_property "$1" WORLD_STORAGE_INACTIVE_PATH
local world_name
# Record the index at which worlds for this server will start
SERVER_WORLD_OFFSET[$1]="$NUM_WORLDS"
if [[ -d "${SERVER_WORLD_STORAGE_PATH[$1]}" ]]; then
while IFS= read -r -d $'\0' path; do
world_allocate "$1" "$path"
done < <(find "${SERVER_WORLD_STORAGE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" ]]; then
while IFS= read -r -d $'\0' path; do
world_allocate "$1" "$path"
done < <(find "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
fi
# Record the number fo worlds this server has
SERVER_NUM_WORLDS[$1]="$(( $NUM_WORLDS - ${SERVER_WORLD_OFFSET[$1]} ))"
}
# Allocates stub varibales, in this context a stub is
# enough data to be able to load in more data via
# the *_property functions.
allocate() {
manager_property SERVER_STORAGE_PATH
# Dermine server names (but don't load them)
if [ -d "$SETTINGS_SERVER_STORAGE_PATH" ]; then
while IFS= read -r -d $'\0' path; do
server_allocate "$path"
server_worlds_allocate "$RETURN"
done < <(find "$SETTINGS_SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0)
fi
}
# Called if the script is interrupted before exiting naturally
interrupt() {
local exit_message="false"
for ((i=0; $i<$NUM_SERVERS; i++)); do
if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
if [[ "$exit_message" == "false" ]]; then
echo -e "\nInterrupted..."
exit_message="true"
fi
server_eval "$i" "say ${SERVER_MESSAGE_STOP_ABORT[$i]}"
echo "Server \"${SERVER_NAME[$i]}\" shutdown was aborted."
fi
if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
if [[ "$exit_message" == "false" ]]; then
echo -e "\nInterrupted..."
exit_message="true"
fi
server_eval "$i" "say ${SERVER_MESSAGE_RESTART_ABORT[$i]}"
echo "Server \"${SERVER_NAME[$i]}\" restart was aborted."
fi
done
exit
}
# The main function which starts the script
main() {
register_settings
register_commands
allocate
# Trap interrupts to the script by calling the interrupt function
trap interrupt EXIT
# This function call matches the user input to a registered command
# signature, and then calls that command's handler function with positional
# arguments containing any "variable" strings.
call_command "$@"
}
### Start point
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
# MSM was called from the command line
main "$@"
exit 0
else
# MSM was sourced from another script.
# Just register settings instead.
register_settings
allocate
fi