#!/bin/bash # Minecraft Server Manager # ======================== # # A single init script for managing multiple Minecraft servers. # Created by Marcus Whybrow # # https://github.com/msmhq/msm # ### BEGIN INIT INFO # Provides: msm # Required-Start: $local_fs $remote_fs # Required-Stop: $local_fs $remote_fs # Should-Start: $network # Should-Stop: $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: MSM: Minecraft init script # Description: Minecraft Server Manager, an init script for Minecraft/Bukkit servers ### END INIT INFO # See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for # more information on debian init.d scripts, which may help you understand # this script. # The Minecraft Server Manager version, use "msm version" to check yours. VERSION="0.9.4" # Source, if it exists, the msm profile.d script if [ -f "/etc/profile.d/msm.sh" ]; then source "/etc/profile.d/msm.sh" fi # $1: The file to follow links for follow_links() { unset RETURN local file="$1" while [[ -L "$file" ]]; do file="$(readlink "$file")" done RETURN="$file" } # Get real script file location follow_links "$0"; SCRIPT="$RETURN" # Get the MSM_CONF environment variable or use the default location CONF="${MSM_CONF:-/etc/msm.conf}" # Get the MSM_BASH_COMPLETION environment variable or use default location COMPLETION="${MSM_BASH_COMPLETION:-/etc/bash_completion.d/msm}" follow_links "$COMPLETION"; COMPLETION="$RETURN" ### Config variables the user should not need/want to change # Lazy allocation status ALLOCATED_SERVERS="false" ALLOCATED_WORLDS="false" # Global totals NUM_WORLDS=0 NUM_SERVERS=0 COMMAND_COUNT=0 SETTING_COUNT=0 SERVER_SETTING_COUNT=0 VERSIONS_COUNT=0 ### Utility Functions # Executes the command "$2" as user "$1" # $1: The user to execute the command as # $2: The command to execute as_user() { local user="$(whoami)" if [ "$user" == "$1" ]; then bash -c "$2" else if [ "$user" == "root" ]; then su - "$1" -s /bin/bash -c "$2" else if [[ "$1" == "root" ]]; then error_exit INVALID_USER "This command must be executed as the user \"$1\"." else error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"." fi 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 -e "$@" 1>&2 } COLOUR_PURPLE="\e[1;35m" COLOUR_RED="\e[1;31m" COLOUR_CYAN="\e[1;36m" COLOUR_GREEN="\e[1;32m" COLOUR_RESET="\e[0m" # Creates a coloured warning line # $1 The warning to echo msm_warning() { echoerr "${COLOUR_PURPLE}[MSM Warning: ${1}]${COLOUR_RESET}" } msm_error() { echoerr "${COLOUR_RED}[MSM Error: ${1}]${COLOUR_RESET}" } msm_info() { echo -e "${COLOUR_CYAN}[MSM Info: ${1}]${COLOUR_RESET}" } msm_success() { echo -e "${COLOUR_CYAN}[MSM: ${1}]${COLOUR_RESET}" } # Echoes the first non-empty string in the arguments list # $1->: Candidate strings for echoing echo_fallback() { for arg in "$@"; do [ -z "$arg" ] && continue echo "$arg" && break done } # $1: The string to echo if present echo_if() { [ ! -z "$1" ] && echo "$1" } # 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;; JAVA_NOT_INSTALLED) code=75;; esac echo "${2:-"Unknown Error"}" 1>&2 exit "${code:-$1}" } # Tests the bash version installed # $1: The bash version required is_bash_version() { if [[ "$BASH_VERSION" =~ ^$1 ]]; then return 0 fi return 1 } # Converts a string to be ready for use as a global # variable name. # $1: The string to convert # RETURN: The name in uppercase and with underscores to_global_name() { unset RETURN # Translate to uppercase, and replace dashes with underscores local result="$1" if is_bash_version 4; then # Much faster than the `tr` command result="${result//-/_}" result="${result//./_}" result="${result^^}" # to uppercase else result="$(echo "$result" | tr '[\-\.a-z]' '[\_\_A-Z]')" fi RETURN="$result" } # Converts a global BASH variable name to a server.properties file # variable name. # $1: The string to convert # RETURN: The name in lowercase and with dashes to_properties_name() { unset RETURN # Translate to uppercase, and replace dashes with underscores local result="$1" if is_bash_version 4; then # Much faster than the `tr` command result="${result//_/-}" result="${result,,}" # to lowercase else result="$(echo "$result" | tr '[\_A-Z]' '[\-a-z]')" fi RETURN="$result" } # A custom basename function which is faster # than opening a subshell # $1: The path to get the basename of # RETURN: The basename of the path quick_basename() { unset RETURN if [[ "$1" =~ \/([^\/]*)$ ]]; then RETURN="${BASH_REMATCH[1]}" fi } # A function used to print debug messages to stdout. Prevents messages from # appearing unless in debug mode, and allows debug statements to be easily # distinguished from necessary echo statements. # $1: The message to output debug() { manager_property DEBUG if [[ "$SETTINGS_DEBUG" == "true" ]]; then echoerr "$1" fi } # Determines whether "$1" is a valid name for a server or jar group directory # It must only contain upper or lower case letters, digits, dashes or # underscores. # It must also not be one of a list of reserved names. # $1: The name to check is_valid_name() { local valid="^[a-zA-Z0-9\_\-]+$" local invalid="^(start|stop|restart|version|server|jargroup|all|config|update|help|\-\-.*)$" if [[ "$1" =~ $valid ]]; then if [[ "$1" =~ $invalid ]]; then error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\", \"all\", \"config\", \"update\" or \"help\" or start with two dashes (--)." else return 0 fi else error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and underscores." fi } # Gets the latest jar from a jar group, based upon the date and time encoded # in the file name. # $1: The directory to search # RETURN: The latest file get_latest_file() { unset RETURN local best_time=0 local best_file="" while IFS= read -r -d $'\0' file; do # Remove the path, leaving just the file name local date_time="$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}')" # Get the time in seconds since 1970 from file name local seconds="$(date -d "$date_time" "+%s" 2> /dev/null)" # If that is newer than the current best, override variables if [[ "$seconds" -gt "$best_time" ]]; then best_time="$seconds" best_file="$file" fi done < <(find "$1" -maxdepth 1 -type f -print0) RETURN="$best_file" } # Returns the current time as a UNIX timestamp (in seconds since 1970) now() { date +%s } ### Log Utility Functions # Gets the UNIX timestamp for a server log line # $1: A server log line # returns: Time in seconds since 1970-01-01 00:00:00 UTC log_line_get_time() { time_string="$(echo "$1" | awk -F'[] [/:]+' '{print $1 " " $2 ":" $3 ":" $4}')" date -d "$time_string" "+%s" 2> /dev/null } ### World Utility Functions ### ----------------------- # Moves a world to RAM # $1: the ID of the world to move world_to_ram() { manager_property RAMDISK_STORAGE_ENABLED manager_property RAMDISK_STORAGE_PATH server_property "${WORLD_SERVER_ID[$1]}" USERNAME world_property "$1" RAMDISK_PATH world_property "$1" FLAG_INRAM world_property "$1" PATH if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"${WORLD_RAMDISK_PATH[$1]}\" && rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_PATH[$1]}/\" \"${WORLD_RAMDISK_PATH[$1]}\"" fi } # Moves a world in RAM to disk # $1: the ID of the world to move world_to_disk() { server_property "${WORLD_SERVER_ID[$1]}" USERNAME world_property "$1" FLAG_INRAM world_property "$1" RAMDISK_PATH world_property "$1" PATH as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\"" } # Toggles a worlds ram disk state # $1: The ID of the world world_toggle_ramdisk_state() { world_property "$1" FLAG_INRAM world_property "$1" RAMDISK_PATH local sid="${WORLD_SERVER_ID[$1]}" server_property "$sid" USERNAME if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then echo -n "Synchronising world \"${WORLD_NAME[$1]}\" to disk... " world_to_disk "$1" echo "Done." echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... " as_user "${SERVER_USERNAME[$sid]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\"" echo "Done." echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... " as_user "${SERVER_USERNAME[$sid]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\"" echo "Done." else echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... " as_user "${SERVER_USERNAME[$sid]}" "touch \"${WORLD_FLAG_INRAM[$1]}\"" echo "Done." echo -n "Copying world to RAM... " world_to_ram "$1" echo "Done." fi echo "Changes will only take effect after server is restarted." } # Backs up a world # $1: The ID of the world world_backup() { manager_property WORLD_ARCHIVE_ENABLED manager_property RDIFF_BACKUP_ENABLED manager_property RSYNC_BACKUP_ENABLED local server_id="${WORLD_SERVER_ID[$1]}" local containing_dir="$(dirname "${WORLD_PATH[$1]}")" local dir_name="$(basename "${WORLD_PATH[$1]}")" world_property "$1" PATH world_property "$1" BACKUP_PATH echo -n "Entering in backup function ... " if [[ "$SETTINGS_WORLD_ARCHIVE_ENABLED" == "true" ]]; then echo -n "Backing up world \"${WORLD_NAME[$1]}\"... " file_name="$(date "+%F-%H-%M-%S").zip" server_property "$server_id" USERNAME as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\"" echo "Done." fi if [[ "$SETTINGS_RDIFF_BACKUP_ENABLED" == "true" ]]; then echo -n "rdiff-backup world \"${WORLD_NAME[$1]}\"... " server_property "$server_id" USERNAME as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RDIFF_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup \"${dir_name}\" \"${RDIFF_BACKUP_PATH[$1]}\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup --remove-older-than \"$SETTINGS_RDIFF_BACKUP_ROTATION\"D --force \"${RDIFF_BACKUP_PATH[$1]}\"" echo "Done." fi if [[ "$SETTINGS_RSYNC_BACKUP_ENABLED" == "true" ]]; then echo -n "rsync-backup world \"${WORLD_NAME[$1]}\"... " file_name="$(date "+%F-%H-%M-%S")" server_property "$server_id" USERNAME as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RSYNC_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && rsync -aH --link-dest=\"${RSYNC_BACKUP_PATH[$1]}/latest\" \"${dir_name}\" \"${RSYNC_BACKUP_PATH[$1]}/${file_name}\" && rm -f \"${RSYNC_BACKUP_PATH[$1]}/latest\" && ln -s \"${file_name}\" \"${RSYNC_BACKUP_PATH[$1]}/latest\"" echo "Done." fi } # Activates a world # $1: The ID of the world world_activate() { server_property "${WORLD_SERVER_ID[$1]}" USERNAME server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_PATH world_property "$1" INACTIVE_PATH world_property "$1" ACTIVE_PATH if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... " local new_path="${SERVER_WORLD_STORAGE_PATH[${WORLD_SERVER_ID[$1]}]}" as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_INACTIVE_PATH[$1]}\" \"$new_path\"" echo "Done." else if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then echo "World \"${WORLD_NAME[$1]}\" is already activate." else error_exit DIR_NOT_FOUND "Directory \"${WORLD_INACTIVE_PATH[$1]}\" could not be found." fi fi } # Deactivates a world # $1: The ID of the world world_deactivate() { server_property "${WORLD_SERVER_ID[$1]}" USERNAME server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_INACTIVE_PATH world_property "$1" ACTIVE_PATH world_property "$1" INACTIVE_PATH world_property "$1" PATH if server_is_running "${WORLD_SERVER_ID[$1]}"; then error_exit 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="${SERVER_WORLD_STORAGE_INACTIVE_PATH[${WORLD_SERVER_ID[$1]}]}" as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_PATH[$1]}\" \"$new_path\"" echo "Done." else if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then echo "World \"${WORLD_NAME[$1]}\" is already deactivate." else error_exit DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found." fi fi fi } # Get the value of a world property # $1: The world ID # $2: The property name world_property() { # Get the current value eval local value=\"\${WORLD_$2[$1]}\" # If it is empty, then set it if [ -z "$value" ]; then local sid="${WORLD_SERVER_ID[$1]}" case "$2" in NAME|PATH) # Defined at allocation return 0 ;; ACTIVE_PATH) server_property "$sid" WORLD_STORAGE_PATH WORLD_ACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_PATH[$sid]}/${WORLD_NAME[$1]}" ;; INACTIVE_PATH) server_property "$sid" WORLD_STORAGE_INACTIVE_PATH WORLD_INACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/${WORLD_NAME[$1]}" ;; STATUS) world_property "$1" ACTIVE_PATH if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then WORLD_STATUS[$1]="active" else world_property "$1" INACTIVE_PATH if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then WORLD_STATUS[$1]="inactive" else WORLD_STATUS[$1]="unknown" fi fi ;; FLAG_INRAM) world_property "$1" PATH server_property "$sid" WORLDS_FLAG_INRAM WORLD_FLAG_INRAM[$1]="${WORLD_PATH[$1]}/${SERVER_WORLDS_FLAG_INRAM[$sid]}" ;; LINK) server_property "$sid" PATH WORLD_LINK[$1]="${SERVER_PATH[$sid]}/${WORLD_NAME[$1]}" ;; BACKUP_PATH) manager_property WORLD_ARCHIVE_PATH manager_property WORLD_RDIFF_PATH manager_property WORLD_RSYNC_PATH WORLD_BACKUP_PATH[$1]="$SETTINGS_WORLD_ARCHIVE_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}" RDIFF_BACKUP_PATH[$1]="$SETTINGS_WORLD_RDIFF_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}" RSYNC_BACKUP_PATH[$1]="$SETTINGS_WORLD_RSYNC_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}" ;; RAMDISK_PATH) manager_property RAMDISK_STORAGE_ENABLED # If the ram disk path is set, get the path for this world if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then manager_property RAMDISK_STORAGE_PATH WORLD_RAMDISK_PATH[$1]="${SETTINGS_RAMDISK_STORAGE_PATH}/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}" fi ;; INRAM) world_property "$1" FLAG_INRAM # Detect whether this world should be in ram if [[ -e "${WORLD_FLAG_INRAM[$1]}" ]]; then WORLD_INRAM[$1]="true" else WORLD_INRAM[$1]="false" fi ;; esac fi } # $1: The world ID world_dirty_properties() { local index # Removes properties for all servers if an index # is not specified if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then index="[$1]" else index="" fi unset WORLD_NAME$index unset WORLD_PATH$index unset WORLD_ACTIVE_PATH$index unset WORLD_INACTIVE_PATH$index unset WORLD_STATUS$index unset WORLD_FLAG_INRAM$index unset WORLD_LINK$index unset WORLD_BACKUP_PATH$index unset RDIFF_BACKUP_PATH$index unset RSYNC_BACKUP_PATH$index unset WORLD_RAMDISK_PATH$index unset WORLD_INRAM$index } ### Server Utility Functions ### ------------------------ # Returns the ID for a server. # An ID is given to a server when loaded into memory, and can be used to lookup # config information for that server # $1: The name of the server server_get_id() { unset RETURN for ((server=0; server<$NUM_SERVERS; server++)); do if [[ "${SERVER_NAME[$server]}" == "$1" ]]; then RETURN="$server" return 0 fi done error_exit NAME_NOT_FOUND "Could not find id for server name \"$1\"." } # Returns the ID of a server's world. # $1: The ID of the server # $2: The name of the world server_world_get_id() { server_property "$1" WORLD_STORAGE_PATH server_property "$1" WORLD_STORAGE_INACTIVE_PATH unset RETURN if [ -d "${SERVER_WORLD_STORAGE_PATH[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}/$2" ]; then # If the directory exists local start="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: for ((i=$start; i<$max; i++)); do if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then RETURN="$i" return 0 fi done fi error_exit NAME_NOT_FOUND "Could not find id for world \"$2\" for server \"${SERVER_NAME[$1]}\"." } # Returns 0 if the server $1 is running and 1 if not # $1: The ID of the server server_is_running() { server_property "$1" SCREEN_NAME server_property "$1" INVOCATION if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null then return 0 else return 1 fi } # Ensures the server has a jar file where it is expected to be # $1: The id of the server server_ensure_jar() { server_property "$1" JAR_PATH if [ -f "${SERVER_JAR_PATH[$1]}" ]; then return 0 fi error_exit FILE_NOT_FOUND "Could not find jar for server \"${SERVER_NAME[$1]}\": Expected \"${SERVER_JAR_PATH[$1]}\"." } # Read a value from the server configuration file # $1: The id of the server # $2: The setting name to read server_read_config() { unset RETURN # Convert name into uppercase with underscores # msm-setting => SERVER_SETTING # setting => SERVER_PROPERTIES_SETTING if [[ "$2" =~ ^msm\-(.*)$ ]]; then to_global_name "${BASH_REMATCH[1]}" else to_global_name "PROPERTIES_$2" fi local name="$RETURN" # Display the value of that setting unset RETURN server_property "$1" "$name" eval RETURN=\"\${SERVER_$name[$1]}\" } # Creates symbolic links in the server directory (SETTINGS_SERVER_STORAGE_PATH) for each # of the Minecraft worlds located in the worldstorage directory. # $1: The id of the server for which links should be ensured server_ensure_links() { server_property "$1" USERNAME server_property "$1" WORLD_STORAGE_PATH # Ensure a directory for level-name exists in worldstorage. # This allows a symlink to be created, and prevents new worlds # being generated outside of worldstorage. command_server_config "$1" "level-name" as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_WORLD_STORAGE_PATH[$1]}/$RETURN\"" server_worlds_allocate "$1" echo -n "Maintaining world symbolic links... " local start="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))" local output="false" for ((i=$start; i<$max; i++)); do world_property "$i" STATUS world_property "$i" LINK if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then # Remove the symbolic link if it exists as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" continue fi world_property "$i" INRAM # -L checks for the path being a link rather than a file # ! -a, since it is within double square brackets means: the negation of # the existence of the file. In other words: true if does not exist if [[ -L "${WORLD_LINK[$i]}" || ! -a "${WORLD_LINK[$i]}" ]]; then # If there is a symbolic link in the server directory to this world, # or there is not a directory in the server directory containing this world. # Get the original file path the symbolic link is pointing to # If there is no link, link_target will contain nothing link_target="$(readlink "${WORLD_LINK[$i]}")" if "${WORLD_INRAM[$i]}"; then # If this world is marked as loaded into RAM world_property "$i" RAMDISK_PATH if [ "${link_target}" != "${WORLD_RAMDISK_PATH[$i]}" ]; then # If the symbolic link does not point to the RAM version of the world # Remove the symbolic link if it exists as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" # Create a new symbolic link pointing to the RAM version of the world as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_RAMDISK_PATH[$i]}\" \"${WORLD_LINK[$i]}\"" fi else # Otherwise the world is not loaded into RAM, and is just on disk world_property "$i" PATH if [ "${link_target}" != "${WORLD_PATH[$i]}" ]; then # If the symbolic link does not point to the disk version of the world # Remove the symbolic link if it exists as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\"" # Create a new symbolic link pointing to the disk version of the world as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_PATH[$i]}\" \"${WORLD_LINK[$i]}\"" fi fi else echoerr -en "\n Error: Could not create link for world \"${WORLD_NAME[$i]}\". The file \"${WORLD_LINK[$i]}\" already exists, and should not be overwritten automatically. Either remove this file, or rename \"${WORLD_NAME[$i]}\"." output="true" fi done if [[ "$output" == "true" ]]; then echo -e "\nDone." else echo "Done." fi } # Moves a servers worlds into RAM # $1: The ID of the server server_worlds_to_ram() { manager_property RAMDISK_STORAGE_ENABLED # Only proceed if there is a ram disk path set in config if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then echo -n "Synchronising flagged worlds on disk to RAM... " local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do world_property "$i" INRAM world_property "$i" LINK if "${WORLD_INRAM[$i]}" && [ -L "${WORLD_LINK[$i]}" ]; then world_to_ram "$i" fi i="$(( $i + 1 ))" done echo "Done." fi } # Moves a servers "in RAM" worlds back to disk # $1: The ID of the server server_worlds_to_disk() { manager_property RAMDISK_STORAGE_ENABLED if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then echo -n "Synchronising worlds in RAM to disk... " local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: while [[ "$i" -lt "$max" ]]; do world_property "$i" RAMDISK_PATH if [ -d "${WORLD_RAMDISK_PATH[$i]}" ]; then world_to_disk "$i" fi i="$(( $i + 1 ))" done echo "Done." fi } # Watches a server's log for a specific line # $1: The ID for the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after # $3: The regex that matches log lines # $4: A timeout in seconds # returns: When the line is found server_log_get_line() { server_property "$1" USERNAME server_property "$1" LOG_PATH server_property "$1" CONSOLE_EVENT_REGEX unset RETURN local regex="${SERVER_CONSOLE_EVENT_OUTPUT_REGEX[$1]} ($3)" local timeout_deadline=$(( $(now) + $4 )) # Read log, break if nothing is read in $4 seconds while read -t $4 line; do line_time="$(log_line_get_time "$line")" # If the time is after the timeout deadline, break [[ "$(now)" -gt "$timeout_deadline" ]] && break # If the entry is old enough if [[ "$line_time" -ge "$2" ]] && [[ "$line" =~ $regex ]]; then # Return the line RETURN="${BASH_REMATCH[1]}" return 0 fi done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=20 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\" 2>/dev/null") } # The same as server_log_get_line, but prints a dot instead of the log line # to stdout, and returns when line is found. # $1: the ID of the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after # $3: The regex that matches log lines # $4: A timeout in seconds # returns: When the line is found server_log_dots_for_lines() { server_property "$1" USERNAME server_property "$1" LOG_PATH server_property "$1" CONSOLE_EVENT_REGEX local regex="${SERVER_CONSOLE_EVENT_OUTPUT_REGEX[$1]} ($3)" local timeout_deadline=$(( $(now) + $4 )) # Read log, break if nothing is read in $4 seconds while read -t $4 line; do line_time="$(log_line_get_time "$line")" # If the time is after the timeout deadline, break [[ "$(now)" -gt "$timeout_deadline" ]] && break # If the entry is old enough if [[ "$line_time" -ge "$2" ]]; then # Print a dot for this line echo -n '.' # and if it matches the regular expression, return if [[ "$line" =~ $regex ]]; then return 0 fi fi done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\" 2>/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() { server_property "$1" USERNAME server_property "$1" SCREEN_NAME as_user "${SERVER_USERNAME[$1]}" "screen -p 0 -S ${SERVER_SCREEN_NAME[$1]} -X eval 'stuff \"$2\"\015'" } # The same as server_eval, but also waits for a log entry before returning # $1: The ID of the server # $2: A line of text to enter into the server console # $3: The regex that matches log lines # $4: A timeout in seconds # RETURN: The full entry found in the logs server_eval_and_get_line() { unset RETURN time_now="$(now)" server_eval "$1" "$2" server_log_get_line "$1" "$time_now" "$3" "$4" RETURN="$RETURN" } # The same as server_eval_and_get_line, but does not set RETURN server_eval_and_wait() { server_eval_and_get_line "$@" unset RETURN # Do not return anything } # Executes a "version correct" command in a server's console. # If the command has output to watch for, then wait until that # output is found and return it, or until the timeout for that # command # $1: The ID of the server # $2: The name of the command # $3->: Command arguments in the form "argname=argvalue" # $RETURN: The output found, if any server_command() { unset RETURN # Load variables eval server_property $1 CONSOLE_COMMAND_OUTPUT_$2 eval server_property $1 CONSOLE_COMMAND_PATTERN_$2 eval server_property $1 CONSOLE_COMMAND_TIMEOUT_$2 eval local output_regex=\"\${SERVER_CONSOLE_COMMAND_OUTPUT_$2[$1]}\" eval local pattern=\"\${SERVER_CONSOLE_COMMAND_PATTERN_$2[$1]}\" # Replace arguments in pattern for arg in "${@:3}"; do if [[ "$arg" =~ (.*)=(.*) ]]; then pattern="${pattern//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}" output_regex="${output_regex//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}" fi done # If there is no output to watch for, execute the command immediately # and return immediately if [ -z "$output_regex" ]; then server_eval "$1" "$pattern" unset RETURN else # Otherwise execute the command and wait for the specified output # or the timeout eval local timeout=\"\${SERVER_CONSOLE_COMMAND_TIMEOUT_$2[$1]}\" server_eval_and_get_line "$1" "$pattern" "$output_regex" "$timeout" RETURN="$RETURN" fi } # Gets the process ID for a server if running, otherwise it outputs nothing # $1: The ID of the server server_pid() { server_property "$1" SCREEN_NAME server_property "$1" INVOCATION ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" | awk '{print $1}' } # Waits for a server to stop by polling 10 times a second # This approach is fairly intensive, so only use when you are expecting the # server to stop soon # $1: The ID of the server to wait for server_wait_for_stop() { local pid="$(server_pid "$1")" # if the process is still running, wait for it to stop if [ ! -z "$pid" ]; then while ps -p "$pid" > /dev/null; do sleep 0.1 done fi } # Sets a server's active/inactive state # $1: The ID of the server # $2: A string containing "active" or "inactive" server_set_active() { server_property "$1" USERNAME server_property "$1" FLAG_ACTIVE_PATH case "$2" in active) as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE_PATH[$1]}\"" SERVER_ACTIVE[$1]="true" ;; inactive) as_user "${SERVER_USERNAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE_PATH[$1]}\"" SERVER_ACTIVE[$1]="false" ;; *) error_exit INVALID_ARGUMENT "Invalid argument." ;; esac } ### Jar Group Functions ### ------------------- # Lists the jar files grouped by jar groups. jargroup_list() { manager_property JAR_STORAGE_PATH if [[ -d "${SETTINGS_JAR_STORAGE_PATH}" ]]; then local jargroup_name local jar_name while IFS= read -r -d $'\0' jargroup_path; do jargroup_name="$(basename "${jargroup_path}")" echo "$jargroup_name" while IFS= read -r -d $'\0' jar_path; do jar_name="$(basename "${jar_path}")" if [[ "$jar_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then echo " $jar_name" fi done < <(find "${SETTINGS_JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0) done < <(find "${SETTINGS_JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0) fi } # Creates a new jargroup # $1: The name for the jargroup # $2: The URL target for the jargroup jargroup_create() { if is_valid_name "$1"; then manager_property JAR_STORAGE_PATH manager_property USERNAME manager_property JARGROUP_TARGET if [[ ! -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then echo -n "Creating jar group... " local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p \"$SETTINGS_JAR_STORAGE_PATH/$1\"")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi error="$(as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET\"")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi echo "Done." else error_exit DUPLICATE_NAME "A jar group with that name already exists." fi fi } # Changes an existing jargroups target URL # $1: The jargroup name to change the url of # $2: The new target URL to set jargroup_changeurl() { manager_property JAR_STORAGE_PATH manager_property USERNAME manager_property JARGROUP_TARGET echo -n "Changing target URL... " local target="$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET" if [ -e "${target}" ]; then as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"${target}\"" echo "Done." else echo "Failed." error_exit FILE_NOT_FOUND "Could not find URL target file \"${target}\"" fi } # Downloads the latest version for a jargroup, using the target URL for that # group. Saves the download with the date and time encoded in the start of the # file name, in the jar group directory in question. Removes the file if there # is no difference between it and the current version. # $1: The jargroup name to download the latest version for jargroup_getlatest() { if is_valid_name "$1"; then manager_property JAR_STORAGE_PATH manager_property JARGROUP_TARGET manager_property USERNAME manager_property JARGROUP_DOWNLOAD_DIR if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then if [[ -f "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET" ]]; then printf "Downloading latest version... " # Try and make local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'")" if [[ "$error" != "" ]]; then echo "Failed." error_exit FILE_NOT_FOUND "$error" fi # test wget for --trust-server-names option local wget_opts="--trust-server-names" wget $wget_opts >/dev/null 2>&1 if [[ $? != 1 ]]; then wget_opts="" fi # If target contains the word 'minecraft' or 'minecraft-snapshot', check JSON version file for correct filename # This method allows for backwards compatibility with previous releases local target="$(as_user "$SETTINGS_USERNAME" "cat $SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET")" if [[ "$target" =~ ^minecraft ]]; then if [[ "$target" == "minecraft" ]]; then local versions_target="release" elif [[ "$target" == "minecraft-snapshot" ]]; then local versions_target="snapshot" fi printf "Checking minecraft version JSON... " local versions_url="http://s3.amazonaws.com/Minecraft.Download/versions/versions.json" local versions_file="/tmp/minecraft_versions.json" as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate -O '$versions_file' '$versions_url'" local latest_version=$(as_user "$SETTINGS_USERNAME" "sed -n '/"latest"/,/}/p' $versions_file | grep $versions_target | egrep -o '([0-9]+\.?)+|([0-9]+[a-zA-Z])+'") if [[ -n "$latest_version" ]]; then local jar_url="https://s3.amazonaws.com/Minecraft.Download/versions/$latest_version/minecraft_server.$latest_version.jar" fi fi if [[ -n "$jar_url" ]]; then as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' '$jar_url'" else as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --input-file='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET' --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'" fi echo "Done." local num_files="$(as_user "$SETTINGS_USERNAME" "ls -1 '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' | wc -l")" if [[ "$num_files" == 1 ]]; then # There was 1 file downloaded local file_name="$(ls -1 "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR")" local new_name="$(date +%F-%H-%M-%S)-$file_name" get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$1" local most_recent_jar="$RETURN" if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then # There is not a previous version to do a comparison against, or # The previous version is different: # Add it to the group [[ -f "$most_recent_jar" ]] local was_previous="$?" as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name' '$SETTINGS_JAR_STORAGE_PATH/$1/$new_name'" if [[ ! -z "$most_recent_jar" ]]; then echo "Downloaded version was different to previous latest. Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"." else echo "Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"." fi else echo "Existing version \"$most_recent_jar\" was already up to date." fi elif [[ "$num_files" == 0 ]]; then # No file was downloaded echo "Failed. No files were downloaded." else # Multiple files were echo "Error. URL downloads multiple files." fi # Clean up the temp download folder as_user "$SETTINGS_USERNAME" "rm -fr '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'" else error_exit FILE_NOT_FOUND "Target URL not found, use $0 jargroup seturl " fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } # Deletes an existing jargroup # $1: The name of the existing jargroup jargroup_delete() { if is_valid_name "$1"; then manager_property JAR_STORAGE_PATH manager_property USERNAME if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete this jar group [y/N]: " read answer if [[ "$answer" =~ ^y|Y|yes$ ]]; then as_user "$SETTINGS_USERNAME" "rm -rf \"$SETTINGS_JAR_STORAGE_PATH/$1\"" echo "Jar group deleted." else echo "Jar group was NOT deleted." fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } # Renames an existing jargroup # $1: The name of the existing jargroup # $2: The new name jargroup_rename() { if is_valid_name "$1"; then manager_property JAR_STORAGE_PATH manager_property USERNAME if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then # If the jar group name is valid, # and there is no other jar group with the name $1 if is_valid_name "$2"; then if [[ -e "$SETTINGS_JAR_STORAGE_PATH/$2" ]]; then error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"." else # TODO: Update any symbolic links which point to a jar in this directory as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1' '$SETTINGS_JAR_STORAGE_PATH/$2'" echo "Renamed jar group \"$1\" to \"$2\"." fi fi else error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." fi fi } ### Server Functions ### ---------------- # Echoes a list of servers in the SETTINGS_SERVER_STORAGE_PATH server_list() { if [ "$NUM_SERVERS" -gt 0 ]; then for ((server=0; server<$NUM_SERVERS; server++)); do server_property "$server" ACTIVE if "${SERVER_ACTIVE[$server]}"; then echo -n "[ ACTIVE ] " else echo -n "[INACTIVE] " fi echo -n "\"${SERVER_NAME[$server]}\" " if "${SERVER_ACTIVE[$server]}"; then if server_is_running "$server"; then echo "is running. Everything is OK." else echo "is stopped. Server is down!" fi else if server_is_running "$server"; then echo "is running. It should not be running!" else echo "is stopped. Everything is OK." fi fi done else echo "[There are no servers]" fi } # Creates a new server # $1: The server name to create server_create() { if is_valid_name "$1"; then manager_property USERNAME manager_property SERVER_STORAGE_PATH manager_property DEFAULT_WHITELIST_PATH manager_property DEFAULT_BANNED_IPS_PATH manager_property DEFAULT_BANNED_PLAYERS_PATH manager_property DEFAULT_OPS_PATH manager_property DEFAULT_OPS_LIST manager_property SERVER_PROPERTIES manager_property DEFAULT_WORLD_STORAGE_PATH manager_property JAR_STORAGE_PATH if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then error_exit DUPLICATE_NAME "A server with that name already exists." else printf "Creating server directory... " as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1'" as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WHITELIST_PATH'" as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_IPS_PATH'" as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_PLAYERS_PATH'" as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'" # Set default ops users as appropriate if [ ! -z "$SETTINGS_DEFAULT_OPS_LIST" ]; then IFS=","; for default_ops_user in $SETTINGS_DEFAULT_OPS_LIST; do as_user "$SETTINGS_USERNAME" "echo $default_ops_user | tr -d ' ' >> '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'" done fi as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES'" as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH'" as_user "$SETTINGS_USERNAME" "echo \"MSM requires all your worlds be moved into this directory.\" > '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH/readme.txt'" echo "Done." # Creates a server stub in memory, enough to use server_properties for. SERVER_NAME[$NUM_SERVERS]="$1" SERVER_PATH[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1" SERVER_CONF[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES" NUM_SERVERS=$(($NUM_SERVERS+1)) # TODO: Dirty all server variables, or don't allow further in script access # TODO: Handle server default setup stuff better than just using # the "minecraft" jar group. And make it configurable. if [ -d "$SETTINGS_JAR_STORAGE_PATH/minecraft" ]; then server_get_id "$1" server_set_jar "$RETURN" "minecraft" fi fi fi } # Deletes an existing server # $1: The server name to delete server_delete() { if is_valid_name "$1"; then manager_property SERVER_STORAGE_PATH manager_property USERNAME if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then printf "Are you sure you want to delete server \"$1\" and its worlds? (note: backups are preserved) [y/N]: " read answer if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then server_get_id "$1" local existing_id="$RETURN" if server_is_running "$existing_id"; then echo "Server \"$1\" is running." server_stop_now "$existing_id" fi as_user "$SETTINGS_USERNAME" "rm -rf '$SETTINGS_SERVER_STORAGE_PATH/$1'" echo "Server deleted." else echo "Server was NOT deleted." fi else error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"." fi fi } # Renames an existing server # $1: The server name to change # $2: The new name for the server server_rename() { if is_valid_name "$1"; then manager_property SERVER_STORAGE_PATH manager_property USERNAME if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]; then # If the server name is valid and exists server_get_id "$1" local existing_id="$RETURN" if server_is_running "$existing_id"; then error_exit SERVER_RUNNING "Can only rename a stopped server." else if is_valid_name "$2"; then # If the server name is valid if [[ -e "$SETTINGS_SERVER_STORAGE_PATH/$2" ]]; then # and there is not already a server with the name $2 error_exit DUPLICATE_NAME "Could not be renamed, there is already a server with the name \"$2\"." else as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_SERVER_STORAGE_PATH/$1' '$SETTINGS_SERVER_STORAGE_PATH/$2'" echo "Renamed server \"$1\" to \"$2\"." fi fi fi else error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"." fi fi } # Starts a single server # $1: The ID of the server server_start() { server_property "$1" USERNAME server_property "$1" SCREEN_NAME server_property "$1" INVOCATION server_property "$1" CONSOLE_EVENT_START if server_is_running "$1"; then echo "Server \"${SERVER_NAME[$1]}\" is already running!" else if ! which java > /dev/null; then error_exit JAVA_NOT_INSTALLED "Could not start server as Java is not installed." fi server_ensure_jar "$1" server_ensure_links "$1" server_worlds_to_ram "$1" local time_now="$(now)" printf "Starting server..." # This is the important line! Let's start this server! as_user "${SERVER_USERNAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}" # Wait for the server to fully start server_log_dots_for_lines "$1" "$time_now" "${SERVER_CONSOLE_EVENT_OUTPUT_START[$1]}" "${SERVER_CONSOLE_EVENT_TIMEOUT_START[$1]}" if [[ -f "${SERVER_PATH[$1]}"/eula.txt ]]; then if ! grep -q -i 'eula=true' "${SERVER_PATH[$1]}"/eula.txt; then echo " Could not start the server as you first need to agree to an EULA. See eula.txt for more info." return fi fi 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... " server_command "$1" SAVE_ALL 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... " server_command "$1" SAVE_OFF echo "Done." # Writes any in-memory data managed 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... " server_command "$1" SAVE_ON echo "Done." else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Stops a single server after a delay # $1: The ID of the server server_stop() { server_property "$1" MESSAGE_STOP server_property "$1" STOP_DELAY if server_is_running "$1"; then # Change the state of the script STOP_COUNTDOWN[$1]="true" server_eval "$1" "say ${SERVER_MESSAGE_STOP[$1]}" echo "Issued the warning \"${SERVER_MESSAGE_STOP[$1]}\" to players." echo -n "Shutting down... " for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now "$1" else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Stops a single server right now # $1: The ID of the server server_stop_now() { if server_is_running "$1"; then server_save_all "$1" echo -n "Stopping the server... " server_eval "$1" "stop" STOP_COUNTDOWN[$1]="false" RESTART_COUNTDOWN[$1]="false" server_wait_for_stop "$1" echo "Done." # Synchronise all worlds in RAM to disk server_worlds_to_disk "$1" else echo "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Restarts a single server after a delay # $1: The ID of the server server_restart() { server_property "$1" MESSAGE_RESTART server_property "$1" RESTART_DELAY # Restarts the server if it is already running if server_is_running "$1"; then # Change the state of the script RESTART_COUNTDOWN[$1]="true" server_eval "$1" "say ${SERVER_MESSAGE_RESTART[$1]}" echo "Issued the warning \"${SERVER_MESSAGE_RESTART[$1]}\" to players." echo -n "Restarting... " for ((i="${SERVER_RESTART_DELAY[$1]}"; i>0; i--)); do tput sc # Save cursor position echo -n "in $i seconds." sleep 1 tput rc # Restore cursor to position of last `sc' tput el # Clear to end of line done echo -e "Now." server_stop_now "$1" fi server_start "$1" } # Restarts a single server right away # $1: The ID of the server server_restart_now() { # Restarts the server if it is already running if server_is_running "$1"; then server_stop_now "$1" fi server_start "$1" } # List the worlds available for a server # $1: The ID of the server server_worlds_list() { if [[ "${SERVER_NUM_WORLDS[$1]}" -eq 0 ]]; then echo "There are no worlds in worldstorage." return 0 fi local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: for ((i=$i; i<$max; i++)); do world_property "$i" INRAM if "${WORLD_INRAM[$i]}"; then echo "RAM ${WORLD_NAME[$i]}" else echo " ${WORLD_NAME[$i]}" fi done } # Backs up the worlds for a server # $1: The ID of the server server_worlds_backup() { local i="${SERVER_WORLD_OFFSET[$1]}" local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))" # For each of the servers worlds: for ((i=$i; i<$max; i++)); do world_property "$i" STATUS if [[ "${WORLD_STATUS[$i]}" == "active" ]]; then world_backup "$i" fi done } # Moves a servers log into another file, leaving the original log file empty # $1: The ID of the server server_log_roll() { server_property "$1" LOG_PATH server_property "$1" USERNAME server_property "$1" LOG_ARCHIVE_PATH # Moves and Gzips the logfile, a big log file slows down the # server A LOT # Creates the server log if not already present. Prevents errors. as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_LOG_PATH[$1]}\"" local log_lines="$(cat "${SERVER_LOG_PATH[$1]}" | wc -l )" if [ "$log_lines" -le '1' ]; then echo "No new log entries to roll. No change made." return 0 fi echo -n "Rolling server logs... " if [ -e "${SERVER_LOG_PATH[$1]}" ]; then file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log" as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_LOG_ARCHIVE_PATH[$1]}\" && cp \"${SERVER_LOG_PATH[$1]}\" \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\" && gzip \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\"" if [ -e "${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}.gz" ]; then as_user "${SERVER_USERNAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG_PATH[$1]}\"" as_user "${SERVER_USERNAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_ARCHIVE_PATH[$1]}\\\"\" > \"${SERVER_LOG_PATH[$1]}\"" else echo "Failed." error_exit LOGS_NOT_ROLLED "Logs were not rolled." fi fi echo "Done." } # Backups a server's directory # $1: The ID of the server server_backup() { manager_property SERVER_STORAGE_PATH server_property "$1" COMPLETE_BACKUP_FOLLOW_SYMLINKS server_property "$1" BACKUP_PATH server_property "$1" USERNAME echo -n "Backing up the entire server directory... " zip_flags="-rq" # Add the "y" flag if symbolic links should not be followed if [ "${SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]}" != "true" ]; then zip_flags="${zip_flags}y" fi # Zip up the server directory file_name="${SERVER_BACKUP_PATH[$1]}/$(date "+%F-%H-%M-%S").zip" as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$SETTINGS_SERVER_STORAGE_PATH\" && zip ${zip_flags} \"${file_name}\" \"${SERVER_NAME[$1]}\"" echo "Done." } # Sets a server's jar file # $1: The ID of the server # $2: The name of the jar group # $3: Optionally, a specific jar to use. server_set_jar() { manager_property JAR_STORAGE_PATH server_property "$1" JAR_PATH server_property "$1" USERNAME if [ -d "$SETTINGS_JAR_STORAGE_PATH/$2" ]; then if [ -z "$3" ]; then # If a specific jar file is not mentioned # Download the latest version jargroup_getlatest "$2" get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$2" local jar="$RETURN" else # If a specific jar IS mentioned use that local jar="$SETTINGS_JAR_STORAGE_PATH/$2/$3" if [[ ! -e "$jar" ]]; then error_exit NAME_NOT_FOUND "There is no jar named \"$3\" in jargroup \"$2\"." fi fi if [[ ! -z "$jar" ]]; then as_user "${SERVER_USERNAME[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR_PATH[$1]}\"" echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"." fi else error_exit NAME_NOT_FOUND "There is no jargroup named \"$2\"." fi } # Lists the players currently connected to a server # $1: The ID of the server server_connected() { if server_is_running "$1"; then server_command "$1" CONNECTED echo_fallback "$RETURN" "No players are connected." else echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected." fi } # Sets the value of a server property # $1: The ID of the server # $2: The name of the server property # $3: The value for the property server_set_property() { eval SERVER_$2[$1]=\"$3\" ### Changes to values before setting case "$2" in *_PATH) if [[ ! "$3" =~ ^/.+ ]]; then server_property "$1" PATH eval SERVER_$2[$1]=\"${SERVER_PATH[$1]}/$3\" fi ;; SCREEN_NAME) eval SERVER_$2[$1]=\"${SERVER_SCREEN_NAME[$1]//\{SERVER_NAME\}/${SERVER_NAME[$1]}}\" ;; MESSAGE_STOP) server_property "$1" STOP_DELAY eval SERVER_$2[$1]=\"${SERVER_MESSAGE_STOP[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}\" ;; MESSAGE_RESTART) server_property "$1" RESTART_DELAY eval SERVER_$2[$1]=\"${SERVER_MESSAGE_RESTART[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}\" ;; INVOCATION) server_property "$1" RAM server_property "$1" JAR_PATH eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}\" eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR_PATH[$1]}}\" ;; esac } # Get the value of a server property # $1: The ID of the server # $2: The name of the server property server_property() { local versionable_properties="LOG_PATH;WHITELIST_PATH;BANNED_PLAYERS_PATH;BANNED_IPS_PATH;OPS_PATH;OPS_LIST;" # Do nothing if we want to load a property handled # by a versioning file that is already loaded. if [[ "$2" =~ ^CONSOLE_ ]] && [ "${SERVER_VERSIONING_LOADED[$1]}" == "true" ]; then return 0 fi eval local value=\"\${SERVER_$2[$1]}\" if [ -z "$value" ]; then # If the value is empty it has not been loaded yet # These properties are not overridable case "$2" in NAME|PATH) # Defined at allocation return 0 ;; CONF) manager_property SERVER_PROPERTIES server_set_property "$1" "$2" "${SERVER_PATH[$1]}/$SETTINGS_SERVER_PROPERTIES" return 0 ;; VERSION_CONF) manager_property VERSIONING_STORAGE_PATH server_property "$1" VERSION get_closest_version "${SERVER_VERSION[$1]}" local version="$RETURN" if [[ "$version" == "unknown" ]]; then # Use the latest Minecraft version if there is no explicit setting if [[ -z "${VERSIONS_NEWEST_MINECRAFT_PATH}" ]]; then msm_warning "No version set for server, and no default found. Please use 'msm update' to download defaults" else if [[ ! "$arg" =~ logroll|config ]]; then msm_info "Assuming 'minecraft/${VERSIONS_NEWEST_MINECRAFT_VERSION}' for this server. You should override this value by adding 'msm-version=minecraft/x.x.x' to '${SERVER_CONF[$1]}' to make this message go away" fi SERVER_VERSION_CONF[$1]="${VERSIONS_NEWEST_MINECRAFT_PATH}" fi else SERVER_VERSION_CONF[$1]="${SETTINGS_VERSIONING_STORAGE_PATH}/${version}.${SETTINGS_VERSIONING_FILE_EXTENSION}" fi return 0 ;; BACKUP_PATH) manager_property BACKUP_ARCHIVE_PATH server_set_property "$1" "$2" "$SETTINGS_BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}" return 0 ;; LOG_ARCHIVE_PATH) manager_property LOG_ARCHIVE_PATH server_set_property "$1" "$2" "$SETTINGS_LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}" return 0 ;; ACTIVE) server_property "$1" FLAG_ACTIVE_PATH if [[ -e "${SERVER_FLAG_ACTIVE_PATH[$1]}" ]]; then server_set_property "$1" "$2" "true" else server_set_property "$1" "$2" "false" fi return 0 ;; esac # If its a command lookup or server path, load from versioning files if [[ "$2" =~ ^CONSOLE_ ]] || [[ "$versionable_properties" == *"$2;"* ]]; then server_property "$1" VERSION_CONF if [[ -f "${SERVER_VERSION_CONF[$1]}" ]]; then VERSIONING_SERVER_ID="$1" source "${SERVER_VERSION_CONF[$1]}" unset VERSIONING_SERVER_ID SERVER_VERSIONING_LOADED[$1]="true" fi if [[ "$2" =~ ^CONSOLE_ ]]; then return 0 fi fi # If not a non-overridable load from conf read_server_conf "$1" "$2" local target_varname=SERVER_$2[$1] if [ -z ${!target_varname} ]; then # if its still empty use the default value manager_property "DEFAULT_$2" server_set_property "$1" "$2" "\$SETTINGS_DEFAULT_$2" fi fi } #Checks server config for overriding value # $1: The ID of the server # $2: The name of the server property read_server_conf(){ server_property "$1" CONF to_properties_name "$2" local name="$RETURN" if [[ "$name" =~ ^properties\-(.*)$ ]]; then name="${BASH_REMATCH[1]}" else name="msm-$name" fi local from_conf="$(sed -rn "s/^$name=('|\"|)(.*)\1/\2/ip" "${SERVER_CONF[$1]}" | tail -n 1)" if [ ! -z "$from_conf" ]; then # If the value is found in the server conf file (server.properties) # then set that as value for the property server_set_property "$1" "$2" "$from_conf" fi } # $1: The server ID server_dirty_properties() { local index # Removes properties for all servers if an index # is not specified if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then index="[$1]" else index="" fi for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do eval unset SERVER_${SERVER_SETTING_NAME[$i]}$index done unset SERVER_CONF$index unset SERVER_BACKUP_PATH$index unset SERVER_LOG_ARCHIVE_PATH$index unset SERVER_ACTIVE$index } ### Manager Functions ### ----------------- # Stops all running servers after a servers specified delay # $1: String containing "stop" or "restart". Represents whether the stop is # with a mind to stop the server, or just to restart it. And affects # 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" server_property "$server" STOP_DELAY server_property "$server" MESSAGE_STOP server_property "$server" MESSAGE_RESTART if [[ "${SERVER_STOP_DELAY[$server]}" -gt "$max_countdown" ]]; then max_countdown="${SERVER_STOP_DELAY[$server]}" fi # Send a warning message to the server case "$1" in stop) server_eval "$server" "say ${SERVER_MESSAGE_STOP[$server]}";; restart) server_eval "$server" "say ${SERVER_MESSAGE_RESTART[$server]}";; esac # Send message to stdout echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:" case "$1" in stop) echo " Issued the warning \"${SERVER_MESSAGE_STOP[$server]}\" to players.";; restart) echo " Issued the warning \"${SERVER_MESSAGE_RESTART[$server]}\" to players.";; esac case "${SERVER_STOP_DELAY[$server]}" in 0) echo " Stopping without delay.";; 1) echo " Stopping after 1 second.";; *) echo " Stopping after ${SERVER_STOP_DELAY[$server]} seconds.";; esac else echo "Server \"${SERVER_NAME[$server]}\" was NOT running." was_running[$server]="false" fi done if "$any_running"; then # Wait for the maximum possible delay, stopping servers # at the correct times echo -n "All servers will have been issued the stop command... " for ((tick="${max_countdown}"; tick>=0; tick--)); do tput sc # Save cursor position if [[ "$tick" -le 1 ]]; then echo -n "in $tick second." else echo -n "in $tick seconds." fi # Each second check all servers, to see if it's their time to # stop. If so issue the stop command, and don't wait. 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 any_running="true" was_running[$server]="true" echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping." server_eval "$server" "stop" else echo "Server \"${SERVER_NAME[$server]}\" was NOT running." was_running[$server]="false" fi done if "$any_running"; then # Ensure all the servers have stopped for ((server=0; server<${NUM_SERVERS}; server++)); do if "${was_running[$server]}"; then echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... " server_wait_for_stop "$server" echo "Done." fi done else echo "No servers were running." fi } # Get the value of a global manager property # $1: The name of the property manager_property() { local from_conf="$(sed -rn "s/^$1=('|\"|)(.*)\1/\2/ip" "$CONF" | tail -n 1)" # If this property has not yet been loaded, load it: eval local loaded=\"\$LOADED_$1\" if [ ! -z "$loaded" ] && ! "$loaded"; then if [ ! -z "$from_conf" ]; then # Override the default value eval SETTINGS_$1=\"$from_conf\" fi # State that this property has now been loaded eval LOADED_$1=\"true\" fi } manager_dirty_properties() { for ((i=0; i<$SETTING_COUNT; i++)); do eval LOADED_${SETTING_NAME[$i]}=\"false\" done } manager_dirty_all() { manager_dirty_properties server_dirty_properties world_dirty_properties } ### Command Handler Functions ### ------------------------- # Starts all servers command_start() { # Required start option, for debian init.d scripts for ((server=0; server<${NUM_SERVERS}; server++)); do server_property "$server" ACTIVE # 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:" manager_stop_all_servers "restart" echo "Starting servers:" command_start } # Restarts all servers without delay command_restart_now() { echo "Stopping servers:" manager_stop_all_servers_now echo "Starting servers:" command_start } # Displays the MSM version command_version() { local version="$VERSION" if [ "${version:0:1}" -eq 0 ]; then version="$version Beta" fi echo "Minecraft Server Manager $version" } # Displays config values used by MSM command_config() { for ((i=0; i<$SETTING_COUNT; i++)); do manager_property "${SETTING_NAME[$i]}" echo -n "${SETTING_NAME[$i]}=\"" eval echo -n \"\$SETTINGS_${SETTING_NAME[$i]}\" echo '"' done } # Downloads latest versions of all MSM files command_update() { echo -n "Checking for updates to version ${VERSION}..." local any_files_updated="false" # Check flags, semi-colon ';' delimits flags for example # COMMAND_FLAGS could contain ";--noinput;--quiet;-q;-ni;" if [[ "$COMMAND_FLAGS" =~ \;--noinput\; ]]; then local noinput="true" fi manager_property UPDATE_URL manager_property USERNAME # Create the temp download directory local output_dir="/tmp/msmupdate" # Clean up the temp directory created for downloads cleanup() { as_user "$SETTINGS_USERNAME" "rm -rf \"${output_dir}\"" } # Remove the directory if it exists already cleanup # $1: The file name to download download_file() { local dir_name="$(dirname "${output_dir}/${1}")" as_user "${SETTINGS_USERNAME}" "mkdir -p \"${dir_name}\"" as_user "${SETTINGS_USERNAME}" "wget --quiet --no-check-certificate ${SETTINGS_UPDATE_URL}/$1 -O ${output_dir}/$1" } # $1: The newly download file (relative to download dir) # $2: The current file that may be overwritten # $RETURN: The "current file" path if it should be overwritten # since it is different to the new version compare_file() { unset RETURN local new_file # Make relative URLs absolute, using the download dir if [[ "$1" =~ ^/ ]]; then new_file="$1" else new_file="${output_dir}/$1" fi # If the new file path is wrong return [ ! -e "$new_file" ] && return 1 if [ -e "$2" ]; then if diff -q "$new_file" "$2" >/dev/null 2>/dev/null; then return 1 else RETURN="$2" fi fi } # Download the latest MSM script and check its version number download_file "init/msm" local latest_version="$(sed -rn "s/^VERSION=('|\"|)(.*)\1/\2/ip" "${output_dir}/init/msm" | tail -n 1)" # Download the other files if that version is different (implicitly better) to the current version if [[ "$VERSION" == "$latest_version" ]]; then echo " Already at latest version." else echo " $latest_version is available." fi ### BEGIN Fancy warnings echo -n "Checking if any files need to be updated..." download_file "bash_completion/msm" download_file "versioning/versions.txt" # Downloads all versioning files in the latest MSM version download_upstream_versions() { manager_property VERSIONING_FILE_EXTENSION while read line; do if [[ "$line" =~ ^([^#]{1}.*)$ ]]; then download_file "versioning/${BASH_REMATCH[1]}.${SETTINGS_VERSIONING_FILE_EXTENSION}" fi done < "${output_dir}/versioning/versions.txt" } # $returns: 0 if at least one file needs updating, 1 otherwise files_need_updating() { compare_file "bash_completion/msm" "$COMPLETION" [ ! -z "$RETURN" ] && return 0 compare_file "init/msm" "$SCRIPT" [ ! -z "$RETURN" ] && return 0 manager_property VERSIONING_STORAGE_PATH local version_name regex regex="/(([^/]+/[^/]+)\.[^/\.]*)$" while IFS= read -r -d $'\0' path; do if [[ "$path" =~ $regex ]]; then version_name="${BASH_REMATCH[1]}" version_name_without_ext="${BASH_REMATCH[2]}" compare_file "versioning/$version_name" "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" [ ! -z "$RETURN" ] && return 0 fi done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0) return 1 } files_need_creating() { [ ! -e "$COMPLETION" ] && return 0 [ ! -e "$SCRIPT" ] && return 0 manager_property VERSIONING_STORAGE_PATH local version_name while IFS= read -r -d $'\0' path; do if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then version_name_without_ext="${BASH_REMATCH[1]}" [ ! -e "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" ] && return 0 fi done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0) return 1 } download_upstream_versions local updating="false" local creating="false" files_need_updating && updating="true" files_need_creating && creating="true" if [[ "$updating" == "false" ]] && [[ "$creating" == "false" ]]; then echo " No. We're all done." return 0 else echo " Done." fi if [[ "$updating" == "true" ]]; then echo "Updating will overwrite the following files:" compare_file "init/msm" "$SCRIPT" [ ! -z "$RETURN" ] && echo " > The main MSM script: $SCRIPT" compare_file "bash_completion/msm" "$COMPLETION" [ ! -z "$RETURN" ] && echo " > The bash completion script: $COMPLETION" manager_property VERSIONING_STORAGE_PATH local version_name version_path regex regex="/(([^/]+/[^/]+)\.[^/\.]*)$" while IFS= read -r -d $'\0' path; do if [[ "$path" =~ $regex ]]; then version_name="${BASH_REMATCH[1]}" version_name_without_ext="${BASH_REMATCH[2]}" version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" compare_file "versioning/$version_name" "$version_path" [ ! -z "$RETURN" ] && echo " > Version file: $version_path" fi done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0) fi if [[ "$creating" == "true" ]]; then echo "Updating will create the following files:" [ ! -e "$SCRIPT" ] && echo " > The main MSM script: $SCRIPT" [ ! -e "$COMPLETION" ] && echo " > The bash completion script: $COMPLETION" manager_property VERSIONING_STORAGE_PATH local version_name version_path while IFS= read -r -d $'\0' path; do if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then version_name="${BASH_REMATCH[1]}" version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name}.${SETTINGS_VERSIONING_FILE_EXTENSION}" [ ! -e "$version_path" ] && echo " > Version file: $version_path" fi done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0) fi ### END Fancy warnings if [[ ! "$noinput" ]]; then echo -n "Do you want to continue [y/N]: " read answer else answer="y" fi if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then echo "Updating MSM to ${latest_version}:" # Overwrite bash completion file local created="false" compare_file "bash_completion/msm" "$COMPLETION" if [ ! -z "$RETURN" ] || [ ! -e "$COMPLETION" ]; then [ ! -e "$COMPLETION" ] && created="true" any_files_updated="true" local dir="$(dirname "$COMPLETION")" as_user "root" "mkdir -p \"${dir}\"" as_user "root" "mv -f \"${output_dir}/bash_completion/msm\" \"$COMPLETION\"" source "$COMPLETION" if "$created"; then echo " > Created: $COMPLETION" else echo " > Updated: $COMPLETION" fi fi # Overwrite the MSM script itself created="false" compare_file "init/msm" "$SCRIPT" if [ ! -z "$RETURN" ] || [ ! -e "$SCRIPT" ]; then [ ! -e "$SCRIPT" ] && created="true" any_files_updated="true" dir="$(dirname "$SCRIPT")" as_user "root" "mkdir -p \"${dir}\"" as_user "root" "mv -f \"${output_dir}/init/msm\" \"$SCRIPT\"" as_user "root" "chmod +x \"$SCRIPT\"" if "$created"; then echo " > Created: $SCRIPT" else echo " > Updated: $SCRIPT" fi fi # Overwrite the versioning files manager_property VERSIONING_STORAGE_PATH local version_name version_path regex regex="/(([^/]+/[^/]+)\.[^/\.]*)$" while IFS= read -r -d $'\0' path; do created="false" if [[ "$path" =~ $regex ]]; then version_name="${BASH_REMATCH[1]}" version_name_without_ext="${BASH_REMATCH[2]}" version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" compare_file "${output_dir}/versioning/$version_name" "$version_path" if [ ! -z "$RETURN" ] || [ ! -e "$version_path" ]; then [ ! -e "$version_path" ] && created="true" any_files_updated="true" dir="$(dirname ${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name})" as_user "root" "mkdir -p \"${dir}\"" as_user "root" "mv -f \"$path\" \"$version_path\"" as_user "root" "chmod +x \"$version_path\"" as_user "root" "chown ${SETTINGS_USERNAME}:${SETTINGS_USERNAME} \"$version_path\"" if "$created"; then echo " > Created: $version_path" else echo " > Updated: $version_path" fi fi fi done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0) echo "Done." else echo "MSM was not updated." fi cleanup # This script will now be replaced. So run the new script's # update code, in case there are new things to update that # this version of MSM does not know about yet. if [[ "$any_files_updated" == "true" ]]; then $0 update fi } # 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_changeurl() { jargroup_changeurl "$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 Management 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 " console Connects to the interactive console. Access may be limited" echo -e " config [ ] Lists server settings, or sets a specific setting." 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 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" echo -e " config Displays a list of the config values used by MSM" echo -e " update [--noinput] Replaces MSM files with the latest recommended versions" } # 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 in ram 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 in ram worlds back to disk for an individual server # $1: The server ID command_server_worlds_todisk() { if server_is_running "$1"; then server_save_off "$1" server_save_all "$1" fi server_worlds_to_disk "$1" if server_is_running "$1"; then server_save_on "$1" fi } # Makes a backup of all worlds for an individual server # $1: The server ID command_server_worlds_backup() { if server_is_running "$1"; then server_property "$1" MESSAGE_WORLD_BACKUP_STARTED server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_STARTED[$1]}" server_save_off "$1" server_save_all "$1" fi server_worlds_to_disk "$1" server_worlds_backup "$1" if server_is_running "$1"; then server_save_on "$1" server_property "$1" MESSAGE_WORLD_BACKUP_FINISHED server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_FINISHED[$1]}" fi echo "Backup took $SECONDS seconds". } # Enables a world to be used by its server # $1: The server ID # $2: The world ID command_server_worlds_on() { world_activate "$2" } # Disables a world from being used by its server, also prevents it from being # backed up with the other worlds. # $1: The server ID # $2: The world ID command_server_worlds_off() { world_deactivate "$2" } # Moves an individual server's log text to another file, leaving it empty # $1: The server ID command_server_logroll() { server_log_roll "$1" } # Makes a backup of an entire server directory # $1: The server ID command_server_backup() { if server_is_running "$1"; then server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_STARTED[$1]}" server_save_off "$1" server_save_all "$1" fi server_worlds_to_disk "$1" server_backup "$1" if server_is_running "$1"; then server_save_on "$1" server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_FINISHED[$1]}" fi echo "Backup took $SECONDS seconds". } # Sets an individual server's jar file to use when starting up # $1: The server ID # $2: The jar group name # $3: Optionally a specific jar file name which exists within that jargroup, if # not provided the latest version will be used. command_server_jar() { server_set_jar "$1" "$2" "$3" } # Turns a server's whitelist protection on # $1: The server ID command_server_whitelist_on() { if server_is_running "$1"; then server_command "$1" WHITELIST_ON echo_fallback "$RETURN" "Whitelist enabled." else command_server_config "$1" "white-list" "true" fi } # Turns a server's whitelist protection off # $1: The server ID command_server_whitelist_off() { if server_is_running "$1"; then server_command "$1" WHITELIST_OFF echo_fallback "$RETURN" "Whitelist disabled." else command_server_config "$1" "white-list" "false" fi } # Adds a player name to a server's whitelist # $1: The server ID # $2->: The player names command_server_whitelist_add() { # TODO: Support whitelisting multiple players (see blacklist player add) if server_is_running "$1"; then # Whitelist players for player in "${@:2}"; do server_command "$1" WHITELIST_ADD player="$player" echo_fallback "$RETURN" "Player $player is now whitelisted." done else server_property "$1" WHITELIST_PATH for player in "${@:2}"; do if ! grep "^$player\$" "${SERVER_WHITELIST_PATH[$1]}" >/dev/null; then echo "$player" >> "${SERVER_WHITELIST_PATH[$1]}" echo_fallback "$RETURN" "Player $player is now whitelisted." fi done fi } # Removes a player name from a server's whitelist # $1: The server ID # $2->: The player names command_server_whitelist_remove() { # TODO: Support multiple player names if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" WHITELIST_REMOVE player="$player" echo_fallback "$RETURN" "Player $player is no longer whitelisted." done else server_property "$1" WHITELIST_PATH for player in "${@:2}"; do sed -ri "/^$player\$/d" "${SERVER_WHITELIST_PATH[$1]}" echo_fallback "$RETURN" "Player $player is no longer whitelisted." done fi } # Displays a list of whitelisted players for an individual server # $1: The server ID command_server_whitelist_list() { server_property "$1" WHITELIST_PATH if [ -f "${SERVER_WHITELIST_PATH[$1]}" ]; then local players="$(cat "${SERVER_WHITELIST_PATH[$1]}")" if [ -z "$players" ]; then echo "No players are whitelisted." else echo "$players" fi else echo "No players are whitelisted." fi } # Adds player names to a server's ban list # $1: The server ID # $2->: The player names command_server_blacklist_player_add() { if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" BLACKLIST_PLAYER_ADD player="$player" echo_fallback "$RETURN" "Player $player is now blacklisted." done else server_property "$1" BANNED_PLAYERS_PATH for player in "${@:2}"; do if ! grep "^$player\$" "${SERVER_BANNED_PLAYERS_PATH[$1]}" >/dev/null; then echo "$player" >> "${SERVER_BANNED_PLAYERS_PATH[$1]}" echo "Player $player is now blacklisted." fi done fi } # Removes player names from a server's ban list # $1: The server ID # $2->: The player names command_server_blacklist_player_remove() { if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" BLACKLIST_PLAYER_REMOVE player="$player" echo_fallback "$RETURN" "Player $player is no longer blacklisted." done else server_property "$1" BANNED_PLAYERS_PATH for player in "${@:2}"; do sed -ri "/^$player\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}" echo "Player $player is no longer blacklisted." done fi } # Adds ip addresses to a server's ban list # $1: The server ID # $2->: The ip addresses command_server_blacklist_ip_add() { if server_is_running "$1"; then for address in "${@:2}"; do server_command "$1" BLACKLIST_IP_ADD address="$address" echo_fallback "$RETURN" "IP address $address is now blacklisted." done else server_property "$1" BANNED_IPS_PATH for address in "${@:2}"; do if ! grep "^$address\$" "${SERVER_BANNED_IPS_PATH[$1]}" >/dev/null; then echo "$address" >> "${SERVER_BANNED_IPS_PATH[$1]}" echo "IP address $address is now blacklisted." fi done fi } # Removes ip addresses to a server's ban list # $1: The server ID # $2->: The ip addresses command_server_blacklist_ip_remove() { if server_is_running "$1"; then for address in "${@:2}"; do server_command "$1" BLACKLIST_IP_REMOVE address="$address" echo_fallback "$RETURN" "IP address $address is no longer blacklisted." done else server_property "$1" BANNED_PLAYERS_PATH for address in "${@:2}"; do sed -ri "/^$address\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}" echo "IP address $address is no longer blacklisted." done fi } # Displays a server's banned player names and ip addresses # $1: The server ID command_server_blacklist_list() { server_property "$1" BANNED_PLAYERS_PATH server_property "$1" BANNED_IPS_PATH local players local ips if [ -f "${SERVER_BANNED_PLAYERS_PATH[$1]}" ]; then players="$(cat "${SERVER_BANNED_PLAYERS_PATH[$1]}")" fi if [ -f "${SERVER_BANNED_IPS_PATH[$1]}" ]; then ips="$(cat "${SERVER_BANNED_IPS_PATH[$1]}")" fi if [[ -z "$players" && -z "$ips" ]]; then echo "The blacklist is empty." else if [[ ! -z "$players" ]]; then echo "Players:" for name in $players; do echo " $name" done fi if [[ ! -z "$ips" ]]; then echo "IP Addresses:" for address in $ips; do echo " $address" done fi fi } # Adds a player name to a server's list of operators # $1: The server ID # $2->: The player name command_server_operator_add() { if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" OP_ADD player="$player" echo_fallback "$RETURN" "Player $player is now an operator." done else server_property "$1" OPS_PATH for player in "${@:2}"; do if ! grep "^$player\$" "${SERVER_OPS_PATH[$1]}" >/dev/null; then echo "$player" >> "${SERVER_OPS_PATH[$1]}" fi done fi if [[ $# -gt 2 ]]; then echo -n "The following players are now operators: " echo -n "$2" for player in "${@:3}"; do echo -n ", $player" done echo "." else echo "\"$2\" is now an operator." fi } # Removes a player name to a server's list of operators # $1: The server ID # $2: The player name command_server_operator_remove() { # TODO: Support multiple player names if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" OP_REMOVE player="$player" echo_fallback "$RETURN" "Player $player is no longer an operator." done else server_property "$1" OPS_PATH for player in "${@:2}"; do for player in "${@:2}"; do sed -ri "/^$player\$/d" "${SERVER_OPS_PATH[$1]}" done done fi if [[ $# -gt 2 ]]; then echo -n "The following players are no longer operators: " echo -n "$2" for player in "${@:3}"; do echo -n ", $player" done echo "." else echo "\"$2\" is no longer an operator." fi } # Displays a list of operators for an individual server # $1: The server ID command_server_operator_list() { server_property "$1" OPS_PATH if [ -f "${SERVER_OPS_PATH[$1]}" ]; then local players="$(cat "${SERVER_OPS_PATH[$1]}")" if [ ! -z "$players" ]; then echo "$players" return 0 fi fi echo "No players are operators." } # Sets the game mode for # $1: The server ID # $2: The game mode # $3->: The player name command_server_gamemode() { if server_is_running "$1"; then for player in "${@:3}"; do server_command "$1" GAMEMODE player="$player" mode="$2" echo_fallback "$RETURN" "No output found. It may have worked." done else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Kicks a connected player from a server # $1: The server ID # $2->: The player name command_server_kick() { if server_is_running "$1"; then for player in "${@:2}"; do server_command "$1" KICK player="$player" echo_fallback "$RETURN" "Player $player has been kicked." done else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Broadcasts a message to all connected players for a server # $1: The server ID # $2->: Words of the message, will be concatenated with spaces command_server_say() { if server_is_running "$1"; then server_command "$1" SAY message="${*:2}" echo_fallback "$RETURN" "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 server_command "$1" TIME_SET time="$2" echo_fallback "$RETURN" "Time set to $2." 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 server_command "$1" TIME_ADD time="$2" echo_fallback "$RETURN" "Time increased by $2." 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 server_command "$1" TOGGLEDOWNFALL echo_fallback "$RETURN" "Downfall toggled." 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 server_command "$1" GIVE player="$2" item="$3" amount="$4" damage="$5" local amount="x1" [ ! -z "$4" ] && amount="x$4" echo_fallback "$RETURN" "Given item $3 ${amount} to $2." 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 server_command "$1" XP player="$2" amount="$3" echo_fallback "$RETURN" "Given $3 experience to $2." 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 concatenated 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 concatenated with spaces command_server_cmdlog() { if server_is_running "$1"; then server_property "$1" LOG_PATH server_property "$1" USERNAME echo "Now watching logs (press Ctrl+C to exit):" echo "..." server_eval "$1" "${*:2}" as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=5 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]} 2>/dev/null" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Resumes a server's screen session (requires ssh-ed in as server user, using # the `su` command will not work.) # $1: The server ID command_server_console() { if server_is_running "$1"; then server_property "$1" USERNAME as_user "${SERVER_USERNAME[$1]}" "script -q -c 'screen -r ${SERVER_SCREEN_NAME[$1]}' /dev/null" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi } # Sets a parameter in the config file if it exists, otherwise inserts the # parameter. # $1: The server ID # $2: Optionally, a setting name # $3: Optionally, a value to set for $2 command_server_config() { # If both a setting name and value are given if [ ! -z "$2" ] && [ ! -z "$3" ]; then server_property "$1" CONF if [[ -f "${SERVER_CONF[$1]}" ]]; then if grep "$2" "${SERVER_CONF[$1]}" >/dev/null; then sed -i /$2=/s/.*/"$2=$3"/g "${SERVER_CONF[$1]}" else echo "$2=$3" >> "${SERVER_CONF[$1]}" fi if server_is_running "$1"; then echo "Changes to config may require a server restart to take effect: sudo $0 ${SERVER_NAME[$1]} restart"; fi fi return 0 fi # If only a setting name is given if [ ! -z "$2" ]; then server_read_config "$1" "$2" echo "$RETURN" fi # If no parameter name is given if [ -z "$2" ]; then # List all parameters for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do server_property "$1" "${SERVER_SETTING_NAME[$i]}" to_properties_name "${SERVER_SETTING_NAME[$i]}" eval echo "msm-$RETURN=\\\"\${SERVER_${SERVER_SETTING_NAME[$i]}[$1]}\\\"" done fi } ### Register Functions ### ------------------ # Registers a setting that can be defined in /etc/msm.conf # $1: Setting name to register # $2: Optionally a default value for this setting register_setting() { # Create the default version of the variable eval SETTINGS_$1=\"$2\" # State that the variable has not yet been loaded eval LOADED_$1=\"false\" # Keep track of the setting name in a list SETTING_NAME[$SETTING_COUNT]="$1" SETTING_COUNT=$(( $SETTING_COUNT + 1 )) } # Registers a setting that can be defined for each server # $1: Server setting name to register # $2: Optionally a default value register_server_setting() { register_setting "DEFAULT_$1" "$2" SERVER_SETTING_NAME[$SERVER_SETTING_COUNT]="$1" SERVER_SETTING_COUNT=$(( $SERVER_SETTING_COUNT + 1 )) } # Register possible settings register_settings() { register_setting DEBUG "false" register_setting USERNAME "minecraft" register_setting SERVER_STORAGE_PATH "/opt/msm/servers" register_setting JAR_STORAGE_PATH "/opt/msm/jars" register_setting VERSIONING_STORAGE_PATH "/opt/msm/versioning" register_setting VERSIONING_FILE_EXTENSION "sh" register_setting RAMDISK_STORAGE_ENABLED "true" register_setting RAMDISK_STORAGE_PATH "/dev/shm/msm" register_setting WORLD_ARCHIVE_ENABLED "true" register_setting WORLD_RDIFF_PATH "/opt/msm/rdiff-backup/worlds" register_setting RDIFF_BACKUP_ENABLED "false" register_setting RDIFF_BACKUP_NICE "19" register_setting RDIFF_BACKUP_ROTATION "7" register_setting UPDATE_URL "https://raw.githubusercontent.com/msmhq/msm/latest" register_setting WORLD_ARCHIVE_PATH "/opt/msm/archives/worlds" register_setting LOG_ARCHIVE_PATH "/opt/msm/archives/logs" register_setting BACKUP_ARCHIVE_PATH "/opt/msm/archives/backups" register_setting RSYNC_BACKUP_ENABLED "false" register_setting WORLD_RSYNC_PATH "/opt/msm/rsync/worlds" register_setting JARGROUP_TARGET "target.txt" register_setting JARGROUP_DOWNLOAD_DIR "downloads" register_setting SERVER_PROPERTIES "server.properties" register_server_setting USERNAME "minecraft" register_server_setting SCREEN_NAME "msm-{SERVER_NAME}" register_server_setting VERSION "unknown" register_server_setting WORLD_STORAGE_PATH "worldstorage" register_server_setting WORLD_STORAGE_INACTIVE_PATH "worldstorage_inactive" register_server_setting LOG_PATH "server.log" register_server_setting WHITELIST_PATH "white-list.txt" register_server_setting BANNED_PLAYERS_PATH "banned-players.txt" register_server_setting BANNED_IPS_PATH "banned-ips.txt" register_server_setting OPS_PATH "ops.txt" register_server_setting OPS_LIST "" register_server_setting JAR_PATH "server.jar" register_server_setting FLAG_ACTIVE_PATH "active" register_server_setting COMPLETE_BACKUP_FOLLOW_SYMLINKS "false" register_server_setting WORLDS_FLAG_INRAM "inram" register_server_setting RAM "1024" register_server_setting INVOCATION "java -Xms{RAM}M -Xmx{RAM}M -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -jar {JAR} nogui" register_server_setting STOP_DELAY "10" register_server_setting RESTART_DELAY "10" # Message that are displayed in-game by the server register_server_setting MESSAGE_STOP "SERVER SHUTTING DOWN IN {DELAY} SECONDS!" register_server_setting MESSAGE_STOP_ABORT "Server shut down aborted." register_server_setting MESSAGE_RESTART "SERVER REBOOT IN {DELAY} SECONDS!" register_server_setting MESSAGE_RESTART_ABORT "Server reboot aborted." register_server_setting MESSAGE_WORLD_BACKUP_STARTED "Backing up world." register_server_setting MESSAGE_WORLD_BACKUP_FINISHED "Backup complete." register_server_setting MESSAGE_COMPLETE_BACKUP_STARTED "Backing up entire server." register_server_setting MESSAGE_COMPLETE_BACKUP_FINISHED "Backup complete." # No need for defaults, values fall back on versioning file info register_server_setting CONFIRM_SAVE_ON register_server_setting CONFIRM_SAVE_OFF register_server_setting CONFIRM_SAVE_ALL register_server_setting CONFIRM_START register_server_setting CONFIRM_KICK register_server_setting CONFIRM_TIME_SET register_server_setting CONFIRM_TIME_ADD register_server_setting CONFIRM_TOGGLEDOWNFALL register_server_setting CONFIRM_GAMEMODE register_server_setting CONFIRM_GIVE register_server_setting CONFIRM_XP } # 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 derived # 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 case "$word" in "") regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* " ;; "") regex="${regex:0:${#regex}-1}( ((--|-)[^ ]+)( (--|-)[^ ]+)*)? " ;; *) regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") " ;; esac 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 variables in order to register this command COMMAND_SIGNATURE[$COMMAND_COUNT]="$1" COMMAND_REGEX[$COMMAND_COUNT]="$regex" COMMAND_HANDLER[$COMMAND_COUNT]="$2" COMMAND_COUNT=$(( $COMMAND_COUNT + 1 )) else error_exit FATAL_ERROR "Fatal error: Sorry about this, would you be so kind as to file a bug at http://git.io/2f_x-A and cite: \"Erroneous command regex '${regex}' for signature '${1}'\"" fi } # Match and call a command from user input # $*: User input call_command() { manager_property SERVER_STORAGE_PATH local args local space="\ " for arg in "$@"; do if [[ "$arg" =~ $space ]]; then args="$args\"$arg\" " else args="$args$arg " fi done if [ ${#args} -ge 1 ]; then args="${args:0:${#args}-1}" fi # Clear any command flags that might exist # Start it with the delimiter necessary later on COMMAND_FLAGS=";" for ((command=0; command<$COMMAND_COUNT; command++)); do if [[ "$args" =~ ${COMMAND_REGEX[$command]} ]]; then unset args local word_offset=1 local args local arg_offset=0 local sid=-1 local wid=-1 # Helper function to build the argument list # $1: The argument to push onto the list push_arg() { args[$arg_offset]="$1" arg_offset="$(( $arg_offset + 1 ))" } # The following loop builds a set of arguments to pass to the # matched command handler function. Rather than passing all args # given to the script, to the handler (which may contain constant # strings), it only includes variables. for word in ${COMMAND_SIGNATURE[$command]}; do # Whether a positional argument is a variable 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 # command 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 expects any string without spaces that # starts with one or two dashes: "--noinput -q" are examples. # All flags are consumed and stored in the COMMAND_FLAGS # variable. "") local num_flags=0 for potential_flag in "${@:$word_offset}"; do if [[ "$potential_flag" =~ ^(\-\-|\-)[^\ ]+$ ]]; then COMMAND_FLAGS="${COMMAND_FLAGS}${potential_flag};" num_flags=$(( $num_flags + 1 )) else # Stop processing words, since all flags must be # contiguous break fi done # We may have consumed more than one "word", the outer # loop expects us to only take one, so must correct for # this if we have take two words or more if [[ "$num_flags" -ge 2 ]]; then word_offset=$(( $word_offset + $num_flags - 1 )) fi ;; # 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 "$SETTINGS_SERVER_STORAGE_PATH/$specified_name" ]; then server_get_id "$specified_name" sid="$RETURN" fi fi if [[ "$sid" -eq "-1" ]]; then error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"." fi fi push_arg "$sid" ;; # The "" 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 error_exit 1 "Ill-defined command $*. Please file an issue by opening the following link: https://github.com/msmhq/msm/issues" fi if [[ "$sid" -eq "-2" ]]; then if [[ "$specified_name" == "all" ]]; then wid="world:all" else error_exit INVALID_ARGUMENT "When specifying \"all\" servers, \"all\" worlds must be specified also." fi fi if [[ "$sid" -ge "0" ]]; then if is_valid_name "$specified_name"; then server_property "$sid" WORLD_STORAGE_PATH server_property "$sid" WORLD_STORAGE_INACTIVE_PATH if [ -d "${SERVER_WORLD_STORAGE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/$specified_name" ]; then server_world_get_id "$sid" "$specified_name" wid="$RETURN" fi fi if [[ "$wid" -eq "-1" ]]; then error_exit NAME_NOT_FOUND "There is no world with the name \"$specified_name\"." fi push_arg "$wid" fi ;; esac word_offset=$(( $word_offset + 1 )) done # The argument list for the call to the command handler has been # built. But there are several ways to call a handler. Either just # once, or multiple times based upon if multiple servers or worlds # were specified. # This code block calls the handler for all possible servers and # all possible worlds. if [[ "$sid" == "server:all" ]] && [[ "$wid" == "world:all" ]]; then for ((j=0; j<$NUM_WORLDS; j++)); do # Replace server and world id place holders with actual id's local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//server:all/${WORLD_SERVER_ID[$j]}}" replaced_args[$k]="${args[$k]//world:all/$j}" done # Call the function with the specific replaced args ${COMMAND_HANDLER[$command]} "${replaced_args[@]}" done # Prevent the default singular call later on. unset COMMAND_FLAGS; return fi # This calls the handler for all possible servers, and preserves # all other arguments. if [[ "$sid" == "server:all" ]]; then for ((j=0; j<$NUM_SERVERS; j++)); do local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//server\:all/$j}" done ${COMMAND_HANDLER[$command]} "${replaced_args[@]}" done unset COMMAND_FLAGS; return fi # This calls the handlers for all possible worlds for a specific # server. if [[ "$sid" != "server:all" ]] && [[ "$wid" == "world:all" ]]; then for ((j=${SERVER_WORLD_OFFSET[$sid]}; j<${SERVER_NUM_WORLDS[$sid]}; j++)); do local replaced_args for k in ${!args[@]}; do replaced_args[$k]="${args[$k]//world:all/$j}" done ${COMMAND_HANDLER[$command]} "${replaced_args[@]}" done unset COMMAND_FLAGS; return fi # Otherwise it's a simple single call of the handler. ${COMMAND_HANDLER[$command]} "${args[@]}" unset COMMAND_FLAGS; return fi done echo "No such command. See $0 help" } # Defines every MSM command. register_commands() { # The following section registers commands to be available for use. The # register_command function accepts a command_signature and a # command_handler_function_name as positional arguments 1 and 2 # respectively. # # A command signature consists of multiple elements separated by spaces, # the available options are as follows: # # fixedstring Matches an argument containing the specified # characters, in this case the characters "fixedstring" # # 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 # # Matches a list of space separated flags, such as # "--noinput --quiet -p -d". Not passed as a positional # argument. Instead set as the value of COMMAND_FLAGS. # # 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 opposed to the "fixedstring" element # which is arbitrary. # # Variables passed to handler functions are of course positional and there # position matches the position of that element in the command signature. register_command "start" "command_start" register_command "stop" "command_stop" register_command "stop now" "command_stop_now" register_command "restart" "command_restart" register_command "restart now" "command_restart_now" register_command "version" "command_version" register_command "config" "command_config" register_command "update " "command_update" 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 changeurl " "command_jargroup_changeurl" 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 " console" "command_server_console" register_command " config" "command_server_config" register_command " config " "command_server_config" register_command " config " "command_server_config" 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" } # $1: Server path server_allocate() { unset RETURN # Get an ID for this new server local server_id="$NUM_SERVERS" # Store the path for this new server SERVER_PATH[$server_id]="$1" # Store the name for this server quick_basename "${SERVER_PATH[$server_id]}" SERVER_NAME[$server_id]="$RETURN" NUM_SERVERS=$(( $NUM_SERVERS + 1 )) RETURN="$server_id" } # $1: Server ID server_worlds_allocate() { local world_id # A server's worlds require contiguous ID's # thus they are loaded one after another all at once. # $1: Server ID # $2: World path world_allocate() { # Get an ID for this new world world_id="$NUM_WORLDS" # Store the path for this new world WORLD_PATH[$world_id]="$2" # Store the name for this world quick_basename "${WORLD_PATH[$world_id]}" WORLD_NAME[$world_id]="$RETURN" # Store the server ID this world belongs to WORLD_SERVER_ID[$world_id]="$1" NUM_WORLDS=$(( $NUM_WORLDS + 1 )) } server_property "$1" WORLD_STORAGE_PATH server_property "$1" WORLD_STORAGE_INACTIVE_PATH local world_name # Record the index at which worlds for this server will start SERVER_WORLD_OFFSET[$1]="$NUM_WORLDS" if [[ -d "${SERVER_WORLD_STORAGE_PATH[$1]}" ]]; then while IFS= read -r -d $'\0' path; do world_allocate "$1" "$path" done < <(find "${SERVER_WORLD_STORAGE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) fi if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" ]]; then while IFS= read -r -d $'\0' path; do world_allocate "$1" "$path" done < <(find "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) fi # Record the number of worlds this server has SERVER_NUM_WORLDS[$1]="$(( $NUM_WORLDS - ${SERVER_WORLD_OFFSET[$1]} ))" } # Allocates stub variables, in this context a stub is # enough data to be able to load in more data via # the *_property functions. allocate() { manager_property SERVER_STORAGE_PATH # Determine server names (but don't load them) if [ -d "$SETTINGS_SERVER_STORAGE_PATH" ]; then while IFS= read -r -d $'\0' path; do server_allocate "$path" server_worlds_allocate "$RETURN" done < <(find "$SETTINGS_SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0) fi } # Loads stub data for available versions load_versions() { manager_property USERNAME manager_property VERSIONING_STORAGE_PATH if [ -e "$SETTINGS_VERSIONING_STORAGE_PATH" ]; then local newest_minecraft_version="0.0.0" while IFS= read -r -d $'\0' path; do local dir="$(dirname "$path")" local file_name="$(basename "$path")" local version="${file_name%.*}" local version_type="$(basename "$dir")" # Determine the newest minecraft version if [[ "$version_type" == "minecraft" ]]; then _newest_version "$version" "$newest_minecraft_version" newest_minecraft_version="$RETURN" fi VERSIONS[$VERSIONS_COUNT]="${version_type}/$version" VERSIONS_PATH[$VERSIONS_COUNT]="$path" VERSIONS_COUNT=$(( $VERSIONS_COUNT + 1 )) done < <(find "$SETTINGS_VERSIONING_STORAGE_PATH" -mindepth 1 -type f -print0) # Record the latest minecraft version to use as a default if [[ "$newest_minecraft_version" == "0.0.0" ]]; then msm_warning "Could not find versioning files, please use 'msm update' to download them" else VERSIONS_NEWEST_MINECRAFT_VERSION="${newest_minecraft_version}" VERSIONS_NEWEST_MINECRAFT_PATH="${SETTINGS_VERSIONING_STORAGE_PATH}/minecraft/${newest_minecraft_version}.${SETTINGS_VERSIONING_FILE_EXTENSION}" fi else msm_warning "Could not find versioning files, please use 'msm update' to download them" fi } # $1: Version one # $2: Version two # $RETURN: The greater version _newest_version() { unset RETURN # Compare the major versions [].0.0 component_one=`echo $1 | awk -F'.' '{print $1}'` component_two=`echo $2 | awk -F'.' '{print $1}'` if [[ "$component_one" -lt "$component_two" ]]; then # Give up if the given major version is less than this one's RETURN="$2"; return 0 fi # Compare the minor versions 0.[].0 component_one=`echo $1 | awk -F'.' '{print $2}'` component_two=`echo $2 | awk -F'.' '{print $2}'` if [[ "$component_one" -lt "$component_two" ]]; then # Give up if the given minor version is less than this one's RETURN="$2"; return 0 fi # Compare the patch versions 0.0.[] component_one=`echo $1 | awk -F'.' '{print $3}'` component_two=`echo $2 | awk -F'.' '{print $3}'` if [[ "$component_one" -lt "$component_two" ]]; then # Give up if the given patch version is less than this one's RETURN="$2"; return 0 fi RETURN="$1" } # Checks available versions MSM supports and returns the # closes match. # $1: Version name preferred # $RETURN: The closest available version, older or equal # to the given version $1 get_closest_version() { unset RETURN local given_type="${1%/*}" local given_version="${1##*/}" local closest_version cv_val local v v_version v_type v_full v_val given_val closest_version="0.0.0" for ((v=0; v<$VERSIONS_COUNT; v++)); do v_full="${VERSIONS[$v]}" v_type="${v_full%/*}" v_version="${v_full##*/}" if [[ "$given_type" == "$v_type" ]]; then # If this version type is the same as the given type (i.e. "minecraft") # Then check the version is before or equal to this version: _newest_version "$given_version" "$v_version" if [[ "$RETURN" == "$given_version" ]]; then # This version is older than or equal to the given version _newest_version "$closest_version" "$v_version" if [[ "$RETURN" == "$v_version" ]]; then # This version is newer than or equal to the closest version closest_version="$v_version" fi fi fi done if [[ "$closest_version" == "0.0.0" ]]; then RETURN="unknown" else RETURN="${given_type}/${closest_version}" fi } # Called if the script is interrupted before exiting naturally interrupt() { local exit_message="false" for ((i=0; $i<$NUM_SERVERS; i++)); do if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval "$i" "say ${SERVER_MESSAGE_STOP_ABORT[$i]}" echo "Server \"${SERVER_NAME[$i]}\" shutdown was aborted." fi if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then if [[ "$exit_message" == "false" ]]; then echo -e "\nInterrupted..." exit_message="true" fi server_eval "$i" "say ${SERVER_MESSAGE_RESTART_ABORT[$i]}" echo "Server \"${SERVER_NAME[$i]}\" restart was aborted." fi done exit } ### Versioning Functions ### -------------------- # Sources another versioning file # $1: The name of the versioning file extends() { manager_property VERSIONING_STORAGE_PATH source "${SETTINGS_VERSIONING_STORAGE_PATH}/$1.${SETTINGS_VERSIONING_FILE_EXTENSION}" } # Defines a servers console event variables, VERSIONING_SERVER_ID # must be set before calling this function # $1: The name of the event # $2->: The log lines to accept as confirmation console_event() { # Build a regex with all lines in local lines="$2" for line in "${@:3}"; do lines="$lines|$line" done local event_name event_timeout if [[ "$1" =~ (.*):(.*) ]]; then # If there is a colon in the name, use that # to extract the included delay event_name="${BASH_REMATCH[1]}" event_timeout="${BASH_REMATCH[2]}" else event_name="$1" event_timeout="1" fi # Set server variable eval SERVER_CONSOLE_EVENT_OUTPUT_${event_name}[$VERSIONING_SERVER_ID]=\"$lines\" eval SERVER_CONSOLE_EVENT_TIMEOUT_${event_name}[$VERSIONING_SERVER_ID]=\"$event_timeout\" } # Defines a servers console command variables, VERSIONING_SERVER_ID # must be set before calling this function # $1: The name of the command # $2: The command pattern # $3->: The log lines to accept as confirmation console_command() { local command_name command_timeout if [[ "$1" =~ (.*):(.*) ]]; then # If there is a colon in the name, use that # to extract the included delay command_name="${BASH_REMATCH[1]}" command_timeout="${BASH_REMATCH[2]}" else command_name="$1" command_timeout="1" fi eval SERVER_CONSOLE_COMMAND_PATTERN_${command_name}[$VERSIONING_SERVER_ID]=\"$2\" # Build a regex with all lines in local lines="$3" for line in "${@:4}"; do lines="$lines|$line" done eval SERVER_CONSOLE_COMMAND_OUTPUT_${command_name}[$VERSIONING_SERVER_ID]=\"$lines\" eval SERVER_CONSOLE_COMMAND_TIMEOUT_${command_name}[$VERSIONING_SERVER_ID]=\"$command_timeout\" } # Defines a servers property variables, VERSIONING_SERVER_ID # must be set before calling this function # $1: The name of the property # $2: The value of the property set_property() { server_set_property "$VERSIONING_SERVER_ID" "$1" "$2" read_server_conf "$VERSIONING_SERVER_ID" "$1" } ### Starting Code ### ------------- # The main function which starts the script main() { register_settings register_commands load_versions allocate # Trap interrupts to the script by calling the interrupt function trap interrupt EXIT # This function call matches the user input to a registered command # signature, and then calls that command's handler function with positional # arguments containing any "variable" strings. call_command "$@" } ### Start point if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then # MSM was called from the command line main "$@" exit 0 else # MSM was sourced from another script. # Just register settings instead. register_settings load_versions allocate fi