From aaaa69dbd6c45ff0eb7dd69b756b4103abadfbc4 Mon Sep 17 00:00:00 2001 From: Marcus Whybrow Date: Sat, 19 May 2012 17:50:22 +0100 Subject: [PATCH] Initial (not tested, and most likely not working) files. Before this initial commit, I worked on the code in a github gist. The code in this commit comes from this gist commit: https://gist.github.com/2725916/ce58be848998aa93572f99e62f2e39e06db9463a --- README.markdown | 24 ++ manager.config | 77 ++++++ minecraft | 640 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 741 insertions(+) create mode 100644 README.markdown create mode 100644 manager.config create mode 100644 minecraft diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..ddd3b8b --- /dev/null +++ b/README.markdown @@ -0,0 +1,24 @@ +# Marcus' Minecraft/Bukkit Init Script + +## Features + +* Multi-server compatible, run two or more servers on one machine +* Start, stop and restart your server +* Create [WorldEdit snapshot][we-snapshot] compatible backups of your worlds +* Backup the entire server folder for complete protection +* Load world's into RAM for faster access (reduces lag) +* Easily configurable, change directory locations, in-game messages etc. from one simple file + +[we-snapshot]: http://wiki.sk89q.com/wiki/WorldEdit/Snapshots + +## Upcomming features + +* **Provisioning:** Create, move, delete and manage multiple servers from one script. +* **Restore:** Roll-back to an old world or whole server backup automatically. +* **QuickBackup:** If you store your backups non-locally (maybe on a NAS), QuickBackup optionally creates a backup locally for speed, and then moves it after your players are building again! + +## Acknowledgements + +This code grew out of an old version of [Ahtenus' Minecraft Init Script][ahtenus-minecraft-init]. + +[ahtenus-minecraft-init]: https://github.com/Ahtenus/minecraft-init \ No newline at end of file diff --git a/manager.config b/manager.config new file mode 100644 index 0000000..5be5a1a --- /dev/null +++ b/manager.config @@ -0,0 +1,77 @@ +#!/bin/bash +# /opt/minecraft/server2/manager.config + + +### Important Names + +# A short name representation for this server, used for consistent directory names: +SERVER_NAME="server2" + +# Name to use for the screen instance (must be unique system wide): +SCREEN_NAME="minecraft2" + +# User that should start and interact with the server +SERVER_USER="minecraft" + + +### Server Directories + +# Path to minecraft directory: +SERVER_PATH="/opt/minecraft/${SERVER_NAME}" + +# Where the worlds are located on the disk. Can not be the same as SERVER_PATH: +WORLD_STORAGE_PATH="${SERVER_PATH}/worldstorage" + +# Path to plugin folder: +SERVER_PLUGIN_PATH="${SERVER_PATH}/plugins" + + +### Backups and Archives + +# When a snapshot (backup) is created of a world it will be stored in this directory +WORLD_SNAPSHOT_PATH="/mnt/Drobo/Public/Shares/Backups/Minecraft/WorldBackups/${SERVER_NAME}" + +# When the server log is compressed and archived, it is stored here: +LOG_ARCHIVE_PATH="/mnt/Drobo/Public/Shares/Backups/Minecraft/Logs/${SERVER_NAME}" + +# When a complete backup of the entire server directory is make, it is stored here: +COMPLETE_BACKUP_PATH="/mnt/Drobo/Public/Shares/Backups/Minecraft/WholeBackups/${SERVER_NAME}" + +# Symbolic links usually point to large or centralised files which should not be +# included in a complete backup of this singular server. +# options: true, false +COMPLETE_BACKUP_FOLLOW_SYMLINKS=false + +# When a backup of Bukkit plugin config files is made, it is stored here: +CONFIG_BACKUP_PATH="/mnt/Drobo/Public/Shares/Backups/Minecraft/ConfigBackups/${SERVER_NAME}" + +# The pattern used to search for config files within the plugin directory +CONFIG_BACKUP_PATTERN="*.yml" + + +### Speedy Ramdisk + +# options: true, false +RAMDISK_ENABLED=true + +# Path to the the mounted ramdisk default in Ubuntu: /dev/shm +RAMDISK_PATH="/dev/shm" + + +### Jar configuration + +# Location of the server jar to run (minecraft_server.jar, bukkit.jar etc.): +JAR="${SERVER_PATH}/server.jar" + +# The amount of memory the server will use when started: +INITAL_MEMORY="2048M" + +# The maximum amount of memory the server can use at any time: +MAXIMUM_MEMORY="2048M" + +# Number of CPUs/cores to use at runtime: +CPU_COUNT=2 + +# The command which launches the Minecraft server using the variables +# provided thus far: +INVOCATION="java -Xmx$MAXMEM -Xms$INITMEM -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:ParallelGCThreads=${CPU_COUNT} -XX:+AggressiveOpts -jar $JAR nogui" \ No newline at end of file diff --git a/minecraft b/minecraft new file mode 100644 index 0000000..3ff337c --- /dev/null +++ b/minecraft @@ -0,0 +1,640 @@ +#!/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 +} + +as_user() { + if [ $(whoami) == $USER ] ; then + bash -c "$1" + else + su - $USER -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" +} + +# 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() { + regex="^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{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 + fi + done < <(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 $(server_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 "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 + rm ${WORLD_STORAGE_PATH}/$1/ramdisk + rm ${RAMDISK_PATH}/$1 + echo "Removed the RAM flag from \"$1\", and deleted it from RAM." + else + touch ${WORLD_STORAGE_PATH}/$1/ramdisk + echo "Added the RAM flag to \"$1\"." + printf "Copying world to RAM... " + $(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" + $(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... " + $(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... " + $(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..." + $(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 + sleep ${CONFIG_SERVER_STOP_DELAY} + + server_saveall + + printf "Stopping the server... " + server_eval "stop" + + server_wait_for_stop + echo "Done." + else + echo "$JAR was not running." + fi + + worlds_to_disk +} + +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 -eq $FALSE ] + then + zip_flags="${zip_flags}y" + fi + + # Zip up the server directory + printf "Backing up the entire server directory... " + as_user "cd ${SERVER_PATH} && zip -${zip_flags} $path $SERVER_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 ${dir} && 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?) + + as_user "mkdir -p $LOG_PATH" + path=${LOG_ARCHIVE_PATH}/${SERVER_NAME}-$(date +%FT%T).log + + printf "Rolling server logs... " + as_user "mv $SERVER_LOG $path && gzip $path" + echo "Done." +} + + +### Script switch statement + +case "$1" in + start) + # Starts the server + server_start + ;; + stop) + # Stops the server + + if server_is_running + 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..." + fi + + server_stop + ;; + restart) + if server_is_running + 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..." + fi + + server_restart + ;; + 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}" + ;; + 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) + if server_is_running + then + server_eval "say ${MESSAGE_SERVER_LOG_ROLL_RESTART_WARNING}" + echo "Issued the warning \"${MESSAGE_SERVER_LOG_ROLL_RESTART_WARNING}\" to players." + echo "Restarting to roll logs in ${CONFIG_SERVER_LOG_ROLL_RESTART_DELAY} seconds..." + + sleep $CONFIG_SERVER_LOG_ROLL_RESTART_DELAY + + server_stop + log_roll + server_start + else + log_roll + fi + ;; + last) + # 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) + change_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: /etc/init.d/minecraft command" + echo + echo "start - Starts the server" + echo "stop - stops the server" + echo "restart - restarts the server" + echo "console - opens the screen process (Press Ctr+A then D to exit)" + echo "backup - backups the worlds defined in the script" + echo "complete-backup - backups the entire server folder" + echo "update - fetches the latest version of minecraft.jar server and Bukkit" + echo "log-roll - Moves and gzips the logfile" + echo "to-disk - copies the worlds from the ramdisk to worldstorage" + echo "connected - lists connected users" + echo "status - Shows server status" + echo "version - returs Bukkit version" + echo "links - creates nessesary symlinks" + echo "last - shows recently connected users" + echo "worlds - shows a list of available worlds" + echo "ramdisk WORLD - toggles ramdisk configuration for WORLD" + ;; + *) + echo "No such command see /etc/init.d/minecraft help" + exit 1 + ;; +esac + +exit 0 \ No newline at end of file