#!/bin/bash ### BEGIN INIT INFO # Provides: msm # Required-Start: $local_fs $remote_fs # Required-Stop: $local_fs $remote_fs # Should-Start: $network # Should-Stop: $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: # Description: ### END INIT INFO # See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for # more information on debain init.d scripts, which may help you understand # this script. ### The configuration file # Get the MSM_CONF environment variable or use the default location CONF="${MSM_CONF:-/etc/msm.conf}" ### The Minecraft Server Manager version, use "msm version" to check yours. VERSION="0.3.4" ### Config variables the user should not need/want to change # Jar group file which contains the download target URL declare -r JARGROUP_TARGET="target.txt" # Jar group directory name to download new jars to, is deleted afterwards declare -r JARGROUP_DOWNLOAD_DIR="downloads" # The server configuration file name declare -r SERVER_CONF_NAME="server.properties" # The start of a regex to find a log line declare -r LOG_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\]" ### Script State Variables # "true" whilst the script is counting down a delay to stop the server declare STOP_COUNTDOWN # "true" whilst the script is counting down a delay to restart the server declare RESTART_COUNTDOWN ### Utility Functions # Executes the command "$2" as user "$1" # $1: The user to execute the command as # $2: The command to execute as_user() { local user="$(whoami)" if [ "$user" == "$1" ]; then bash -c "$2" else if [ "$user" == "root" ]; then su - "$1" -s /bin/bash -c "$2" else error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"." fi fi } # Executes the command "$1" as SERVER_USER but returns stderr instead as_user_stderr() { as_user "$@" > /dev/null 2>&1 } # Echo to stderr echoerr() { echo "$@" 1>&2 } # Exit's the script error_exit() { case "$1" in INVALID_USER) code=64;; INVALID_COMMAND) code=65;; INVALID_ARGUMENT) code=66;; SERVER_STOPPED) code=67;; SERVER_RUNNING) code=68;; NAME_NOT_FOUND) code=69;; FILE_NOT_FOUND) code=70;; DUPLICATE_NAME) code=71;; LOGS_NOT_ROLLED) code=72;; CONF_ERROR) code=73;; FATAL_ERROR) code=74;; esac echo "${2:-"Unknown Error"}" 1>&2 exit "${code:-$1}" } # A function used to print debug messages to stdout. Prevents messages from # appearing unless in debug mode, and allows debug statements to be easily # distinguished from necessary echo statements. # $1: The message to output debug() { if [[ "$DEBUG" == "true" ]]; then echoerr "$1" fi } # Determines whether "$1" is a valid name for a server or jar group directory # It must only contain upper or lower case letters, digits, dashes or # underscores. # It must also not be one of a list of reserved names. # $1: The name to check is_valid_name() { local valid="^[a-zA-Z0-9\_\-]+$" local invalid="^(start|stop|restart|version|server|jargroup|all)$" if [[ "$1" =~ $valid ]]; then if [[ "$1" =~ $invalid ]]; then error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\" or \"all\"." else return 0 fi else error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and unscores." fi } # Gets the latest jar from a jar group, based upon the date and time encoded # in the file name. # $1: The directory to search get_latest_file() { local best_time=0 local best_file="" while IFS= read -r -d $'\0' file; do # Remove the path, leaving just the file name local date_time="$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}')" # Get the time in seconds since 1970 from file name local seconds="$(date -d "$date_time" "+%s" 2> /dev/null)" # If that is newer than the current best, override variables if [[ "$seconds" -gt "$best_time" ]]; then best_time="$seconds" best_file="$file" fi done < <(find "$1" -maxdepth 1 -type f -print0) echo "$best_file" } # Returns the current time as a UNIX timestamp (in seconds since 1970) now() { date +%s } ### Log Utility Functions # Gets the UNIX timestamp for a server log line # $1: A server log line # returns: Time in seconds since 1970-01-01 00:00:00 UTC log_line_get_time() { time_string="$(echo "$1" | awk '{print $1 " " $2}')" date -d "$time_string" "+%s" 2> /dev/null } ### World Utility Functions # Moves a world to RAM # $1: the ID of the world to move world_to_ram() { if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"${WORLD_RAMDISK_PATH[$1]}\" && rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_PATH[$1]}/\" \"${WORLD_RAMDISK_PATH[$1]}\"" fi } # Moves a world in RAM to disk # $1: the ID of the world to move world_to_disk() { as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\"" } # Toggles a worlds ramdisk state # $1: The ID of the world world_toggle_ramdisk_state() { if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... " as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\"" echo "Done." echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... " as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\"" echo "Done." else echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... " as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "touch \"${WORLD_FLAG_INRAM[$1]}\"" echo "Done." echo -n "Copying world to RAM... " world_to_ram "$1" echo "Done." fi echo "Changes will only take effect after server is restarted." } # Backs up a world # $1: The ID of the world world_backup() { echo -n "Backing up world \"${WORLD_NAME[$1]}\"... " file_name="$(date "+%F-%H-%M-%S").zip" local server_id="${WORLD_SERVER_ID[$1]}" local containing_dir="$(dirname "${WORLD_PATH[$1]}")" local dir_name="$(basename "${WORLD_PATH[$1]}")" as_user "${SERVER_USER_NAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\"" echo "Done." } # Activates a world # $1: The ID of the world world_activate() { if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... " local new_path="${WORLD_ACTIVE_PATH[$1]}" as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_INACTIVE_PATH[$1]}\" \"$new_path\"" echo "Done." else if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then echo "World \"${WORLD_NAME[$1]}\" is already activate." else error_exit DIR_NOT_FOUND "Directory \"${WORLD_INACTIVE_PATH[$1]}\" could not be found." fi fi } # Deactivates a world # $1: The ID of the world world_deactivate() { if server_is_running "${WORLD_SERVER_ID[$1]}"; then exit_error 68 "Worlds cannot be deactivated whilst the server is running." else if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then echo -n "Moving world \"${WORLD_NAME[$1]}\" to the inactive worldstorage directory... " local new_path="${WORLD_INACTIVE_PATH[$1]}" as_user "${SERVER_USER_NAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_PATH[$1]}\" \"$new_path\"" echo "Done." else if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then echo "World \"${WORLD_NAME[$1]}\" is already deactivate." else exit_error DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found." fi fi fi } ### Server Utility Functions # Returns the ID for a server. # An ID is given to a server when loaded into memory, and can be used to lookup # config information for that server # $1: The name of the server server_get_id() { for ((i=0; i<$num_servers; i++)); do if [[ "${SERVER_NAME[$i]}" == "$1" ]]; then echo "$i" return 0 fi done error_exit NAME_NOT_FOUND "Could not find id for server name \"$1\"." } # Returns the ID of a server's world. # $1: The ID of the server # $2: The name of the world server_world_get_id() { if [ -d "${SERVER_WORLD_STORAGE[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE[$1]}/$2" ]; then # If the directory exists local start="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: for ((i=$start; i<$max; i++)); do if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then echo "$i" return 0 fi done fi error_exit NAME_NOT_FOUND "Could not find id for world \"$2\" for server \"${SERVER_NAME[$1]}\"." } # Returns 0 if the server $1 is running and 1 if not # $1: The ID of the server server_is_running() { if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null then return 0 else return 1 fi } # Creates symbolic links in the server directory (SERVER_STORAGE_PATH) for each # of the Minecraft worlds located in the world storage directory. # $1: The id of the server for which links should be ensured server_ensure_links() { echo -n "Maintaining world symbolic links... " local start="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))" local output="false" for ((i=$start; i<$max; i++)); do if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then # Remove the symbolic link if it exists as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" continue fi # -L checks for the path being a link rather than a file # ! -a, since it is within double square brackets means: the negation of # the existence of the file. In other words: true if does not exist if [[ -L "${WORLD_LINK[$i]}" || ! -a "${WORLD_LINK[$i]}" ]]; then # If there is a symbolic link in the server direcotry to this world, # or there is not a directory in the server directory containing this world. # Get the original file path the symbolic link is pointing to # If there is no link, link_target will contain nothing link_target="$(readlink "${WORLD_LINK[$i]}")" if "${WORLD_INRAM[$i]}"; then # If this world is marked as loaded into RAM if [ "${link_target}" != "${WORLD_RAMDISK_PATH[$i]}" ]; then # If the symbolic link does not point to the RAM version of the world # Remove the symbolic link if it exists as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" # Create a new symbolic link pointing to the RAM version of the world as_user "${SERVER_USER_NAME[$1]}" "ln -s \"${WORLD_RAMDISK_PATH[$i]}\" \"${WORLD_LINK[$i]}\"" fi else # Otherwise the world is not loaded into RAM, and is just on disk if [ "${link_target}" != "${WORLD_PATH[$i]}" ]; then # If the symbolic link does not point to the disk version of the world # Remove the symbolic link if it exists as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" # Create a new symbolic link pointing to the disk version of the world as_user "${SERVER_USER_NAME[$1]}" "ln -s \"${WORLD_PATH[$i]}\" \"${WORLD_LINK[$i]}\"" fi fi else echoerr -en "\n Error: Could not create link for world \"${WORLD_NAME[$i]}\". The file \"${WORLD_LINK[$i]}\" already exists, and should not be overwritten automatically. Either remove this file, or rename \"${WORLD_NAME[$i]}\"." output="true" fi done if [[ "$output" == "true" ]]; then echo -e "\nDone." else echo "Done." fi } # Moves a servers worlds into RAM # $1: The ID of the server server_worlds_to_ram() { # Only proceed if there is a ramdisk path set in config if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then echo -n "Synchronising flagged worlds on disk to RAM... " local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do if "${WORLD_INRAM[$i]}" && [ -L "${WORLD_LINK[$i]}" ]; then world_to_ram "$i" fi i="$(( $i + 1 ))" done echo "Done." fi } # Moves a servers "in RAM" worlds back to disk # $1: The ID of the server server_worlds_to_disk() { if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then echo -n "Synchronising worlds in RAM to disk... " local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do if [ -d "${WORLD_RAMDISK_PATH[$i]}" ]; then world_to_disk "$i" fi i="$(( $i + 1 ))" done echo "Done." fi } # Watches a server's log for a specific line # $1: The ID for the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after # $3->: The line or lines in the log to wait for # returns: When the line is found server_log_get_line() { # Make sure there is a server log to check as_user "${SERVER_USER_NAME[$1]}" "touch ${SERVER_LOG[$1]}" while read line; do line_time="$(log_line_get_time "$line")" # If the entry is old enough if [[ "$line_time" -ge "$2" ]]; then for search_line in "${@:3}"; do local regex="${LOG_REGEX} ${search_line}" # and matches the regular expression if [[ "$line" =~ $regex ]]; then echo "${line}" return 0 fi done fi done < <(as_user "${SERVER_USER_NAME[$1]}" "tail --pid=$$ --follow --lines=100 --sleep-interval=0.1 \"${SERVER_LOG[$1]}\"") } # The same as server_log_get_line, but does not print the line to stdout # when found. server_log_wait_for_line() { server_log_get_line "$@" > /dev/null } # Sends as string to a server for execution # $1: The ID of the server # $2: The line of text to enter into the server console server_eval() { as_user "${SERVER_USER_NAME[$1]}" "screen -p 0 -S ${SERVER_SCREEN_NAME[$1]} -X eval 'stuff \"$2\"\015'" } # The same as server_eval, but also waits for a log entry before returning # $1: The ID of the server # $2: A line of text to enter into the server console # $3->: The line or lines of text in the log to wait for # stdout: The full entry found in the logs server_eval_and_get_line() { time_now="$(now)" server_eval "$1" "$2" server_log_get_line "$1" "$time_now" "${@:3}" } # The same as server_eval_and_get_line, but does not print anything to stdout server_eval_and_wait() { server_eval_and_get_line "$@" > /dev/null } # Gets the process ID for a server if running, otherwise it outputs nothing # $1: The ID of the server server_pid() { ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" | awk '{print $1}' } # Waits for a server to stop by polling 10 times a second # This approach is fairyl intensive, so only use when you are expecting the # server to stop soon # $1: The ID of the server to wait for server_wait_for_stop() { local pid="$(server_pid "$1")" # if the process is still running, wait for it to stop if [ ! -z "$pid" ]; then while ps -p "$pid" > /dev/null; do sleep 0.1 done fi } # Sets a server's active/inactive state # $1: The ID of the server # $2: A string containing "active" or "inactive" server_set_active() { case "$2" in active) as_user "${SERVER_USER_NAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE[$1]}\"" SERVER_ACTIVE[$1]="true" ;; inactive) as_user "${SERVER_USER_NAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE[$1]}\"" SERVER_ACTIVE[$1]="false" ;; *) error_exit INVALID_ARGUMENT "Invalid argument." ;; esac } ### Jar Group Functions # Lists the jar files grouped by jar groups. jargroup_list() { if [[ -d "${JAR_STORAGE_PATH}" ]]; then local jargroup_name local jar_name while IFS= read -r -d $'\0' jargroup_path; do jargroup_name="$(basename "${jargroup_path}")" echo "$jargroup_name" while IFS= read -r -d $'\0' jar_path; do jar_name="$(basename "${jar_path}")" if [[ "$jar_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then echo " $jar_name" fi done < <(find "${JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0) done < <(find "${JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0) fi } # Creates a new jargroup # $1: The name for the jargroup jargroup_create() { if is_valid_name "$1"; then if [[ ! -d "$JAR_STORAGE_PATH/$1" ]]; then printf "Creating jar group... " local error="$(as_user_stderr "$USERNAME" "mkdir -p \"$JAR_STORAGE_PATH/$1\"")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi error="$(as_user "$USERNAME" "echo \"$2\" > \"$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET\"")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi echo "Done." else error_exit DUPLICATE_NAME "A jar group with that name already exists." fi fi } # Downloads the latest version for a jargroup, using the target URL for that # group. Saves the download with the date and time encoded in the start of the # file name, in the jar group directory in question. Removes the file if there # is no difference between it and the current version. # $1: The jargroup name to download the latest version for jargroup_getlatest() { if is_valid_name "$1"; then if [[ -d "$JAR_STORAGE_PATH/$1" ]]; then if [[ -f "$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET" ]]; then printf "Downloading latest version... " # Try and make local error="$(as_user_stderr "$USERNAME" "mkdir -p '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi as_user "$USERNAME" "wget --quiet --trust-server-names --no-check-certificate --input-file='$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET' --directory-prefix='$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'" echo "Done." local num_files="$(as_user "$USERNAME" "ls -1 '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR' | wc -l")" if [[ "$num_files" == 1 ]]; then # There was 1 file downloaded local file_name="$(ls -1 "$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR")" local new_name="$(date +%F-%H-%M-%S)-$file_name" local most_recent_jar="$(get_latest_file "$JAR_STORAGE_PATH/$1")" if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then # There is not a previous version to do a comparison against, or # The previous version is different: # Add it to the group [[ -f "$most_recent_jar" ]] local was_previous="$?" as_user "$USERNAME" "mv '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR/$file_name' '$JAR_STORAGE_PATH/$1/$new_name'" if [[ ! -z "$most_recent_jar" ]]; then echo "Downloaded version was different to previous latest. Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"." else echo "Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"." fi else echo "Existing version \"$JAR_STORAGE_PATH/$1/$new_name\" was already up to date." fi elif [[ "$num_files" == 0 ]]; then # No file was downloaded echo "Failed. No files were downloaded." else # Multiple files were echo "Error. URL downloads multiple files." fi # Clean up the temp download folder as_user "$USERNAME" "rm -fr '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'" else error_exit FILE_NOT_FOUND "Target URL not found, use $0 jargroup seturl " fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } # Deletes an existing jargroup # $1: The name of the existing jargroup jargroup_delete() { if is_valid_name "$1"; then if [[ -d "$JAR_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete this jar group [y/N]: " read answer if [[ "$answer" =~ ^y|Y|yes$ ]]; then as_user "$USERNAME" "rm -rf \"$JAR_STORAGE_PATH/$1\"" echo "Jar group deleted." else echo "Jar group was NOT deleted." fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } # Renames an existing jargroup # $1: The name of the existing jargroup # $2: The new name jargroup_rename() { if is_valid_name "$1"; then if [[ -d "$JAR_STORAGE_PATH/$1" ]]; then # If the jar group name is valid, # and there is no other jar group with the name $1 if is_valid_name "$2"; then if [[ -e "$JAR_STORAGE_PATH/$2" ]]; then error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"." else # TODO: Update any symbolic links which point to a jar in this directory as_user "$USERNAME" "mv '$JAR_STORAGE_PATH/$1' '$JAR_STORAGE_PATH/$2'" echo "Renamed jar group \"$1\" to \"$2\"." fi fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } ### Server Functions # Echos a list of servers in the SERVER_STORAGE_PATH server_list() { if [ -d "$SERVER_STORAGE_PATH" ]; then ls -1 "$SERVER_STORAGE_PATH" else echo "[There are no servers]" fi } # Creates a new server # $1: The server name to create server_create() { if is_valid_name "$1"; then if [[ -d "$SERVER_STORAGE_PATH/$1" ]]; then error_exit DUPLICATE_NAME "A server with that name already exists." else printf "Creating server directory... " as_user "$USERNAME" "mkdir -p '$SERVER_STORAGE_PATH/$1'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_WHITELIST'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_IPS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_PLAYERS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_OPS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_PROPERTIES'" echo "Done." # Now that the new server has been created, we must call init again # to ensure it is recognised. init # TODO: Handle server default setup stuff better than just using # the "minecraft" jar group. And make it configurable. if [ -d "$JAR_STORAGE_PATH/minecraft" ]; then server_set_jar "$(server_get_id "$1")" "minecraft" fi fi fi } # Deletes an existing server # $1: The server name to delete server_delete() { if is_valid_name "$1"; then if [[ -d "$SERVER_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete this server and its worlds (note: backups are preserved) [y/N]: " read answer if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then # TODO: stop the server if running first as_user "$USERNAME" "rm -rf '$SERVER_STORAGE_PATH/$1'" echo "Server deleted." else echo "Server was NOT deleted." fi else error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"." fi fi } # Renames an existing server # $1: The server name to change # $2: The new name for the server server_rename() { if is_valid_name "$1"; then if [ -d "$SERVER_STORAGE_PATH/$1" ]; then # If the server name is valid and exists local existing_id="$(server_get_id "$1")" if server_is_running "$existing_id"; then error_exit SERVER_RUNNING "Can only rename a stopped server." else if is_valid_name "$2"; then # If the server name is valid if [[ -e "$SERVER_STORAGE_PATH/$2" ]]; then # and there is not already a server with the name $2 error_exit DUPLICATE_NAME "Could not be renamed, there is already a server with the name \"$2\"." else as_user "$USERNAME" "mv '$SERVER_STORAGE_PATH/$1' '$SERVER_STORAGE_PATH/$2'" echo "Renamed server \"$1\" to \"$2\"." fi fi fi else error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"." fi fi } # Starts a single server # $1: The ID of the server server_start() { if server_is_running "$1"; then echo "Server \"${SERVER_NAME[$1]}\" is already running!" else server_ensure_links "$1" server_worlds_to_ram "$1" local time_now="$(now)" printf "Starting server... " as_user "${SERVER_USER_NAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}" server_log_wait_for_line "$1" "$time_now" "${SERVER_CONFIRM_START[$1]}" echo "Done." fi } # Sends the "save-all" command to a server # $1: The ID of the server server_save_all() { if server_is_running "$1"; then echo -n "Forcing save... " # Send the "save-all" command and wait for it to finish server_eval_and_wait "$1" "save-all" "${SERVER_CONFIRM_SAVE_ALL[$1]}" echo "Done." else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Sends the "save-off" command to a server # $1: The ID of the server server_save_off() { if server_is_running "$1"; then echo -n "Disabling level saving... " # Send the "save-off" command and wait for it to finish server_eval_and_wait "$1" "save-off" "${SERVER_CONFIRM_SAVE_OFF[$1]}" echo "Done." # Writes any in-memory data manged by the kernel to disk sync else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Sends the "save-on" command to a server # $1: The ID of the server server_save_on() { if server_is_running "$1"; then echo -n "Enabling level saving... " # Send the "save-on" command and wait for it to finish server_eval_and_wait "$1" "save-on" "${SERVER_CONFIRM_SAVE_ON[$1]}" echo "Done." else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Stops a single server after a delay # $1: The ID of the server server_stop() { if server_is_running "$1"; then # Change the state of the script STOP_COUNTDOWN[$1]="true" server_eval "$1" "say ${SERVER_STOP_MESSAGE[$1]}" echo "Issued the warning \"${SERVER_STOP_MESSAGE[$1]}\" to players." echo -n "Shutting down... " for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now "$1" else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Stops a single server right now # $1: The ID of the server server_stop_now() { if server_is_running "$1"; then server_save_all "$1" echo -n "Stopping the server... " server_eval "$1" "stop" STOP_COUNTDOWN[$1]="false" RESTART_COUNTDOWN[$1]="false" server_wait_for_stop "$1" echo "Done." # Synchronise all worlds in RAM to disk server_worlds_to_disk "$1" else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Restarts a single server after a delay # $1: The ID of the server server_restart() { # Restarts the server if it is already running if server_is_running "$1"; then # Change the state of the script RESTART_COUNTDOWN[$1]="true" server_eval "$1" "say ${SERVER_RESTART_MESSAGE[$1]}" echo "Issued the warning \"${SERVER_RESTART_MESSAGE[$1]}\" to players." echo -n "Restarting... " for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now "$1" fi server_start "$1" } # Restarts a single server right away # $1: The ID of the server server_restart_now() { # Restarts the server if it is already running if server_is_running "$1"; then server_stop_now "$1" fi server_start "$1" } # List the worlds available for a server # $1: The ID of the server server_worlds_list() { local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do if "${WORLD_INRAM[$i]}"; then echo "[RAM] ${WORLD_NAME[$i]}" else echo "[DSK] ${WORLD_NAME[$i]}" fi i="$(( $i + 1 ))" done } # Backs up the worlds for a server # $1: The ID of the server server_worlds_backup() { local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do world_backup "$i" i="$(( $i + 1 ))" done } # Moves a servers log into another file, leaving the original log file empty # $1: The ID of the server server_log_roll() { # Moves and Gzips the logfile, a big log file slows down the # server A LOT (what was notch thinking?) printf "Rolling server logs... " if [ -e "${SERVER_LOG[$1]}" ]; then file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log" as_user "${SERVER_USER_NAME[$1]}" "mkdir -p \"${SERVER_LOG_archive_path[$1]}\" && cp \"${SERVER_LOG[$1]}\" \"${SERVER_LOG_archive_path[$1]}/${file_name}\" && gzip \"${SERVER_LOG_archive_path[$1]}/${file_name}\"" if [ -e "${SERVER_LOG_archive_path[$1]}/${file_name}.gz" ]; then as_user "${SERVER_USER_NAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG[$1]}\"" as_user "${SERVER_USER_NAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_archive_path[$1]}\\\"\" > \"${SERVER_LOG[$1]}\"" else echo "Failed." error_exit LOGS_NOT_ROLLED "Logs were not rolled." fi fi echo "Done." } # Backups a server's directory # $1: The ID of the server server_backup() { echo -n "Backing up the entire server directory... " zip_flags="-rq" # Add the "y" flag if symbolic links should not be followed if [ "${SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]}" != "true" ]; then zip_flags="${zip_flags}y" fi # Zip up the server directory file_name="${SERVER_BACKUP_PATH[$1]}/$(date "+%F-%H-%M-%S").zip" as_user "${SERVER_USER_NAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$SERVER_STORAGE_PATH\" && zip ${zip_flags} \"${file_name}\" \"${SERVER_NAME[$1]}\"" echo "Done." } # Sets a server's jar file # $1: The ID of the server # $2: The name of the jar group # $3: Optionally, a specific jar to use. server_set_jar() { if [ -d "$JAR_STORAGE_PATH/$2" ]; then if [ -z "$3" ]; then # If a specific jar file is not mentioned # Download the latest version jargroup_getlatest "$2" local jar="$(get_latest_file "$JAR_STORAGE_PATH/$2")" else # If a specific jar IS mentioned use that local jar="$JAR_STORAGE_PATH/$2/$3" if [[ ! -e "$jar" ]]; then error_exit NAME_NOT_FOUND "There is no jar named \"$3\" in jargroup \"$2\"." fi fi if [[ ! -z "$jar" ]]; then as_user "${SERVER_USER[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR[$1]}\"" echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"." fi else error_exit NAME_NOT_FOUND "There is no jargorup named \"$2\"." fi } # Lists the players currently connected to a server # $1: The ID of the server server_connected() { if server_is_running "$1"; then local line="$(server_eval_and_get_line "$1" "list" "Connected players:")" # Cuts the start off the line, and the last three (invisible) # characters from the end. local players="${line:46}" if [ -z "$players" ]; then echo "No players are connected." else echo "$players" fi else echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected." fi } ### Manager Functions # Stops all running servers after a servers specified delay # $1: String containing "stop" or "restart". Represents whether the stop is # with a mind to stop the server, or just to restart it. And effects # the message issued to players on a server. manager_stop_all_servers() { # An array of true/false for each server local was_running # False if no servers were running at all local any_running="false" # For all running servers issue the stop warning local max_countdown=0 for ((server=0; server<${num_servers}; server++)); do if server_is_running "$server"; then any_running="true" was_running[$server]="true" STOP_COUNTDOWN[$server]="true" if [[ "${SERVER_STOP_DELAY[$i]}" -gt "$max_countdown" ]]; then max_countdown="${SERVER_STOP_DELAY[$server]}" fi # Send a warning message to the server case "$1" in stop) server_eval "$server" "say ${SERVER_STOP_MESSAGE[$server]}";; restart) server_eval "$server" "say ${SERVER_RESTART_MESSAGE[$server]}";; esac # Send message to stdout echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:" case "$1" in stop) echo " Issued the warning \"${SERVER_STOP_MESSAGE[$server]}\" to players.";; restart) echo " Issued the warning \"${SERVER_RESTART_MESSAGE[$server]}\" to players.";; esac case "${SERVER_STOP_DELAY[$server]}" in 0) echo " Stopping without delay.";; 1) echo " Stopping after 1 second.";; *) echo " Stopping after ${SERVER_STOP_DELAY[$server]} seconds.";; esac else echo "Server \"${SERVER_NAME[$server]}\" was NOT running." was_running[$server]="false" fi done if "$any_running"; then # Wait for the maximum possible delay, stopping servers # at the correct times echo -n "All servers will have been issued the stop command... " for ((tick="${max_countdown}"; tick>=0; tick--)); do tput sc # Save cursor position if [[ "$tick" -le 1 ]]; then echo -n "in $tick second." else echo -n "in $tick seconds." fi # Each second check all server, to see if its their time to # stop. If so issue the stop command, and don't hang. for ((server=0; server<${num_servers}; server++)); do if server_is_running "$server"; then stop_tick="$(( ${max_countdown} - ${SERVER_STOP_DELAY[$server]} ))" if [[ "$stop_tick" == "$tick" ]]; then server_eval "$server" "stop" STOP_COUNTDOWN[$server]="false" fi fi done if [[ "$tick" > 0 ]]; then sleep 1 fi tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done # Start a new line echo "Now." # Finally check all servers have stopped for ((server=0; server<${num_servers}; server++)); do if "${was_running[$server]}"; then echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... " server_wait_for_stop "$server" echo "Done." fi done else echo "No servers were running." fi } # Stops all running servers without delay manager_stop_all_servers_now() { # An array of true/false for each server local was_running # False if no servers were running at all local any_running="false" # Stop all servers at the same time for ((server=0; server<${num_servers}; server++)); do if server_is_running "$server"; then was_running[$server]="true" any_running="true" echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping." server_eval "$server" "stop" else echo "Server \"${SERVER_NAME[$server]}\" was NOT running." was_running[$server]="false" fi done if "$any_running"; then # Ensure all the servers have stopped for ((server=0; server<${num_servers}; server++)); do if "${was_running[$server]}"; then echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... " server_wait_for_stop "$server" echo "Done." fi done else echo "No servers were running." fi } ### Command Functions # Starts all servers command_start() { # Required start option, for debian init.d scripts for ((server=0; server<${num_servers}; server++)); do # Only starts active servers if "${SERVER_ACTIVE[$server]}"; then if server_is_running "$server"; then echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" already started." else echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" starting:" server_start "$server" fi else if server_is_running "$server"; then echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" already started. It should not be running! Use \"$0 ${SERVER_NAME[$server]} stop\" to stop this server." else echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" leaving stopped, as this server is inactive." fi fi done } # Stops all servers after a delay command_stop() { manager_stop_all_servers "stop" } # Stops all servers without delay command_stop_now() { manager_stop_all_servers_now } # Restarts all servers command_restart() { echo "Stopping servers:" command_stop echo "Starting servers:" command_start } # Restarts all servers without delay command_restart_now() { echo "Stopping servers:" command_stop_now echo "Starting servers:" command_start } # Displays the MSM version command_version() { local version="$VERSION" if [ "${version:0:1}" -eq 0 ]; then version="$version Beta" fi echo "Minecraft Server Manager $version" } # Displays a list of servers command_server_list() { server_list } # Creates a new server with name $1 # $1: The new (valid) server name command_server_create() { server_create "$1" } # Deletes an existing server with name $1 # $1: The name of the existing server command_server_delete() { server_delete "$1" } # Renames an existing server # $1: The existing server name # $2: The new (valid) server name command_server_rename() { server_rename "$1" "$2" } # Displays a list of all jar's in jar groups command_jargroup_list() { jargroup_list } # Creates a new jar group # $1: The new (valid) jar group name # $2: The URL to use as the jar group target command_jargroup_create() { jargroup_create "$1" "$2" } # Deletes and existing jar group # $1: The name of a jar group to delete command_jargroup_delete() { jargroup_delete "$1" } # Renames an existing jar group # $1: The name of the existing jar group # $2: The new (valid) name for the jar group command_jargroup_rename() { jargroup_rename "$1" "$2" } # Changes a jar group's target url for automatic downloads # $1: The jar group name # $2: The new URL to use command_jargroup_changetarget() { jargroup_settarget "$1" "$2" } # Downloads the latest jar for a jar group # $1: The name of the jar group command_jargroup_getlatest() { jargroup_getlatest "$1" } # Displays a list of possible commands and help strings command_help() { # Outputs a list of all commands echo -e "Usage: $0 command:" echo -e echo -e "--Setup Commands------------------------------------------------" echo -e " server list List servers" echo -e " server create Creates a new Minecraft server" echo -e " server delete Deletes an existing Minecraft server" echo -e " server rename Renames an existing Minecraft server" echo -e echo -e "--Server Mangement Commands-------------------------------------" echo -e " start Starts a server" echo -e " stop [now] Stops a server after warning players, or right now" echo -e " restart [now] Restarts a server after warning players, or right now" echo -e " status Show the running/stopped status of a server" echo -e " connected List a servers connected players" echo -e " worlds list Lists the worlds a server has" echo -e " worlds load Creates links to worlds in storage for a server" echo -e " worlds ram Toggles a world's \"in RAM\" status" echo -e " worlds todisk Synchronises any \"in RAM\" worlds to disk a server has" echo -e " worlds backup Makes a backup of all worlds a server has" echo -e " worlds on|off Activate or deactivate a world, inactive worlds are not backed up" echo -e " logroll Move a server log to a gziped archive, to reduce lag" echo -e " backup Makes a backup of an entire server directory" echo -e " jar [] Sets a server's jar file" echo -e echo -e "--Server Pass Through Commands----------------------------------" echo -e " wl on|off Enables/disables server whitelist checking" echo -e " wl add|remove Add/remove a player to/from a server's whitelist" echo -e " wl list List the players whitelisted for a server" echo -e " bl player add|remove Ban/pardon a player from/for a server" echo -e " bl ip add|remove Ban/pardon an IP address from/for a server" echo -e " bl list Lists the banned players and IP address for a server" echo -e " op add|remove Add/remove operator status for a player on a server" echo -e " op list Lists the operator players for a server" echo -e " gm survival|creative Change the game mode for a player on a server" echo -e " kick Forcibly disconnect a player from a server" echo -e " say Broadcast a (pink) message to all players on a server" echo -e " time set|add Set/increment time on a server (0-24000)" echo -e " toggledownfall Toggles rain and snow on a server" echo -e " give [amount] [data] Gives an entity to a player" echo -e " xp Gives XP to, or takes away (when negative) XP from, a player" echo -e " save on|off Enable/disable writing world changes to file" echo -e " save all Force the writing of all non-saved world changes to file" echo -e " cmd Send a command string to the server and return" echo -e " cmdlog Same as 'cmd' but shows log output afterwards (Ctrl+C to exit)" echo -e " console Connects to the interactive console. Access may be limited" echo -e echo -e "--Jar Commands--------------------------------------------------" echo -e " jargroup list List the stored jar files." echo -e " jargroup create Create a new jar group, with a URL for new downloads" echo -e " jargroup delete Delete a jar group" echo -e " jargroup rename Rename a jar group" echo -e " jargroup changeurl Change the download URL for a jar group" echo -e " jargroup getlatest Download the latest jar file for a jar group" echo -e echo -e "--Global Commands-----------------------------------------------" echo -e " start Starts all active servers" echo -e " stop [now] Stops all running servers" echo -e " restart [now] Restarts all active servers" echo -e " version Prints the Minecraft Server Manager version installed" } # Starts an individual server # $1: The server ID command_server_start() { server_set_active "$1" "active" server_start "$1" } # Stops an individual server after a delay # $1: The server ID command_server_stop() { server_set_active "$1" "inactive" server_stop "$1" } # Stops an individual server without delay # $1: The server ID command_server_stop_now() { server_set_active "$1" "inactive" server_stop_now "$1" } # Restarts an individual server after a delay # $1: The server ID command_server_restart() { server_set_active "$1" "active" server_restart "$1" } # Restarts an individual server without delay # $1: The server ID command_server_restart_now() { server_set_active "$1" "active" server_restart_now "$1" } # Displays the running/stopped status of an individual server # $1: The server ID command_server_status() { if server_is_running "$1"; then echo "Server \"${SERVER_NAME[$1]}\" is running." else echo "Server \"${SERVER_NAME[$1]}\" is stopped." fi } # Displays a list of connected players for an individual server # $1: The server ID command_server_connected() { server_connected "$1" } # Displays a list of worlds for an individual server # $1: The server ID command_server_worlds_list() { server_worlds_list "$1" } # Creates symlinks for all active worlds so they can be used by the Minecraft # server when running # $1: The server ID command_server_worlds_load() { server_ensure_links "$1" } # Toggles a world's inram status # $1: The server ID # $2: The world ID command_server_worlds_ram() { if server_is_running "$1"; then error_exit SERVER_RUNNING "Server \"${SERVER_NAME[$1]}\" is running. Please stop the server before altering a worlds in-ram status." else world_toggle_ramdisk_state "$2" fi } # Synchronises all inram worlds back to disk for an individual server # $1: The server ID command_server_worlds_todisk() { server_save_off "$1" server_save_all "$1" server_worlds_to_disk "$1" server_save_on "$1" } # Makes a backup of all worlds for an individual server # $1: The server ID command_server_worlds_backup() { if server_is_running "$1"; then server_eval "$1" "say ${SERVER_WORLD_BACKUP_STARTED[$1]}" server_save_off "$1" server_save_all "$1" fi server_worlds_to_disk "$1" server_worlds_backup "$1" if server_is_running "$1"; then server_save_on "$1" server_eval "$1" "say ${SERVER_WORLD_BACKUP_FINISHED[$1]}" fi echo "Backup took $SECONDS seconds". } # Enables a world to be used by its server # $1: The server ID # $2: The world ID command_server_worlds_on() { world_activate "$2" } # Disables a world from being used by its server, also prevents it from being # backed up with the other worlds. # $1: The server ID # $2: The world ID command_server_worlds_off() { world_deactivate "$2" } # Moves an individual server's log text to another file, leaving it empty # $1: The server ID command_server_logroll() { server_log_roll "$1" } # Makes a backup of an entire server directory # $1: The server ID command_server_backup() { if server_is_running "$1"; then server_eval "$1" "say ${SERVER_COMPLETE_BACKUP_STARTED[$1]}" server_save_off "$1" server_save_all "$1" fi server_worlds_to_disk "$1" server_backup "$1" if server_is_running "$1"; then server_save_on "$1" server_eval "$1" "say ${SERVER_COMPLETE_BACKUP_FINISHED[$1]}" fi echo "Backup took $SECONDS seconds". } # Sets an individual server's jar file to use when starting up # $1: The server ID # $2: The jar group name # $3: Optionally a specific jar file name which exists within that jargroup, if # not provided the latest version will be used. command_server_jar() { server_set_jar "$1" "$2" "$3" } # Turns a server's whitelist protection on # $1: The server ID command_server_whitelist_on() { if server_is_running "$1"; then server_eval "$1" "whitelist on" echo "Whitelist enabled" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Turns a server's whitelist protection off # $1: The server ID command_server_whitelist_off() { if server_is_running "$1"; then server_eval "$1" "whitelist off" echo "Whitelist disabled" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Adds a player name to a server's whitelist # $1: The server ID # $2: The player name command_server_whitelist_add() { # TODO: Support whitelisting multiple players (see blacklist player add) if server_is_running "$1"; then server_eval "$1" "whitelist add $2" echo "Added \"$2\" to the whitelist." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Removes a player name from a server's whitelist # $1: The server ID # $2: The player name command_server_whitelist_remove() { # TODO: Support multiple player names if server_is_running "$1"; then server_eval "$1" "whitelist remove $2" echo "Removed \"$2\" from the whitelist." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Displays a list of whitelisted players for an individual server # $1: The server ID command_server_whitelist_list() { if [ -f "${SERVER_WHITELIST[$1]}" ]; then local players="$(cat "${SERVER_WHITELIST[$1]}")" if [ -z "$players" ]; then echo "No players are whitelisted." else echo "$players" fi else echo "No players are whitelisted." fi } # Adds player names to a server's ban list # $1: The server ID # $2->: The player names command_server_blacklist_player_add() { for player in "${@:2}"; do server_eval "$1" "ban $player" done if [[ $# -gt 2 ]]; then echo -n "Blacklisted the following players: " echo -n "$2" for player in "${@:3}"; do echo -n ", $player" done echo "." else echo "Blacklisted \"$2\"." fi } # Removes player names from a server's ban list # $1: The server ID # $2->: The player names command_server_blacklist_player_remove() { for player in "${@:2}"; do server_eval "$1" "pardon $player" done if [[ $# -gt 2 ]]; then echo -n "Removed the following players from the blacklist: " echo -n "$2" for player in "${@:3}"; do echo -n ", $player" done echo "." else echo "Removed \"$2\" from the blacklist." fi } # Adds ip addresses to a server's ban list # $1: The server ID # $2->: The ip addresses command_server_blacklist_ip_add() { for address in "${@:2}"; do server_eval "$1" "ban-ip $address" done if [[ $# -gt 2 ]]; then echo -n "Blacklisted the following ip addresses: " echo -n "$2" for player in "${@:3}"; do echo -n ", $address" done echo "." else echo "Blacklisted \"$2\"." fi } # Removes ip addresses to a server's ban list # $1: The server ID # $2->: The ip addresses command_server_blacklist_ip_remove() { for address in "${@:2}"; do server_eval "$1" "pardon-ip $address" done if [[ $# -gt 2 ]]; then echo -n "Removed the following ip addresses from the blacklist: " echo -n "$2" for player in "${@:3}"; do echo -n ", $address" done echo "." else echo "Removed \"$2\" from the ip blacklist." fi } # Displays a server's banned player names and ip addresses # $1: The server ID command_server_blacklist_list() { local players local ips if [ -f "${SERVER_BANNED_PLAYERS[$1]}" ]; then players="$(cat "${SERVER_BANNED_PLAYERS[$1]}")" fi if [ -f "${SERVER_BANNED_IPS[$1]}" ]; then ips="$(cat "${SERVER_BANNED_IPS[$1]}")" fi if [[ -z "$players" && -z "$ips" ]]; then echo "The blacklist is empty." else if [[ ! -z "$players" ]]; then echo "Players:" for name in $players; do echo " $name" done fi if [[ ! -z "$ips" ]]; then echo "IP Addresses:" for address in $ips; do echo " $address" done fi fi } # Adds a player name to a server's list of operators # $1: The server ID # $2: The player name command_server_operator_add() { # TODO: Support multiple player names if server_is_running "$1"; then server_eval "$1" "op $2" echo "The player \"$2\" is now an operator for the server." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Removes a player name to a server's list of operators # $1: The server ID # $2: The player name command_server_operator_remove() { # TODO: Support multiple player names if server_is_running "$1"; then server_eval "$1" "deop $2" echo "The player \"$2\" is no longer an operator for the server." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Displays a list of operators for an individual server # $1: The server ID command_server_operator_list() { # TODO: Protect against non-existent files local players="$(cat "${SERVER_OPS[$1]}")" if [ -z "$players" ]; then echo "No players are operators." else echo "$players" fi } # Sets the game mode for # $1: The server ID # $2: The game mode # $3: The player name command_server_gamemode() { # TODO: Support multiple player names if server_is_running "$1"; then local mode line regex case "$2" in creative|1) mode=1;; survival|0) mode=0;; *) error_exit INVALID_ARGUMENT "Invalid gamemode type \"$2\" options are \"survival\", \"creative\", \"0\" or \"1\".";; esac line="$(server_eval_and_get_line "$1" "gamemode $3 $mode" "${SERVER_CONFIRM_GAMEMODE[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}")" regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE[$1]}" if [[ "$line" =~ $regex ]]; then echo "Changed game mode of \"$3\" to \"$2\"." fi regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" if [[ "$line" =~ $regex ]]; then echo "The player \"$3\" was not found to be logged on." fi regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}" if [[ "$line" =~ $regex ]]; then echo "The player \"$3\" was already in mode \"$2\"." fi else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Kicks a connected player from a server # $1: The server ID # $2: The player name command_server_kick() { # TODO: Support multiple player names if server_is_running "$1"; then local line regex line="$(server_eval_and_get_line "$1" "kick $2" "${SERVER_CONFIRM_KICK[$1]}" "${SERVER_CONFIRM_KICK_FAIL[$1]}")" regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK[$1]}" if [[ "$line" =~ $regex ]]; then echo "Kicked \"$2\" from game." fi regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK_FAIL[$1]}" if [[ "$line" =~ $regex ]]; then echo "The player \"$2\" is not connected." fi else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Broadcasts a message to all connected players for a server # $1: The server ID # $2->: Words of the message, will be concatinated with spaces command_server_say() { if server_is_running "$1"; then server_eval "$1" "say ${*:2}" echo "Message sent to players." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Sets the time on an individual server # $1: The server ID # $2: The time command_server_time_set() { if server_is_running "$1"; then local line regex line="$(server_eval_and_get_line "$1" "time set $2" "${SERVER_CONFIRM_TIME_SET[$1]}" "${SERVER_CONFIRM_TIME_SET_FAIL[$1]}")" regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET[$1]}" if [[ "$line" =~ $regex ]]; then echo "Set time to \"$2\"." fi regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET_FAIL[$1]}" if [[ "$line" =~ $regex ]]; then error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time." fi else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Increments the time on an individual server # $1: The server ID # $2: The time to add command_server_time_add() { if server_is_running "$1"; then local line regex line="$(server_eval_and_get_line "$1" "time add $2" "${SERVER_CONFIRM_TIME_ADD[$1]}" "${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}")" regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD[$1]}" if [[ "$line" =~ $regex ]]; then echo "Added \"$2\" to time." fi regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}" if [[ "$line" =~ $regex ]]; then error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time." fi else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Toggles the downfall of rain and snow on an individual server # $1: The server ID command_server_toggledownfall() { if server_is_running "$1"; then local line regex line="$(server_eval_and_get_line "$1" "toggledownfall" "${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" "${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}")" regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" if [[ "$line" =~ $regex ]]; then echo "${line:36}" fi regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}" if [[ "$line" =~ $regex ]]; then echo "${line:34}" fi else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # 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 line="$(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]}")" 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 line="$(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]}")" 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_eval "$1" "${*:2}" echo "Now watching logs (press Ctrl+C to exit):" as_user "${SERVER_USER_NAME[$1]}" "tail --pid=$$ --follow --lines=0 --sleep-interval=0.1 ${SERVER_LOG[$1]}" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Resumes a server's screen session (requires ssh-ed in as server user, using # the `su` command will not work.) # $1: The server ID command_server_console() { if server_is_running "$1"; then as_user "${SERVER_USER_NAME[$1]}" "screen -r ${SERVER_SCREEN_NAME[$1]}" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } ### Main Functions # Initialises a server's world # $1: The server id # $2: The world id to use # $3: The name of the world server_world_init() { WORLD_SERVER_ID[$2]="$1" WORLD_NAME[$2]="$3" WORLD_ACTIVE_PATH[$2]="${SERVER_WORLD_STORAGE[$1]}/${WORLD_NAME[$2]}" WORLD_INACTIVE_PATH[$2]="${SERVER_WORLD_STORAGE_INACTIVE[$1]}/${WORLD_NAME[$2]}" # Set the status of this world (active/inactive) if [ -d "${WORLD_ACTIVE_PATH[$2]}" ]; then WORLD_STATUS[$2]="active" WORLD_PATH[$2]="${WORLD_ACTIVE_PATH[$2]}" else if [ -d "${WORLD_INACTIVE_PATH[$2]}" ]; then WORLD_STATUS[$2]="inactive" WORLD_PATH[$2]="${WORLD_INACTIVE_PATH[$2]}" else WORLD_STATUS[$2]="unknown" error_exit NAME_NOT_FOUND "World cannot be found in either \"${WORLD_ACTIVE_PATH[$2]}\" or \"${WORLD_INACTIVE_PATH[$2]}\"." fi fi # TODO: Allow the inram flag location to be overridable. WORLD_FLAG_INRAM[$2]="${WORLD_PATH[$2]}/inram" WORLD_LINK[$2]="${SERVER_PATH[$1]}/${WORLD_NAME[$2]}" WORLD_BACKUP_PATH[$2]="$WORLD_ARCHIVE_PATH/${SERVER_NAME[$1]}/${WORLD_NAME[$2]}" # If the ramdisk path is set, get the path for this world if [ ! -z "$RAMDISK_STORAGE_PATH" ]; then WORLD_RAMDISK_PATH[$2]="${RAMDISK_STORAGE_PATH}/${SERVER_NAME[$1]}/${WORLD_NAME[$2]}" fi # Detect whether this world should be in ram if [[ -e "${WORLD_FLAG_INRAM[$2]}" ]]; then WORLD_INRAM[$2]="true" else WORLD_INRAM[$2]="false" fi } # Load a config file for a server # $1: The id of the server to laod server_load_config() { local name value if [[ -f "${SERVER_CONF[$1]}" ]]; then while read line; do # ignore comment lines echo "$line" | grep "^#" >/dev/null 2>&1 && continue # if not empty, set the property using declare if [ ! -z "$line" ]; then name="$(echo $line | awk -F '=' '{print $1}')" value="$(echo $line | awk -F '=' '{print $2}')" fi case "$name" in # Minecraft settings: allow-flight) SERVER_PROPERTIES_ALLOW_FLIGHT="$value";; allow-nether) SERVER_PROPERTIES_ALLOW_NETHER="$value";; difficulty) SERVER_PROPERTIES_DIFFICULTY="$value";; enable-query) SERVER_PROPERTIES_ENABLE_QUERY="$value";; enable-rcon) SERVER_PROPERTIES_ENABLE_RCON="$value";; gamemode) SERVER_PROPERTIES_GAMEMODE="$value";; generate-structures) SERVER_PROPERTIES_GENERATE_STRUCTURES="$value";; level-name) SERVER_PROPERTIES_LEVEL_NAME="$value";; level-seed) SERVER_PROPERTIES_LEVEL_SEED="$value";; level-type) SERVER_PROPERTIES_LEVEL_TYPE="$value";; max-build-height) SERVER_PROPERTIES_MAX_BUILD_HEIGHT="$value";; max-players) SERVER_PROPERTIES_MAX_PLAYERS="$value";; motd) SERVER_PROPERTIES_MOTD="$value";; texture-pack) SERVER_PROPERTIES_TEXTURE_PACK="$value";; online-mode) SERVER_PROPERTIES_ONLINE_MODE="$value";; pvp) SERVER_PROPERTIES_PVP="$value";; query.port) SERVER_PROPERTIES_QUERY_PORT="$value";; rcon.password) SERVER_PROPERTIES_RCON_PASSWORD="$value";; rcon.port) SERVER_PROPERTIES_RCON_PORT="$value";; server-ip) SERVER_PROPERTIES_SERVER_IP="$value";; server-port) SERVER_PROPERTIES_SERVER_PORT="$value";; spawn-animals) SERVER_PROPERTIES_SPAWN_ANIMALS="$value";; spawn-monsters) SERVER_PROPERTIES_SPAWN_MONSTERS="$value";; spawn-npcs) SERVER_PROPERTIES_SPAWN_NPCS="$value";; view-distance) SERVER_PROPERTIES_VIEW_DISTANCE="$value";; white-list) SERVER_PROPERTIES_WHITE_LIST="$value";; # MSM setttings: msm-server-user) SERVER_USER_NAME[$1]="$value";; msm-screen-name) SERVER_SCREEN_NAME[$1]="$value";; msm-world-storage-path) SERVER_WORLD_STORAGE[$1]="${SERVER_PATH[$1]}/$value";; msm-world-storage-inactive-path) SERVER_WORLD_STORAGE_INACTIVE[$1]="${SERVER_PATH[$1]}/$value";; msm-log) SERVER_LOG[$1]="${SERVER_PATH[$1]}/$value";; msm-whitelist) SERVER_WHITELIST[$1]="${SERVER_PATH[$1]}/$value";; msm-banned-players) SERVER_BANNED_PLAYERS[$1]="${SERVER_PATH[$1]}/$value";; msm-banned-ips) SERVER_BANNED_IPS[$1]="${SERVER_PATH[$1]}/$value";; msm-ops) SERVER_OPS[$1]="${SERVER_PATH[$1]}/$value";; msm-jar) SERVER_JAR[$1]="${SERVER_PATH[$1]}/$value";; msm-ram) SERVER_RAM[$1]="$value";; msm-invocation) SERVER_INVOCATION[$1]="$value";; msm-stop-delay) SERVER_STOP_DELAY[$1]="$value";; msm-restart-delay) SERVER_RESTART_DELAY[$1]="$value";; msm-stop-message) SERVER_STOP_MESSAGE[$1]="$value";; msm-stop-abort) SERVER_STOP_ABORT[$1]="$value";; msm-restart-message) SERVER_RESTART_MESSAGE[$1]="$value";; msm-restart-abort) SERVER_RESTART_ABORT[$1]="$value";; msm-world-backup-started) SERVER_WORLD_BACKUP_STARTED[$1]="$value";; msm-world-backup-finished) SERVER_WORLD_BACKUP_FINISHED[$1]="$value";; msm-complete-backup-started) SERVER_COMPLETE_BACKUP_STARTED[$1]="$value";; msm-complete-backup-finished) SERVER_COMPLETE_BACKUP_FINISHED[$1]="$value";; msm-complete-backup-follow-symlinks) SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]="$value";; msm-confirm-save-on) SERVER_CONFIRM_SAVE_ON[$1]="$value";; msm-confirm-save-off) SERVER_CONFIRM_SAVE_OFF[$1]="$value";; msm-confirm-save-all) SERVER_CONFIRM_SAVE_ALL[$1]="$value";; msm-confirm-start) SERVER_CONFIRM_START[$1]="$value";; msm-confirm-whitelist-list) SERVER_CONFIRM_WHITELIST_LIST[$1]="$value";; msm-confirm-kick) SERVER_CONFIRM_CONFIRM_KICK[$1]="$value";; msm-confirm-kick-fail) SERVER_CONFIRM_KICK_FAIL[$1]="$value";; msm-confirm-time-set) SERVER_CONFIRM_TIME_SET[$1]="$value";; msm-confirm-time-set-fail) SERVER_CONFIRM_TIME_SET_FAIL[$1]="$value";; msm-confirm-time-add) SERVER_CONFIRM_TIME_ADD[$1]="$value";; msm-confirm-time-add-fail) SERVER_CONFIRM_TIME_ADD_FAIL[$1]="$value";; msm-confirm-toggledownfall) SERVER_CONFIRM_TOGGLEDOWNFALL[$1]="$value";; msm-confirm-toggledownfall-fail) SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]="$value";; msm-confirm-gamemode) SERVER_CONFIRM_GAMEMODE[$1]="$value";; msm-confirm-gamemode-fail-no-user) SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]="$value";; msm-confirm-gamemode-fail-no-change) SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]="$value";; msm-confirm-give) SERVER_CONFIRM_GIVE[$1]="$value";; msm-confirm-give-fail-no-user) SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]="$value";; msm-confirm-give-fail-no-item) SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]="$value";; msm-confirm-xp) SERVER_CONFIRM_XP[$1]="$value";; msm-confirm-xp-fail-no-user) SERVER_CONFIRM_XP_FAIL_NO_USER[$1]="$value";; msm-confirm-xp-fail-invalid-amount) SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]="$value";; esac done < "${SERVER_CONF[$1]}" fi } # Initialise a server's variables # $1: The id to use for this server # $2: The name of the server server_init() { SERVER_NAME[$1]="$2" SERVER_PATH[$1]="$SERVER_STORAGE_PATH/$2" SERVER_CONF[$1]="${SERVER_PATH[$1]}/$SERVER_CONF_NAME" SERVER_FLAG_ACTIVE[$1]="${SERVER_PATH[$1]}/active" SERVER_BACKUP_PATH[$1]="$BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}" SERVER_LOG_archive_path[$1]="$LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}" if [[ -e "${SERVER_FLAG_ACTIVE[$1]}" ]]; then SERVER_ACTIVE[$1]="true" else SERVER_ACTIVE[$1]="false" fi # Setup defaults # Note: screen_name will at this stage have the {SERVER_NAME} tag in it # which needs to be replaced. # Invocation may also the {RAM} and {JAR} tags. SERVER_USER_NAME[$1]="$DEFAULT_SERVER_USER" SERVER_SCREEN_NAME[$1]="${DEFAULT_SCREEN_NAME//\{SERVER_NAME\}/${SERVER_NAME[$1]}}" # Replace tags now, they cannot change SERVER_WORLD_STORAGE[$1]="${SERVER_PATH[$1]}/$DEFAULT_WORLD_STORAGE_PATH" SERVER_WORLD_STORAGE_INACTIVE[$1]="${SERVER_PATH[$1]}/$DEFAULT_WORLD_STORAGE_INACTIVE_PATH" SERVER_LOG[$1]="${SERVER_PATH[$1]}/$DEFAULT_LOG" SERVER_WHITELIST[$1]="${SERVER_PATH[$1]}/$DEFAULT_WHITELIST" SERVER_BANNED_PLAYERS[$1]="${SERVER_PATH[$1]}/$DEFAULT_BANNED_PLAYERS" SERVER_BANNED_IPS[$1]="${SERVER_PATH[$1]}/$DEFAULT_BANNED_IPS" SERVER_OPS[$1]="${SERVER_PATH[$1]}/$DEFAULT_OPS" SERVER_JAR[$1]="${SERVER_PATH[$1]}/$DEFAULT_JAR" SERVER_RAM[$1]="$DEFAULT_RAM" SERVER_INVOCATION[$1]="$DEFAULT_INVOCATION" # Don't replace tags yet, they may change SERVER_STOP_DELAY[$1]="$DEFAULT_STOP_DELAY" SERVER_RESTART_DELAY[$1]="$DEFAULT_RESTART_DELAY" SERVER_STOP_MESSAGE[$1]="$DEFAULT_STOP_MESSAGE" SERVER_STOP_ABORT[$1]="$DEFAULT_STOP_ABORT" SERVER_RESTART_MESSAGE[$1]="$DEFAULT_RESTART_MESSAGE" SERVER_RESTART_ABORT[$1]="$DEFAULT_RESTART_ABORT" SERVER_WORLD_BACKUP_STARTED[$1]="$DEFAULT_WORLD_BACKUP_STARTED" SERVER_WORLD_BACKUP_FINISHED[$1]="$DEFAULT_WORLD_BACKUP_FINISHED" SERVER_COMPLETE_BACKUP_STARTED[$1]="$DEFAULT_COMPLETE_BACKUP_STARTED" SERVER_COMPLETE_BACKUP_FINISHED[$1]="$DEFAULT_COMPLETE_BACKUP_FINISHED" SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]="$DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS" SERVER_CONFIRM_SAVE_ON[$1]="$DEFAULT_CONFIRM_SAVE_ON" SERVER_CONFIRM_SAVE_OFF[$1]="$DEFAULT_CONFIRM_SAVE_OFF" SERVER_CONFIRM_SAVE_ALL[$1]="$DEFAULT_CONFIRM_SAVE_ALL" SERVER_CONFIRM_START[$1]="$DEFAULT_CONFIRM_START" SERVER_CONFIRM_KICK[$1]="$DEFAULT_CONFIRM_KICK" SERVER_CONFIRM_KICK_FAIL[$1]="$DEFAULT_CONFIRM_KICK_FAIL" SERVER_CONFIRM_TIME_SET[$1]="$DEFAULT_CONFIRM_TIME_SET" SERVER_CONFIRM_TIME_SET_FAIL[$1]="$DEFAULT_CONFIRM_TIME_SET_FAIL" SERVER_CONFIRM_TIME_ADD[$1]="$DEFAULT_CONFIRM_TIME_ADD" SERVER_CONFIRM_TIME_ADD_FAIL[$1]="$DEFAULT_CONFIRM_TIME_ADD_FAIL" SERVER_CONFIRM_TOGGLEDOWNFALL[$1]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL" SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL" SERVER_CONFIRM_GAMEMODE[$1]="$DEFAULT_CONFIRM_GAMEMODE" SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER" SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE" SERVER_CONFIRM_GIVE[$1]="$DEFAULT_CONFIRM_GIVE" SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]="$DEFAULT_CONFIRM_GIVE_FAIL_NO_USER" SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]="$DEFAULT_CONFIRM_GIVE_FAIL_NO_ITEM" SERVER_CONFIRM_XP[$1]="$DEFAULT_CONFIRM_XP" SERVER_CONFIRM_XP_FAIL_NO_USER[$1]="$DEFAULT_CONFIRM_XP_FAIL_NO_USER" SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]="$DEFAULT_CONFIRM_XP_FAIL_INVALID_AMOUNT" # Load config overrides from server config file if present server_load_config "$1" # Replace tags in delay messages SERVER_STOP_MESSAGE[$1]="${SERVER_STOP_MESSAGE[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}" SERVER_RESTART_MESSAGE[$1]="${SERVER_RESTART_MESSAGE[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}" # Replace tags in server invocation SERVER_INVOCATION[$1]="${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}" SERVER_INVOCATION[$1]="${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR[$1]}}" # Load worlds if there is a world storage directory present SERVER_WORLD_OFFSET[$1]=0 SERVER_NUM_WORLDS[$1]=0 # Start world id's for this server's worlds at the end of the array local id="$num_worlds" # Record the index at which worlds for this server start SERVER_WORLD_OFFSET[$1]="$id" if [[ -d "${SERVER_WORLD_STORAGE[$1]}" ]]; then # Load active worlds while IFS= read -r -d $'\0' path; do local name="$(basename "$path")" server_world_init "$1" "$id" "$name" # Build the server_worlds comma separated list if [[ "$id" == "${SERVER_WORLD_OFFSET[$1]}" ]]; then SERVER_WORLDS[$1]="$name" else SERVER_WORLDS[$1]="${SERVER_WORLDS[$1]}, $name" fi id="$(($id+1))" num_worlds="$id" done < <(find "${SERVER_WORLD_STORAGE[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) fi if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE[$1]}" ]]; then # Load inactive worlds while IFS= read -r -d $'\0' path; do local name="$(basename "$path")" server_world_init "$1" "$id" "$name" # Build the server_worlds_inactive comma separated list if [[ "$id" == "${SERVER_WORLD_OFFSET[$1]}" ]]; then SERVER_WORLDS[$1]="$name" else SERVER_WORLDS[$1]="${SERVER_WORLDS[$1]}, $name" fi id="$(($id+1))" num_worlds="$id" done < <(find "${SERVER_WORLD_STORAGE_INACTIVE[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) fi # Record the number of worlds this server has SERVER_NUM_WORLDS[$1]="$(( $id - ${SERVER_WORLD_OFFSET[$1]} ))" } # Asserts that a variable has been set in the config file # $1: The name of the variable to test assert_is_set_in_config() { [[ ! ${!1} && ${!1-_} ]] && { error_exit CONF_ERROR "Have you recently upgraded? \"$1\" is a new required setting, and must be set in $CONF. See https://raw.github.com/marcuswhybrow/minecraft-server-manager/${VERSION}/msm.conf and look for the line starting with \"$1\". Add this line to your settings. Sorry about this... A better sollution for upgrading is being worked on." } } # Ensures all non-optional settings have been set init_ensure_settings() { assert_is_set_in_config USERNAME assert_is_set_in_config SERVER_STORAGE_PATH assert_is_set_in_config JAR_STORAGE_PATH assert_is_set_in_config RAMDISK_STORAGE_PATH assert_is_set_in_config WORLD_ARCHIVE_PATH assert_is_set_in_config LOG_ARCHIVE_PATH assert_is_set_in_config BACKUP_ARCHIVE_PATH assert_is_set_in_config DEFAULT_SERVER_USER assert_is_set_in_config DEFAULT_SCREEN_NAME assert_is_set_in_config DEFAULT_WORLD_STORAGE_PATH assert_is_set_in_config DEFAULT_WORLD_STORAGE_INACTIVE_PATH assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS assert_is_set_in_config DEFAULT_LOG assert_is_set_in_config DEFAULT_PROPERTIES assert_is_set_in_config DEFAULT_WHITELIST assert_is_set_in_config DEFAULT_BANNED_PLAYERS assert_is_set_in_config DEFAULT_BANNED_IPS assert_is_set_in_config DEFAULT_OPS assert_is_set_in_config DEFAULT_JAR assert_is_set_in_config DEFAULT_RAM assert_is_set_in_config DEFAULT_INVOCATION assert_is_set_in_config DEFAULT_STOP_DELAY assert_is_set_in_config DEFAULT_RESTART_DELAY assert_is_set_in_config DEFAULT_STOP_MESSAGE assert_is_set_in_config DEFAULT_STOP_ABORT assert_is_set_in_config DEFAULT_RESTART_MESSAGE assert_is_set_in_config DEFAULT_RESTART_ABORT assert_is_set_in_config DEFAULT_WORLD_BACKUP_STARTED assert_is_set_in_config DEFAULT_WORLD_BACKUP_FINISHED assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_STARTED assert_is_set_in_config DEFAULT_COMPLETE_BACKUP_FINISHED assert_is_set_in_config DEFAULT_CONFIRM_SAVE_ON assert_is_set_in_config DEFAULT_CONFIRM_SAVE_OFF assert_is_set_in_config DEFAULT_CONFIRM_SAVE_ALL assert_is_set_in_config DEFAULT_CONFIRM_START assert_is_set_in_config DEFAULT_CONFIRM_KICK assert_is_set_in_config DEFAULT_CONFIRM_KICK_FAIL assert_is_set_in_config DEFAULT_CONFIRM_TIME_SET assert_is_set_in_config DEFAULT_CONFIRM_TIME_SET_FAIL assert_is_set_in_config DEFAULT_CONFIRM_TIME_ADD assert_is_set_in_config DEFAULT_CONFIRM_TIME_ADD_FAIL assert_is_set_in_config DEFAULT_CONFIRM_TOGGLEDOWNFALL assert_is_set_in_config DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER assert_is_set_in_config DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE assert_is_set_in_config DEFAULT_CONFIRM_GIVE assert_is_set_in_config DEFAULT_CONFIRM_GIVE_FAIL_NO_USER assert_is_set_in_config DEFAULT_CONFIRM_GIVE_FAIL_NO_ITEM assert_is_set_in_config DEFAULT_CONFIRM_XP assert_is_set_in_config DEFAULT_CONFIRM_XP_FAIL_NO_USER assert_is_set_in_config DEFAULT_CONFIRM_XP_FAIL_INVALID_AMOUNT } init() { # Sourcing $CONF will override the previous defaults, with new values source "$CONF" init_ensure_settings num_worlds=0 num_servers=0 if [ -d "$SERVER_STORAGE_PATH" ]; then local id=0 while IFS= read -r -d $'\0' path; do local name="$(basename "$path")" server_init "$id" "$name" id="$(($id+1))" num_servers="$id" done < <(find "$SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0) fi } # Called if the script is interrupted before exiting naturally interrupt() { local exit_message="false" for ((i=0; $i<$num_servers; i++)); do if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval "$i" "say ${SERVER_STOP_ABORT[$i]}" echo "Server \"${SERVER_NAME[$i]}\" shutdown was aborted." fi if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval "$i" "say ${SERVER_RESTART_ABORT[$i]}" echo "Server \"${SERVER_NAME[$i]}\" restart was aborted." fi done exit } COMMAND_COUNT=0 # Adds a command to the list, allowing it to be called from the command line. # $1: The command signature, a coded string describing the structure of the # command. # $2: The handler function to call, if this command is identified. register_command() { # Here we build a regular expression which will match any user input # that could be passed to the given handler function. It is derrived # automatically from the given command signature. local regex="^" # Iterate over each element in the command signature for word in $1; do # Variables are denoted by angle brackets (e.g. "") and can # at this stage be accepted as any non-zero string if [[ "$word" =~ ^\<.*\>$ ]]; then if [[ "$word" == "" ]]; 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() { 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 ((i=0; i<$COMMAND_COUNT; i++)); do if [[ "$args" =~ ${COMMAND_REGEX[$i]} ]]; then unset args local word_offset=1 local args local arg_offset=0 local sid=-1 local wid=-1 # Helper function to build the argument list # $1: The argument to push onto the list push_arg() { args[$arg_offset]="$1" arg_offset="$(( $arg_offset + 1 ))" } # The following loop builds a set of arguments to pass to the # matched command handler function. Rather than passing all args # given to the script, to the handler (which may contain constant # strings), it only includes variables. for word in ${COMMAND_SIGNATURE[$i]}; do # Whether a positional argument is a varibale or not is # determined by the respective element in the command signature # given when registering. # # This case statement handles each possible type of signature # token, and pushes the respective user input onto the stack of # arguments. case "$word" in # The "" token expects any type of string argument, # accepting spaces, limited to one argument. "") # Do no checks, just push the argument onto the stack push_arg "${!word_offset}" ;; # The "" 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. "") # 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 "" token is similar to "" but adds an # extra assurance that the string is a valid name, as used # for creating servers and other things. "") # 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 "" token improves on "" 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. "") local specified_name="${!word_offset}" if [[ "$specified_name" == "all" ]]; then # Do for all servers sid="server:all" else if is_valid_name "$specified_name"; then if [ -d "$SERVER_STORAGE_PATH/$specified_name" ]; then sid="$(server_get_id "$specified_name")" fi fi if [[ "$sid" -eq "-1" ]]; then error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"." fi fi push_arg "$sid" ;; # The "" token also improves upon "" 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. "") local specified_name="${!word_offset}" if [[ "$sid" -eq "-1" ]]; then # Server id not set yet exit_error 1 "Ill-defined command $*. Please file an issue by opening the following link: https://github.com/marcuswhybrow/minecraft-server-manager/issues" fi if [[ "$sid" -eq "-2" ]]; then if [[ "$specified_name" == "all" ]]; then wid="world:all" else exit_error INVALID_ARGUMENT "When specifying \"all\" servers, \"all\" worlds must be specified also." fi fi if [[ "$sid" -ge "0" ]]; then if is_valid_name "$specified_name"; then if [ -d "${SERVER_WORLD_ACTIVE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_INACTIVE_PATH[$sid]}/$specified_name" ]; then wid="$(server_world_get_id "$sid" "$specified_name")" fi fi if [ -z "$wid" ]; then error_exit NAME_NOT_FOUND "There is no world with the name \"$specified_name\"." fi push_arg "$wid" fi ;; esac word_offset=$(( $word_offset + 1 )) done # The argument list for the call to the command handler has been # built. But there are several ways to call a handler. Either just # once, or multiple times based upon if multiple servers or worlds # were specified. # This code block calls the handler for all possible servers and # all possible worlds. if [[ "$sid" == "server:all" ]] && [[ "$wid" == "world:all" ]]; then for ((j=0; j<$num_worlds; j++)); do # Replace server and world id placeholders with actual id's local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//server:all/${WORLD_SERVER_ID[$j]}}" replaced_args[$k]="${args[$k]//world:all/$j}" done # Call the function with the specific replaced args ${COMMAND_HANDLER[$i]} "${replaced_args[@]}" done # Prevent the default singular call later on. return fi # This calls the handler for all possible servers, and preserves # all other arguments. if [[ "$sid" == "server:all" ]]; then for ((j=0; j<$num_servers; j++)); do local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//server\:all/$j}" done ${COMMAND_HANDLER[$i]} "${replaced_args[@]}" done return fi # This calls the handlers for all possible worlds for a specific # server. if [[ "$sid" != "server:all" ]] && [[ "$wid" == "world:all" ]]; then for ((j=${SERVER_WORLD_OFFSET[$sid]}; j<${SERVER_NUM_WORLDS[$sid]}; j++)); do local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//world:all/$j}" done ${COMMAND_HANDLER[$i]} "${replaced_args[@]}" done return fi # Otherwise it's a simple single call of the handler. ${COMMAND_HANDLER[$i]} "${args[@]}" return fi done echo "No such command. See $0 help" } # The main function which starts the script main() { # Initialises variables that represent system state init # Trap interrupts to the script by calling the interrupt function trap interrupt EXIT # The following section registers commands to be available for use. The # register_command function accepts a command_signature and a # command_handler_function_name as positional arguments 1 and 2 # respectively. # # A command signature consists of multiple elements separated by spaces, # the available options are as follows: # # fixedstring Matches an argument containing the specified # characters, in this case the characters "fixedstring" # # Same as "fixedstring", but is variable and the value # is passed to the handler function as a positional # argument # # Same as "", but matches multiple arguments, # must be final element # # Same as "", also ensures it's a valid name # using the is_valid_name function # # Same as "", also converts value to server id or # fails if the server does not exist # # Same as "", also converts value to world id or # fails if the world does not exist. Must only be # included after a "" element. # # Elements listed above encapsulated within angle brackets must be included # within a signature verbatim, as apposed to the "fixedstring" element # which is arbitrary. # # Variables passed to handler functions are of course positional and there # position matches the position of that element in the command signature. register_command "start" "command_start" register_command "stop" "command_stop" register_command "stop now" "command_stop_now" register_command "restart" "command_restart" register_command "restart now" "command_restart_now" register_command "version" "command_version" register_command "server list" "command_server_list" register_command "server create " "command_server_create" register_command "server delete " "command_server_delete" register_command "server rename " "command_server_rename" register_command "jargroup list" "command_jargroup_list" register_command "jargroup create " "command_jargroup_create" register_command "jargroup delete " "command_jargroup_delete" register_command "jargroup rename " "command_jargroup_rename" register_command "jargroup changetarget " "command_jargroup_changetarget" register_command "jargroup getlatest " "command_jargroup_getlatest" register_command "help" "command_help" register_command " start" "command_server_start" register_command " stop" "command_server_stop" register_command " stop now" "command_server_stop_now" register_command " restart" "command_server_restart" register_command " restart now" "command_server_restart_now" register_command " status" "command_server_status" register_command " connected" "command_server_connected" register_command " worlds list" "command_server_worlds_list" register_command " worlds load" "command_server_worlds_load" register_command " worlds ram " "command_server_worlds_ram" register_command " worlds todisk" "command_server_worlds_todisk" register_command " worlds backup" "command_server_worlds_backup" register_command " worlds on " "command_server_worlds_on" register_command " worlds off " "command_server_worlds_off" register_command " logroll" "command_server_logroll" register_command " backup" "command_server_backup" register_command " jar " "command_server_jar" register_command " jar " "command_server_jar" register_command " whitelist|wl on" "command_server_whitelist_on" register_command " whitelist|wl off" "command_server_whitelist_off" register_command " whitelist|wl add " "command_server_whitelist_add" register_command " whitelist|wl remove " "command_server_whitelist_remove" register_command " whitelist|wl list" "command_server_whitelist_list" register_command " blacklist|bl player add " "command_server_blacklist_player_add" register_command " blacklist|bl player remove " "command_server_blacklist_player_remove" register_command " blacklist|bl ip add " "command_server_blacklist_ip_add" register_command " blacklist|bl ip remove " "command_server_blacklist_ip_remove" register_command " blacklist|bl list" "command_server_blacklist_list" register_command " operator|op add " "command_server_operator_add" register_command " operator|op remove " "command_server_operator_remove" register_command " operator|op list" "command_server_operator_list" register_command " gamemode|gm " "command_server_gamemode" register_command " kick " "command_server_kick" register_command " say " "command_server_say" register_command " time set " "command_server_time_set" register_command " time add " "command_server_time_add" register_command " toggledownfall|tdf" "command_server_toggledownfall" register_command " give " "command_server_give" register_command " give " "command_server_give" register_command " give " "command_server_give" register_command " xp " "command_server_xp" register_command " save on" "command_server_save_on" register_command " save off" "command_server_save_off" register_command " save all" "command_server_save_all" register_command " cmd " "command_server_cmd" register_command " cmdlog " "command_server_cmdlog" register_command " console" "command_server_console" # This function call matches the user input to a registered command # signature, and then calls that commands handler function with positional # arguments containing any variable strings. call_command "$@" } ### Start point main "$@" exit 0