#!/bin/bash ### BEGIN INIT INFO # Provides: minecraft # 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: Minecraft server # Description: Init script for minecraft/bukkit server, with rolling logs and use of ramdisk for less lag. ### END INIT INFO # Minecraft/Bukkit init script by Marcus Whybrow # - Modified from a script created by Ahtenus (https://github.com/Ahtenus/minecraft-init) # - Which was apparently based upon http://www.minecraftwiki.net/wiki/Server_startup_script # The location of the configuration file for this script CONFIG="/opt/minecraft/server1/manager.config" ### Load configuration variables source $CONFIG ### General Utility functions # Returns the current time as a UNIX timestamp (in seconds since 1970) now() { date +%s } # This function is used to quite the stdout of another function # like this: quite $(other_func) quite() { return $(true) } as_user() { if [ $(whoami) == $SERVER_USER ] ; then bash -c "$1" else su - $SERVER_USER -s /bin/bash -c "$1" fi } ### Log Utility Functions # Gets the time 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 '{print $1 " " $2}') date -d "$time_string" "+%s" 2> /dev/null } # Watches the log # $1: The line in the log to wait for # $2: A UNIX timestamp (seconds since 1970) which the $1 line must be after # returns: When the line is found log_wait_for_line() { # Make sure there is a server log to check as_user "touch $SERVER_LOG" regex="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\] ${1}" while read LINE do time=$(log_line_get_time "$LINE") # If the entry is old enough and matches the regular expression if [[ $time -ge $2 && $LINE =~ $regex ]] then echo $LINE break fi done < <(as_user "tail --follow --lines=100 --sleep-interval=0.1 $SERVER_LOG") } ### Server Utility Functions server_is_running() { if ps ax | grep -v grep | grep "$SCREEN_NAME $INVOCATION" > /dev/null then return $(true) else return $(false) fi } # Gets the process ID for the server if running, otherwise it outputs nothing server_pid() { ps ax | grep -v grep | grep "$SCREEN_NAME $INVOCATION" | awk '{print $1}' } server_wait_for_stop() { pid=$(server_pid) while ps -p $pid > /dev/null do sleep 0.1 done } # $1: A line of text to enter into the server console server_eval() { as_user "screen -p 0 -S $SCREEN_NAME -X eval 'stuff \"$1\"\015'" } # $1: A line of text to enter into the server console # $2: The line of text in the log to wait for server_eval_and_wait() { time=$(now) server_eval $1 log_wait_for_line "$2" "$time" } ### World Handling Functions worlds_get() { a=1 for NAME in $(ls ${WORLD_STORAGE_PATH}) do if [ -d ${WORLD_STORAGE_PATH}/$NAME ] then WORLDNAME[$a]=$NAME if [ -e ${WORLD_STORAGE_PATH}/$NAME/ramdisk ] then WORLDRAM[$a]=true else WORLDRAM[$a]=false fi a=$a+1 fi done } # Creates symbolic links in the server directory (SERVER_PATH) for each # of the Minecraft worlds located in the world storage directory (WORD_STORAGE_PATH). worlds_ensure_links() { echo "Updating world symbolic links..." worlds_get for INDEX in ${!WORLDNAME[@]} do # -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 ${SERVER_PATH}/${WORLDNAME[$INDEX]} || ! -a ${SERVER_PATH}/${WORLDNAME[$INDEX]} ]] then # If there is a symbolic link in the SERVER_PATH to this world or there is not # a directory in the SERVER_PATH for 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 ${SERVER_PATH}/${WORLDNAME[$INDEX]}) if ${WORLDRAM[$INDEX]} then # If this world is marked as loaded into RAM if [ "${link_target}" != "${RAMDISK_PATH}/${WORLDNAME[$INDEX]}" ] then # If the symbolic link does not point to the RAM version of the world # Remove the symbolic link if it exists as_user "rm -f ${SERVER_PATH}/${WORLDNAME[$INDEX]}" # Create a new symbolic link pointing to the RAM version of the world as_user "ln -s ${RAMDISK_PATH}/${WORLDNAME[$INDEX]} ${SERVER_PATH}/${WORLDNAME[$INDEX]}" echo "Created a link for ${WORLDNAME[$INDEX]} which is in RAM: ${RAMDISK_PATH}/${WORLDNAME[$INDEX]}" fi else # Otherwise the world is not loaded into RAM, and is just on disk if [ "${link_target}" != "${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}" ] then # If the symbolic link does not point to the disk version of the world # Remove the symbolic link if it exists as_user "rm -f ${SERVER_PATH}/${WORLDNAME[$INDEX]}" # Create a new symbolic link pointing to the disk version of the world as_user "ln -s ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]} ${SERVER_PATH}/${WORLDNAME[$INDEX]}" echo "Created a link for ${WORLDNAME[$INDEX]} which on disk: ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}" fi fi else # There was no symbolic link, and there was a directory, meaning a world # directory has been placed in SERVER_PATH, this is not allowed: echo "Could not process ${WORLDNAME[$INDEX]}. please move all worlds to ${WORLD_STORAGE_PATH}." exit 1 fi done echo "Finished updating world symbolic links." } world_to_ram() { as_user "mkdir -p ${RAMDISK_PATH}/$1 && rsync -rt --exclude 'ramdisk' ${WORLD_STORAGE_PATH}/$1/ ${RAMDISK_PATH}/$1" } worlds_to_ram() { echo "Synchronising worlds to RAM..." worlds_get for INDEX in ${!WORLDNAME[@]} do if ${WORLDRAM[$INDEX]} then if [ -L ${SERVER_PATH}/${WORLDNAME[$INDEX]} ] then printf "[RAM] \"${WORLDNAME[$INDEX]}\": Synchronising to RAM... " world_to_ram "${WORLDNAME[$INDEX]}" echo "Done." fi else echo "[DSK] \"${WORLDNAME[$INDEX]}\": Nothing to do." fi done echo "Finished synchronising worlds to RAM." } worlds_to_disk() { echo "Synchronising worlds in RAM to disk..." worlds_get for INDEX in ${!WORLDNAME[@]} do if [ -e ${RAMDISK_PATH}/${WORLDNAME[$INDEX]} ] then printf "[RAM] \"${WORLDNAME[$INDEX]}\": Synchronising to disk... " as_user "rsync -rt --exclude 'ramdisk' ${RAMDISK_PATH}/${WORLDNAME[$INDEX]}/ ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}" echo "Done." else echo "[DSK] \"${WORLDNAME[$INDEX]}\": Nothing to do." fi done echo "Finished synchronising worlds in RAM to disk." } world_toggle_ramdisk_state() { if [ ! -e ${WORLD_STORAGE_PATH}/$1 ] then echo "World \"$1\" not found." exit 1 fi if [ -e ${WORLD_STORAGE_PATH}/$1/ramdisk ] then as_user "rm ${WORLD_STORAGE_PATH}/$1/ramdisk" as_user "rm -r ${RAMDISK_PATH}/$1" echo "Removed the RAM flag from \"$1\", and deleted it from RAM." else as_user "touch ${WORLD_STORAGE_PATH}/$1/ramdisk" echo "Added the RAM flag to \"$1\"." printf "Copying world to RAM... " quite $(world_to_ram "$1") echo "Done." fi echo "Changes will only take effect after server is restarted." } ### Server Control Functions server_start() { if server_is_running then echo "$JAR is already running!" else worlds_ensure_links worlds_to_ram time=$(now) printf "Starting server... " as_user "cd $SERVER_PATH && screen -dmS $SCREEN_NAME $INVOCATION" quite $(log_wait_for_line "$CONFIRMATIONS_START" "$time") echo "Done." fi } server_saveall() { if server_is_running then # Send the "save-all" command and wait for it to finish printf "Forcing save... " quite $(server_eval_and_wait "save-all" "$CONFIRMATIONS_SAVE_ALL") echo "Done." else echo "$JAR was not running. Not forcing save." fi } server_saveoff() { if server_is_running then # Send the "save-off" command and wait for it to finish printf "Disabling level saving... " quite $(server_eval_and_wait "save-off" "$CONFIRMATIONS_SAVE_OFF") echo "Done." # Also save all server_saveall # Not sure what this does, might be important sync else echo "$JAR was not running. Not suspending saves." fi } server_saveon() { if server_is_running then # Send the "save-on" command and wait for it to finish printf "Enabling level saving... " quite $(server_eval_and_wait "save-on" "$CONFIRMATIONS_SAVE_ON") echo "Done." else echo "$JAR was not running. Not resuming saves." fi } server_stop() { if server_is_running then server_saveall printf "Stopping the server... " server_eval "stop" server_wait_for_stop echo "Done." else echo "$JAR was not running." fi } server_restart() { # Restarts the server if it is already running if server_is_running then # Stop the server server_stop # Synchronise all worlds in RAM to disk worlds_to_disk # Ensure world symbolic links are up to date worlds_ensure_links # Synchronise worlds back to RAM worlds_to_ram # Start the server again server_start else echo "$JAR is not running. Only running servers may be restarted." fi } # Not really tested this server_update_jars() { if server_is_running then echo "$JAR is running! Will not start update." else # Update minecraft_server.jar echo "Updating minecraft_server.jar...." MC_SERVER_URL=http://minecraft.net/$(wget -q -O - http://www.minecraft.net/download.jsp | grep minecraft_server.jar\ | cut -d \" -f 2) as_user "cd ${SERVER_PATH} && wget -q -O ${SERVER_PATH}/minecraft_server.jar.update $MC_SERVER_URL" if [ -f ${SERVER_PATH}/minecraft_server.jar.update ] then if $(diff ${SERVER_PATH}/minecraft_server.jar ${SERVER_PATH}/minecraft_server.jar.update >/dev/null) then echo "You are already running the latest version of the Minecraft server." as_user "rm ${SERVER_PATH}/minecraft_server.jar.update" else as_user "mv ${SERVER_PATH}/minecraft_server.jar.update $MCPATH/minecraft_server.jar" echo "Minecraft successfully updated." fi else echo "Minecraft update could not be downloaded." fi # Update craftbukkit echo "Updating craftbukkit...." as_user "cd ${SERVER_PATH} && wget -q -O ${SERVER_PATH}/craftbukkit.jar.update http://dl.bukkit.org/latest-rb/craftbukkit.jar" if [ -f ${SERVER_PATH}/craftbukkit.jar.update ] then if $(diff ${SERVER_PATH}/craftbukkit-0.0.1-SNAPSHOT.jar $MCPATH/craftbukkit.jar.update > /dev/null) then echo "You are already running the latest version of CraftBukkit." as_user "rm ${SERVER_PATH}/craftbukkit.jar.update" else as_user "mv ${SERVER_PATH}/craftbukkit.jar.update $MCPATH/craftbukkit-0.0.1-SNAPSHOT.jar" echo "CraftBukkit successfully updated." fi else echo "CraftBukkit update could not be downloaded." fi fi } ### Backup Functions backup_server() { path=${COMPLETE_BACKUP_PATH}/$(date "+%Y-%m-%d-%H-%M-%S").zip as_user "mkdir -p $COMPLETE_BACKUP_PATH" zip_flags="-rq" # Add the "y" flag if symbolic links should not be followed if [ "$COMPLETE_BACKUP_FOLLOW_SYMLINKS" != "true" ] then zip_flags="${zip_flags}y" fi # Zip up the server directory printf "Backing up the entire server directory... " as_user "mkdir -p ${COMPLETE_BACKUP_PATH} && cd ${SERVER_PATH} && zip ${zip_flags} $path ." echo "Done." } backup_worlds() { worlds_get echo "Backing up worlds..." for INDEX in ${!WORLDNAME[@]} do printf "Backing up world \"${WORLDNAME[$INDEX]}\"... " dir="${WORLD_SNAPSHOT_PATH}/${WORLDNAME[$INDEX]}" file_name="$(date "+%Y-%m-%d-%H-%M-%S").zip" as_user "mkdir -p ${dir} && cd ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]} && zip -rq ${dir}/${file_name} ." echo "Done." done echo "Finished backing up worlds." } # An experimental function which backs up config files for all # Bukkit plugins. Only recognises .yml files currently # I use the backup_server function instead to get everything # for sure. backup_configs() { dir=${CONFIG_BACKUP_PATH} file_name="$(date "+%Y-%m-%d-%H-%M-%S").zip" printf "Backing up plugin config files... " as_user "mkdir -p ${dir} && cd ${SERVER_PLUGIN_PATH} && zip -Rq ${dir}/${file_name} '${CONFIG_BACKUP_PATTERN}'" echo "Done." } ### Maintenance Functions log_roll() { # Moves and Gzips the logfile, a big log file slows down the # server A LOT (what was notch thinking?) path=${LOG_ARCHIVE_PATH}/${SERVER_NAME}-$(date +%F-%H-%M-%S).log printf "Rolling server logs... " as_user "mkdir -p $LOG_ARCHIVE_PATH && cp $SERVER_LOG $path && gzip $path" if [ $? -eq 0 ] then as_user "cp /dev/null $SERVER_LOG && echo \"Previous logs rolled to $path\" > $SERVER_LOG" else echo "Failed to rotate logs to $path.gz" fi echo "Done." } ### Script switch statement case "$1" in start) # Starts the server server_start ;; stop) # Stops the server if server_is_running then if [[ $2 != "now" ]] then server_eval "say ${MESSAGE_SERVER_STOP_WARNING}" echo "Issued the warning \"${MESSAGE_SERVER_STOP_WARNING}\" to players." echo "Shutting down in ${CONFIG_SERVER_STOP_DELAY} seconds..." sleep ${CONFIG_SERVER_STOP_DELAY} fi server_stop worlds_to_disk else echo "$JAR was not running." fi ;; restart) if server_is_running then if [[ $2 != "now" ]] then server_eval "say ${MESSAGE_SERVER_RESTART_WARNING}" echo "Issued the warning \"${MESSAGE_SERVER_RESTART_WARNING}\" to players." echo "Restarting in ${CONFIG_SERVER_RESTART_DELAY} seconds..." sleep ${CONFIG_SERVER_RESTART_DELAY} fi server_restart else echo "$JAR was not running. Cannot restart a stopped server. Try \"start\" instead." fi ;; backup) # Backs up worlds if server_is_running then server_eval "say ${MESSAGE_SERVER_WORLDS_BACKUP_STARTED}" server_saveoff worlds_to_disk backup_worlds server_saveon server_eval "say ${MESSAGE_SERVER_WORLDS_BACKUP_FINISHED}" else backup_worlds fi ;; complete-backup) # Backup everything if server_is_running then server_eval "say ${MESSAGE_SERVER_COMPLETE_BACKUP_STARTED}" server_saveoff backup_server server_saveon server_eval "say ${MESSAGE_SERVER_COMPLETE_BACKUP_FINISHED}" fi ;; update) echo "Not supported yet." # Update minecraft_server.jar and craftbukkit.jar (thanks karrth) #server_eval "say SERVER UPDATE IN 10 SECONDS." #server_stop #worlds_to_disk #backup_server #server_update_jars #check_links #server_start ;; to-disk) # Writes from the ramdisk to disk, in case the server crashes. # Using ramdisk speeds things up a lot, especially if you allow # teleportation on the server. server_saveoff worlds_to_disk server_saveon ;; connected) # Lists connected users # Send the list command to the server, and wait for the response server_eval_and_wait "list" "Connected players:" ;; log-roll) log_roll ;; last) echo "Not yet supported." # greps for recently logged in users #echo Recently logged in users: #cat $SERVER_LOG | awk '/entity|conn/ {sub(/lost/,"disconnected");print $1,$2,$4,$5}' ;; status) # Shows server status if server_is_running then echo "$JAR is running." else echo "$JAR is not running." fi ;; version) echo "Not supported yet." ;; links) worlds_ensure_links ;; ramdisk) world_toggle_ramdisk_state $2 ;; worlds) worlds_get for INDEX in ${!WORLDNAME[@]} do if ${WORLDRAM[$INDEX]} then echo "[RAM] ${WORLDNAME[$INDEX]}" else echo "[DSK] ${WORLDNAME[$INDEX]}" fi done ;; console) # This only works if the terminal invoking this script # is logged in as the user $USER_NAME, this is a security # measure enforced by screen. as_user "screen -r ${SCREEN_NAME}" ;; help) echo "Usage: $0 command" echo echo "start - Starts the server" echo "stop - stops the server gracefully, after warning players" echo "stop now - stops the server gracefully, right now!" echo "restart - restarts the server gracefully, after warning players" echo "restart now - restarts the server gracefully, right now!" echo "console - opens the screen process (Press Ctr+A then D to exit)" echo "backup - backs up worlds in \"world storage\"" echo "complete-backup - backups the entire server folder" echo "log-roll - Moves and gzips the logfile" echo "to-disk - copies any worlds in RAM to disk" echo "connected - lists connected users" echo "status - Shows server status" echo "links - creates nessesary symlinks" echo "worlds - shows a list of available worlds" echo "ramdisk WORLD - toggles ramdisk configuration for WORLD" echo "update - Not yet supported" echo "version - Not yet supported" echo "last - Not yet supported" ;; *) echo "No such command see $0 help" exit 1 ;; esac exit 0