#!/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 CONFIG="/etc/msm.conf" ### Config variables the user should need/want to change # Jar group file which contains the download target URL declare -r JARGROUP_TARGET="target.txt" # Jar group directory name to download new jars to, is deleted afterwards declare -r JARGROUP_DOWNLOAD_DIR="downloads" # The flag which indicates the world should be put in RAM declare -r WORLD_FLAG_INRAM="inram" # The flag which indicates the server is active declare -r SERVER_FLAG_ACTIVE="active" # The start of a regex to find a log line declare -r LOG_REGEX="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\]" ### Script State Variables # "true" whilst the script is counting down a delay to stop the server declare STOP_COUNTDOWN # "true" whilst the script is counting down a delay to restart the server declare RESTART_COUNTDOWN ### Utility Functions # Executes the command "$2" as user "$1" # $1: The user to execute the command as # $2: The command to execute as_user() { local user="$(whoami)" if [ "$user" == "$1" ]; then bash -c "$2" else if [ "$user" == "root" ]; then su - $1 -s /bin/bash -c "$2" else echoerr "This command must be executed as the user \"$1\" or \"root\"." exit 1 fi fi } # Executes the command "$1" as SERVER_USER but returns stderr instead as_user_stderr() { as_user "$@" > /dev/null 2>&1 } # Echo to stderr echoerr() { echo "$@" 1>&2 } # Determines whether "$1" is a valid name for a server or jar group directory # It must only contain upper or lower case letters, digits, dashes or # underscores. # It must also not be one of a list of reserved names. is_valid_name() { local valid="^[a-zA-Z0-9\_\-]+$" local invalid="^server|jargroup|start|stop|restart|all$" if [[ $1 =~ $valid ]]; then if [[ $1 =~ $invalid ]]; then echoerr "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"jargroup\" or \"all\"." return 1 else return 0 fi else echoerr "Invalid name \"$1\": A name may only contain letters, numbers, dashes and unscores." return 1 fi } # Gets the latest jar from a jar group, based upon the date and time encoded # in the file name. # $1: The directory to search get_latest_file() { local best_time=0 local best_file="" while IFS= read -r -d $'\0' file; do # Remove the path, leaving just the file name local date_time=$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}') # Get the time in seconds since 1970 from file name local seconds=$(date -d "$date_time" "+%s" 2> /dev/null) # If that is newer than the current best, override variables if [[ $seconds > $best_time ]]; then best_time=$seconds best_file=$file fi done < <(find "$1" -maxdepth 1 -type f -print0) echo $best_file } # Returns the current time as a UNIX timestamp (in seconds since 1970) now() { date +%s } ### Log Utility Functions # Gets the UNIX timestamp for a server log line # $1: A server log line # returns: Time in seconds since 1970-01-01 00:00:00 UTC log_line_get_time() { time_string=$(echo $1 | awk '{print $1 " " $2}') date -d "$time_string" "+%s" 2> /dev/null } ### World Utility Functions # Moves a world to RAM # $1: the ID of the world to move world_to_ram() { if [ ! -z $RAMDISK_STORAGE_PATH ]; then as_user ${server_user_name[${world_server_id[$1]}]} "mkdir -p ${world_ramdisk_path[$1]} && rsync -rt --exclude '$WORLD_FLAG_INRAM' \"${world_path[$1]}/\" \"${world_ramdisk_path[$1]}\"" fi } # Moves a world in RAM to disk # $1: the ID of the world to move world_to_disk() { as_user ${server_user_name[${world_server_id[$1]}]} "rsync -rt --exclude '$WORLD_FLAG_INRAM' \"${world_ramdisk_path[$1]}/\" \"${world_path[$1]}\"" } # Toggles a worlds ramdisk state # $1: The ID of the world world_toggle_ramdisk_state() { if [ -e ${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]} as_user ${server_user_name[$server_id]} "mkdir -p \"${world_backup_path[$1]}\" && cd \"${server_world_storage[$server_id]}\" && zip -rq \"${world_backup_path[$1]}/${file_name}\" \"${world_name[$1]}\"" echo "Done." } ### Server Utility Functions # Returns the ID for a server. # An ID is given to a server when loaded into memory, and can be used to lookup # config information for that server # $1: The name of the server server_get_id() { local i=0 while [[ $i < $num_servers ]]; do if [[ "${server_name[$i]}" == "$1" ]]; then echo "$i" return 0 fi i=$(( $i + 1 )) done return 1 } # Returns the ID of a server's world. # $1: The ID of the server # $2: The name of the world server_world_get_id() { local i=${server_world_offset[$1]} local max=$(( $i + ${server_num_worlds[$1]} )) # For each of the servers worlds: while [[ $i < $max ]]; do if [[ "${world_name[$i]}" == "$2" ]]; then echo $i return 0 fi i=$(( $i + 1 )) done return 1 } # Returns 0 if the server $1 is running and 1 if not # $1: The ID of the server server_is_running() { if ps ax | grep -v grep | grep "${server_screen_name[$1]} ${server_invocation[$1]}" > /dev/null then return 0 else return 1 fi } # Creates symbolic links in the server directory (SERVER_STORAGE_PATH) for each # of the Minecraft worlds located in the world storage directory. # $1: The id of the server for which links should be ensured server_ensure_links() { echo -n "Maintaining world symbolic links... " local i=${server_world_offset[$1]} local max=$(( $i + ${server_num_worlds[$1]} )) while [[ $i -lt $max ]]; do # -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 fi i=$(( $i + 1 )) done echo "Done." } # 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 < $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 < $max ]]; do if [ -e ${world_ramdisk_path[$i]} ]; then world_to_disk $i fi i=$(( $i + 1 )) done echo "Done." fi } # Watches a server's log for a specific line # $1: The ID for the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after # $3->: The line or lines in the log to wait for # returns: When the line is found server_log_get_line() { # Make sure there is a server log to check as_user ${server_user_name[$1]} "touch ${server_log[$1]}" while read line; do line_time=$(log_line_get_time "$line") # If the entry is old enough if [[ $line_time -ge $2 ]]; then for search_line in "${@:3}"; do local regex="${LOG_REGEX} ${search_line}" # and matches the regular expression if [[ $line =~ $regex ]]; then echo $line return 0 fi done fi done < <(as_user ${server_user_name[$1]} "tail --follow --lines=100 --sleep-interval=0.1 ${server_log[$1]}") } # The same as server_log_get_line, but does not print the line to stdout # when found. server_log_wait_for_line() { server_log_get_line "$@" > /dev/null } # Sends as string to a server for execution # $1: The ID of the server # $2: The line of text to enter into the server console server_eval() { as_user ${server_user_name[$1]} "screen -p 0 -S ${server_screen_name[$1]} -X eval 'stuff \"$2\"\015'" } # The same as server_eval, but also waits for a log entry before returning # $1: The ID of the server # $2: A line of text to enter into the server console # $3->: The line or lines of text in the log to wait for # stdout: The full entry found in the logs server_eval_and_get_line() { time_now=$(now) server_eval $1 "$2" server_log_get_line $1 "$time_now" "${@:3}" } # The same as server_eval_and_get_line, but does not print anything to stdout server_eval_and_wait() { server_eval_and_get_line "$@" > /dev/null } # Gets the process ID for a server if running, otherwise it outputs nothing # $1: The ID of the server server_pid() { ps ax | grep -v grep | grep "${server_screen_name[$1]} ${server_invocation[$1]}" | awk '{print $1}' } # Waits for a server to stop by polling 10 times a second # This approach is fairyl intensive, so only use when you are expecting the # server to stop soon # $1: The ID of the server to wait for server_wait_for_stop() { local pid=$(server_pid $1) # if the process is still running, wait for it to stop if [ ! -z $pid ]; then while ps -p $pid > /dev/null; do sleep 0.1 done fi } # Sets a server's active/inactive state # $1: The ID of the server # $2: A string containing "active" or "inactive" server_set_active() { case "$2" in active) as_user ${server_user_name[$1]} "touch ${server_flag_active[$1]}" server_active[$1]="true" ;; inactive) as_user ${server_user_name[$1]} "rm -f ${server_flag_active[$1]}" server_active[$1]="false" ;; *) return 1 ;; esac } ### Jar Group Functions # Lists the jar files grouped by jar groups. jargroup_list() { for group in $(ls -1 "$JAR_STORAGE_PATH"); do echo "${group}:" for jar in $(ls -1r $JAR_STORAGE_PATH/$group); do if [[ $jar =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then echo " $jar" fi done done } # Creates a new jargroup # $1: The name for the jargroup jargroup_create() { if is_valid_name "$1"; then if [[ ! -e "$JAR_STORAGE_PATH/$1" ]]; then printf "Creating jar group... " local error=$(as_user_stderr "$USERNAME" "mkdir -p '$JAR_STORAGE_PATH/$1'") if [[ "$error" != "" ]]; then echo "Failed." echo "Reason: $error" return 1 fi error=$(as_user "$USERNAME" "echo \"$2\" > '$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET'") if [[ "$error" != "" ]]; then echo "Failed." echo "Reason: $error" return 1 fi echo "Done." # Download the latest version now jargroup_getlatest "$1" else echo "A jar group with that name already exists." return 1 fi else return 1 fi } # Downloads the latest version for a jargroup, using the target URL for that # group. Saves the download with the date and time encoded in the start of the # file name, in the jar group directory in question. Removes the file if there # is no difference between it and the current version. # $1: The jargroup name to download the latest version for jargroup_getlatest() { if is_valid_name "$1"; then if [[ -e "$JAR_STORAGE_PATH/$1" ]]; then if [[ -e "$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET" ]]; then printf "Downloading latest version... " # Try and make local error=$(as_user_stderr "$USERNAME" "mkdir -p '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'") if [[ "$error" != "" ]]; then echo "Failed." echo "Reason: $error" return 1 fi as_user "$USERNAME" "wget --quiet --trust-server-names --no-check-certificate --input-file='$JAR_STORAGE_PATH/$1/$JARGROUP_TARGET' --directory-prefix='$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'" echo "Done." local num_files=$(as_user "$USERNAME" "ls -1 '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR' | wc -l") if [[ $num_files == 1 ]]; then # There was 1 file downloaded local file_name=$(ls -1 "$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR") local new_name="$(date +%F-%H-%M-%S)-$file_name" local most_recent_jar=$(get_latest_file "$JAR_STORAGE_PATH/$1") if [[ ! -e "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then # There is not a previous version to do a comparison against, or # The previous version is different: # Add it to the group [[ -e "$most_recent_jar" ]] local was_previous=$? as_user "$USERNAME" "mv '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR/$file_name' '$JAR_STORAGE_PATH/$1/$new_name'" if [[ ! -z $most_recent_jar ]]; then echo "Downloaded version was different to previous latest. Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"." else echo "Saved as \"$JAR_STORAGE_PATH/$1/$new_name\"." fi else echo "Existing version \"$JAR_STORAGE_PATH/$1/$new_name\" was already up to date." fi elif [[ $num_files == 0 ]]; then # No file was downloaded echo "Failed. No files were downloaded." else # Multiple files were echo "Error. URL downloads multiple files." fi # Clean up the temp download folder as_user "$USERNAME" "rm -fr '$JAR_STORAGE_PATH/$1/$JARGROUP_DOWNLOAD_DIR'" else echo "Target URL not found, use $0 jargroup seturl " return 1 fi else echo "There is no jar group with the name \"$1\"." return 1 fi else return 1 fi } # Deletes an existing jargroup # $1: The name of the existing jargroup jargroup_delete() { if is_valid_name "$1"; then if [[ -e "$JAR_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete this jar group [y/N]: " read answer if [[ $answer =~ ^y|Y|yes$ ]]; then as_user "$USERNAME" "rm -rf $JAR_STORAGE_PATH/$1" echo "Jar group deleted." else echo "Jar group was NOT deleted." fi else echo "There is no jar group with the name \"$1\"." return 1 fi else return 1 fi } # Renames an existing jargroup # $1: The name of the existing jargroup # $2: The new name jargroup_rename() { if is_valid_name "$1"; then if [[ -e "$JAR_STORAGE_PATH/$1" ]]; then # If the jar group name is valid, # and there is no other jar group with the name $1 if is_valid_name "$2"; then if [[ -e "$JAR_STORAGE_PATH/$2" ]]; then echo "Could not be renamed, there is already a jar group with the name \"$2\"." return 1 else # TODO: Update any symbolic links which point to a jar in this directory as_user "$USERNAME" "mv '$JAR_STORAGE_PATH/$1' '$JAR_STORAGE_PATH/$2'" echo "Renamed jar group \"$1\" to \"$2\"." fi else return 1 fi else echo "There is no jar group with the name \"$1\"." return 1 fi else return 1 fi } ### Server Functions # Echos a list of servers in the SERVER_STORAGE_PATH server_list() { echo "Servers:" for server in "$SERVER_STORAGE_PATH/*"; do echo " $(basename $server)" done } # Creates a new server # $1: The server name to create server_create() { if is_valid_name "$1"; then if [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then echo "A server with that name already exists." return 1 else printf "Creating server directory... " as_user "$USERNAME" "mkdir -p '$SERVER_STORAGE_PATH/$1'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_WHITELIST'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_IPS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_BANNED_PLAYERS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_OPS'" as_user "$USERNAME" "touch '$SERVER_STORAGE_PATH/$1/$DEFAULT_PROPERTIES'" echo "Done." # Now that the new server has been created, we must call init again # to ensure it is recognised. init server_set_jar $(server_get_id "$1") "minecraft" fi else return 1 fi } # Deletes an existing server # $2: The server name to delete server_delete() { if is_valid_name "$1"; then if [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete this server and its worlds (note: backups are preserved) [y/N]: " read answer if [[ $answer =~ ^y|Y|yes$ ]]; then # TODO: stop the server if running first as_user "$USERNAME" "rm -rf '$SERVER_STORAGE_PATH/$1'" echo "Server deleted." else echo "Server was NOT deleted." fi else echo "There is no server with the name \"$1\"." return 1 fi else return 1 fi } # Renames an existing server # $1: The server name to change # $2: The new name for the server server_rename() { if is_valid_name "$1"; then if [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then # If the server name is valid and exists # TODO: Check that the server is not running first, return if it is if is_valid_name "$2"; then # If the server name is valid if [[ -e "$SERVER_STORAGE_PATH/$2" ]]; then # and there is not already a server with the name $2 echo "Could not be renamed, there is already a server with the name \"$2\"." return 1 else as_user "$USERNAME" "mv '$SERVER_STORAGE_PATH/$1' '$SERVER_STORAGE_PATH/$2'" echo "Renamed server \"$1\" to \"$2\"." fi else return 1 fi else echo "There is no server with the name \"$1\"." return 1 fi else return 1 fi } # Starts a single server # $1: The ID of the server server_start() { if server_is_running $1; then echo "Server \"${server_name[$1]}\" is already running!" else server_ensure_links $1 server_worlds_to_ram $1 local time_now=$(now) printf "Starting server... " as_user ${server_user_name[$1]} "cd ${server_path[$1]} && screen -dmS ${server_screen_name[$1]} ${server_invocation[$1]}" server_log_wait_for_line $1 "$time_now" "${server_confirm_start[$1]}" echo "Done." fi } # Sends the "save-all" command to a server # $1: The ID of the server server_save_all() { if server_is_running $1; then echo -n "Forcing save... " # Send the "save-all" command and wait for it to finish server_eval_and_wait $1 "save-all" "${server_confirm_save_all[$1]}" echo "Done." else echo "Server \"${server_name[$1]}\" is not running." fi } # Sends the "save-off" command to a server # $1: The ID of the server server_save_off() { if server_is_running $1; then echo -n "Disabling level saving... " # Send the "save-off" command and wait for it to finish server_eval_and_wait $1 "save-off" "${server_confirm_save_off[$1]}" echo "Done." # Writes any in-memory data manged by the kernel to disk sync else echo "Server \"${server_name[$1]}\" is not running." fi } # Sends the "save-on" command to a server # $1: The ID of the server server_save_on() { if server_is_running $1; then echo -n "Enabling level saving... " # Send the "save-on" command and wait for it to finish server_eval_and_wait $1 "save-on" "${server_confirm_save_on[$1]}" echo "Done." else echo "Server \"${server_name[$1]}\" is not running." fi } # Stops a single server after a delay # $1: The ID of the server server_stop() { if server_is_running $1; then # Change the state of the script STOP_COUNTDOWN[$id]="true" server_eval $id "say ${server_stop_message[$id]}" echo "Issued the warning \"${server_stop_message[$id]}\" to players." echo -n "Shutting down... " for ((i=${server_stop_delay[$id]}; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now $1 else echo "Server \"${server_name[$1]}\" is not running." fi } # Stops a single server right now # $1: The ID of the server server_stop_now() { if server_is_running $1; then server_save_all $1 echo -n "Stopping the server... " server_eval $1 "stop" STOP_COUNTDOWN[$1]="false" RESTART_COUNTDOWN[$1]="false" server_wait_for_stop $1 echo "Done." # Synchronise all worlds in RAM to disk server_worlds_to_disk $1 else echo "Server \"${server_name[$1]}\" is not running." fi } # Restarts a single server after a delay # $1: The ID of the server server_restart() { # Restarts the server if it is already running if server_is_running $1; then # Change the state of the script RESTART_COUNTDOWN[$id]="true" server_eval $id "say ${server_restart_message[$id]}" echo "Issued the warning \"${server_restart_message[$id]}\" to players." echo -n "Restarting... " for ((i=${server_stop_delay[$id]}; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now $1 fi server_start $1 } # Restarts a single server right away # $1: The ID of the server server_restart_now() { # Restarts the server if it is already running if server_is_running $1; then server_stop_now $1 fi server_start $1 } # List the worlds available for a server # $1: The ID of the server server_worlds_list() { local i=${server_world_offset[$1]} local max=$(( $i + ${server_num_worlds[$1]} )) # For each of the servers worlds: while [[ $i -lt $max ]]; do if ${world_inram[$i]}; then echo "[RAM] ${world_name[$i]}" else echo "[DSK] ${world_name[$i]}" fi i=$(( $i + 1 )) done } # Backs up the worlds for a server # $1: The ID of the server server_worlds_backup() { local i=${server_world_offset[$1]} local max=$(( $i + ${server_num_worlds[$1]} )) # For each of the servers worlds: while [[ $i -lt $max ]]; do world_backup $i i=$(( $i + 1 )) done } # Moves a servers log into another file, leaving the original log file empty # $1: The ID of the server server_log_roll() { # Moves and Gzips the logfile, a big log file slows down the # server A LOT (what was notch thinking?) printf "Rolling server logs... " if [ -e "${server_log[$1]}" ]; then file_name="${server_name[$1]}-$(date +%F-%H-%M-%S).log" as_user ${server_user_name[$1]} "mkdir -p \"${server_log_archive_path[$1]}\" && cp \"${server_log[$1]}\" \"${server_log_archive_path[$1]}/${file_name}\" && gzip \"${server_log_archive_path[$1]}/${file_name}\"" if [ -e "${server_log_archive_path[$1]}/${file_name}.gz" ]; then as_user ${server_user_name[$1]} "cp \"/dev/null\" ${server_log[$1]}" as_user ${server_user_name[$1]} "echo \"Previous logs can be found at \\\"${server_log_archive_path[$1]}\\\"\" > ${server_log[$1]}" else echoerr "Failed." return 1 fi fi echo "Done." } # Backups a server's directory # $1: The ID of the server server_backup() { echo -n "Backing up the entire server directory... " zip_flags="-rq" # Add the "y" flag if symbolic links should not be followed if [ "${server_complete_backup_follow_symlinks[$1]}" != "true" ]; then zip_flags="${zip_flags}y" fi # Zip up the server directory file_name="${server_backup_path[$1]}/$(date "+%F-%H-%M-%S").zip" as_user ${server_user_name[$1]} "mkdir -p \"${server_backup_path[$1]}\" && cd \"${server_path[$1]}\" && zip ${zip_flags} \"${file_name}\" ." echo "Done." } # Sets a server's jar file # $1: The ID of the server # $2: The name of the jar group # $3: Optionally, a specific jar to use. server_set_jar() { if [ -e "$JAR_STORAGE_PATH/$3" ]; then if [ -z "$3" ]; then # If a specific jar file is not mentioned # Download the latest version jargroup_getlatest "$2" local jar=$(get_latest_file "$JAR_STORAGE_PATH/$2") else # If a specific jar IS mentioned use that local jar="$JAR_STORAGE_PATH/$2/$3" if [[ ! -e "$jar" ]]; then echo "There is no jar named \"$3\" in jargroup \"$2\"." return 1 fi fi if [[ ! -z "$jar" ]]; then as_user "${server_user[$1]}" "ln -sf $jar ${server_jar[$1]}" echo "Server \"${server_name[$1]}\" is now using \"$jar\"." fi else echo "There is no jargorup named \"$2\"." fi } # Lists the players currently connected to a server # $1: The ID of the server server_connected() { if server_is_running $1; then local line=$(server_eval_and_get_line $1 "list" "Connected players:") # Cuts the start off the line, and the last three (invisible) # characters from the end. local players=${line:46:(-3)} if [ -z "$players" ]; then echo "No players are connected." else echo "$players" fi else echo "Server \"${server_name[$id]}\" is not running. No users are connected." fi } ### Manager Functions # Stops all running servers after a servers specified delay # $1: String containing "stop" or "restart". Represents whether the stop is # with a mind to stop the server, or just to restart it. And effects # the message issued to players on a server. manager_stop_all_servers() { # An array of true/false for each server local was_running # False if no servers were running at all local any_running="false" # For all running servers issue the stop warning local max_countdown=0 for ((server=0; server<${num_servers}; server++)); do if server_is_running $server; then any_running="true" was_running[$server]="true" STOP_COUNTDOWN[$server]="true" if [[ ${server_stop_delay[$i]} > $max_countdown ]]; then max_countdown=${server_stop_delay[$server]} fi # Send a warning message to the server case "$1" in stop) server_eval $server "say ${server_stop_message[$server]}";; restart) server_eval $server "say ${server_restart_message[$server]}";; esac # Send message to stdout echo "Server \"${server_name[$server]}\" was running, now stopping:" case "$1" in stop) echo " Issued the warning \"${server_stop_message[$server]}\" to players.";; restart) echo " Issued the warning \"${server_restart_message[$server]}\" to players.";; esac case ${server_stop_delay[$server]} in 0) echo " Stopping without delay.";; 1) echo " Stopping after 1 second.";; *) echo " Stopping after ${server_stop_delay[$server]} seconds.";; esac else echo "Server \"${server_name[$server]}\" was NOT running." was_running[$server]="false" fi done if "$any_running"; then # Wait for the maximum possible delay, stopping servers # at the correct times echo -n "All servers will have been issued the stop command... " for ((tick=${max_countdown}; tick>=0; tick--)); do tput sc # Save cursor position if [[ $tick -le 1 ]]; then echo -n "in $tick second." else echo -n "in $tick seconds." fi # Each second check all server, to see if its their time to # stop. If so issue the stop command, and don't hang. for ((server=0; server<${num_servers}; server++)); do if server_is_running $server; then stop_tick=$(( ${max_countdown} - ${server_stop_delay[$server]} )) if [[ $stop_tick == $tick ]]; then server_eval $server "stop" STOP_COUNTDOWN[$server]="false" fi fi done if [[ $tick > 0 ]]; then sleep 1 fi tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done # Start a new line echo "Now." # Finally check all servers have stopped for ((server=0; server<${num_servers}; server++)); do if "${was_running[$server]}"; then echo -n "Ensuring server \"${server_name[$server]}\" has stopped... " server_wait_for_stop $server echo "Done." fi done else echo "No servers were running." fi } # Stops all running servers without delay manager_stop_all_servers_now() { # An array of true/false for each server local was_running # False if no servers were running at all local any_running="false" # Stop all servers at the same time for ((server=0; server<${num_servers}; server++)); do if server_is_running $server; then was_running[$server]="true" any_running="true" echo "Server \"${server_name[$server]}\" was running, now stopping." server_eval $server "stop" else echo "Server \"${server_name[$server]}\" was NOT running." was_running[$server]="false" fi done if $any_running; then # Ensure all the servers have stopped for ((server=0; server<${num_servers}; server++)); do if ${was_running[$server]}; then echo -n "Ensuring server \"${server_name[$server]}\" has stopped... " server_wait_for_stop $server echo "Done." fi done else echo "No servers were running." fi } ### Main Functions init() { # These defaults can be overriden in the file specified by $CONFIG USERNAME="minecraft" SERVER_STORAGE_PATH="/opt/msm/servers" JAR_STORAGE_PATH="/opt/msm/jars" RAMDISK_STORAGE_PATH="/dev/shm/msm" WORLD_ARCHIVE_PATH="/opt/msm/archives/worlds" LOG_ARCHIVE_PATH="/opt/msm/archives/logs" BACKUP_ARCHIVE_PATH="/opt/msm/archives/backups" DEFAULT_SERVER_CONF="server.conf" DEFAULT_SERVER_USER="minecraft" DEFAULT_SCREEN_NAME="msm-{SERVER_NAME}" DEFAULT_WORLD_STORAGE_PATH="worldstorage" DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS="false" DEFAULT_LOG="server.log" DEFAULT_PROPERTIES="server.properties" DEFAULT_WHITELIST="white-list.txt" DEFAULT_BANNED_PLAYERS="banned-players.txt" DEFAULT_BANNED_IPS="banned-ips.txt" DEFAULT_OPS="ops.txt" DEFAULT_JAR="server.jar" DEFAULT_RAM="1024" DEFAULT_INVOCATION="java -Xms{RAM}M -Xmx{RAM}M -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -jar {JAR} nogui" DEFAULT_STOP_DELAY=10 DEFAULT_RESTART_DELAY=10 DEFAULT_STOP_MESSAGE="SERVER SHUTTING DOWN IN {DELAY} SECONDS!" DEFAULT_STOP_ABORT="Server shut down aborted." DEFAULT_RESTART_MESSAGE="SERVER REBOOT IN {DELAY} SECONDS!" DEFAULT_RESTART_ABORT="Server reboot aborted." DEFAULT_WORLD_BACKUP_STARTED="Backing up world." DEFAULT_WORLD_BACKUP_FINISHED="Backup complete." DEFAULT_COMPLETE_BACKUP_STARTED="Backing up entire server." DEFAULT_COMPLETE_BACKUP_FINISHED="Backup complete." DEFAULT_CONFIRM_SAVE_ON="CONSOLE: Enabling level saving.." DEFAULT_CONFIRM_SAVE_OFF="CONSOLE: Disabling level saving.." DEFAULT_CONFIRM_SAVE_ALL="CONSOLE: Save complete." DEFAULT_CONFIRM_START="Done" DEFAULT_CONFIRM_KICK="CONSOLE: Kicking " DEFAULT_CONFIRM_KICK_FAIL="Can't find user " DEFAULT_CONFIRM_TIME_SET="CONSOLE: Set time to" DEFAULT_CONFIRM_TIME_SET_FAIL="Unable to convert time value" DEFAULT_CONFIRM_TIME_ADD="CONSOLE: Added .+ to time" DEFAULT_CONFIRM_TIME_ADD_FAIL="Unable to convert time value" DEFAULT_CONFIRM_TOGGLEDOWNFALL="CONSOLE: Toggling downfall on|off for world" DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL=".\[31m;1mNo world exists with the name" DEFAULT_CONFIRM_GAMEMODE="CONSOLE: Setting .+ to game mode (1|0)" DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER="Can't find user .+" DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE=".+ already has game mode (1|0)" # Sourcing $CONFIG will override the previous defaults, with new values source $CONFIG local i=0 local j=0 for server in $(ls -1 "$SERVER_STORAGE_PATH"); do server_name[$i]="$server" server_path[$i]="$SERVER_STORAGE_PATH/$server" server_conf[$i]="${server_path[$i]}/$DEFAULT_SERVER_CONF" server_flag_active[$i]="${server_path[$i]}/$SERVER_FLAG_ACTIVE" server_backup_path[$i]="$BACKUP_ARCHIVE_PATH/${server_name[$i]}" server_log_archive_path[$i]="$LOG_ARCHIVE_PATH/${server_name[$i]}" if [[ -e "${server_flag_active[$i]}" ]]; then server_active[$i]="true" else server_active[$i]="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[$i]="$DEFAULT_SERVER_USER" server_screen_name[$i]="${DEFAULT_SCREEN_NAME//\{SERVER_NAME\}/${server_name[$i]}}" # Replace tags now, they cannot change server_world_storage[$i]="${server_path[$i]}/$DEFAULT_WORLD_STORAGE_PATH" server_log[$i]="${server_path[$i]}/$DEFAULT_LOG" server_properties[$i]="${server_path[$i]}/$DEFAULT_PROPERTIES" server_whitelist[$i]="${server_path[$i]}/$DEFAULT_WHITELIST" server_banned_players[$i]="${server_path[$i]}/$DEFAULT_BANNED_PLAYERS" server_banned_ips[$i]="${server_path[$i]}/$DEFAULT_BANNED_IPS" server_ops[$i]="${server_path[$i]}/$DEFAULT_OPS" server_jar[$i]="${server_path[$i]}/$DEFAULT_JAR" server_ram[$i]="$DEFAULT_RAM" server_invocation[$i]="$DEFAULT_INVOCATION" # Don't replace tags yet, they may change server_stop_delay[$i]="$DEFAULT_STOP_DELAY" server_restart_delay[$i]="$DEFAULT_RESTART_DELAY" server_stop_message[$i]="$DEFAULT_STOP_MESSAGE" server_stop_abort[$i]="$DEFAULT_STOP_ABORT" server_restart_message[$i]="$DEFAULT_RESTART_MESSAGE" server_restart_abort[$i]="$DEFAULT_RESTART_ABORT" server_world_backup_started[$i]="$DEFAULT_WORLD_BACKUP_STARTED" server_world_backup_finished[$i]="$DEFAULT_WORLD_BACKUP_FINISHED" server_complete_backup_started[$i]="$DEFAULT_COMPLETE_BACKUP_STARTED" server_complete_backup_finished[$i]="$DEFAULT_COMPLETE_BACKUP_FINISHED" server_confirm_save_on[$i]="$DEFAULT_CONFIRM_SAVE_ON" server_confirm_save_off[$i]="$DEFAULT_CONFIRM_SAVE_OFF" server_confirm_save_all[$i]="$DEFAULT_CONFIRM_SAVE_ALL" server_confirm_start[$i]="$DEFAULT_CONFIRM_START" server_confirm_kick[$i]="$DEFAULT_CONFIRM_KICK" server_confirm_kick_fail[$i]="$DEFAULT_CONFIRM_KICK_FAIL" server_confirm_time_set[$i]="$DEFAULT_CONFIRM_TIME_SET" server_confirm_time_set_fail[$i]="$DEFAULT_CONFIRM_TIME_SET_FAIL" server_confirm_time_add[$i]="$DEFAULT_CONFIRM_TIME_ADD" server_confirm_time_add_fail[$i]="$DEFAULT_CONFIRM_TIME_ADD_FAIL" server_confirm_toggledownfall[$i]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL" server_confirm_toggledownfall_fail[$i]="$DEFAULT_CONFIRM_TOGGLEDOWNFALL_FAIL" server_confirm_gamemode[$i]="$DEFAULT_CONFIRM_GAMEMODE" server_confirm_gamemode_fail_no_user[$i]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_USER" server_confirm_gamemode_fail_no_change[$i]="$DEFAULT_CONFIRM_GAMEMODE_FAIL_NO_CHANGE" server_complete_backup_follow_symlinks[$i]="$DEFAULT_COMPLETE_BACKUP_FOLLOW_SYMLINKS" # Load config overrides from server config file if present if [[ -e "${server_conf[$i]}" ]]; then local name local value while read line; do # ignore comment lines echo "$line" | grep "^#" >/dev/null 2>&1 && continue # if not empty, set the property using declare if [ ! -z "$line" ]; then name="$(echo $line | awk -F '=' '{print $1}')" value="$(echo $line | awk -F '\"' '{print $2}')" fi case "$name" in SERVER_USER) server_user_name[$i]="$value";; SCREEN_NAME) server_screen_name[$i]="$value";; WORLD_STORAGE_DIR) server_world_storage[$i]="${server_path[$i]}/$value";; LOG) server_log[$i]="${server_path[$i]}/$value";; PROPERTIES) server_properties[$i]="$value";; WHITELIST) server_whitelist[$i]="$value";; BANNED_PLAYERS) server_banned_players[$i]="$value";; BANNED_IPS) server_banned_ips[$i]="$value";; OPS) server_ops[$i]="$value";; JAR) server_jar[$i]="${server_path[$i]}/$value";; RAM) server_ram[$i]="$value";; INVOCATION) server_invocation[$i]="$value";; STOP_DELAY) server_stop_delay[$i]="$value";; RESTART_DELAY) server_restart_delay[$i]="$value";; STOP_MESSAGE) server_stop_message[$i]="$value";; STOP_ABORT) server_stop_abort[$i]="$value";; RESTART_MESSAGE) server_restart_message[$i]="$value";; RESTART_ABORT) server_restart_abort[$i]="$value";; WORLD_BACKUP_STARTED) server_world_backup_started[$i]="$value";; WORLD_BACKUP_FINISHED) server_world_backup_finished[$i]="$value";; COMPLETE_BACKUP_STARTED) server_complete_backup_started[$i]="$value";; COMPLETE_BACKUP_FINISHED) server_complete_backup_finished[$i]="$value";; CONFIRM_SAVE_ON) server_confirm_save_on[$i]="$value";; CONFIRM_SAVE_OFF) server_confirm_save_off[$i]="$value";; CONFIRM_SAVE_ALL) server_confrim_save_all[$i]="$value";; CONFIRM_START) server_confrim_start[$i]="$value";; CONFIRM_WHITELIST_LIST) server_confirm_whitelist_list[$i]="$value";; CONFIRM_KICK) server_confirm_kick[$i]="$value";; CONFIRM_KICK_FAIL) server_confirm_kick_fail[$i]="$value";; CONFIRM_TIME_SET) server_confirm_time_set[$i]="$value";; CONFIRM_TIME_SET_FAIL) server_confirm_time_set_fail[$i]="$value";; CONFIRM_TIME_ADD) server_confirm_time_add[$i]="$value";; CONFIRM_TIME_ADD_FAIL) server_confirm_time_add_fail[$i]="$value";; CONFIRM_TOGGLEDOWNFALL) server_confirm_toggledownfall[$i]="$value";; CONFIRM_TOGGLEDOWNFALL_FAIL) server_confirm_toggledownfall_fail[$i]="$value";; CONFIRM_GAMEMODE) server_confirm_gamemode[$i]="$value";; CONFIRM_GAMEMODE_FAIL_NO_USER) server_confirm_gamemode_fail_no_user[$i]="$value";; CONFIRM_GAMEMODE_FAIL_NO_CHANGE) server_confirm_gamemode_fail_no_change[$i]="$value";; COMPLETE_BACKUP_FOLLOW_SYMLINKS) server_complete_backup_follow_symlinks[$i]="$value";; esac done < "${server_conf[$i]}" fi # Replace tags in delay messages server_stop_message[$i]="${server_stop_message[$i]//\{DELAY\}/${server_stop_delay[$i]}}" server_restart_message[$i]="${server_restart_message[$i]//\{DELAY\}/${server_restart_delay[$i]}}" # Replace tags in server invocation server_invocation[$i]="${server_invocation[$i]//\{RAM\}/${server_ram[$i]}}" server_invocation[$i]="${server_invocation[$i]//\{JAR\}/${server_jar[$i]}}" # Load worlds if there is a world storage directory present server_world_offset[$i]=0 server_num_worlds[$i]=0 if [[ -e "${server_world_storage[$i]}" ]]; then server_worlds[$i]=$(ls -1 "${server_world_storage[$i]}") # Record the index at which worlds for this server start server_world_offset[$i]=$j for world in ${server_worlds[$i]}; do world_server_id[$j]="$i" world_name[$j]="$world" world_path[$j]="${server_world_storage[$i]}/${world_name[$j]}" world_link[$j]="${server_path[$i]}/${world_name[$j]}" world_flag_inram[$j]="${world_path[$j]}/$WORLD_FLAG_INRAM" world_backup_path[$j]="$WORLD_ARCHIVE_PATH/${server_name[$i]}/${world_name[$j]}" if [ ! -z $RAMDISK_STORAGE_PATH ]; then world_ramdisk_path[$j]="${RAMDISK_STORAGE_PATH}/${server_name[$i]}/${world_name[$j]}" fi if [[ -e "${world_path[$j]}/$WORLD_FLAG_INRAM" ]]; then world_inram[$j]="true" else world_inram[$j]="false" fi j=$(($j+1)) done # Record the number of worlds this server has server_num_worlds[$i]=$(( $j - ${server_world_offset[$i]} )) fi i=$(($i+1)) done num_servers=$i num_worlds=$j } # Called if the script is interrupted before exiting naturally interrupt() { local exit_message="false" for ((i=0; $i<$num_servers; i++)); do if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running $i; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval $i "say ${server_stop_abort[$i]}" echo "Server \"${server_name[$i]}\" shutdown was aborted." fi if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running $i; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval $i "say ${server_restart_abort[$i]}" echo "Server \"${server_name[$i]}\" restart was aborted." fi done exit } # The main function which starts the script main() { # Initialise variables that represent system state init # Trap interrupts to the script by calling the interrupt function trap interrupt EXIT case "$1" in start) # Required start option, for debian init.d scripts for ((server=0; server<${num_servers}; server++)); do # Only starts active servers if ${server_active[$server]}; then if server_is_running $server; then echo "[ACTIVE] Server \"${server_name[$server]}\" already started." else echo "[ACTIVE] Server \"${server_name[$server]}\" starting:" server_start $server fi else if server_is_running $server; then echo "[INACTIVE] Server \"${server_name[$server]}\" already started. It should not be running! Use \"$0 ${server_name[$server]} stop\" to stop this server." else echo "[INACTIVE] Server \"${server_name[$server]}\" leaving stopped, as this server is inactive." fi fi done ;; stop) if [ "$2" != "now" ]; then # If the the now flag is not specified manager_stop_all_servers "stop" else # If the now flag is specified manager_stop_all_servers_now fi ;; restart) # Required restart option, for debian init.d scripts # Stop any running servers, now if specified echo "Stopping servers:" main "stop" "$2" echo "Starting servers:" main "start" ;; server) case "$2" in list) # Lists the existing servers server_list ;; create) if [ -z "$3" ]; then # If a server name is not provided echo "Invalid command." else # Create a new server server_create "$3" fi ;; delete) if [ -z "$3" ]; then # If a server name is not provided echo "Invalid command." else # Delete an existing server, with confirmation server_delete "$3" fi ;; rename) if [ -z "$3" ] || [ -z "$4" ]; then # If a server name is not provided echo "Invalid command." else # Rename an existing server server_rename "$3" "$4" fi ;; *) # "server" is not a valid command echo "Invalid command." ;; esac ;; jargroup) case "$2" in list) # Lists the jars grouped by jargroup jargroup_list ;; create) if [ -z "$3" ] || [ -z "$4" ]; then echo "Invlaid command." else jargroup_create "$3" "$4" fi ;; delete) if [ -z "$3" ]; then echo "Invalid command." else jargroup_delete "$3" fi ;; rename) if [ -z "$3" ] || [ -z "$4" ]; then echo "Invalid command." else jargroup_rename $3 $4 fi ;; changetarget) if [ -z "$3" ] || [ -z "$4" ]; then echo "Invalid command." else jargroup_settarget "$3" "$4" fi ;; getlatest) if [ -z "$3" ]; then echo "Invalid command." else jargroup_getlatest "$3" fi ;; *) # "jargroup" is not a valid command echo "Invalid command." ;; esac ;; help) # Outputs a list of all commands echo -e "Usage: $0 command:" echo -e echo -e "--Setup Commands------------------------------------------------" echo -e " server list \t\t\t\t\tList servers" echo -e " server create \t\t\t\tCreates a new Minecraft server" echo -e " server delete \t\t\t\tDeletes an existing Minecraft server" echo -e " server rename \t\tRenames an existing Minecraft server" echo -e echo -e "--Server Mangement Commands-------------------------------------" echo -e " start \t\t\t\tStarts a server" echo -e " stop [now] \t\t\t\tStops a server after warning players, or right now" echo -e " restart [now] \t\t\tRestarts a server after warning players, or right now" echo -e " status \t\t\t\tShow the running/stopped status of a server" echo -e " connected \t\t\t\tList a servers connected players" echo -e " worlds list \t\t\t\tLists the worlds a server has" echo -e " worlds load \t\t\t\tCreates links to worlds in storage for a server" echo -e " worlds ram \t\t\tToggles a world's \"in RAM\" status" echo -e " worlds toram \t\t\tSynchronises any RAM enabled worlds to RAM a server has" echo -e " worlds todisk \t\t\tSynchronises any \"in RAM\" worlds to disk a server has" echo -e " worlds backup \t\t\tMakes a backup of all worlds a server has" echo -e " logroll \t\t\t\tMove a server log to a gziped archive, to reduce lag" echo -e " backup \t\t\t\tMakes a backup of an entire server directory" echo -e echo -e "--Server Pass Through Commands----------------------------------" echo -e " wl on|off \t\t\tEnabled/disable server whitelist check" echo -e " wl add|remove \t\tAdd/remove a player to/from a server's whitelist" echo -e " wl list \t\t\t\tList the players whitelisted for a server" echo -e " bl player add|remove \tBan/pardon a player from/for a server" echo -e " bl ip add|remove \tBan/pardon an IP address from/for a server" echo -e " bl list \t\t\t\tLists the banned players and IP address for a server" echo -e " op add|remove \t\tAdd/remove operator status for a player on a server" echo -e " gm survival|creative \tChange the game mode for a player on a server" echo -e " kick \t\t\tForcibly disconnect a player from a server" echo -e " say \t\t\tBroadcast a (pink) message to all players on a server" echo -e " time set|add \t\tSet/increment time on a server (0-24000)" echo -e " toggledownfall \t\t\tToggles rain and snow on a server" echo -e " save on|off \t\t\t\tEnable/disable writing world changes to file" echo -e " save all \t\t\t\tForce the writing of all non-saved world changes to file" echo -e " cmd \t\t\tSend a command string to the server and return" echo -e " cmdlog \t\t\tSame as 'cmd' but shows log output afterwards (Ctrl+C to exit)" echo -e echo -e "--Jar Commands--------------------------------------------------" echo -e " jargroup list \t\t\t\tList the stored jar files." echo -e " jargroup create \tCreate a new jar group, with a URL for new downloads" echo -e " jargroup delete \t\t\tDelete a jar group" echo -e " jargroup rename \t\tRename a jar group" echo -e " jargroup changeurl \tChange the download URL for a jar group" echo -e " jargroup getlatest \t\t\tDownload the latest jar file for a jar group" echo -e echo -e "--Global Commands-----------------------------------------------" echo -e " start \t\t\t\t\tStarts all active servers" echo -e " stop [now]\t\t\t\t\tStops all running servers" echo -e " restart [now]\t\t\t\t\tRestarts all active servers" ;; "") echo "No such command see: $0 help" ;; *) if [[ "$1" == "all" ]] || [[ -e "$SERVER_STORAGE_PATH/$1" ]]; then local id=$(server_get_id "$1") case "$2" in start) server_set_active $id "active" server_start $id ;; stop) server_set_active $id "inactive" if [[ $3 != "now" ]]; then server_stop $id else server_stop_now $id fi ;; restart) server_set_active $1 "active" if [[ $3 != "now" ]]; then server_restart $id else server_restart_now $id fi ;; status) if server_is_running $id; then echo "Server \"${server_name[$id]}\" is running." else echo "Server \"${server_name[$id]}\" is stopped." fi ;; connected) server_connected $1 ;; worlds) case "$3" in list) server_worlds_list $id ;; load) server_ensure_links $id ;; ram) if [ -z "$4" ]; then echo "Invalid command." else world_id=$(server_world_get_id $id "$4") if [ ! -z $world_id ]; then world_toggle_ramdisk_state $world_id else echo "Server \"${server_name[$id]}\" has no world with that name." fi fi ;; todisk) server_save_off $id server_save_all $id server_worlds_to_disk $id server_save_on $id ;; backup) if server_is_running $id; then server_eval $id "say ${server_world_backup_started[$id]}" server_save_off $id server_save_all $id fi server_worlds_to_disk $id server_worlds_backup $id if server_is_running $id; then server_save_on $id server_eval $id "say ${server_world_backup_finished[$id]}" fi echo "Backup took $SECONDS seconds". ;; *) echo "Invalid command." ;; esac ;; logroll) server_log_roll $id ;; backup) if server_is_running $id; then server_eval $id "say ${server_complete_backup_started[$id]}" server_save_off $id server_save_all $id fi server_worlds_to_disk $id server_backup $id if server_is_running $id; then server_save_on $id server_eval $id "say ${server_complete_backup_finished[$id]}" fi echo "Backup took $SECONDS seconds". ;; jar) if [ -z "$3" ]; then echo "Invalid command." else server_set_jar $id "$3" fi ;; whitelist|wl) case "$3" in on) if server_is_running $id; then server_eval $id "whitelist on" echo "Whitelist enabled" else echo "Server \"${server_name[$id]}\" is not running." fi ;; off) if server_is_running $id; then server_eval $id "whitelist off" echo "Whitelist disabled" else echo "Server \"${server_name[$id]}\" is not running." fi ;; add) if [ -z "$4" ]; then echo "Invalid command." else if server_is_running $id; then server_eval $id "whitelist add $4" echo "Added \"$4\" to the whitelist." else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; remove) if [ -z "$4" ]; then echo "Invalid command." else if server_is_running $id; then server_eval $id "whitelist remove $4" echo "Removed \"$4\" from the whitelist." else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; list) if server_is_running $id; then local players=$(cat ${server_whitelist[$id]}) if [ -z "$players" ]; then echo "No players are whitelisted." else echo "$players" fi else echo "Server \"${server_name[$id]}\" is not running." fi ;; *) echo "Invalid command." ;; esac ;; blacklist|bl) case "$3" in player) if [ -z "$5" ]; then echo "Invalid command." else case "$4" in add) for player in ${*:5}; do server_eval $id "ban $player" done if [[ $# > 5 ]]; then echo -n "Blacklisted the following players: " echo -n "$5" for player in ${*:6}; do echo -n ", $player" done echo "." else echo "Blacklisted \"$5\"." fi ;; remove) for player in ${*:5}; do server_eval $id "pardon $player" done if [[ $# > 5 ]]; then echo -n "Removed the following players from the blacklist: " echo -n "$5" for player in ${*:6}; do echo -n ", $player" done echo "." else echo "Removed \"$5\" from the blacklist." fi ;; *) echo "Invalid command." ;; esac fi ;; ip) case "$4" in add) for address in ${*:5}; do server_eval $id "ban-ip $address" done if [[ $# > 5 ]]; then echo -n "Blacklisted the following ip addresses: " echo -n "$5" for player in ${*:6}; do echo -n ", $address" done echo "." else echo "Blacklisted \"$5\"." fi ;; remove) for address in ${*:5}; do server_eval $id "pardon-ip $address" done if [[ $# > 5 ]]; then echo -n "Removed the following ip addresses from the blacklist: " echo -n "$5" for player in ${*:6}; do echo -n ", $address" done echo "." else echo "Removed \"$5\" from the ip blacklist." fi ;; *) echo "Invalid command." ;; esac ;; list) if server_is_running $id; then local players=$(cat ${server_banned_players[$id]}) local ips=$(cat ${server_banned_ips[$id]}) if [[ -z "$players" && -z "$ips" ]]; then echo "The blacklist is empty." else if [[ ! -z "$players" ]]; then echo "Players:" for name in $players; do echo " $name" done fi if [[ ! -z "$ips" ]]; then echo "IP Addresses:" for address in $ips; do echo " $address" done fi fi else echo "Server \"${server_name[$id]}\" is not running." fi ;; *) echo "Invalid command." ;; esac ;; operator|op) case "$3" in add) if server_is_running $id; then server_eval $id "op $4" echo "The player \"$4\" is now an operator for the server." else echo "Server \"${server_name[$id]}\" is not running." fi ;; remove) if server_is_running $id; then server_eval $id "deop $4" echo "The player \"$4\" is no longer an operator for the server." else echo "Server \"${server_name[$id]}\" is not running." fi ;; *) echo "Invalid command" ;; esac ;; gamemode|gm) case "$3" in survival|creative|0|1) if [ -z "$4" ]; then echo "Invalid command." else if server_is_running $id; then case "$3" in creative|1) local mode=1;; survival|0) local mode=0;; *) echoerr "Invalid mode"; exit 1;; esac local line=$(server_eval_and_get_line $id "gamemode $4 $mode" "${server_confirm_gamemode[$id]}" "${server_confirm_gamemode_fail_no_user[$id]}" "${server_confirm_gamemode_fail_no_change[$id]}") local regex="${LOG_REGEX} ${server_confirm_gamemode[$id]}" if [[ $line =~ $regex ]]; then echo "Changed game mode of \"$4\" to \"$3\"." fi local regex="${LOG_REGEX} ${server_confirm_gamemode_fail_no_user[$id]}" if [[ $line =~ $regex ]]; then echo "The player \"$4\" was not found to be logged on." fi local regex="${LOG_REGEX} ${server_confirm_gamemode_fail_no_change[$id]}" if [[ $line =~ $regex ]]; then echo "The player \"$4\" was already in \"$3\" mode." fi else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; *) echo "Invalid command." ;; esac ;; kick) if [ -z "$3" ]; then echo "Invalid command." else if server_is_running $id; then local line=$(server_eval_and_get_line $id "kick $3" "${server_confirm_kick[$id]}" "${server_confirm_kick_fail[$id]}") local regex="${LOG_REGEX} ${server_confirm_kick[$id]}" if [[ $line =~ $regex ]]; then echo "Kicked \"$3\" from game." fi local regex="${LOG_REGEX} ${server_confirm_kick_fail[$id]}" if [[ $line =~ $regex ]]; then echo "The player \"$3\" is not connected." fi else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; say) if [ -z "$3" ]; then echo "Invalid command." else if server_is_running $id; then server_eval $id "say ${*:3}" echo "Message sent to players." else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; time) case "$3" in set) if [ -z "$4" ]; then echo "Invalid command." else if server_is_running $id; then local line=$(server_eval_and_get_line $id "time set $4" "${server_confirm_time_set[$id]}" "${server_confirm_time_set_fail[$id]}") local regex="${LOG_REGEX} ${server_confirm_time_set[$id]}" if [[ $line =~ $regex ]]; then echo "Set time to \"$4\"." fi local regex="${LOG_REGEX} ${server_confirm_time_set_fail[$id]}" if [[ $line =~ $regex ]]; then echo "Unable to convert \"$4\" to a time." fi else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; add) if [ -z "$4" ]; then echo "Invalid command." else if server_is_running $id; then local line=$(server_eval_and_get_line $id "time add $4" "${server_confirm_time_add[$id]}" "${server_confirm_time_add_fail[$id]}") local regex="${LOG_REGEX} ${server_confirm_time_add[$id]}" if [[ $line =~ $regex ]]; then echo "Added \"$4\" to time." fi local regex="${LOG_REGEX} ${server_confirm_time_add_fail[$id]}" if [[ $line =~ $regex ]]; then echo "Unable to convert \"$4\" to a time." fi else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; *) echo "Invalid command." ;; esac ;; toggledownfall|tdf) if server_is_running $id; then local line=$(server_eval_and_get_line $id "toggledownfall $3" "${server_confirm_toggledownfall[$id]}" "${server_confirm_toggledownfall_fail[$id]}") local regex="${LOG_REGEX} ${server_confirm_toggledownfall[$id]}" if [[ $line =~ $regex ]]; then echo ${line:36:(-3)} fi local regex="${LOG_REGEX} ${server_confirm_toggledownfall_fail[$id]}" if [[ $line =~ $regex ]]; then echo ${line:34:(-3)} fi else echo "Server \"${server_name[$id]}\" is not running." fi ;; save) case "$3" in on) server_save_on $id ;; off) server_save_off $id ;; all) server_save_all $id ;; *) echo "Invalid command." ;; esac ;; cmd) if [ -z "$3" ]; then echo "Invalid command." else if server_is_running $id; then server_eval $id "${*:3}" echo "Command sent." else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; cmdlog) if [ -z "$3" ]; then echo "Invalid command." else if server_is_running $id; then server_eval $id "${*:3}" echo "Now watching logs (press Ctrl+C to exit):" as_user ${server_user_name[$id]} "tail --follow --lines=0 --sleep-interval=0.1 ${server_log[$id]}" else echo "Server \"${server_name[$id]}\" is not running." fi fi ;; console) if server_is_running $id; then as_user ${server_user_name[$1]} "screen -r ${server_screen_name[$1]}" else echo "Server \"${server_name[$id]}\" is not running." fi ;; *) echo "Invalid command." ;; esac else echo "No server with that name." fi ;; esac } ### Start point main "$@" exit 0