-
-
Save Next-Door-Tech/11981cd9c5421962b665db2161a3229c to your computer and use it in GitHub Desktop.
Escaped quotes break Vim syntax highlighting at line #647 among other places
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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.10" | |
# 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 " [0-9]\+:[0-9]\{2\} ${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 a 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]}" "tmux send-keys -t \"${SERVER_SCREEN_NAME[$1]}:0.0\" '$2' Enter" | |
} | |
# 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 " [0-9]\+:[0-9]\{2\} ${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 | |
#} | |
# Waits for a server to stop by checking if its tmux session exists 10 times a second | |
# Checking PID was ineffective because of zombie processes | |
# $1: The ID of the server to wait for | |
server_wait_for_stop() { | |
# if the screen session is still running, wait for it to stop | |
if as_user "${SERVER_USERNAME[$1]}" "tmux has-session -t \"${SERVER_SCREEN_NAME[$1]}\"" >/dev/null; then | |
while as_user "${SERVER_USERNAME[$1]}" "tmux has-session -t \"${SERVER_SCREEN_NAME[$1]}\"" >/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 | |
local versions_target="release" | |
if [[ "$target" == "minecraft-snapshot" ]]; then | |
local versions_target="snapshot" | |
fi | |
# Check if jq is installed on the local computer | |
as_user "$SETTINGS_USERNAME" "which jq > /dev/null" | |
if [[ "$?" != "0" ]]; then | |
echo "jq is required to download server updates. Please ensure it is installed and the path is set correctly." | |
return 1 | |
fi | |
printf "Checking minecraft version JSON... " | |
local versions_url="https://launchermeta.mojang.com/mc/game/version_manifest.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_package_url=$(as_user "$SETTINGS_USERNAME" "cat $versions_file | jq -r '.versions | sort_by(.releaseTime) | map(select(.type | contains ("\""$versions_target"\""))) | last | .url'") | |
local latest_version=$(as_user "$SETTINGS_USERNAME" "echo ${latest_package_url##*/} | sed s/.json//") | |
if [[ -n "$latest_package_url" ]]; then | |
local package_file="/tmp/minecraft_package.json" | |
as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate -O '$package_file' '$latest_package_url'" | |
local jar_url=$(as_user "$SETTINGS_USERNAME" "cat $package_file | jq -r '.downloads.server.url'") | |
fi | |
fi | |
if [[ -n "$jar_url" ]]; then | |
as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate -O '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/minecraft_server.$latest_version.jar' '$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 <download-url>" | |
fi | |
else | |
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." | |
fi | |
fi | |
} | |
# Deletes an existing jargroup | |
# $1: The name of the existing jargroup | |
jargroup_delete() { | |
if is_valid_name "$1"; then | |
manager_property JAR_STORAGE_PATH | |
manager_property USERNAME | |
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then | |
printf "Are you sure you want to delete this jar group [y/N]: " | |
read answer | |
if [[ "$answer" =~ (^y|Y|yes$) ]]; then | |
as_user "$SETTINGS_USERNAME" "rm -rf \"$SETTINGS_JAR_STORAGE_PATH/$1\"" | |
echo "Jar group deleted." | |
else | |
echo "Jar group was NOT deleted." | |
fi | |
else | |
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." | |
fi | |
fi | |
} | |
# Renames an existing jargroup | |
# $1: The name of the existing jargroup | |
# $2: The new name | |
jargroup_rename() { | |
if is_valid_name "$1"; then | |
manager_property JAR_STORAGE_PATH | |
manager_property USERNAME | |
if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then | |
# If the jar group name is valid, | |
# and there is no other jar group with the name $1 | |
if is_valid_name "$2"; then | |
if [[ -e "$SETTINGS_JAR_STORAGE_PATH/$2" ]]; then | |
error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"." | |
else | |
# TODO: Update any symbolic links which point to a jar in this directory | |
as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1' '$SETTINGS_JAR_STORAGE_PATH/$2'" | |
echo "Renamed jar group \"$1\" to \"$2\"." | |
fi | |
fi | |
else | |
error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"." | |
fi | |
fi | |
} | |
### Server Functions | |
### ---------------- | |
# 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]}\" && tmux -f /dev/null new-session -d -s \"${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 (${SERVER_PATH[$1]}/eula.txt)." | |
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 | |
NURSERY_MIN_RAM=$(( ${SERVER_RAM[$1]} / 2 )) | |
NURSERY_MAX_RAM=$(( ${SERVER_RAM[$1]} / 5 * 4 )) | |
# Sets NURSERY_MIN to 50% and NURSERY_MAX to 80% of allocated RAM | |
server_property "$1" JAR_PATH | |
eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}\" | |
eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{NURSERY_MIN\}/$NURSERY_MIN_RAM}\" | |
eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{NURSERY_MAX\}/$NURSERY_MAX_RAM}\" | |
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" | |
eval 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 <name> Creates a new Minecraft server" | |
echo -e " server delete <name> Deletes an existing Minecraft server" | |
echo -e " server rename <name> <new-name> Renames an existing Minecraft server" | |
echo -e | |
echo -e "--Server Management Commands------------------------------------" | |
echo -e " <server> start Starts a server" | |
echo -e " <server> stop [now] Stops a server after warning players, or right now" | |
echo -e " <server> restart [now] Restarts a server after warning players, or right now" | |
echo -e " <server> status Show the running/stopped status of a server" | |
echo -e " <server> connected List a servers connected players" | |
echo -e " <server> worlds list Lists the worlds a server has" | |
echo -e " <server> worlds load Creates links to worlds in storage for a server" | |
echo -e " <server> worlds ram <world> Toggles a world's \"in RAM\" status" | |
echo -e " <server> worlds todisk Synchronises any \"in RAM\" worlds to disk a server has" | |
echo -e " <server> worlds backup Makes a backup of all worlds a server has" | |
echo -e " <server> worlds on|off <world> Activate or deactivate a world, inactive worlds are not backed up" | |
echo -e " <server> logroll Move a server log to a gziped archive, to reduce lag" | |
echo -e " <server> backup Makes a backup of an entire server directory" | |
echo -e " <server> jar <jargroup> [<file>] Sets a server's jar file" | |
echo -e " <server> console Connects to the interactive console. Access may be limited" | |
echo -e " <server> config [<setting> <value>] Lists server settings, or sets a specific setting." | |
echo -e | |
echo -e "--Server Pass Through Commands----------------------------------" | |
echo -e " <server> wl on|off Enables/disables server whitelist checking" | |
echo -e " <server> wl add|remove <player> Add/remove a player to/from a server's whitelist" | |
echo -e " <server> wl list List the players whitelisted for a server" | |
echo -e " <server> bl player add|remove <player> Ban/pardon a player from/for a server" | |
echo -e " <server> bl ip add|remove <ip address> Ban/pardon an IP address from/for a server" | |
echo -e " <server> bl list Lists the banned players and IP address for a server" | |
echo -e " <server> op add|remove <player> Add/remove operator status for a player on a server" | |
echo -e " <server> op list Lists the operator players for a server" | |
echo -e " <server> gm survival|creative <player> Change the game mode for a player on a server" | |
echo -e " <server> kick <player> Forcibly disconnect a player from a server" | |
echo -e " <server> say <message> Broadcast a (pink) message to all players on a server" | |
echo -e " <server> time set|add <number> Set/increment time on a server (0-24000)" | |
echo -e " <server> toggledownfall Toggles rain and snow on a server" | |
echo -e " <server> give <player> <item> [amount] [data] Gives an entity to a player" | |
echo -e " <server> xp <player> <amount> Gives XP to, or takes away (when negative) XP from, a player" | |
echo -e " <server> save on|off Enable/disable writing world changes to file" | |
echo -e " <server> save all Force the writing of all non-saved world changes to file" | |
echo -e " <server> cmd <command> Send a command string to the server and return" | |
echo -e " <server> cmdlog <command> Same as 'cmd' but shows log output afterwards (Ctrl+C to exit)" | |
echo -e | |
echo -e "--Jar Commands--------------------------------------------------" | |
echo -e " jargroup list List the stored jar files." | |
echo -e " jargroup create <name> <download-url> Create a new jar group, with a URL for new downloads" | |
echo -e " jargroup delete <name> Delete a jar group" | |
echo -e " jargroup rename <name> <new-name> Rename a jar group" | |
echo -e " jargroup changeurl <name> <download-url> Change the download URL for a jar group" | |
echo -e " jargroup getlatest <name> Download the latest jar file for a jar group" | |
echo -e | |
echo -e "--Global Commands-----------------------------------------------" | |
echo -e " start Starts all active servers" | |
echo -e " stop [now] Stops all running servers" | |
echo -e " restart [now] Restarts all active servers" | |
echo -e " version Prints the Minecraft Server Manager version installed" | |
echo -e " config Displays a list of the config values used by MSM" | |
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]}" "tmux attach-session -t ${SERVER_SCREEN_NAME[$1]}" | |
else | |
error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running." | |
fi | |
} | |
# Sets a parameter in the config file if it exists, otherwise inserts the | |
# 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. "<variable>") and can | |
# at this stage be accepted as any non-zero string | |
if [[ "$word" =~ ^\<.*\>$ ]]; then | |
case "$word" in | |
"<strings>") | |
regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* " | |
;; | |
"<flags>") | |
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 "<string>" token expects any type of string argument, | |
# accepting spaces, limited to one argument. | |
"<string>") | |
# Do no checks, just push the argument onto the stack | |
push_arg "${!word_offset}" | |
;; | |
# The "<strings>" token must only be placed at the end of a | |
# command signature, and allows an arbitrary amount of | |
# arguments to be passed to the command handler function. | |
"<strings>") | |
# Put all remaining user input onto the argument stack | |
for input_arg in "${@:$word_offset}"; do | |
push_arg "$input_arg" | |
done | |
# Break from analysing the rest of the input | |
break | |
;; | |
# The "<flags>" 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. | |
"<flags>") | |
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 "<name>" token is similar to "<string>" but adds an | |
# extra assurance that the string is a valid name, as used | |
# for creating servers and other things. | |
"<name>") | |
# Check the argument is a valid name and then add push it onto the argument stack | |
local specified_name="${!word_offset}" | |
if is_valid_name "$specified_name"; then | |
push_arg "$specified_name" | |
fi | |
;; | |
# The "<name:server>" token improves on "<name>" by also | |
# checking that the server exists, and passing the argument | |
# on as the server id, instead of the server name to | |
# command handler functions. | |
"<name:server>") | |
local specified_name="${!word_offset}" | |
if [[ "$specified_name" == "all" ]]; then | |
# Do for all servers | |
sid="server:all" | |
else | |
if is_valid_name "$specified_name"; then | |
if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$specified_name" ]; then | |
server_get_id "$specified_name" | |
sid="$RETURN" | |
fi | |
fi | |
if [[ "$sid" -eq "-1" ]]; then | |
error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"." | |
fi | |
fi | |
push_arg "$sid" | |
;; | |
# The "<name:world>" token also improves upon "<name>" by | |
# ensuring that the world actually exists, and passes the | |
# argument on to command handlers as the world ID, rather | |
# than the original world name input by the user. | |
"<name:world>") | |
local specified_name="${!word_offset}" | |
if [[ "$sid" -eq "-1" ]]; then | |
# Server id not set yet | |
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" | |
# | |
# <string> Same as "fixedstring", but is variable and the value | |
# is passed to the handler function as a positional | |
# argument | |
# | |
# <strings> Same as "<string>", but matches multiple arguments, | |
# must be final element | |
# | |
# <flags> 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. | |
# | |
# <name> Same as "<string>", also ensures it's a valid name | |
# using the is_valid_name function | |
# | |
# <name:server> Same as "<name>", also converts value to server id or | |
# fails if the server does not exist | |
# | |
# <name:world> Same as "<name>", also converts value to world id or | |
# fails if the world does not exist. Must only be | |
# included after a "<name:server>" element. | |
# | |
# Elements listed above encapsulated within angle brackets must be included | |
# within a signature verbatim, as 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 <flags>" "command_update" | |
register_command "server list" "command_server_list" | |
register_command "server create <name>" "command_server_create" | |
register_command "server delete <name>" "command_server_delete" | |
register_command "server rename <name> <name>" "command_server_rename" | |
register_command "jargroup list" "command_jargroup_list" | |
register_command "jargroup create <name> <string>" "command_jargroup_create" | |
register_command "jargroup delete <name>" "command_jargroup_delete" | |
register_command "jargroup rename <name> <name>" "command_jargroup_rename" | |
register_command "jargroup changeurl <name> <string>" "command_jargroup_changeurl" | |
register_command "jargroup getlatest <name>" "command_jargroup_getlatest" | |
register_command "help" "command_help" | |
register_command "<name:server> start" "command_server_start" | |
register_command "<name:server> stop" "command_server_stop" | |
register_command "<name:server> stop now" "command_server_stop_now" | |
register_command "<name:server> restart" "command_server_restart" | |
register_command "<name:server> restart now" "command_server_restart_now" | |
register_command "<name:server> status" "command_server_status" | |
register_command "<name:server> connected" "command_server_connected" | |
register_command "<name:server> worlds list" "command_server_worlds_list" | |
register_command "<name:server> worlds load" "command_server_worlds_load" | |
register_command "<name:server> worlds ram <name:world>" "command_server_worlds_ram" | |
register_command "<name:server> worlds todisk" "command_server_worlds_todisk" | |
register_command "<name:server> worlds backup" "command_server_worlds_backup" | |
register_command "<name:server> worlds on <name:world>" "command_server_worlds_on" | |
register_command "<name:server> worlds off <name:world>" "command_server_worlds_off" | |
register_command "<name:server> logroll" "command_server_logroll" | |
register_command "<name:server> backup" "command_server_backup" | |
register_command "<name:server> jar <name>" "command_server_jar" | |
register_command "<name:server> jar <name> <string>" "command_server_jar" | |
register_command "<name:server> console" "command_server_console" | |
register_command "<name:server> config" "command_server_config" | |
register_command "<name:server> config <string>" "command_server_config" | |
register_command "<name:server> config <string> <string>" "command_server_config" | |
register_command "<name:server> whitelist|wl on" "command_server_whitelist_on" | |
register_command "<name:server> whitelist|wl off" "command_server_whitelist_off" | |
register_command "<name:server> whitelist|wl add <strings>" "command_server_whitelist_add" | |
register_command "<name:server> whitelist|wl remove <strings>" "command_server_whitelist_remove" | |
register_command "<name:server> whitelist|wl list" "command_server_whitelist_list" | |
register_command "<name:server> blacklist|bl player add <strings>" "command_server_blacklist_player_add" | |
register_command "<name:server> blacklist|bl player remove <strings>" "command_server_blacklist_player_remove" | |
register_command "<name:server> blacklist|bl ip add <strings>" "command_server_blacklist_ip_add" | |
register_command "<name:server> blacklist|bl ip remove <strings>" "command_server_blacklist_ip_remove" | |
register_command "<name:server> blacklist|bl list" "command_server_blacklist_list" | |
register_command "<name:server> operator|op add <strings>" "command_server_operator_add" | |
register_command "<name:server> operator|op remove <strings>" "command_server_operator_remove" | |
register_command "<name:server> operator|op list" "command_server_operator_list" | |
register_command "<name:server> gamemode|gm <string> <strings>" "command_server_gamemode" | |
register_command "<name:server> kick <strings>" "command_server_kick" | |
register_command "<name:server> say <strings>" "command_server_say" | |
register_command "<name:server> time set <string>" "command_server_time_set" | |
register_command "<name:server> time add <string>" "command_server_time_add" | |
register_command "<name:server> toggledownfall|tdf" "command_server_toggledownfall" | |
register_command "<name:server> give <string> <string>" "command_server_give" | |
register_command "<name:server> give <string> <string> <string>" "command_server_give" | |
register_command "<name:server> give <string> <string> <string> <string>" "command_server_give" | |
register_command "<name:server> xp <string> <string>" "command_server_xp" | |
register_command "<name:server> save on" "command_server_save_on" | |
register_command "<name:server> save off" "command_server_save_off" | |
register_command "<name:server> save all" "command_server_save_all" | |
register_command "<name:server> cmd <strings>" "command_server_cmd" | |
register_command "<name:server> cmdlog <strings>" "command_server_cmdlog" | |
} | |
# $1: Server path | |
server_allocate() { | |
unset RETURN | |
# Get an ID for this new server | |
local server_id="$NUM_SERVERS" | |
# Store the path for this new server | |
SERVER_PATH[$server_id]="$1" | |
# Store the name for this server | |
quick_basename "${SERVER_PATH[$server_id]}" | |
SERVER_NAME[$server_id]="$RETURN" | |
NUM_SERVERS=$(( $NUM_SERVERS + 1 )) | |
RETURN="$server_id" | |
} | |
# $1: Server ID | |
server_worlds_allocate() { | |
local world_id | |
# A server's worlds require contiguous ID's | |
# thus they are loaded one after another all at once. | |
# $1: Server ID | |
# $2: World path | |
world_allocate() { | |
# Get an ID for this new world | |
world_id="$NUM_WORLDS" | |
# Store the path for this new world | |
WORLD_PATH[$world_id]="$2" | |
# Store the name for this world | |
quick_basename "${WORLD_PATH[$world_id]}" | |
WORLD_NAME[$world_id]="$RETURN" | |
# Store the server ID this world belongs to | |
WORLD_SERVER_ID[$world_id]="$1" | |
NUM_WORLDS=$(( $NUM_WORLDS + 1 )) | |
} | |
server_property "$1" WORLD_STORAGE_PATH | |
server_property "$1" WORLD_STORAGE_INACTIVE_PATH | |
local world_name | |
# Record the index at which worlds for this server will start | |
SERVER_WORLD_OFFSET[$1]="$NUM_WORLDS" | |
if [[ -d "${SERVER_WORLD_STORAGE_PATH[$1]}" ]]; then | |
while IFS= read -r -d $'\0' path; do | |
world_allocate "$1" "$path" | |
done < <(find "${SERVER_WORLD_STORAGE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) | |
fi | |
if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" ]]; then | |
while IFS= read -r -d $'\0' path; do | |
world_allocate "$1" "$path" | |
done < <(find "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0) | |
fi | |
# Record the number 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment