Skip to content

Instantly share code, notes, and snippets.

@Next-Door-Tech
Created July 3, 2020 02:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Next-Door-Tech/11981cd9c5421962b665db2161a3229c to your computer and use it in GitHub Desktop.
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
#!/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