├── Makefile ├── README.md ├── minecraftd-backup.service.in ├── minecraftd-backup.timer.in ├── minecraftd.conf.in ├── minecraftd.service.in ├── minecraftd.sh.in ├── minecraftd.sysusers.in └── minecraftd.tmpfiles.in /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/sh 2 | INSTALL = install 3 | INSTALL_PROGRAM = $(INSTALL) -m755 4 | INSTALL_DATA = $(INSTALL) -m644 5 | confdir = /etc/conf.d 6 | prefix = /usr 7 | bindir = $(prefix)/bin 8 | libdir = $(prefix)/lib 9 | datarootdir = $(prefix)/share 10 | mandir = $(prefix)/share/man 11 | man1dir = $(mandir)/man1 12 | 13 | SOURCES = minecraftd.sh.in minecraftd.conf.in minecraftd.service.in minecraftd.sysusers.in minecraftd.tmpfiles.in minecraftd-backup.service.in minecraftd-backup.timer.in 14 | OBJECTS = $(SOURCES:.in=) 15 | 16 | GAME = minecraft 17 | INAME = minecraftd 18 | SERVER_ROOT = /srv/$(GAME) 19 | BACKUP_DEST = $(SERVER_ROOT)/backup 20 | BACKUP_PATHS = world 21 | BACKUP_FLAGS = -z 22 | KEEP_BACKUPS = 10 23 | GAME_USER = $(GAME) 24 | MAIN_EXECUTABLE = minecraft_server.jar 25 | SESSION_NAME = $(GAME) 26 | SERVER_START_CMD = java -Xms512M -Xmx1024M -jar ./$${MAIN_EXECUTABLE} nogui 27 | SERVER_START_SUCCESS = done 28 | IDLE_SERVER = false 29 | IDLE_SESSION_NAME = idle_server_$${SESSION_NAME} 30 | GAME_PORT = 25565 31 | CHECK_PLAYER_TIME = 30 32 | IDLE_IF_TIME = 1200 33 | GAME_COMMAND_DUMP = /tmp/$${INAME}_$${SESSION_NAME}_command_dump.txt 34 | MAX_SERVER_START_TIME = 150 35 | MAX_SERVER_STOP_TIME = 100 36 | 37 | .MAIN = all 38 | 39 | define replace_all 40 | cp -a $(1) $(2) 41 | sed -i \ 42 | -e 's#@INAME@#$(INAME)#g' \ 43 | -e 's#@GAME@#$(GAME)#g' \ 44 | -e 's#@SERVER_ROOT@#$(SERVER_ROOT)#g' \ 45 | -e 's#@BACKUP_DEST@#$(BACKUP_DEST)#g' \ 46 | -e 's#@BACKUP_PATHS@#$(BACKUP_PATHS)#g' \ 47 | -e 's#@BACKUP_FLAGS@#$(BACKUP_FLAGS)#g' \ 48 | -e 's#@KEEP_BACKUPS@#$(KEEP_BACKUPS)#g' \ 49 | -e 's#@GAME_USER@#$(GAME_USER)#g' \ 50 | -e 's#@MAIN_EXECUTABLE@#$(MAIN_EXECUTABLE)#g' \ 51 | -e 's#@SESSION_NAME@#$(SESSION_NAME)#g' \ 52 | -e 's#@SERVER_START_CMD@#$(SERVER_START_CMD)#g' \ 53 | -e 's#@SERVER_START_SUCCESS@#$(SERVER_START_SUCCESS)#g' \ 54 | -e 's#@IDLE_SERVER@#$(IDLE_SERVER)#g' \ 55 | -e 's#@IDLE_SESSION_NAME@#$(IDLE_SESSION_NAME)#g' \ 56 | -e 's#@GAME_PORT@#$(GAME_PORT)#g' \ 57 | -e 's#@CHECK_PLAYER_TIME@#$(CHECK_PLAYER_TIME)#g' \ 58 | -e 's#@IDLE_IF_TIME@#$(IDLE_IF_TIME)#g' \ 59 | -e 's#@GAME_COMMAND_DUMP@#$(GAME_COMMAND_DUMP)#g' \ 60 | -e 's#@MAX_SERVER_START_TIME@#$(MAX_SERVER_START_TIME)#g' \ 61 | -e 's#@MAX_SERVER_STOP_TIME@#$(MAX_SERVER_STOP_TIME)#g' \ 62 | $(2) 63 | endef 64 | 65 | all: $(OBJECTS) 66 | echo $(OBJECTS) 67 | 68 | %.sh: %.sh.in 69 | $(call replace_all,$<,$@) 70 | 71 | %.conf: %.conf.in 72 | $(call replace_all,$<,$@) 73 | 74 | %.service: %.service.in 75 | $(call replace_all,$<,$@) 76 | 77 | %.sysusers: %.sysusers.in 78 | $(call replace_all,$<,$@) 79 | 80 | %.tmpfiles: %.tmpfiles.in 81 | $(call replace_all,$<,$@) 82 | 83 | %.timer: %.timer.in 84 | $(call replace_all,$<,$@) 85 | 86 | clean: 87 | rm -f $(OBJECTS) 88 | 89 | distclean: clean 90 | 91 | maintainer-clean: clean 92 | 93 | install: 94 | $(INSTALL_PROGRAM) -D minecraftd.sh "$(DESTDIR)$(bindir)/$(INAME)" 95 | $(INSTALL_DATA) -D minecraftd.conf "$(DESTDIR)$(confdir)/$(GAME)" 96 | $(INSTALL_DATA) -D minecraftd.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME).service" 97 | $(INSTALL_DATA) -D minecraftd-backup.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.service" 98 | $(INSTALL_DATA) -D minecraftd-backup.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.timer" 99 | $(INSTALL_DATA) -D minecraftd.sysusers "$(DESTDIR)$(libdir)/sysusers.d/$(INAME).conf" 100 | $(INSTALL_DATA) -D minecraftd.tmpfiles "$(DESTDIR)$(libdir)/tmpfiles.d/$(INAME).conf" 101 | 102 | uninstall: 103 | rm -f "$(bindir)/$(INAME)" 104 | rm -f "$(confdir)/$(GAME)" 105 | rm -f "$(libdir)/systemd/system/$(INAME).service" 106 | rm -f "$(libdir)/systemd/system/$(INAME)-backup.service" 107 | rm -f "$(libdir)/systemd/system/$(INAME)-backup.timer" 108 | rm -f "$(libdir)/sysusers.d/$(INAME).conf" 109 | rm -f "$(libdir)/tmpfiles.d/$(INAME).conf" 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Management Script for Minecraft Servers 2 | 3 | ## Content 4 | 5 | The main purpose of this repository is to develop a minecraft server management script. 6 | Its driving goals are minimalism, versatility (spigot/papermc/cuberite/...) and feature completeness. 7 | 8 | The script depends on `tmux` to fork the server into the background and communicate with it. 9 | All the communication namely querying for online users and issuing commands in the console of the server is done using `tmux`. 10 | While the server is offline, the script can listen on the minecraft port for incoming connections and start up the server as soon as a user connects. 11 | 12 | ## Installation 13 | 14 | ### Dependencies 15 | 16 | * bash 17 | * awk 18 | * sed 19 | * sudo -- privilege separation 20 | * tmux -- communication with server 21 | 22 | * netcat -- listen on the minecraft port for incoming connections while the server is down (optional) 23 | * tar -- take world backups (optional) 24 | 25 | ### Build 26 | 27 | ``` 28 | make 29 | ``` 30 | 31 | ### Installation 32 | 33 | ``` 34 | make install 35 | ``` 36 | 37 | ### Build and Install for a different Flavor of Minecraft 38 | 39 | ``` 40 | make GAME=spigot \ 41 | INAME=spigot \ 42 | SERVER_ROOT=/srv/craftbukkit \ 43 | BACKUP_PATHS="world world_nether world_the_end" \ 44 | GAME_USER=craftbukkit \ 45 | MAIN_EXECUTABLE=spigot.jar \ 46 | SERVER_START_CMD="java -Xms512M -Xmx1024M -jar ./spigot.jar nogui" 47 | ``` 48 | 49 | ``` 50 | make install \ 51 | GAME=spigot \ 52 | INAME=spigot 53 | ``` 54 | 55 | ## FAQ 56 | 57 | ### Where are the Server Files Located 58 | 59 | The world data is stored under /srv/minecraft and the server runs as minecraft user to increase security. 60 | Use the minecraft script under /usr/bin/minecraftd to start, stop or backup the server. 61 | 62 | ### How to configure the Server 63 | 64 | Adjust the configuration file under /etc/conf.d/minecraft to your liking. 65 | 66 | ### Server does not start 67 | 68 | For the server to start you have to accept the EULA in /srv/minecraft/eula.txt ! 69 | The EULA file is generated after the first server start. 70 | 71 | ## License 72 | 73 | Unless otherwise stated, the files in this project may be distributed under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version. This work is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. See [version 2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html) and [version 3](https://www.gnu.org/copyleft/gpl-3.0.html) of the GNU General Public License for more details. 74 | -------------------------------------------------------------------------------- /minecraftd-backup.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=@GAME@ Server World Backup 3 | After=local-fs.target 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/bin/@INAME@ backup 8 | User=@GAME_USER@ 9 | Group=@GAME_USER@ 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /minecraftd-backup.timer.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Daily @GAME@ Server Backup 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | AccuracySec=5min 7 | Persistent=true 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /minecraftd.conf.in: -------------------------------------------------------------------------------- 1 | # THIS IS THE CONFIGURATION FILE FOR THE MANAGING SCRIPT NOT FOR THE ACTUAL SERVER 2 | # Variables are interpreted in bash. Simply using bash-syntax is sufficient. 3 | 4 | # General parameters 5 | SERVER_ROOT="@SERVER_ROOT@" 6 | BACKUP_DEST="@BACKUP_DEST@" 7 | BACKUP_PATHS="@BACKUP_PATHS@" # World paths separated by spaces relative to SERVER_ROOT 8 | BACKUP_FLAGS="@BACKUP_FLAGS@" 9 | KEEP_BACKUPS="@KEEP_BACKUPS@" 10 | GAME_USER="@GAME_USER@" 11 | MAIN_EXECUTABLE="@MAIN_EXECUTABLE@" 12 | SESSION_NAME="@SESSION_NAME@" 13 | 14 | # System parameters for java 15 | # -Xms sets the intial heap size (must be a multiple of 1024 and greater than 2MB, no spaces!) 16 | # -Xmx sets the maximum heap size (must be a multiple of 1024 and greater than 2MB, no spaces!) 17 | SERVER_START_CMD="@SERVER_START_CMD@" 18 | 19 | # System parameters for the actual game server 20 | # Describes whether a daemon process which stops the server if it is not used by a player 21 | # within IDLE_IF_TIME seconds should be started. The GAME_PORT is not inhereted to the server! 22 | IDLE_SERVER=@IDLE_SERVER@ # true or false 23 | # Ensure that if SESSION_NAME is passed through the command line and therefore set to read only by the script, 24 | # IDLE_SESSION_NAME gets altered according to the command line and not the configurtion file, hence invoke the variable 25 | IDLE_SESSION_NAME="@IDLE_SESSION_NAME@" 26 | GAME_PORT="@GAME_PORT@" # used to listen for incoming connections when the server is down 27 | CHECK_PLAYER_TIME="@CHECK_PLAYER_TIME@" # in seconds 28 | IDLE_IF_TIME="@IDLE_IF_TIME@" # in seconds 29 | -------------------------------------------------------------------------------- /minecraftd.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=@GAME@ Server 3 | After=local-fs.target network.target multi-user.target 4 | 5 | [Service] 6 | Type=forking 7 | ExecStart=/usr/bin/@INAME@ start 8 | ExecStop=/usr/bin/@INAME@ stop 9 | User=@GAME_USER@ 10 | Group=@GAME_USER@ 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /minecraftd.sh.in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The actual program name (name of the interface) 4 | declare -r INAME="@INAME@" 5 | declare -r GAME="@GAME@" 6 | 7 | # General rule for the variable-naming-schema: 8 | # Variables in capital letters may be passed through the command line others not. 9 | # Avoid altering any of those later in the code since they may be readonly (IDLE_SERVER is an exception!) 10 | 11 | # You may use this script for any game server of your choice, just alter the config file 12 | [[ -n "${SERVER_ROOT}" ]] && declare -r SERVER_ROOT=${SERVER_ROOT} || SERVER_ROOT="@SERVER_ROOT@" 13 | [[ -n "${BACKUP_DEST}" ]] && declare -r BACKUP_DEST=${BACKUP_DEST} || BACKUP_DEST="@BACKUP_DEST@" 14 | [[ -n "${BACKUP_PATHS}" ]] && declare -r BACKUP_PATHS=${BACKUP_PATHS} || BACKUP_PATHS="@BACKUP_PATHS@" 15 | [[ -n "${BACKUP_FLAGS}" ]] && declare -r BACKUP_FLAGS=${BACKUP_FLAGS} || BACKUP_FLAGS="@BACKUP_FLAGS@" 16 | [[ -n "${KEEP_BACKUPS}" ]] && declare -r KEEP_BACKUPS=${KEEP_BACKUPS} || KEEP_BACKUPS="@KEEP_BACKUPS@" 17 | [[ -n "${GAME_USER}" ]] && declare -r GAME_USER=${GAME_USER} || GAME_USER="@GAME_USER@" 18 | [[ -n "${MAIN_EXECUTABLE}" ]] && declare -r MAIN_EXECUTABLE=${MAIN_EXECUTABLE} || MAIN_EXECUTABLE="@MAIN_EXECUTABLE@" 19 | [[ -n "${SESSION_NAME}" ]] && declare -r SESSION_NAME=${SESSION_NAME} || SESSION_NAME="@SESSION_NAME@" 20 | 21 | # Command and parameter declaration with which to start the server 22 | [[ -n "${SERVER_START_CMD}" ]] && declare -r SERVER_START_CMD=${SERVER_START_CMD} || SERVER_START_CMD="@SERVER_START_CMD@" 23 | [[ -n "${SERVER_START_SUCCESS}" ]] && declare -r SERVER_START_SUCCESS=${SERVER_START_SUCCESS} || SERVER_START_SUCCESS="@SERVER_START_SUCCESS@" 24 | 25 | # System parameters for the control script 26 | [[ -n "${IDLE_SERVER}" ]] && tmp_IDLE_SERVER=${IDLE_SERVER} || IDLE_SERVER="@IDLE_SERVER@" 27 | [[ -n "${IDLE_SESSION_NAME}" ]] && declare -r IDLE_SESSION_NAME=${IDLE_SESSION_NAME} || IDLE_SESSION_NAME="@IDLE_SESSION_NAME@" 28 | [[ -n "${GAME_PORT}" ]] && declare -r GAME_PORT=${GAME_PORT} || GAME_PORT="@GAME_PORT@" 29 | [[ -n "${CHECK_PLAYER_TIME}" ]] && declare -r CHECK_PLAYER_TIME=${CHECK_PLAYER_TIME} || CHECK_PLAYER_TIME="@CHECK_PLAYER_TIME@" 30 | [[ -n "${IDLE_IF_TIME}" ]] && declare -r IDLE_IF_TIME=${IDLE_IF_TIME} || IDLE_IF_TIME="@IDLE_IF_TIME@" 31 | 32 | # Additional configuration options which only few may need to alter 33 | [[ -n "${GAME_COMMAND_DUMP}" ]] && declare -r GAME_COMMAND_DUMP=${GAME_COMMAND_DUMP} || GAME_COMMAND_DUMP="@GAME_COMMAND_DUMP@" 34 | [[ -n "${MAX_SERVER_START_TIME}" ]] && declare -r MAX_SERVER_START_TIME=${MAX_SERVER_START_TIME} || MAX_SERVER_START_TIME="@MAX_SERVER_START_TIME@" 35 | [[ -n "${MAX_SERVER_STOP_TIME}" ]] && declare -r MAX_SERVER_STOP_TIME=${MAX_SERVER_STOP_TIME} || MAX_SERVER_STOP_TIME="@MAX_SERVER_STOP_TIME@" 36 | 37 | # Variables passed over the command line will always override the one from a config file 38 | source /etc/conf.d/"@GAME@" 2>/dev/null || >&2 echo "Could not source /etc/conf.d/@GAME@" 39 | 40 | # Preserve the content of IDLE_SERVER without making it readonly 41 | [[ -n ${tmp_IDLE_SERVER} ]] && IDLE_SERVER=${tmp_IDLE_SERVER} 42 | 43 | 44 | # Strictly disallow uninitialized Variables 45 | set -u 46 | # Exit if a single command breaks and its failure is not handled accordingly 47 | set -e 48 | 49 | # Check whether sudo is needed at all 50 | if [[ "$(whoami)" == "${GAME_USER}" ]]; then 51 | SUDO_CMD=() 52 | else 53 | SUDO_CMD=("sudo" "-u" "${GAME_USER}") 54 | fi 55 | 56 | # Choose which flavor of netcat is to be used 57 | if command -v netcat &> /dev/null; then 58 | NETCAT_CMD=("netcat") 59 | elif command -v ncat &> /dev/null; then 60 | NETCAT_CMD=("ncat") 61 | else 62 | NETCAT_CMD=() 63 | fi 64 | 65 | # Check for sudo rigths 66 | if [[ "$("${SUDO_CMD[@]}" whoami)" != "${GAME_USER}" ]]; then 67 | >&2 echo -e "You have \e[39;1mno permission\e[0m to run commands as $GAME_USER user." 68 | exit 21 69 | fi 70 | 71 | # Pipe any given argument to the game server console, 72 | # sleep for $sleep_time and return its output if $return_stdout is set 73 | game_command() { 74 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" wait-for -L "command_lock" 75 | if [[ -z "${return_stdout:-}" ]]; then 76 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" send-keys -t "${SESSION_NAME}":0.0 "$*" Enter 77 | else 78 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" pipe-pane -t "${SESSION_NAME}":0.0 "cat > ${GAME_COMMAND_DUMP}" 79 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" send-keys -t "${SESSION_NAME}":0.0 "$*" Enter 80 | sleep "${sleep_time:-0.3}" 81 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" pipe-pane -t "${SESSION_NAME}":0.0 82 | "${SUDO_CMD[@]}" cat "${GAME_COMMAND_DUMP}" 83 | fi 84 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" wait-for -U "command_lock" 85 | } 86 | 87 | # Check whether there are player on the server through list 88 | is_player_online() { 89 | response="$(sleep_time=0.6 return_stdout=true game_command list)" 90 | # Delete leading line and fancy characters from free response string 91 | response="$(echo "${response}" | sed -r -e 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[JKmsuG]//g')" 92 | # The list command prints a line containing the usernames after the last occurrence of ": " 93 | # and since playernames may not contain this string the clean player-list can easily be retrieved. 94 | # Otherwise check the first digit after the last occurrence of "There are". If it is 0 then there 95 | # are no players on the server. Should this test fail as well. Assume that a player is online. 96 | if [[ $(echo "${response}" | sed -n -r -e 's/.*\: ?//p' | tr -d '\n' | wc -c) -le 1 ]]; then 97 | # No player is online 98 | return 0 99 | elif [[ "x$(echo "${response}" | sed -n -r -e 's/.*there are[^0-9+]*([0-9]+).*$/\1/ip' | tr -d '\n')" == "x0" ]]; then 100 | # No player is online 101 | return 0 102 | else 103 | # A player is online (or it could not be determined) 104 | return 1 105 | fi 106 | } 107 | 108 | # Check whether the server is visited by a player otherwise shut it down 109 | idle_server_daemon() { 110 | # This function is run within a tmux session of the GAME_USER therefore SUDO_CMD can be omitted 111 | if [[ "$(whoami)" != "${GAME_USER}" ]]; then 112 | >&2 echo "Somehow this hidden function was not executed by the ${GAME_USER} user." 113 | >&2 echo "This should not have happend. Are you messing around with this script? :P" 114 | exit 22 115 | fi 116 | 117 | # Time in seconds for which no player was on the server 118 | no_player=0 119 | 120 | while true; do 121 | printf "no_player: %10ss check_player_time: %10ss idle_if_time: %10ss\n" "${no_player}" "${CHECK_PLAYER_TIME}" "${IDLE_IF_TIME}" 122 | # Retry in ${CHECK_PLAYER_TIME} seconds 123 | sleep "${CHECK_PLAYER_TIME}" 124 | 125 | if socket_has_session "${SESSION_NAME}"; then 126 | # Game server is up and running 127 | # Check for active player 128 | if [[ -n "$(tmux -L "${SESSION_NAME}" list-clients -t "${SESSION_NAME}":0.0 2> /dev/null)" ]]; then 129 | # An administrator is connected to the console, pause player checking 130 | echo "An admin is connected to the console. Pause player checking." 131 | elif is_player_online; then 132 | # No player was seen on the server through list 133 | no_player=$(( no_player + CHECK_PLAYER_TIME )) 134 | # Stop the game server if no player was active for at least ${IDLE_IF_TIME} 135 | if [[ "${no_player}" -ge "${IDLE_IF_TIME}" ]]; then 136 | IDLE_SERVER="false" ${INAME} stop 137 | # Wait for game server to go down 138 | for i in {1..100}; do 139 | socket_has_session "${SESSION_NAME}" || break 140 | [[ $i -eq 100 ]] && echo -e "An \e[39;1merror\e[0m occurred while trying to reset the idle_server!" 141 | sleep 0.1 142 | done 143 | # Reset timer and give the player 300 seconds to connect after pinging 144 | no_player=$(( IDLE_IF_TIME - 300 )) 145 | # Game server is down, listen on port ${GAME_PORT} for incoming connections 146 | echo -n "Netcat: " 147 | "${NETCAT_CMD[@]}" -v -l -p "${GAME_PORT}" 2>&1 | (grep -m1 -i "connect" && pkill -P $$ "${NETCAT_CMD[@]}") || true 148 | echo "Netcat caught a connection. The server is coming up again..." 149 | IDLE_SERVER="false" ${INAME} start 150 | fi 151 | else 152 | # Reset timer since there is an active player on the server 153 | no_player=0 154 | fi 155 | else 156 | # Reset timer and give the player 300 seconds to connect after pinging 157 | no_player=$(( IDLE_IF_TIME - 300 )) 158 | # Game server is down, listen on port ${GAME_PORT} for incoming connections 159 | echo -n "Netcat: " 160 | "${NETCAT_CMD[@]}" -v -l -p "${GAME_PORT}" 2>&1 | (grep -m1 -i "connect" && pkill -P $$ "${NETCAT_CMD[@]}") || true 161 | echo "Netcat caught a connection. The server is coming up again..." 162 | IDLE_SERVER="false" ${INAME} start 163 | fi 164 | done 165 | } 166 | 167 | # Start the server if it is not already running 168 | server_start() { 169 | # Start the game server 170 | if socket_has_session "${SESSION_NAME}"; then 171 | echo "A tmux ${SESSION_NAME} session is already running. Please close it first." 172 | else 173 | echo -en "Starting server..." 174 | "${SUDO_CMD[@]}" rm -f "${GAME_COMMAND_DUMP}" 175 | # Use a plain file as command buffers for the server startup and switch to a FIFO pipe later 176 | "${SUDO_CMD[@]}" touch "${GAME_COMMAND_DUMP}" 177 | # Ensure pipe-pine is started before the server itself by splitting the session creation and server startup 178 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" new-session -s "${SESSION_NAME}" -c "${SERVER_ROOT}" -d /bin/bash 179 | # Mimic GNU screen and allow for both C-a and C-b as prefix 180 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" set -g prefix2 C-a 181 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" wait-for -L "command_lock" 182 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" pipe-pane -t "${SESSION_NAME}":0.0 "cat > ${GAME_COMMAND_DUMP}" 183 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" send-keys -t "${SESSION_NAME}":0.0 "exec ${SERVER_START_CMD}" Enter 184 | for ((i=1; i<=MAX_SERVER_START_TIME; i++)); do 185 | sleep "${sleep_time:-0.1}" 186 | if ! socket_session_is_alive "${SESSION_NAME}"; then 187 | echo -e "\e[39;1m failed\e[0m\n" 188 | >&2 "${SUDO_CMD[@]}" cat "${GAME_COMMAND_DUMP}" 189 | "${SUDO_CMD[@]}" rm -f "${GAME_COMMAND_DUMP}" 190 | # Session is dead but remain-on-exit left it open; close it for sure 191 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" kill-session -t "${SESSION_NAME}" 192 | exit 1 193 | elif "${SUDO_CMD[@]}" grep -q -i "${SERVER_START_SUCCESS}" "${GAME_COMMAND_DUMP}"; then 194 | echo -e "\e[39;1m done\e[0m" 195 | break 196 | elif [[ $i -eq ${MAX_SERVER_START_TIME} ]]; then 197 | echo -e "\e[39;1m skipping\e[0m" 198 | >&2 echo -e "Server startup has not finished yet; continuing anyways" 199 | fi 200 | done 201 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" pipe-pane -t "${SESSION_NAME}":0.0 202 | # Let the command buffer be a FIFO pipe 203 | "${SUDO_CMD[@]}" rm -f "${GAME_COMMAND_DUMP}" 204 | "${SUDO_CMD[@]}" mkfifo "${GAME_COMMAND_DUMP}" 205 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" wait-for -U "command_lock" 206 | fi 207 | 208 | if [[ "${IDLE_SERVER,,}" == "true" ]]; then 209 | # Check for the availability of the netcat (nc) binaries 210 | if [[ -z "${NETCAT_CMD[*]}" ]]; then 211 | >&2 echo "The netcat binaries are needed for suspending an idle server." 212 | exit 12 213 | fi 214 | 215 | # Start the idle server daemon 216 | if socket_has_session "${IDLE_SESSION_NAME}"; then 217 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" kill-session -t "${IDLE_SESSION_NAME}" 218 | # Restart as soon as the idle_server_daemon has shut down completely 219 | for i in {1..100}; do 220 | sleep 0.1 221 | if ! socket_has_session "${IDLE_SESSION_NAME}"; then 222 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" new-session -s "${IDLE_SESSION_NAME}" -d "${INAME} idle_server_daemon" 223 | break 224 | fi 225 | [[ $i -eq 100 ]] && echo -e "An \e[39;1merror\e[0m occurred while trying to reset the idle_server!" 226 | done 227 | else 228 | echo -en "Starting idle server daemon..." 229 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" new-session -s "${IDLE_SESSION_NAME}" -d "${INAME} idle_server_daemon" 230 | echo -e "\e[39;1m done\e[0m" 231 | fi 232 | fi 233 | } 234 | 235 | # Stop the server gracefully by saving everything prior and warning the users 236 | server_stop() { 237 | # Quit the idle daemon 238 | if [[ "${IDLE_SERVER,,}" == "true" ]]; then 239 | # Check for the availability of the netcat (nc) binaries 240 | if [[ -z "${NETCAT_CMD[*]}" ]]; then 241 | >&2 echo "The netcat binaries are needed for suspending an idle server." 242 | exit 12 243 | fi 244 | 245 | if socket_has_session "${IDLE_SESSION_NAME}"; then 246 | echo -en "Stopping idle server daemon..." 247 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" kill-session -t "${IDLE_SESSION_NAME}" 248 | echo -e "\e[39;1m done\e[0m" 249 | else 250 | echo "The corresponding tmux session for ${IDLE_SESSION_NAME} was already dead." 251 | fi 252 | fi 253 | 254 | # Gracefully exit the game server 255 | if socket_has_session "${SESSION_NAME}"; then 256 | # Game server is up and running, gracefully stop the server when there are still active players 257 | 258 | # Check for active player 259 | if is_player_online; then 260 | # No player was seen on the server through list 261 | echo -en "Server is going down..." 262 | game_command stop 263 | else 264 | # Player(s) were seen on the server through list (or an error occurred) 265 | # Warning the users through the server console 266 | game_command say "Server is going down in 10 seconds! HURRY UP WITH WHATEVER YOU ARE DOING!" 267 | game_command save-all 268 | echo -en "Server is going down in..." 269 | for i in {1..10}; do 270 | game_command say "down in... $(( 10 - i ))" 271 | echo -n " $(( 10 - i ))" 272 | sleep 1 273 | done 274 | game_command stop 275 | fi 276 | 277 | # Finish as soon as the server has shut down completely 278 | for ((i=1; i<=MAX_SERVER_STOP_TIME; i++)); do 279 | if ! socket_has_session "${SESSION_NAME}"; then 280 | "${SUDO_CMD[@]}" rm -f "${GAME_COMMAND_DUMP}" 281 | echo -e "\e[39;1m done\e[0m" 282 | break 283 | fi 284 | [[ $i -eq ${MAX_SERVER_STOP_TIME} ]] && echo -e "\e[39;1m timed out\e[0m" 285 | sleep 0.1 286 | done 287 | else 288 | echo "The corresponding tmux session for ${SESSION_NAME} was already dead." 289 | fi 290 | } 291 | 292 | # Print whether the server is running and if so give some information about memory usage and threads 293 | server_status() { 294 | # Print status information about the idle daemon 295 | if [[ "${IDLE_SERVER,,}" == "true" ]]; then 296 | # Check for the availability of the netcat (nc) binaries 297 | if [[ -z "${NETCAT_CMD[*]}" ]]; then 298 | >&2 echo "The netcat binaries are needed for suspending an idle server." 299 | exit 12 300 | fi 301 | 302 | if socket_has_session "${IDLE_SESSION_NAME}"; then 303 | echo -e "Idle server daemon status:\e[39;1m running\e[0m" 304 | else 305 | echo -e "Idle server daemon status:\e[39;1m stopped\e[0m" 306 | fi 307 | fi 308 | 309 | # Print status information for the game server 310 | if socket_has_session "${SESSION_NAME}"; then 311 | echo -e "Status:\e[39;1m running\e[0m" 312 | 313 | # Calculating memory usage 314 | for p in $("${SUDO_CMD[@]}" pgrep -f "${MAIN_EXECUTABLE}"); do 315 | ps -p"${p}" -O rss | tail -n 1; 316 | done | awk '{ count ++; sum += $2 }; END {count --; print "Number of processes =", count, "(tmux +", count, "x server)"; print "Total memory usage =", sum/1024, "MB" ;};' 317 | else 318 | echo -e "Status:\e[39;1m stopped\e[0m" 319 | fi 320 | } 321 | 322 | # Restart the complete server by shutting it down and starting it again 323 | server_restart() { 324 | if socket_has_session "${SESSION_NAME}"; then 325 | server_stop 326 | server_start 327 | else 328 | server_start 329 | fi 330 | } 331 | 332 | # Backup the directories specified in BACKUP_PATHS 333 | backup_files() { 334 | # Check for the availability of the tar binaries 335 | if ! command -v tar &> /dev/null; then 336 | >&2 echo "The tar binaries are needed for a backup." 337 | exit 11 338 | fi 339 | 340 | echo "Starting backup..." 341 | fname="$(date +%Y_%m_%d_%H.%M.%S).tar.gz" 342 | "${SUDO_CMD[@]}" mkdir -p "${BACKUP_DEST}" 343 | if socket_has_session "${SESSION_NAME}"; then 344 | game_command save-off 345 | game_command save-all 346 | sleep "${sleep_time:-0.3}" 347 | sync && wait 348 | "${SUDO_CMD[@]}" tar -C "${SERVER_ROOT}" -cf "${BACKUP_DEST}/${fname}" ${BACKUP_PATHS} --totals ${BACKUP_FLAGS} 2>&1 | grep -v "tar: Removing leading " 349 | game_command save-on 350 | else 351 | "${SUDO_CMD[@]}" tar -C "${SERVER_ROOT}" -cf "${BACKUP_DEST}/${fname}" ${BACKUP_PATHS} --totals ${BACKUP_FLAGS} 2>&1 | grep -v "tar: Removing leading " 352 | fi 353 | echo -e "\e[39;1mbackup completed\e[0m\n" 354 | 355 | echo -n "Only keeping the last ${KEEP_BACKUPS} backups and removing the other ones..." 356 | backup_count=$(for f in "${BACKUP_DEST}"/[0-9_.]*; do echo "${f}"; done | wc -l) 357 | if [[ $(( backup_count - KEEP_BACKUPS )) -gt 0 ]]; then 358 | for old_backup in $(for f in "${BACKUP_DEST}"/[0-9_.]*; do echo "${f}"; done | head -n"$(( backup_count - KEEP_BACKUPS ))"); do 359 | "${SUDO_CMD[@]}" rm "${old_backup}"; 360 | done 361 | echo -e "\e[39;1m done\e[0m ($(( backup_count - KEEP_BACKUPS)) backup(s) pruned)" 362 | else 363 | echo -e "\e[39;1m done\e[0m (no backups pruned)" 364 | fi 365 | } 366 | 367 | # Restore backup 368 | backup_restore() { 369 | # Check for the availability of the tar binaries 370 | if ! command -v tar &> /dev/null; then 371 | >&2 echo "The tar binaries are needed for a backup." 372 | exit 11 373 | fi 374 | 375 | # Only allow the user to restore a backup if the server is down 376 | if socket_has_session "${SESSION_NAME}"; then 377 | >&2 echo -e "The \e[39;1mserver should be down\e[0m in order to restore the world data." 378 | exit 3 379 | fi 380 | 381 | # Either let the user choose a backup or expect one as an argument 382 | if [[ $# -lt 1 ]]; then 383 | echo "Please enter the corresponding number for the backup to be restored: " 384 | i=1 385 | for f in "${BACKUP_DEST}"/[0-9_.]*; do 386 | echo -e " \e[39;1m$i)\e[0m\t$f" 387 | i=$(( i + 1 )) 388 | done 389 | echo -en "Restore backup number: " 390 | 391 | # Read in user input 392 | read -r user_choice 393 | 394 | # Interpeting the input 395 | if [[ $user_choice =~ ^-?[0-9]+$ ]]; then 396 | n=1 397 | for f in "${BACKUP_DEST}"/[0-9_.]*; do 398 | [[ ${n} -eq $user_choice ]] && fname="$f" 399 | n=$(( n + 1 )) 400 | done 401 | if [[ -z $fname ]]; then 402 | >&2 echo -e "\e[39;1mFailed\e[0m to interpret your input. Please enter the digit of the presented options." 403 | exit 5 404 | fi 405 | else 406 | >&2 echo -e "\e[39;1mFailed\e[0m to interpret your input. Please enter a valid digit for one of the presented options." 407 | exit 6 408 | fi 409 | elif [[ $# -eq 1 ]]; then 410 | # Check for the existance of the specified file 411 | if [[ -f "$1" ]]; then 412 | fname="$1" 413 | else 414 | if [[ -f "${BACKUP_DEST}"/"$1" ]]; then 415 | fname="${BACKUP_DEST}"/"$1" 416 | else 417 | >&2 echo -e "Sorry, but '$1', is \e[39;1mnot a valid file\e[0m, neither in your current directory nor in the backup folder." 418 | exit 4 419 | fi 420 | fi 421 | elif [[ $# -gt 1 ]]; then 422 | >&2 echo -e "\e[39;1mToo many arguments.\e[0m Please pass only the filename for the world data as an argument." 423 | >&2 echo "Or alternatively, no arguments at all to choose from a list of available backups." 424 | exit 7 425 | fi 426 | 427 | echo "Restoring backup..." 428 | if "${SUDO_CMD[@]}" tar -xf "${fname}" -C "${SERVER_ROOT}" 2>&1; then 429 | echo -e "\e[39;1mRestoration completed\e[0m" 430 | else 431 | echo -e "\e[39;1mFailed to restore backup.\e[0m" 432 | fi 433 | } 434 | 435 | # Run the given command at the game server console 436 | server_command() { 437 | if [[ $# -lt 1 ]]; then 438 | >&2 echo "No server command specified. Try 'help' for a list of commands." 439 | exit 1 440 | fi 441 | 442 | if socket_has_session "${SESSION_NAME}"; then 443 | return_stdout=true game_command "$@" 444 | else 445 | echo "There is no ${SESSION_NAME} session to connect to." 446 | fi 447 | } 448 | 449 | # Enter the tmux game session 450 | server_console() { 451 | if socket_has_session "${SESSION_NAME}"; then 452 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" attach -t "${SESSION_NAME}":0.0 453 | else 454 | echo "There is no ${SESSION_NAME} session to connect to." 455 | fi 456 | } 457 | 458 | # Check if there is a session available 459 | socket_has_session() { 460 | if [[ "$(whoami)" != "${GAME_USER}" ]]; then 461 | "${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" has-session -t "${1}":0.0 2> /dev/null 462 | return $? 463 | fi 464 | tmux -L "${SESSION_NAME}" has-session -t "${1}":0.0 2> /dev/null 465 | return $? 466 | } 467 | 468 | socket_session_is_alive() { 469 | if socket_has_session "${1}"; then 470 | if [[ "$(whoami)" != "${GAME_USER}" ]]; then 471 | return "$("${SUDO_CMD[@]}" tmux -L "${SESSION_NAME}" list-panes -t "${1}":0.0 -F '#{pane_dead}' 2> /dev/null)" 472 | fi 473 | return "$(tmux -L "${SESSION_NAME}" list-panes -t "${1}":0.0 -F '#{pane_dead}' 2> /dev/null)" 474 | else 475 | return 1 476 | fi 477 | } 478 | 479 | # Help function, no arguments required 480 | help() { 481 | cat <<-EOF 482 | This script was designed to easily control any ${GAME} server. Almost any parameter for a given 483 | ${GAME} server derivative can be changed by editing the variables in the configuration file. 484 | 485 | Usage: ${INAME} {start|stop|restart|status|backup|restore|command |console} 486 | start Start the ${GAME} server 487 | stop Stop the ${GAME} server 488 | restart Restart the ${GAME} server 489 | status Print some status information 490 | backup Backup the world data 491 | restore [filename] Restore the world data from a backup 492 | command Run the given command at the ${GAME} server console 493 | console Enter the server console through a tmux session 494 | 495 | Copyright (c) Gordian Edenhofer 496 | EOF 497 | } 498 | 499 | case "${1:-}" in 500 | start) 501 | server_start 502 | ;; 503 | 504 | stop) 505 | server_stop 506 | ;; 507 | 508 | status) 509 | server_status 510 | ;; 511 | 512 | restart) 513 | server_restart 514 | ;; 515 | 516 | console) 517 | server_console 518 | ;; 519 | 520 | command) 521 | server_command "${@:2}" 522 | ;; 523 | 524 | backup) 525 | backup_files 526 | ;; 527 | 528 | restore) 529 | backup_restore "${@:2}" 530 | ;; 531 | 532 | idle_server_daemon) 533 | # This shall be a hidden function which should only be invoked internally 534 | idle_server_daemon 535 | ;; 536 | 537 | -h|--help) 538 | help 539 | exit 0 540 | ;; 541 | 542 | *) 543 | help 544 | exit 1 545 | ;; 546 | esac 547 | 548 | exit 0 549 | -------------------------------------------------------------------------------- /minecraftd.sysusers.in: -------------------------------------------------------------------------------- 1 | u @GAME_USER@ - "@GAME@ Server" @SERVER_ROOT@ /bin/bash 2 | -------------------------------------------------------------------------------- /minecraftd.tmpfiles.in: -------------------------------------------------------------------------------- 1 | z @SERVER_ROOT@ 2755 @GAME_USER@ @GAME_USER@ - - 2 | d @SERVER_ROOT@/logs 2755 @GAME_USER@ @GAME_USER@ - 3 | z @SERVER_ROOT@/logs - @GAME_USER@ @GAME_USER@ - - 4 | --------------------------------------------------------------------------------