diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 1c2e2d4..69da925 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -11,11 +11,94 @@ Maintainers: Change Log ---------- +### [0.8.11](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.10...0.8.11) + +* Fixed issue [#76][#76], where tab completing "help" or "update" would exit the terminal. +* Made "help" and "update" reserved names to prevent clashing with server names. + +[#76]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/76 + + +### [0.8.10](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.9...0.8.10) + +* Improved versioning to use the latest Minecraft version by default, and tell the user what's happening and how to control it themselves. Fixes issue [#79][#79]. +* Improved install script to also create the default minecraft user, and the default install location directories. + +[#79]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/79 + + +### [0.8.9](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.8...0.8.9) + +* Improved install script to update MSM without input + + +### [0.8.8](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.7...0.8.8) + +* Added an MSM install script. +* Fixed bug in update code which prevent cleaning download directory. + + +### [0.8.7](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.6...0.8.7) + +* Improved upon 0.8.6 to to prevent MSM complaing when not being invoked by either the "root" or MSM user. + + +### [0.8.6](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.5...0.8.6) + +* Prevent script complaing that `This command must be executed as the user "root"` after every command. Fixes [#77][#77] and [#78][#78]. + +[#77]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/77 +[#78]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/78 + + +### [0.8.5](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.4...0.8.5) + +* Fixed `msm update` output showing the "created files" section only when files were being *updated*. + + +### [0.8.4](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.3...0.8.4) + +* Fixed logic in `msm update` which allowed MSM to think everything was already updated. + + +### [0.8.3](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.2...0.8.3) + +* Fixed a bug in `msm update` checks, which caused a `command not found` error. + + +### [0.8.2](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.1...0.8.2) + +* Improved `msm update` output to make more sense. + + +### [0.8.1](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.8.0...0.8.1) + +* Added bash completion for `msm update` +* Improved `msm update` to work when upgrading from older versions, and handle updating the update code. + + +### [0.8.0](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.7.5...0.8.0) + +* Added the `msm update` command, which downloads and installs new versions of MSM. +* Added the `msm-version` server config setting. Add this to `server.properties` to allow MSM to interact correctly with your server (e.g. `msm-version=minecraft/1.3.1`.) +* Removed all confirmation settings, such as `DEFAULT_CONFIRM_SAVE_ON` and `msm-confirm-save-all` etc, this functionality is handled by new [versioning files][versioning-files]. +* Fixed issues [#43][#43], [#58][#58], [#59][#59], [#60][#60], [#61][#61], [#63][#63], [#67][#67] and [#68][#68]. By accomodating different Minecraft versions using versioning files. + +[#43]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/43 +[#58]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/58 +[#59]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/59 +[#60]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/60 +[#61]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/61 +[#63]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/63 +[#67]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/67 +[#68]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/68 +[versioning-files]: https://github.com/marcuswhybrow/minecraft-server-manager/tree/master/versioning + ### [0.7.5](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.7.4...0.7.5) * Fixed issue [#69][#69] where pressing tab after `msm config` or `msm start` would disconnect the terminal session. -[#64]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/69 +[#69]: https://github.com/marcuswhybrow/minecraft-server-manager/issues/69 ### [0.7.4](https://github.com/marcuswhybrow/minecraft-server-manager/compare/0.7.3...0.7.4) diff --git a/README.markdown b/README.markdown index 590a584..eb8c029 100644 --- a/README.markdown +++ b/README.markdown @@ -2,6 +2,14 @@ A single init script which makes running multiple Minecraft/Bukkit servers easier for us admins. +## Quick Debian Install + +Install MSM on a debian box using my install script: + + wget -qO- http://git.io/KxE3HQ | sh + +Have a read of the script first if calms you: http://git.io/KxE3HQ + ## Getting Started * [Install][install] MSM on your box. diff --git a/bash_completion/msm b/bash_completion/msm index c86c8b5..8a106e5 100644 --- a/bash_completion/msm +++ b/bash_completion/msm @@ -33,7 +33,7 @@ _msm() { if [ -d "$SETTINGS_SERVER_STORAGE_PATH" ]; then local servers="$(ls -1 "$SETTINGS_SERVER_STORAGE_PATH")" fi - options="help start stop restart version server jargroup all config $servers" + options="help start stop restart version update server jargroup all config $servers" else case "${COMP_WORDS[1]}" in stop|restart) @@ -68,7 +68,7 @@ _msm() { esac fi ;; - start|config) + start|config|help|update) # Do nothing, just don't execute the server logic ;; *) diff --git a/init/msm b/init/msm index 16e27c5..95f2f30 100755 --- a/init/msm +++ b/init/msm @@ -27,17 +27,32 @@ # this script. +# The Minecraft Server Manager version, use "msm version" to check yours. +VERSION="0.8.11" + + # 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}" - - -# The Minecraft Server Manager version, use "msm version" to check yours. -VERSION="0.7.5" +# 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 @@ -55,6 +70,7 @@ NUM_SERVERS=0 COMMAND_COUNT=0 SETTING_COUNT=0 SERVER_SETTING_COUNT=0 +VERSIONS_COUNT=0 ### Utility Functions @@ -70,7 +86,11 @@ as_user() { if [ "$user" == "root" ]; then su - "$1" -s /bin/bash -c "$2" else - error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"." + 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 } @@ -82,7 +102,45 @@ as_user_stderr() { # Echo to stderr echoerr() { - echo "$@" 1>&2 + 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}" +} + +# Echos 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 @@ -184,11 +242,11 @@ debug() { # $1: The name to check is_valid_name() { local valid="^[a-zA-Z0-9\_\-]+$" - local invalid="^(start|stop|restart|version|server|jargroup|all|config|\-\-.*)$" + 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\" or start with two dashes (--)." + 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 @@ -678,7 +736,8 @@ server_worlds_to_disk() { # Watches a server's log for a specific line # $1: The ID for the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after -# $3->: The line or lines in the log to wait for +# $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 @@ -688,28 +747,31 @@ server_log_get_line() { # Make sure there is a server log to check as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}" - while read line; do + local regex="${LOG_REGEX} ($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 - for search_line in "${@:3}"; do - local regex="${LOG_REGEX} ${search_line}" - # and matches the regular expression - if [[ "$line" =~ $regex ]]; then - RETURN="${line}" - return 0 - fi - done + 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 --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"") + done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=20 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"") } # The same as server_log_get_line, but prints a dot instead of the log line # to stdout, and retruns when line is found. # $1: the ID of the server # $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after -# $3->: The line or lines in the log to wait for +# $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 @@ -718,23 +780,26 @@ server_log_dots_for_lines() { # Make sure there is a server log to check as_user "${SERVER_USERNAME[$1]}" "touch ${SERVER_LOG_PATH[$1]}" - while read line; do + local regex="${LOG_REGEX} ($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 '.' - for search_line in "${@:3}"; do - local regex="${LOG_REGEX} ${search_line}" - # and matches the regular expression - if [[ "$line" =~ $regex ]]; then - # Return if this is the line being looked for - return 0 - fi - done + # 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 --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\"") } @@ -752,14 +817,15 @@ server_eval() { # The same as server_eval, but also waits for a log entry before returning # $1: The ID of the server # $2: A line of text to enter into the server console -# $3->: The line or lines of text in the log to wait for +# $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}" + server_log_get_line "$1" "$time_now" "$3" "$4" RETURN="$RETURN" } @@ -770,6 +836,48 @@ server_eval_and_wait() { 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() { @@ -1161,7 +1269,7 @@ server_start() { server_property "$1" USERNAME server_property "$1" SCREEN_NAME server_property "$1" INVOCATION - server_property "$1" CONFIRM_START + server_property "$1" CONSOLE_EVENT_START if server_is_running "$1"; then echo "Server \"${SERVER_NAME[$1]}\" is already running!" @@ -1173,8 +1281,12 @@ server_start() { 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]}" - server_log_dots_for_lines "$1" "$time_now" "${SERVER_CONFIRM_START[$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]}" echo " Done." fi @@ -1184,13 +1296,8 @@ server_start() { # $1: The ID of the server server_save_all() { if server_is_running "$1"; then - server_property "$1" CONFIRM_SAVE_ALL - echo -n "Forcing save... " - - # Send the "save-all" command and wait for it to finish - server_eval_and_wait "$1" "save-all" "${SERVER_CONFIRM_SAVE_ALL[$1]}" - + server_command "$1" SAVE_ALL echo "Done." else echo "Server \"${SERVER_NAME[$1]}\" is not running." @@ -1201,13 +1308,8 @@ server_save_all() { # $1: The ID of the server server_save_off() { if server_is_running "$1"; then - server_property "$1" CONFIRM_SAVE_OFF - echo -n "Disabling level saving... " - - # Send the "save-off" command and wait for it to finish - server_eval_and_wait "$1" "save-off" "${SERVER_CONFIRM_SAVE_OFF[$1]}" - + server_command "$1" SAVE_OFF echo "Done." # Writes any in-memory data manged by the kernel to disk @@ -1221,13 +1323,8 @@ server_save_off() { # $1: The ID of the server server_save_on() { if server_is_running "$1"; then - server_property "$1" CONFIRM_SAVE_ON - echo -n "Enabling level saving... " - - # Send the "save-on" command and wait for it to finish - server_eval_and_wait "$1" "save-on" "${SERVER_CONFIRM_SAVE_ON[$1]}" - + server_command "$1" SAVE_ON echo "Done." else echo "Server \"${SERVER_NAME[$1]}\" is not running." @@ -1468,17 +1565,8 @@ server_set_jar() { # $1: The ID of the server server_connected() { if server_is_running "$1"; then - server_eval_and_get_line "$1" "list" "Connected players:" - local line="$RETURN" - - # Cuts the start off the line - local players="${line:46}" - - # TODO: Use a reliable method of detecting when no players are online, - # and display a different message. - # This is currently hard as invisible characters make string length - # always non-zero, and also may be ommitted. - echo "$players" + 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 @@ -1496,7 +1584,14 @@ server_set_property() { # $1: The ID of the server # $2: The name of the server property server_property() { + # 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 @@ -1511,6 +1606,25 @@ server_property() { 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 + 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" + 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]}" @@ -1532,6 +1646,20 @@ server_property() { ;; esac + # If its a command lookup, load from versioning files + if [[ "$2" =~ ^CONSOLE_ ]]; 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 + + return 0 + fi + # If not a non-overridable load from conf to_properties_name "$2" local name="$RETURN" @@ -1558,12 +1686,13 @@ server_property() { ### Post-changes to varibales after loading - # If it is a path + # If it is a path make that path absolute if [[ "$2" =~ _PATH$ ]]; then server_property "$1" PATH eval SERVER_$2[$1]=\"${SERVER_PATH[$1]}/\${SERVER_$2[$1]}\" fi + # Replace any placeholders in a property we just loaded case "$2" in SCREEN_NAME) server_set_property "$1" "$2" "${SERVER_SCREEN_NAME[$1]//\{SERVER_NAME\}/${SERVER_NAME[$1]}}" @@ -1872,6 +2001,294 @@ command_config() { 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 verison 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, incase 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 @@ -1998,6 +2415,7 @@ command_help() { 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 @@ -2094,7 +2512,8 @@ command_server_worlds_todisk() { # $1: The server ID command_server_worlds_backup() { if server_is_running "$1"; then - server_eval "$1" "say ${SERVER_MESSAGE_WORLD_BACKUP_STARTED[$1]}" + server_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 @@ -2104,7 +2523,8 @@ command_server_worlds_backup() { if server_is_running "$1"; then server_save_on "$1" - server_eval "$1" "say ${SERVER_MESSAGE_WORLD_BACKUP_FINISHED[$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". @@ -2164,22 +2584,22 @@ command_server_jar() { # $1: The server ID command_server_whitelist_on() { if server_is_running "$1"; then - server_eval "$1" "whitelist on" + server_command "$1" WHITELIST_ON + echo_fallback "$RETURN" "Whitelist enabled." else command_server_config "$1" "white-list" "true" fi - echo "Whitelist enabled" } # Turns a server's whitelist protection off # $1: The server ID command_server_whitelist_off() { if server_is_running "$1"; then - server_eval "$1" "whitelist off" + server_command "$1" WHITELIST_OFF + echo_fallback "$RETURN" "Whitelist disabled." else command_server_config "$1" "white-list" "false" fi - echo "Whitelist disabled" } # Adds a player name to a server's whitelist @@ -2190,7 +2610,8 @@ command_server_whitelist_add() { if server_is_running "$1"; then # Whitelist players for player in "${@:2}"; do - server_eval "$1" "whitelist add $player" + server_command "$1" WHITELIST_ADD player="$player" + echo_fallback "$RETURN" "Player $player is now whitelisted." done else server_property "$1" WHITELIST_PATH @@ -2198,21 +2619,10 @@ command_server_whitelist_add() { 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 - - # Confirmation display - if [[ $# -gt 2 ]]; then - echo -n "Added the following players to the whitelist: " - echo -n "$2" - for player in "${@:3}"; do - echo -n ", $player" - done - echo "." - else - echo "Added \"$2\" to the whitelist." - fi } # Removes a player name from a server's whitelist @@ -2222,27 +2632,17 @@ command_server_whitelist_remove() { # TODO: Support multiple player names if server_is_running "$1"; then for player in "${@:2}"; do - server_eval "$1" "whitelist remove $player" + 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 - - # Confirmation display - if [[ $# -gt 2 ]]; then - echo -n "Removed the following players from the whitelist: " - echo -n "$2" - for player in "${@:3}"; do - echo -n ", $player" - done - echo "." - else - echo "Removed \"$2\" from the whitelist." - fi } # Displays a list of whitelisted players for an individual server @@ -2269,7 +2669,8 @@ command_server_whitelist_list() { command_server_blacklist_player_add() { if server_is_running "$1"; then for player in "${@:2}"; do - server_eval "$1" "ban $player" + server_command "$1" BLACKLIST_PLAYER_ADD player="$player" + echo_fallback "$RETURN" "Player $player is now blacklisted." done else server_property "$1" BANNED_PLAYERS_PATH @@ -2277,20 +2678,10 @@ command_server_blacklist_player_add() { 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 - - if [[ $# -gt 2 ]]; then - echo -n "Added the following players to the blacklist: " - echo -n "$2" - for player in "${@:3}"; do - echo -n ", $player" - done - echo "." - else - echo "Added \"$2\" to the blacklist." - fi } # Removes player names from a server's ban list @@ -2299,26 +2690,17 @@ command_server_blacklist_player_add() { command_server_blacklist_player_remove() { if server_is_running "$1"; then for player in "${@:2}"; do - server_eval "$1" "pardon $player" + 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 - - if [[ $# -gt 2 ]]; then - echo -n "Removed the following players from the blacklist: " - echo -n "$2" - for player in "${@:3}"; do - echo -n ", $player" - done - echo "." - else - echo "Removed \"$2\" from the blacklist." - fi } # Adds ip addresses to a server's ban list @@ -2327,7 +2709,8 @@ command_server_blacklist_player_remove() { command_server_blacklist_ip_add() { if server_is_running "$1"; then for address in "${@:2}"; do - server_eval "$1" "ban-ip $address" + 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 @@ -2335,20 +2718,10 @@ command_server_blacklist_ip_add() { 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 - - if [[ $# -gt 2 ]]; then - echo -n "Added the following ip addresses to the blacklist: " - echo -n "$2" - for address in "${@:3}"; do - echo -n ", $address" - done - echo "." - else - echo "Added \"$2\" to the blacklist." - fi } # Removes ip addresses to a server's ban list @@ -2357,26 +2730,17 @@ command_server_blacklist_ip_add() { command_server_blacklist_ip_remove() { if server_is_running "$1"; then for address in "${@:2}"; do - server_eval "$1" "pardon-ip $address" + 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 - - if [[ $# -gt 2 ]]; then - echo -n "Removed the following players from the blacklist: " - echo -n "$2" - for address in "${@:3}"; do - echo -n ", $address" - done - echo "." - else - echo "Removed \"$2\" from the blacklist." - fi } # Displays a server's banned player names and ip addresses @@ -2421,7 +2785,8 @@ command_server_blacklist_list() { command_server_operator_add() { if server_is_running "$1"; then for player in "${@:2}"; do - server_eval "$1" "op $player" + server_command "$1" OP_ADD player="$player" + echo_fallback "$RETURN" "Player $player is now an operator." done else server_property "$1" OPS_PATH @@ -2452,7 +2817,8 @@ command_server_operator_remove() { # TODO: Support multiple player names if server_is_running "$1"; then for player in "${@:2}"; do - server_eval "$1" "deop $player" + server_command "$1" OP_REMOVE player="$player" + echo_fallback "$RETURN" "Player $player is no longer an operator." done else server_property "$1" OPS_PATH @@ -2499,34 +2865,9 @@ command_server_operator_list() { # $3->: The player name command_server_gamemode() { if server_is_running "$1"; then - local mode line regex - case "$2" in - creative|1) mode=1;; - survival|0) mode=0;; - *) error_exit INVALID_ARGUMENT "Invalid gamemode type \"$2\" options are \"survival\", \"creative\", \"0\" or \"1\".";; - esac - - server_property "$1" CONFIRM_GAMEMODE - server_property "$1" CONFIRM_GAMEMODE_FAIL_NO_USER - server_property "$1" CONFIRM_GAMEMODE_FAIL_NO_CHANGE - for player in "${@:3}"; do - server_eval_and_get_line "$1" "gamemode $player $mode" "${SERVER_CONFIRM_GAMEMODE[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}" - - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "Changed game mode of \"$player\" to \"$2\"." - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_USER[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "The player \"$player\" was not found to be logged on." - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_GAMEMODE_FAIL_NO_CHANGE[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "The player \"$player\" was already in mode \"$2\"." - fi + 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." @@ -2538,23 +2879,9 @@ command_server_gamemode() { # $2->: The player name command_server_kick() { if server_is_running "$1"; then - local line regex - - server_property "$1" CONFIRM_KICK - server_property "$1" CONFIRM_KICK_FAIL - for player in "${@:2}"; do - server_eval_and_get_line "$1" "kick $player" "${SERVER_CONFIRM_KICK[$1]}" "${SERVER_CONFIRM_KICK_FAIL[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "Kicked \"$player\" from game." - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_KICK_FAIL[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "The player \"$player\" is not connected." - fi + 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." @@ -2566,8 +2893,8 @@ command_server_kick() { # $2->: Words of the message, will be concatinated with spaces command_server_say() { if server_is_running "$1"; then - server_eval "$1" "say ${*:2}" - echo "Message sent to players." + 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 @@ -2578,22 +2905,8 @@ command_server_say() { # $2: The time command_server_time_set() { if server_is_running "$1"; then - local line regex - - server_property "$1" CONFIRM_TIME_SET - server_property "$1" CONFIRM_TIME_SET_FAIL - - server_eval_and_get_line "$1" "time set $2" "${SERVER_CONFIRM_TIME_SET[$1]}" "${SERVER_CONFIRM_TIME_SET_FAIL[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "Set time to \"$2\"." - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_SET_FAIL[$1]}" - if [[ "$line" =~ $regex ]]; then - error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time." - fi + 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 @@ -2604,22 +2917,8 @@ command_server_time_set() { # $2: The time to add command_server_time_add() { if server_is_running "$1"; then - local line regex - - server_property "$1" CONFIRM_TIME_ADD - server_property "$1" CONFIRM_TIME_ADD_FAIL - - server_eval_and_get_line "$1" "time add $2" "${SERVER_CONFIRM_TIME_ADD[$1]}" "${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "Added \"$2\" to time." - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_TIME_ADD_FAIL[$1]}" - if [[ "$line" =~ $regex ]]; then - error_exit INVALID_ARGUMENT "Unable to convert \"$2\" to a time." - fi + 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 @@ -2629,22 +2928,8 @@ command_server_time_add() { # $1: The server ID command_server_toggledownfall() { if server_is_running "$1"; then - local line regex - - server_property "$1" CONFIRM_TOGGLEDOWNFALL - server_property "$1" CONFIRM_TOGGLEDOWNFALL_FAIL - - server_eval_and_get_line "$1" "toggledownfall" "${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" "${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:36}" - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_TOGGLEDOWNFALL_FAIL[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:34}" - fi + server_command "$1" TOGGLEDOWNFALL + echo_fallback "$RETURN" "Downfall toggled." else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi @@ -2658,31 +2943,10 @@ command_server_toggledownfall() { # $5: The entity damage value command_server_give() { if server_is_running "$1"; then - local line regex - - if [[ "$3" =~ ^\-[0-9]+$ ]]; then - error_exit INVALID_ARGUMENT "Item ID \"$3\" must be a positive integer or string." - fi - - server_property "$1" CONFIRM_GIVE - server_property "$1" CONFIRM_GIVE_FAIL_NO_USER - server_property "$1" CONFIRM_GIVE_FAIL_NO_ITEM - - server_eval_and_get_line "$1" "give $2 $3 $4 $5" "${SERVER_CONFIRM_GIVE[$1]}" "${SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:36}" - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE_FAIL_NO_USER[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:27}" - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_GIVE_FAIL_NO_ITEM[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:27}" - fi + 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 @@ -2694,27 +2958,8 @@ command_server_give() { # $3: The amount of XP to give (can be negative) command_server_xp() { if server_is_running "$1"; then - local line regex - - server_property "$1" CONFIRM_XP - server_property "$1" CONFIRM_XP_FAIL_NO_USER - server_property "$1" CONFIRM_XP_FAIL_INVALID_AMOUNT - - server_eval_and_get_line "$1" "xp $2 $3" "${SERVER_CONFIRM_XP[$1]}" "${SERVER_CONFIRM_XP_FAIL_NO_USER[$1]}" "${SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]}" - line="$RETURN" - - regex="${LOG_REGEX} ${SERVER_CONFIRM_XP[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:36}" - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_XP_FAIL_NO_USER[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:27}" - fi - regex="${LOG_REGEX} ${SERVER_CONFIRM_XP_FAIL_INVALID_AMOUNT[$1]}" - if [[ "$line" =~ $regex ]]; then - echo "${line:27}" - fi + 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 @@ -2760,8 +3005,9 @@ command_server_cmdlog() { 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 --lines=0 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]}" + as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow --lines=5 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]}" else error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." fi @@ -2876,8 +3122,12 @@ register_settings() { 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_PATH "/dev/shm/msm" + register_setting UPDATE_URL "https://raw.github.com/marcuswhybrow/minecraft-server-manager/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" @@ -2888,6 +3138,7 @@ register_settings() { 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" @@ -2908,6 +3159,7 @@ register_settings() { 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!" @@ -2917,27 +3169,18 @@ register_settings() { register_server_setting MESSAGE_COMPLETE_BACKUP_STARTED "Backing up entire server." register_server_setting MESSAGE_COMPLETE_BACKUP_FINISHED "Backup complete." - register_server_setting CONFIRM_SAVE_ON "CONSOLE: Enabling level saving.." - register_server_setting CONFIRM_SAVE_OFF "CONSOLE: Disabling level saving.." - register_server_setting CONFIRM_SAVE_ALL "CONSOLE: Save complete." - register_server_setting CONFIRM_START "Done" - register_server_setting CONFIRM_KICK "CONSOLE: Kicking " - register_server_setting CONFIRM_KICK_FAIL "Can't find user " - register_server_setting CONFIRM_TIME_SET "CONSOLE: Set time to" - register_server_setting CONFIRM_TIME_SET_FAIL "Unable to convert time value" - register_server_setting CONFIRM_TIME_ADD "CONSOLE: Added .+ to time" - register_server_setting CONFIRM_TIME_ADD_FAIL "Unable to convert time value" - register_server_setting CONFIRM_TOGGLEDOWNFALL "CONSOLE: Toggling downfall on|off for world" - register_server_setting CONFIRM_TOGGLEDOWNFALL_FAIL ".\[31m;1mNo world exists with the name" - register_server_setting CONFIRM_GAMEMODE "CONSOLE: Setting .+ to game mode (1|0)" - register_server_setting CONFIRM_GAMEMODE_FAIL_NO_USER "Can't find user .+" - register_server_setting CONFIRM_GAMEMODE_FAIL_NO_CHANGE ".+ already has game mode (1|0)" - register_server_setting CONFIRM_GIVE "CONSOLE: Giving .+ some .+ (.+)" - register_server_setting CONFIRM_GIVE_FAIL_NO_USER "Can't find user .+" - register_server_setting CONFIRM_GIVE_FAIL_NO_ITEM "There's no item called .+" - register_server_setting CONFIRM_XP "CONSOLE: Giving .+ exp to .+" - register_server_setting CONFIRM_XP_FAIL_NO_USER "Can't find user .+" - register_server_setting CONFIRM_XP_FAIL_INVALID_AMOUNT "Invalid exp count: .+" + # 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. @@ -2956,11 +3199,17 @@ register_command() { # Variables are denoted by angle brackets (e.g. "") and can # at this stage be accepted as any non-zero string if [[ "$word" =~ ^\<.*\>$ ]]; then - if [[ "$word" == "" ]]; then - regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* " - else - regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") " - fi + case "$word" in + "") + regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* " + ;; + "") + regex="${regex:0:${#regex}-1}( ((--|-)[^ ]+)( (--|-)[^ ]+)*)? " + ;; + *) + regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") " + ;; + esac continue fi @@ -3011,6 +3260,10 @@ call_command() { 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 @@ -3059,6 +3312,31 @@ call_command() { # 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 @@ -3164,7 +3442,7 @@ call_command() { done # Prevent the default singular call later on. - return + unset COMMAND_FLAGS; return fi # This calls the handler for all possible servers, and preserves @@ -3179,7 +3457,7 @@ call_command() { ${COMMAND_HANDLER[$command]} "${replaced_args[@]}" done - return + unset COMMAND_FLAGS; return fi # This calls the handlers for all possible worlds for a specific @@ -3194,12 +3472,12 @@ call_command() { ${COMMAND_HANDLER[$command]} "${replaced_args[@]}" done - return + unset COMMAND_FLAGS; return fi # Otherwise it's a simple single call of the handler. ${COMMAND_HANDLER[$command]} "${args[@]}" - return + unset COMMAND_FLAGS; return fi done @@ -3226,6 +3504,10 @@ register_commands() { # 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 # @@ -3250,6 +3532,7 @@ register_commands() { 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" @@ -3407,6 +3690,119 @@ allocate() { 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" "$latest_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: Verions 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 prefered +# $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 "$clostest_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" @@ -3430,19 +3826,107 @@ interrupt() { 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 ot 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\" +} + + + + + + + + + + +### 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. @@ -3462,5 +3946,6 @@ else # Just register settings instead. register_settings + load_versions allocate fi diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..35b5e1b --- /dev/null +++ b/install.sh @@ -0,0 +1,47 @@ +echo "This script requires superuser access to install files to /etc." +echo "You will be prompted for your password by sudo." + +# Clear existing sudo credentials +sudo -k + +# run script as sudo +sudo sh <