minecraft-server-fabric/minecraftd.sh
Gordian Edenhofer 9fafea9acf upgpkg: (minecraft-server|spigot) 1.9.4-2
Pause player checking when in idle_server mode as long as someone is
connected to the server console. Not only does it make sense for any
admin which wants to monitor the console, it also removes annoying list
invocations from the console while e.g. typing in it.

upgpkg: minecraft-server 1.9.4-2
upgpkg: spigot 1.9.4-2
2016-05-20 11:53:30 +02:00

464 lines
16 KiB
Bash
Executable File

#!/bin/bash
# The actual program name
declare -r myname="minecraftd"
declare -r game="minecraft"
# 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
[[ ! -z "${SERVER_ROOT}" ]] && declare -r SERVER_ROOT=${SERVER_ROOT} || SERVER_ROOT="/srv/${game}"
[[ ! -z "${BACKUP_DEST}" ]] && declare -r BACKUP_DEST=${BACKUP_DEST} || BACKUP_DEST="/srv/${game}/backup"
[[ ! -z "${LOGPATH}" ]] && declare -r LOGPATH=${LOGPATH} || LOGPATH="/srv/${game}/logs"
[[ ! -z "${BACKUP_PATHS}" ]] && declare -r BACKUP_PATHS=${BACKUP_PATHS} || BACKUP_PATHS="world"
[[ ! -z "${KEEP_BACKUPS}" ]] && declare -r KEEP_BACKUPS=${KEEP_BACKUPS} || KEEP_BACKUPS="10"
[[ ! -z "${GAME_USER}" ]] && declare -r GAME_USER=${GAME_USER} || GAME_USER="minecraft"
[[ ! -z "${MAIN_EXECUTABLE}" ]] && declare -r MAIN_EXECUTABLE=${MAIN_EXECUTABLE} || MAIN_EXECUTABLE="minecraft_server.jar"
[[ ! -z "${SESSION_NAME}" ]] && declare -r SESSION_NAME=${SESSION_NAME} || SESSION_NAME="${game}"
# System parameters for java
[[ ! -z "${MINHEAP}" ]] && declare -r MINHEAP=${MINHEAP} || MINHEAP="512M"
[[ ! -z "${MAXHEAP}" ]] && declare -r MAXHEAP=${MAXHEAP} || MAXHEAP="1024M"
[[ ! -z "${THREADS}" ]] && declare -r THREADS=${THREADS} || THREADS="1"
[[ ! -z "${JAVA_PARMS}" ]] && declare -r JAVA_PARMS=${JAVA_PARMS} || JAVA_PARMS="-Xmx${MAXHEAP} -Xms${MINHEAP} -XX:ParallelGCThreads=${THREADS}"
# System parameters for the control script
[[ ! -z "${IDLE_SERVER}" ]] && tmp_IDLE_SERVER=${IDLE_SERVER} || IDLE_SERVER="false"
[[ ! -z "${IDLE_SESSION_NAME}" ]] && declare -r IDLE_SESSION_NAME=${IDLE_SESSION_NAME} || IDLE_SESSION_NAME="idle_server"
[[ ! -z "${GAME_PORT}" ]] && declare -r GAME_PORT=${GAME_PORT} || GAME_PORT="25565"
[[ ! -z "${CHECK_PLAYER_TIME}" ]] && declare -r CHECK_PLAYER_TIME=${CHECK_PLAYER_TIME} || CHECK_PLAYER_TIME="30"
[[ ! -z "${IDLE_IF_TIME}" ]] && declare -r IDLE_IF_TIME=${IDLE_IF_TIME} || IDLE_IF_TIME="1200"
# Variables passed over the command line will always override the one from a config file
source /etc/conf.d/${game} || echo "Could not source /etc/conf.d/${game}"
# Preserve the content of IDLE_SERVER without making it readonly
[[ ! -z ${tmp_IDLE_SERVER} ]] && IDLE_SERVER=${tmp_IDLE_SERVER}
# Check whether sudo is needed at all
if [[ $(whoami) == ${GAME_USER} ]]; then
SUDO_CMD=""
else
SUDO_CMD="sudo -u ${GAME_USER}"
fi
# Choose which flavor of netcat is to be used
if which netcat &> /dev/null; then
NETCAT_CMD="netcat"
elif which ncat &> /dev/null; then
NETCAT_CMD="ncat"
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
fi
# Pipe any given argument to the game server console
game_command() {
${SUDO_CMD} screen -S "${SESSION_NAME}" -X stuff "`printf \"$*\r\"`"
}
# Check whether the server is visited by a player otherwise shut it down
idle_server_daemon() {
# This function is run within a screen session of the GAME_USER therefore SUDO_CMD can be omitted
if [[ $(whoami) != ${GAME_USER} ]]; then
>&2 echo "Somehow this hidden function was not executed by the ${GAME_USER} user."
>&2 echo "This should not have happend. Are you messing around with this script? :P"
exit 22
fi
# Time in seconds for which no player was on the server
no_player=0
while true; do
echo -e "no_players: ${no_player}s\tcheck_player_time: ${CHECK_PLAYER_TIME}s\tidle_if_time: ${IDLE_IF_TIME}s"
# Retry in ${CHECK_PLAYER_TIME} seconds
sleep ${CHECK_PLAYER_TIME}
screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
# Game server is up and running
if [[ "$(screen -S "${SESSION_NAME}" -ls | sed -n 2p | awk '{ print $2 }')" == "(Attached)" ]]; then
# An administrator is connected to the console, pause player checking
echo "An admin is connected to the console. Pause player checking."
# The list command prints a line containing the usernames after the last occurrence of ": "
# and since playernames may not contain this string the clean player-list can be easily retrieved.
elif [[ -z $(SUDO_CMD="" game_command list; sleep 0.6; tail -n 1 "${LOGPATH}/latest.log" | sed -r 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' | sed 's/.*\: //' | tr -d '\n') ]]; then
# No player was seen on the server through list
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" ${myname} stop
# Wait for game server to go down
for i in {1..100}; do
screen -S "${SESSION_NAME}" -Q select . > /dev/null
[[ $? -eq 1 ]] && break
[[ $i -eq 100 ]] && echo -e "An \e[39;1merror\e[0m occurred while trying to reset the idle_server!"
sleep 0.1
done
# Reset timer and give the player 300 seconds to connect after pinging
no_player=$(( IDLE_IF_TIME - 300 ))
# Game server is down, listen on port ${GAME_PORT} for incoming connections
echo -n "Netcat: "
${NETCAT_CMD} -v -l -p ${GAME_PORT}
[[ $? -eq 0 ]] && echo "Netcat caught an connection. The server is coming up again..."
IDLE_SERVER="false" ${myname} start
fi
elif [[ $? -eq 0 ]]; then
# Reset timer since there is an active player on the server
no_player=0
fi
else
# Reset timer and give the player 300 seconds to connect after pinging
no_player=$(( IDLE_IF_TIME - 300 ))
# Game server is down, listen on port ${GAME_PORT} for incoming connections
echo -n "Netcat: "
${NETCAT_CMD} -v -l -p ${GAME_PORT}
[[ $? -eq 0 ]] && echo "Netcat caught an connection. The server is coming up again..."
IDLE_SERVER="false" ${myname} start
fi
done
}
# Start the server if it is not already running
server_start() {
# Start the game server
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
echo "A screen ${SESSION_NAME} session is already running. Please close it first."
else
echo -en "Starting server..."
${SUDO_CMD} screen -dmS "${SESSION_NAME}" /bin/bash -c "cd '${SERVER_ROOT}'; java ${JAVA_PARMS} -jar '${SERVER_ROOT}/${MAIN_EXECUTABLE}' nogui"
echo -e "\e[39;1m done\e[0m"
fi
if [[ "${IDLE_SERVER}" == "true" ]]; then
# Check for the availability of the netcat (nc) binaries
if [[ -z "${NETCAT_CMD}" ]]; then
>&2 echo "The netcat binaries are needed for suspending an idle server."
exit 12
fi
# Start the idle server daemon
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -X quit
# Restart as soon as the idle_server_daemon has shut down completely
for i in {1..100}; do
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -Q select . > /dev/null
[[ $? -eq 1 ]] && ${SUDO_CMD} screen -dmS "${IDLE_SESSION_NAME}" /bin/bash -c "${myname} idle_server_daemon" && break
[[ $i -eq 100 ]] && echo -e "An \e[39;1merror\e[0m occurred while trying to reset the idle_server!"
sleep 0.1
done
else
echo -en "Starting idle server daeomon..."
${SUDO_CMD} screen -dmS "${IDLE_SESSION_NAME}" /bin/bash -c "${myname} idle_server_daemon"
echo -e "\e[39;1m done\e[0m"
fi
fi
}
# Stop the server gracefully by saving everything prior and warning the users
server_stop() {
# Quit the idle daemon
if [[ "${IDLE_SERVER}" == "true" ]]; then
# Check for the availability of the netcat (nc) binaries
if [[ -z "${NETCAT_CMD}" ]]; then
>&2 echo "The netcat binaries are needed for suspending an idle server."
exit 12
fi
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
echo -en "Stopping idle server daemon..."
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -X quit
echo -e "\e[39;1m done\e[0m"
else
echo "The corresponding screen session for ${IDLE_SESSION_NAME} was already dead."
fi
fi
# Gracefully exit the game server
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
# Game server is up and running, gracefully stop the server when there are still active players
# The list command prints a line containing the usernames after the last occurrence of ": "
# and since playernames may not contain this string the clean player-list can be easily retrieved.
if [[ -z $(game_command list; sleep 0.6; tail -n 1 "${LOGPATH}/latest.log" | sed -r 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' | sed 's/.*\: //' | tr -d '\n') ]]; then
# No player was seen on the server through list
echo -en "Server is going down..."
game_command stop
else
# Player(s) were seen on the server through list (or an error occurred)
# Warning the users through the server console
game_command say "Server is going down in 10 seconds! HURRY UP WITH WHATEVER YOU ARE DOING!"
game_command save-all
echo -en "Server is going down in..."
for i in {1..10}; do
game_command say "down in... $(expr 10 - $i)"
echo -n " $(expr 10 - $i)"
sleep 1
done
game_command stop
fi
# Finish as soon as the server has shut down completely
for i in {1..100}; do
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
[[ $? -eq 1 ]] && echo -e "\e[39;1m done\e[0m" && break
[[ $i -eq 100 ]] && echo -e "\e[39;1m timed out\e[0m"
sleep 0.1
done
else
echo "The corresponding screen session for ${SESSION_NAME} was already dead."
fi
}
# Print whether the server is running and if so give some information about memory usage and threads
server_status() {
# Print status information about the idle daemon
if [[ "${IDLE_SERVER}" == "true" ]]; then
# Check for the availability of the netcat (nc) binaries
if [[ -z "${NETCAT_CMD}" ]]; then
>&2 echo "The netcat binaries are needed for suspending an idle server."
exit 12
fi
${SUDO_CMD} screen -S "${IDLE_SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
echo -e "Idle server daemon status:\e[39;1m running\e[0m"
else
echo -e "Idle server daemon status:\e[39;1m stopped\e[0m"
fi
fi
# Print status information for the game server
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
echo -e "Status:\e[39;1m running\e[0m"
# Calculating memory usage
for p in $(${SUDO_CMD} pgrep -f "${MAIN_EXECUTABLE}"); do
ps -p${p} -O rss | tail -n 1;
done | gawk '{ count ++; sum += $2 }; END {count --; print "Number of processes =", count, "(screen, bash,", count-2, "x java)"; print "Total memory usage =", sum/1024, "MB" ;};'
else
echo -e "Status:\e[39;1m stopped\e[0m"
fi
}
# Restart the complete server by shutting it down and starting it again
server_restart() {
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
server_stop
server_start
else
server_start
fi
}
# Backup the directories specified in BACKUP_PATHS
backup_files() {
# Check for the availability of the tar binaries
which tar &> /dev/null
if [[ $? -ne 0 ]]; then
>&2 echo "The tar binaries are needed for a backup."
exit 11
fi
echo "Starting backup..."
FILE="$(date +%Y_%m_%d_%H.%M.%S).tar.gz"
${SUDO_CMD} mkdir -p "${BACKUP_DEST}"
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
game_command save-off
game_command save-all
sync && wait
${SUDO_CMD} tar -C "${SERVER_ROOT}" -czf "${BACKUP_DEST}/${FILE}" --totals ${BACKUP_PATHS} 2>&1 | grep -v "tar: Removing leading "
game_command save-on
else
${SUDO_CMD} tar -C "${SERVER_ROOT}" -czf "${BACKUP_DEST}/${FILE}" --totals ${BACKUP_PATHS} 2>&1 | grep -v "tar: Removing leading "
fi
echo -e "\e[39;1mbackup completed\e[0m\n"
echo -n "Only keeping the last ${KEEP_BACKUPS} backups and removing the other ones..."
BACKUP_COUNT=$(for f in "${BACKUP_DEST}"/[0-9_.]*; do echo ${f}; done | wc -l)
if [[ $(expr ${BACKUP_COUNT} - ${KEEP_BACKUPS}) -gt 0 ]]; then
${SUDO_CMD} rm $(for f in "${BACKUP_DEST}"/[0-9_.]*; do echo ${f}; done | head -n$(expr ${BACKUP_COUNT} - ${KEEP_BACKUPS}))
echo -e "\e[39;1m done\e[0m ($(expr ${BACKUP_COUNT} - ${KEEP_BACKUPS}) backup(s) pruned)"
else
echo -e "\e[39;1m done\e[0m (no backups pruned)"
fi
}
# Restore backup
backup_restore() {
# Check for the availability of the tar binaries
which tar &> /dev/null
if [[ $? -ne 0 ]]; then
>&2 echo "The tar binaries are needed for a backup."
exit 11
fi
# Only allow the user to restore a backup if the server is down
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
>&2 echo -e "The \e[39;1mserver should be down\e[0m in order to restore the world data."
exit 3
fi
# Either let the user choose a backup or expect one as an argument
if [[ $# -lt 1 ]]; then
echo "Please enter the corresponding number for the backup to be restored: "
i=1
for f in "${BACKUP_DEST}"/[0-9_.]*; do
echo -e " \e[39;1m$i)\e[0m\t$f"
i=$(( i + 1 ))
done
echo -en "Restore backup number: "
# Read in user input
read user_choice
# Interpeting the input
if [[ $user_choice =~ ^-?[0-9]+$ ]]; then
n=1
for f in "${BACKUP_DEST}"/[0-9_.]*; do
[[ ${n} -eq $user_choice ]] && FILE="$f"
n=$(( n + 1 ))
done
if [[ -z $FILE ]]; then
>&2 echo -e "\e[39;1mFailed\e[0m to interpret your input. Please enter the digit of the presented options."
exit 5
fi
else
>&2 echo -e "\e[39;1mFailed\e[0m to interpret your input. Please enter a valid digit for one of the presented options."
exit 6
fi
elif [[ $# -eq 1 ]]; then
# Check for the existance of the specified file
if [[ -f "$1" ]]; then
FILE="$1"
else
if [[ -f "${BACKUP_DEST}"/"$1" ]]; then
FILE="${BACKUP_DEST}"/"$1"
else
>&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."
exit 4
fi
fi
elif [[ $# -gt 1 ]]; then
>&2 echo -e "\e[39;1mToo many arguments.\e[0m Please pass only the filename for the world data as an argument."
>&2 echo "Or alternatively no arguments at all to chose from a list of available backups."
exit 7
fi
echo "Restoring backup..."
${SUDO_CMD} tar -xf "${FILE}" -C "${SERVER_ROOT}" 2>&1
if [[ $? -eq 0 ]]; then
echo -e "\e[39;1mRestoration completed\e[0m"
else
echo -e "\e[39;1mFailed to restore backup.\e[0m"
fi
}
# Run the given comman at the game server console
server_command() {
if [[ $# -lt 1 ]]; then
>&2 echo "No server command specified. Try 'help' for a list of commands."
exit 1
fi
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
${SUDO_CMD} sleep 0.3 & tail -f --pid=$! -s 0.1 -n 0 "${LOGPATH}/latest.log" &
game_command "$@"
wait
else
echo "There is no ${SESSION_NAME} session to connect to."
fi
}
# Enter the screen game session
server_console() {
${SUDO_CMD} screen -S "${SESSION_NAME}" -Q select . > /dev/null
if [[ $? -eq 0 ]]; then
${SUDO_CMD} screen -S "${SESSION_NAME}" -rx
else
echo "There is no ${SESSION_NAME} session to connect to."
fi
}
# Help function, no arguments required
help() {
cat <<-EOF
This script was design to easily control any ${game} server. Quite every parameter for a given
${game} server derivative can be altered by editing the variables in the configuration file.
Usage: ${myname} {start|stop|status|backup|restore|command <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 <command> Run the given comman at the ${game} server console
console Enter the server console through a screen session
Copyright (c) Gordian Edenhofer <gordian.edenhofer@gmail.com>
EOF
}
case "$1" in
start)
server_start
;;
stop)
server_stop
;;
status)
server_status
;;
restart)
server_restart
;;
console)
server_console
;;
command)
server_command "${@:2}"
;;
backup)
backup_files
;;
restore)
backup_restore "${@:2}"
;;
idle_server_daemon)
# This shell be a hidden function which should only be invoced internally
idle_server_daemon
;;
*|-h|--help)
help
exit 0
esac
exit 0