diff --git a/.gitignore b/.gitignore index d5244af..fd5e95c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ minecraftd-backup.service minecraftd-backup.timer minecraftd-backup@.service minecraftd-backup@.timer +minecraftd-init@.service minecraftd.conf minecraftd.service minecraftd.sh diff --git a/Makefile b/Makefile index 7ddc31e..d74c3f4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ SHELL = /bin/sh INSTALL = install INSTALL_PROGRAM = $(INSTALL) -m755 +INSTALL_DIR = $(INSTALL) -d -m755 INSTALL_DATA = $(INSTALL) -m644 confdir = /etc/conf.d prefix = /usr @@ -20,10 +21,13 @@ CONFIG_PATH = /etc/conf.d/$(GAME) SYSCONFDIR = /etc INSTANCE_CONFIG_DIR = $(SYSCONFDIR)/$(GAME) BACKUP_DEST = $(SERVER_ROOT)/backup +LIBRARY_PATH = $(libdir)/$(GAME) BACKUP_PATHS = world BACKUP_FLAGS = -z KEEP_BACKUPS = 10 GAME_USER = $(GAME) +SERVER_MEMORY_INITIAL = 512 +SERVER_MEMORY_MAXIMUM = 1024 MAIN_EXECUTABLE = minecraft_server.jar ifeq ($(MAIN_EXECUTABLE),$(MAIN_EXECUTABLE:/%=)) MAIN_EXECUTABLE_ABSOLUTE = $(SERVER_ROOT)/$(MAIN_EXECUTABLE) @@ -32,14 +36,14 @@ MAIN_EXECUTABLE_ABSOLUTE = $(MAIN_EXECUTABLE) endif TMUX_SOCKET_DIR = /run/$(GAME)/tmux SESSION_NAME = $(GAME) -SERVER_START_CMD = java -Xms512M -Xmx1024M -jar ./$${MAIN_EXECUTABLE} nogui +SERVER_START_CMD = java -Xms@SERVER_MEMORY_INITIAL@M -Xmx@SERVER_MEMORY_MAXIMUM@M -jar @MAIN_EXECUTABLE@ nogui SERVER_START_SUCCESS = done IDLE_SERVER = false -IDLE_SESSION_NAME = idle_server_$${SESSION_NAME} +IDLE_SESSION_NAME = idle_server_@SESSION_NAME@ GAME_PORT = 25565 CHECK_PLAYER_TIME = 30 IDLE_IF_TIME = 1200 -GAME_COMMAND_DUMP = /tmp/$${INAME}_$${SESSION_NAME}_command_dump.txt +GAME_COMMAND_DUMP = /tmp/@INAME@_@SESSION_NAME@_command_dump.txt .MAIN = all @@ -50,6 +54,7 @@ define replace_all -e 's#@GAME@#$(GAME)#g' \ -e 's#@SERVER_ROOT@#$(SERVER_ROOT)#g' \ -e 's#@CONFIG_PATH@#$(CONFIG_PATH)#g' \ + -e 's#@LIBRARY_PATH@#$(LIBRARY_PATH)#g' \ -e 's#@SYSCONFDIR@#$(SYSCONFDIR)#g' \ -e 's#@INSTANCE_CONFIG_DIR@#$(INSTANCE_CONFIG_DIR)#g' \ -e 's#@BACKUP_DEST@#$(BACKUP_DEST)#g' \ @@ -59,6 +64,8 @@ define replace_all -e 's#@GAME_USER@#$(GAME_USER)#g' \ -e 's#@MAIN_EXECUTABLE@#$(MAIN_EXECUTABLE)#g' \ -e 's#@MAIN_EXECUTABLE_ABSOLUTE@#$(MAIN_EXECUTABLE_ABSOLUTE)#g' \ + -e 's#@SERVER_MEMORY_INITIAL@#$(SERVER_MEMORY_INITIAL)#g' \ + -e 's#@SERVER_MEMORY_MAXIMUM@#$(SERVER_MEMORY_MAXIMUM)#g' \ -e 's#@TMUX_SOCKET_DIR@#$(TMUX_SOCKET_DIR)#g' \ -e 's#@SESSION_NAME@#$(SESSION_NAME)#g' \ -e 's#@SERVER_START_CMD@#$(SERVER_START_CMD)#g' \ @@ -102,15 +109,17 @@ maintainer-clean: clean install: $(INSTALL_PROGRAM) -D minecraftd.sh "$(DESTDIR)$(bindir)/$(INAME)" - $(INSTALL_DATA) -D minecraftd.conf "$(DESTDIR)$(confdir)/$(GAME)" - $(INSTALL_DATA) -D minecraftd.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME).service" - $(INSTALL_DATA) -D minecraftd-backup.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.service" - $(INSTALL_DATA) -D minecraftd-backup.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.timer" + $(INSTALL_DIR) "$(DESTDIR)$(LIBRARY_PATH)" + $(INSTALL_DATA) -D argparse.sh "$(DESTDIR)$(LIBRARY_PATH)/argparse.sh" + $(INSTALL_DATA) -D minecraftd.conf "$(DESTDIR)$(confdir)/$(GAME)" + $(INSTALL_DATA) -D minecraftd.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME).service" + $(INSTALL_DATA) -D minecraftd-backup.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.service" + $(INSTALL_DATA) -D minecraftd-backup.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.timer" $(INSTALL_DATA) -D minecraftd@.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)@.service" $(INSTALL_DATA) -D minecraftd-backup@.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup@.service" $(INSTALL_DATA) -D minecraftd-backup@.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup@.timer" - $(INSTALL_DATA) -D minecraftd.sysusers "$(DESTDIR)$(libdir)/sysusers.d/$(INAME).conf" - $(INSTALL_DATA) -D minecraftd.tmpfiles "$(DESTDIR)$(libdir)/tmpfiles.d/$(INAME).conf" + $(INSTALL_DATA) -D minecraftd.sysusers "$(DESTDIR)$(libdir)/sysusers.d/$(INAME).conf" + $(INSTALL_DATA) -D minecraftd.tmpfiles "$(DESTDIR)$(libdir)/tmpfiles.d/$(INAME).conf" uninstall: rm -f "$(bindir)/$(INAME)" diff --git a/README.md b/README.md index 5b9279b..d7d08d5 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,13 @@ Use the minecraft script under /usr/bin/minecraftd to start, stop or backup the ### How to configure the Server -Adjust the configuration file under /etc/conf.d/minecraft to your liking. +Adjust the configuration file under `/etc/conf.d/minecraft` to your liking. + +If you are running multiple servers, the configuration file `/etc/minecraft/` will be loaded if it exists, and will supersede any options set in the global configuration. + +Any configuration variable can be overridden in the environment. + +To see the effective configuration based on the instance name and all applicable configuration files, run `minecraftd print`, `minecraftd -i server2 print`, etc. To print the plain value of a single configuration key, use the `-k` argument: `minecraftd print -k SERVER_ROOT`. ### Server does not start @@ -83,31 +89,13 @@ systemctl start minecraftd@server-name # (it should start this time) To facilitate the use of a unique environment variable file per instance, the file `/etc/minecraft/` is read in addition to the global environment file, `/etc/conf.d/minecraft`. Note that `minecraft` is the value of the `@GAME@` macro in this case, so change this to `/etc/spigot/`, etc. Values in the instance-specific environment file take precedence over values in the global configuration. -Behind the scenes, the systemd instantiated unit achieves instantiation by changing several environment variables as follows (where `%i` is the instance name, and `@SERVER_ROOT` defaults to `/srv/@GAME@`): - -* `SERVER_ROOT=@SERVER_ROOT@/servers/%i` -* `BACKUP_DEST=@SERVER_ROOT@/servers/%i/backup` -* `SESSION_NAME=@GAME@-%i` - -This means that attempting to override these environment variables in the per-instance environment file may result in the server not starting or files being in the wrong location. - -If you need to start an instantiated server manually for some reason, you can do: - -```sh -env SERVER_ROOT=/srv/minecraft/servers/creative \ - SESSION_NAME=minecraft-creative \ - minecraftd start -``` - -To attach to the console, just override the `SESSION_NAME` variable with `minecraft-` followed by the instance name: - -```sh -$ SESSION_NAME=minecraft-creative minecraftd console -``` +The configuration variables whose defaults change in instantiated mode are: -In the above example, `minecraft` is the value of the `@GAME@` macro, so you might need `spigot-creative`, `papermc-creative`, etc. +* `SERVER_ROOT` changes from `/srv/minecraft` to `/srv/minecraft/servers/` +* `SESSION_NAME` changes from `minecraft` to `minecraft-` +* `BACKUP_DEST` changes from `/srv/minecraft/backup` to `/srv/minecraft/servers//backup` -To take a backup, use instantiated variants of the service and timer, or set `BACKUP_DEST` alongside `SERVER_ROOT` and `SESSION_NAME` when running `minecraftd backup`. +These can also be overridden in the instance-specific configuration file, `/etc/minecraft/`. ## License diff --git a/argparse.sh b/argparse.sh new file mode 100755 index 0000000..ec22b7e --- /dev/null +++ b/argparse.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +# shell script library to parse and validate command line arguments, and generate the --help text. + +set -u + +re_subcmd_vn='^[a-z0-9]+:[A-Za-z0-9_]+$' +COMMAND= + +declare -A args args_help subcmds + +add_arg() { + local short="$1" + local long="$2" + local varname="$3" + local help="$4" + local required="${5:-false}" + [ -v 6 ] && local default="$6" + if [[ "$varname" =~ $re_subcmd_vn ]]; then + local subc _vn + IFS=":" read subc _vn <<< "$varname" + [[ -n "${subcmds[$subc]}" ]] + fi + args[$varname]="$short $long $required" + args_help[$varname]="$help" + # init the global + [[ -v default ]] && declare -g ${varname#*:}="${default}" +} + +add_subcommand() { + local subc="$1" + local help="$2" + + subcmds[$subc]="$help" +} + +usage() { + # help function. generates usage instructions + local short long required help + local n_subcmds=${#subcmds[@]} + local ofd=1 + [[ -v 1 ]] && ofd=2 + local cols=80 + [[ -t $ofd ]] && cols=$(tput cols) + ( + if [[ -v 1 ]]; then + echo "ERROR: $1" + echo "" + fi + if [[ -v DESCRIPTION ]]; then + echo -e "$DESCRIPTION" + echo "" + fi + if [[ $n_subcmds < 1 ]]; then + echo "Usage: $0 [options]" + echo "" + echo "Valid options are:" + else + echo "Usage: $0 [global options] COMMAND [command-specific options]" + echo "" + echo "Global options:" + fi + for varname in ${!args[@]}; do + [[ $varname =~ $re_subcmd_vn ]] && continue + IFS=" " read short long required <<< "${args[$varname]}" + help="${args_help[$varname]}" + printf " -%s, --%-16s %s (required: %s)\n" "$short" "$long" "$help" "$required" + done + + if [[ $n_subcmds > 0 ]]; then + echo "" + echo "Valid commands:" + for subcmd in ${!subcmds[@]}; do + printf " %-12s %s\n" "$subcmd" "${subcmds[$subcmd]}" + done + + for subcmd in ${!subcmds[@]}; do + echo "" + echo "Options for command \"$subcmd\":" + local n=0 + for varname in ${!args[@]}; do + [[ "${varname%:*}" == "$subcmd" ]] || continue + ((n++)) + IFS=" " read short long required <<< "${args[$varname]}" + help="${args_help[$varname]}" + printf " -%s, --%-16s %s (required: %s)\n" "$short" "$long" "$help" "$required" + done + [[ $n == 0 ]] && echo " (None)" + done + echo "" + + [[ -v COPYRIGHT ]] && echo -e "$COPYRIGHT" + fi + ) | fold -w "$cols" -s >&$ofd + exit 1 +} + +parse_args() { + local short long required longprefix found + local n_subcmds=${#subcmds[@]} + declare -a bareargs reqargs + while [[ -v 1 ]]; do + case "$1" in + --help|-h) + usage ;; + -*) + # parse as named option + found=false + for varname in ${!args[@]}; do + IFS=" " read short long required <<< "${args[$varname]}" + if [[ $varname =~ $re_subcmd_vn ]]; then + # subcommand option + local vsubc _vn + IFS=":" read vsubc _vn <<< "$varname" + [[ "$vsubc" = "$COMMAND" ]] || continue + varname=$_vn + fi + longprefix="--$long=" + case "$1" in + -$short|--$long) + [[ -v 2 ]] || usage "value for argument $1 may not be omitted" + declare -g $varname="$2" + found=true + shift + ;; + $longprefix*) + found=true + declare -g $varname=${1:${#longprefix}} + ;; + esac + done + [[ "$found" == "true" ]] || usage "Unknown option: $1" + ;; + *) + if [[ $n_subcmds > 0 && $COMMAND == "" ]]; then + COMMAND="$1" + [[ -v subcmds[$COMMAND] ]] || usage "Undefined command: $COMMAND" + else + bareargs+=("$1") + fi + ;; + esac + shift + done + + [[ $n_subcmds > 0 && $COMMAND == "" ]] && usage "No command was given." + + # build list of missing required args + for varname in ${!args[@]}; do + IFS=" " read short long required <<< "${args[$varname]}" + [[ "$required" == "true" ]] || continue + if [[ $varname =~ $re_subcmd_vn ]]; then + # subcommand option + local vsubc _vn + IFS=":" read vsubc _vn <<< "$varname" + [[ "$vsubc" = "$COMMAND" ]] || continue + [[ -v $_vn ]] && continue + reqargs+=("$varname") + else + [[ -v $varname ]] && continue + reqargs+=("$varname") + fi + done + # process bareword args + while [[ -v bareargs[0] && -v reqargs[0] ]]; do + declare -g ${reqargs[0]#*:}=${bareargs[0]} + bareargs=("${bareargs[@]:1}") + reqargs=("${reqargs[@]:1}") + done + + # if there's any bareword arguments left, we are out of ideas for what to do with them, so fail. + [[ -v bareargs[0] ]] && usage "Unknown argument: ${bareargs[0]}" + + # enforce required args + for varname in ${!args[@]}; do + IFS=" " read short long required <<< "${args[$varname]}" + [[ "$required" == "true" ]] || continue + if [[ $varname =~ $re_subcmd_vn ]]; then + # subcommand arg + local vsubc _vn + IFS=":" read vsubc _vn <<< "$varname" + [[ "$vsubc" == "$COMMAND" ]] || continue + [[ ! -v $_vn ]] && usage "Option is required for command $vsubc but not set: $long" + else + # global arg + [[ ! -v $varname ]] && usage "Required option or positional argument not set: $long" + fi + done +} diff --git a/minecraftd-init@.service.in b/minecraftd-init@.service.in new file mode 100644 index 0000000..fce9707 --- /dev/null +++ b/minecraftd-init@.service.in @@ -0,0 +1,12 @@ +[Unit] +Description=Create server directories for @GAME@ %i instance +After=local-fs.target network.target multi-user.target + +[Service] +Type=oneshot +RemainAfterExit=no +# create the "servers" directory followed by the instance directory +ExecStart=/usr/bin/@INAME@ -i %i init + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/minecraftd.sh.in b/minecraftd.sh.in index 7c75290..45926a6 100755 --- a/minecraftd.sh.in +++ b/minecraftd.sh.in @@ -3,48 +3,137 @@ # The actual program name (name of the interface) declare -r INAME="@INAME@" declare -r GAME="@GAME@" +declare -r LIBRARY_PATH="@LIBRARY_PATH@" + +test -r "$LIBRARY_PATH/argparse.sh" || (echo "Cannot find argparse.sh in $LIBRARY_PATH" >&2; exit 1) + +# Note: this calls "set -u" to disallow the use of uninitialized variables. +source "$LIBRARY_PATH/argparse.sh" + +DESCRIPTION="This script was designed to easily control any ${GAME} server. Almost\n" +DESCRIPTION+="any parameter for a given ${GAME} server derivative can be changed by\n" +DESCRIPTION+="editing the variables in the configuration file." + +COPYRIGHT="Report bugs to .\n" +COPYRIGHT+="Copyright (c) Gordian Edenhofer \n" +COPYRIGHT+="and contributors (see: https://github.com/Edenhofer/minecraft-server/graphs/contributors)\n" +COPYRIGHT+="\n" +COPYRIGHT+="This program is free software; you can redistribute it and/or\n" +COPYRIGHT+="modify it under the terms of the GNU General Public License\n" +COPYRIGHT+="as published by the Free Software Foundation; either version 2\n" +COPYRIGHT+="of the License, or (at your option) any later version.\n" +COPYRIGHT+="\n" +COPYRIGHT+="For details, see: https://github.com/Edenhofer/minecraft-server#license" + +add_subcommand start "Start the server instance." +add_subcommand stop "Stop the server instance." +add_subcommand status "Check status of a running server instance." +add_subcommand restart "Restart the server instance." +add_subcommand console "Attach to the server console. Press Ctrl+B and then Ctrl+D to detach (by default; your tmux settings may be different)." +add_subcommand command "Run a command on the server console and report the result." +add_subcommand backup "Immediately take a backup of the server. Backups are written to the directory specified in the BACKUP_DEST configuration variable." +add_subcommand restore "Restore a server backup from an archive file." +add_subcommand print "Print a variable from the configuration." +add_subcommand init "Create directories for the server." +add_subcommand idle_server_daemon "Internal use only" + +add_arg i instance SERVER_INSTANCE "Server instance. Multiple instances of @GAME@ can run concurrently. If omitted, the default instance is used." false +add_arg c command command:SERVER_COMMAND "Command to run" true +add_arg a archive restore:ARCHIVE_PATH "Path to the backup archive to restore." true +add_arg k key print:CONFIG_VAR "Configuration variable to print (SERVER_ROOT, SESSION_NAME, etc.). If omitted, prints the entire configuration." false + +parse_args "$@" # General rule for the variable-naming-schema: # Variables in capital letters may be passed through the command line others not. # Avoid altering any of those later in the code since they may be readonly (IDLE_SERVER is an exception!) # You may use this script for any game server of your choice, just alter the config file -[[ -n "${SESSION_NAME}" ]] && declare -r session_name_overridden=true || declare -r session_name_overridden=false -[[ -n "${SERVER_ROOT}" ]] && declare -r SERVER_ROOT=${SERVER_ROOT} || SERVER_ROOT="@SERVER_ROOT@" -[[ -n "${CONFIG_PATH}" ]] && declare -r CONFIG_PATH=${CONFIG_PATH} || CONFIG_PATH="@CONFIG_PATH@" -[[ -n "${BACKUP_DEST}" ]] && declare -r BACKUP_DEST=${BACKUP_DEST} || BACKUP_DEST="@BACKUP_DEST@" -[[ -n "${BACKUP_PATHS}" ]] && declare -r BACKUP_PATHS=${BACKUP_PATHS} || BACKUP_PATHS="@BACKUP_PATHS@" -[[ -n "${BACKUP_FLAGS}" ]] && declare -r BACKUP_FLAGS=${BACKUP_FLAGS} || BACKUP_FLAGS="@BACKUP_FLAGS@" -[[ -n "${KEEP_BACKUPS}" ]] && declare -r KEEP_BACKUPS=${KEEP_BACKUPS} || KEEP_BACKUPS="@KEEP_BACKUPS@" -[[ -n "${GAME_USER}" ]] && declare -r GAME_USER=${GAME_USER} || GAME_USER="@GAME_USER@" -[[ -n "${MAIN_EXECUTABLE}" ]] && declare -r MAIN_EXECUTABLE=${MAIN_EXECUTABLE} || MAIN_EXECUTABLE="@MAIN_EXECUTABLE_ABSOLUTE@" -[[ -n "${SESSION_NAME}" ]] && declare -r SESSION_NAME=${SESSION_NAME} || SESSION_NAME="@SESSION_NAME@" +declare -A default_config +default_config[SESSION_NAME]="@SESSION_NAME@" +default_config[SERVER_ROOT]="@SERVER_ROOT@" +default_config[CONFIG_PATH]="@CONFIG_PATH@" +default_config[INSTANCE_CONFIG_DIR]="@INSTANCE_CONFIG_DIR@" +default_config[BACKUP_DEST]="@BACKUP_DEST@" +default_config[BACKUP_PATHS]="@BACKUP_PATHS@" +default_config[BACKUP_FLAGS]="@BACKUP_FLAGS@" +default_config[KEEP_BACKUPS]="@KEEP_BACKUPS@" +default_config[GAME_USER]="@GAME_USER@" +default_config[MAIN_EXECUTABLE]="@MAIN_EXECUTABLE_ABSOLUTE@" +default_config[SESSION_NAME]="@SESSION_NAME@" # Command and parameter declaration with which to start the server -[[ -n "${SERVER_START_CMD}" ]] && declare -r SERVER_START_CMD=${SERVER_START_CMD} || SERVER_START_CMD="@SERVER_START_CMD@" -[[ -n "${SERVER_START_SUCCESS}" ]] && declare -r SERVER_START_SUCCESS=${SERVER_START_SUCCESS} || SERVER_START_SUCCESS="@SERVER_START_SUCCESS@" +default_config[SERVER_MEMORY_INITIAL]="@SERVER_MEMORY_INITIAL@" +default_config[SERVER_MEMORY_MAXIMUM]="@SERVER_MEMORY_MAXIMUM@" +default_config[SERVER_START_CMD]="@SERVER_START_CMD@" +default_config[SERVER_START_SUCCESS]="@SERVER_START_SUCCESS@" # System parameters for the control script -[[ -n "${IDLE_SERVER}" ]] && tmp_IDLE_SERVER=${IDLE_SERVER} || IDLE_SERVER="@IDLE_SERVER@" -[[ -n "${IDLE_SESSION_NAME}" ]] && declare -r IDLE_SESSION_NAME=${IDLE_SESSION_NAME} || IDLE_SESSION_NAME="@IDLE_SESSION_NAME@" -[[ -n "${GAME_PORT}" ]] && declare -r GAME_PORT=${GAME_PORT} || GAME_PORT="@GAME_PORT@" -[[ -n "${CHECK_PLAYER_TIME}" ]] && declare -r CHECK_PLAYER_TIME=${CHECK_PLAYER_TIME} || CHECK_PLAYER_TIME="@CHECK_PLAYER_TIME@" -[[ -n "${IDLE_IF_TIME}" ]] && declare -r IDLE_IF_TIME=${IDLE_IF_TIME} || IDLE_IF_TIME="@IDLE_IF_TIME@" +default_config[IDLE_SERVER]="@IDLE_SERVER@" +default_config[IDLE_SESSION_NAME]="@IDLE_SESSION_NAME@" +default_config[GAME_PORT]="@GAME_PORT@" +default_config[CHECK_PLAYER_TIME]="@CHECK_PLAYER_TIME@" +default_config[IDLE_IF_TIME]="@IDLE_IF_TIME@" # Additional configuration options which only few may need to alter -[[ -n "${GAME_COMMAND_DUMP}" ]] && declare -r GAME_COMMAND_DUMP=${GAME_COMMAND_DUMP} || GAME_COMMAND_DUMP="@GAME_COMMAND_DUMP@" +default_config[GAME_COMMAND_DUMP]="@GAME_COMMAND_DUMP@" + +# Variables whose defaults get overridden in instantiated mode. %i is substituted with the instance name. +declare -A instance_config +instance_config[SESSION_NAME]="@GAME@-%i" +instance_config[SERVER_ROOT]="@SERVER_ROOT@/servers/%i" +instance_config[BACKUP_DEST]="@SERVER_ROOT@/servers/%i/backup" + +# Variables set through the environment can never be overridden by config files. +declare -A environment_overrides +# Variables set through instance_config can be overridden by the instance config file, but not by the +# global configuration file. +declare -A instance_overrides + +# Gather environment variables and make note of overrides. +for var in ${!default_config[@]}; do + if [[ -v $var ]]; then + echo "Configuration variable overridden by environment: ${var}=${!var}" >&2 + environment_overrides[$var]="${!var}" + continue + fi + + if [[ -v SERVER_INSTANCE && -v instance_config[$var] ]]; then + instance_overrides[$var]="${instance_config[$var]//%i/${SERVER_INSTANCE}}" + else + declare $var="${default_config[$var]}" + fi +done # Variables passed over the command line will always override the one from a config file if test -r "${CONFIG_PATH}"; then source "${CONFIG_PATH}" || (echo "Could not source ${CONFIG_PATH}" >&2) fi -# Preserve the content of IDLE_SERVER without making it readonly -[[ -n ${tmp_IDLE_SERVER} ]] && IDLE_SERVER=${tmp_IDLE_SERVER} +# Restore config variables that are always instance-specific. +for var in ${!instance_overrides[@]}; do + declare $var="${instance_overrides[$var]}" +done + +# Load the instance-specific configuration if it exists. +if [[ -v SERVER_INSTANCE ]]; then + INSTANCE_CONFIG_PATH="${INSTANCE_CONFIG_DIR}/${SERVER_INSTANCE}" + if test -r "${INSTANCE_CONFIG_PATH}"; then + source "${INSTANCE_CONFIG_PATH}" || (echo "Could not source ${INSTANCE_CONFIG_PATH}" >&2) + fi +fi +# Expand macros in the config and set final values. +# These are set to readonly so they cannot be accidentally changed later. +for var in ${!default_config[@]}; do + value="${environment_overrides[$var]:-${!var}}" + for ivar in ${!default_config[@]} INAME GAME_USER; do + value="${value//@$ivar@/${!ivar}}" + done + + declare -r $var="$value" +done -# Strictly disallow uninitialized Variables -set -u # Exit if a single command breaks and its failure is not handled accordingly set -e @@ -61,7 +150,7 @@ fi # directory includes the uid. The systemd sandboxing uses a private /tmp, # so to expose the tmux socket directory to the host we need to change to # a socket path that uses the user id. -GAME_USER_UID=$(${SUDO_CMD} id -u) +GAME_USER_UID=$(id -u ${GAME_USER}) LEGACY_TMUX_SOCKET_PATH="/tmp/tmux-${GAME_USER_UID}/${SESSION_NAME}" TMUX_SOCKET_PATH="@TMUX_SOCKET_DIR@/${SESSION_NAME}" # Look for the old socket path prior to version upgrade. If we find it, @@ -83,10 +172,12 @@ else NETCAT_CMD="" fi -# Check for sudo rigths -if [[ "$(${SUDO_CMD} whoami)" != "${GAME_USER}" ]]; then - >&2 echo -e "You have \e[39;1mno permission\e[0m to run commands as $GAME_USER user." - exit 21 +# Check for sudo rights +if [[ "${COMMAND}" != "print" ]]; then + if [[ "$(${SUDO_CMD} whoami)" != "${GAME_USER}" ]]; then + >&2 echo -e "You have \e[39;1mno permission\e[0m to run commands as $GAME_USER user." + exit 21 + fi fi # Pipe any given argument to the game server console, @@ -154,7 +245,7 @@ idle_server_daemon() { no_player=$(( no_player + CHECK_PLAYER_TIME )) # Stop the game server if no player was active for at least ${IDLE_IF_TIME} if [[ "${no_player}" -ge "${IDLE_IF_TIME}" ]]; then - IDLE_SERVER="false" ${INAME} stop + env IDLE_SERVER="false" ${INAME} stop # Wait for game server to go down for i in {1..100}; do socket_has_session "${SESSION_NAME}" || break @@ -167,7 +258,7 @@ idle_server_daemon() { echo -n "Netcat: " ${NETCAT_CMD} -v -l -p ${GAME_PORT} 2>&1 | (grep -m1 -i "connect" && pkill -P $$ ${NETCAT_CMD}) || true echo "Netcat caught a connection. The server is coming up again..." - IDLE_SERVER="false" ${INAME} start + env IDLE_SERVER="false" ${INAME} start fi else # Reset timer since there is an active player on the server @@ -180,7 +271,7 @@ idle_server_daemon() { echo -n "Netcat: " ${NETCAT_CMD} -v -l -p ${GAME_PORT} 2>&1 | (grep -m1 -i "connect" && pkill -P $$ ${NETCAT_CMD}) || true echo "Netcat caught a connection. The server is coming up again..." - IDLE_SERVER="false" ${INAME} start + env IDLE_SERVER="false" ${INAME} start fi done } @@ -497,27 +588,30 @@ socket_session_is_alive() { fi } -# Help function, no arguments required -help() { - cat <<-EOF - This script was designed to easily control any ${GAME} server. Almost any parameter for a given - ${GAME} server derivative can be changed by editing the variables in the configuration file. - - Usage: ${INAME} {start|stop|restart|status|backup|restore|command |console} - start Start the ${GAME} server - stop Stop the ${GAME} server - restart Restart the ${GAME} server - status Print some status information - backup Backup the world data - restore [filename] Restore the world data from a backup - command Run the given command at the ${GAME} server console - console Enter the server console through a tmux session - - Copyright (c) Gordian Edenhofer - EOF +print_config() { + if [[ -n "$1" ]]; then + if [[ ! -v "default_config[$1]" ]]; then + echo "$1 is not a recognized configuration variable." >&2 + return 1 + fi + + echo "${!1}" + else + ( + for var in ${!default_config[@]}; do + echo "$var=${!var}" + done + ) | sort + fi +} + +init_directories() { + for dir in $SERVER_ROOT $BACKUP_DEST; do + $SUDO_CMD install -D -d -m 0700 $dir + done } -case "${1:-}" in +case "$COMMAND" in start) server_start ;; @@ -539,7 +633,7 @@ case "${1:-}" in ;; command) - server_command "${@:2}" + server_command "${SERVER_COMMAND}" ;; backup) @@ -547,22 +641,20 @@ case "${1:-}" in ;; restore) - backup_restore "${@:2}" + backup_restore "${ARCHIVE_PATH}" ;; - idle_server_daemon) - # This shall be a hidden function which should only be invoked internally - idle_server_daemon + print) + print_config "${CONFIG_VAR:-}" ;; - -h|--help) - help - exit 0 + init) + init_directories ;; - *) - help - exit 1 + idle_server_daemon) + # This shall be a hidden function which should only be invoked internally + idle_server_daemon ;; esac diff --git a/minecraftd@.service.in b/minecraftd@.service.in index 39c396e..b1adbd2 100644 --- a/minecraftd@.service.in +++ b/minecraftd@.service.in @@ -1,18 +1,12 @@ [Unit] Description=@GAME@ Server - %i instance -After=local-fs.target network.target multi-user.target +After=local-fs.target network.target multi-user.target @GAME@-init@%i.service +Requires=@GAME@-init@%i.service [Service] Type=forking -Environment=SERVER_ROOT=@SERVER_ROOT@/servers/%i -Environment=BACKUP_DEST=@SERVER_ROOT@/servers/%i/backup -Environment=SESSION_NAME=@GAME@-%i -EnvironmentFile=-@INSTANCE_CONFIG_DIR@/%i -# create the "servers" directory followed by the instance directory -ExecStartPre=+/bin/bash -c "/bin/test -d @SERVER_ROOT@/servers || /bin/install -d -m 0750 -o @GAME_USER@ -g @GAME_USER@ @SERVER_ROOT@/servers" -ExecStartPre=+/bin/bash -c "/bin/test -d @SERVER_ROOT@/servers/%i || /bin/install -d -m 0750 -o @GAME_USER@ -g @GAME_USER@ @SERVER_ROOT@/servers/%i" -ExecStart=/usr/bin/@INAME@ start -ExecStop=/usr/bin/@INAME@ stop +ExecStart=/usr/bin/@INAME@ -i %i start +ExecStop=/usr/bin/@INAME@ -i %i stop User=@GAME_USER@ Group=@GAME_USER@ ProtectSystem=strict