#!/bin/sh ### BEGIN INIT INFO # Provides: minecraft_server # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # chkconfig: 345 50 50 # Description: Minecraft Server Control Script ### END INIT INFO # --------------------------------------------------------------------------- # Copyright (c) 2013, Jason M. Wood # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Minecraft Server Control Script # # A powerful command-line control script for Linux-powered Minecraft servers. # --------------------------------------------------------------------------- # Script Usage # --------------------------------------------------------------------------- USAGE=$(cat < Options: start - Start the Minecraft world server. Start all world servers by default. stop - Stop the Minecraft world server. Stop all world servers by default. force-stop - Forcibly stop the Minecraft world server. Forcibly stop all world servers by default. restart - Restart the Minecraft world server. Restart all world servers by default. force-restart - Forcibly restart the Minecraft world server. Forcibly restart all world servers by default. create - Create a Minecraft world server. delete - Delete a Minecraft world server. disable - Temporarily disable a world server. enable - Enable a disabled world server. status - Display the status of the Minecraft world server. Display the status of all world servers by default. sync - Synchronize the data stored in the mirror images of the Minecraft world server. Synchronizes all of the world servers by default. This option is only available when the mirror image option is enabled. send - Send a command to a Minecraft world server. screen - Display the Screen for the Minecraft world server. watch - Watch the log file for the Minecraft world server. logrotate - Rotate the server.log file. Rotate the server.log file for all worlds by default. backup - Backup the Minecraft world. Backup all worlds by default. map - Run the Minecraft Overviewer mapping software on the Minecraft world. Map all worlds by default. update - Update the client and server software. EOF ) # User Account & Server Location # --------------------------------------------------------------------------- # Who we run as and where we run from. # User name used to run all commands. Be sure to create this user if it # doesn't already exist (sudo adduser minecraft). USER_NAME="minecraft" # The location of server software and data. LOCATION="/home/$USER_NAME" # Required Software # --------------------------------------------------------------------------- # Detect its presence and location for later. JAVA=$(which java) PERL=$(which perl) PYTHON=$(which python) RSYNC=$(which rsync) SCREEN=$(which screen) WGET=$(which wget) RDIFF_BACKUP=$(which rdiff-backup) # Global Server Configuration # --------------------------------------------------------------------------- # Mojang Versions information # --------------------------------------------------------------------------- # Detect the latest version. MCVERSIONS_URL="https://s3.amazonaws.com/Minecraft.Download/versions/versions.json" MCVERSION=$(wget -q -O - $MCVERSIONS_URL | $PERL -ne 'if ($_ =~ /^\s+\"release\": \"([0-9\.]+)\"/) { print $1; }') # Minecraft Server Settings # --------------------------------------------------------------------------- # Choose only one server distribution, leave the other commented out. # Default Mojang server distribution. SERVER_JAR="minecraft_server.jar" SERVER_URL="https://s3.amazonaws.com/Minecraft.Download/versions/$MCVERSION/minecraft_server.$MCVERSION.jar" SERVER_ARGS="nogui" # CraftBukkit server distribution. # SERVER_URL="http://repo.bukkit.org/service/local/artifact/maven/redirect?g=org.bukkit&a=craftbukkit&v=RELEASE&r=releases" # SERVER_JAR="craftbukkit.jar" # SERVER_ARGS="" # Generic server options. INITIAL_MEMORY="128M" MAXIMUM_MEMORY="2048M" SERVER_LOCATION="$LOCATION/minecraft_server" SERVER_COMMAND="$JAVA -Xms$INITIAL_MEMORY -Xmx$MAXIMUM_MEMORY -jar $SERVER_LOCATION/$SERVER_JAR $SERVER_ARGS" # Minecraft Client Settings # --------------------------------------------------------------------------- # Used by Minecraft Overviewer mapping software. CLIENT_JAR="minecraft.jar" CLIENT_URL="https://s3.amazonaws.com/Minecraft.Download/versions/$MCVERSION/$MCVERSION.jar" CLIENT_LOCATION="$LOCATION/.minecraft/bin" # World (Server Instance) Configuration # --------------------------------------------------------------------------- # The location to store files for each world server. WORLDS_LOCATION="$LOCATION/worlds" # The location to store disabled world server files. DISABLED_WORLDS_LOCATION="$LOCATION/worlds-disabled" # Default world name, port, and IP address if the worlds.conf file is # missing. DEFAULT_WORLD="world" DEFAULT_PORT="25565" DEFAULT_IP="" # Global Message Of The Day file (MOTD) # --------------------------------------------------------------------------- # Location of the file to display to users on login. Nothing will be done if # this file does not exist. MOTD="$LOCATION/motd.txt" # NOTE: MOTD can contain color codes as follows: # §0 - black # §1 - blue # §2 - dark green # §3 - aqua # §4 - dark red # §5 - purple # §6 - gold # §7 - gray # §8 - dark gray # §9 - light blue # §a - green # §b - teal # §c - red # §d - magenta # §e - yellow # §f - white # Backup Configuration # --------------------------------------------------------------------------- # Location to store backups. BACKUP_LOCATION="$LOCATION/backups" # Location of the backup log file. BACKUP_LOG="$BACKUP_LOCATION/backup.log" # Length in days that backups survive. BACKUP_DURATION=15 # Server Log Configuration # --------------------------------------------------------------------------- # How many rotations of server.log to keep LOG_COUNT=10 # Mirror Image Options # --------------------------------------------------------------------------- # Create a mirror image of the world data on system startup, and # update that mirror image on system shutdown. # # IMPORTANT: If using this option, the admin should schedule # periodic synchronizations of the mirror image using cron # to avoid data loss. To do this, add a cron task to call # the "sync" option on a VERY regular basis (e.g., # every 5-10 minutes). # # 0 - Do not use a mirror image, default. # 1 - Use a mirror image. ENABLE_MIRROR=0 # The location to store the mirror image. # # NOTE: This is usually a ramdisk, e.g. /dev/shm on Debian/Ubuntu. MIRROR_PATH="/dev/shm/minecraft" # Mincecraft Overviewer Mapping Software Options # --------------------------------------------------------------------------- OVERVIEWER_BIN=$(which overviewer.py) MAPS_URL="http://minecraft.server.com/maps" MAPS_LOCATION="$LOCATION/maps" # Lib-Notify Configuration # --------------------------------------------------------------------------- # Use lib-notify to print a message on your desktop of important server # events. # 0 - Do not use lib-notify. # 1 - Display server events using lib-notify. USE_LIBNOTIFY=0 # The username and display that messages will be routed to. LIBNOTIFY_USER_NAME=$USER_NAME LIBNOTIFY_DISPLAY=":0.0" # --------------------------------------------------------------------------- # Internal Methods # --------------------------------------------------------------------------- # # NOTE: Nothing below this point should need to be edited directly. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Execute the given command. # # @param 1 The command to execute. # @param 2 The user name to execute the command with. # --------------------------------------------------------------------------- execute() { if [ $(id -u) -eq 0 ]; then # Script is running as root, switch user and execute # the command. su -c "$1" $2 else # Script is running as a user, just execute the command. sh -c "$1" fi } # --------------------------------------------------------------------------- # Get the PIDs of the Screen and Java process for the world server. # # @param 1 The world server of interest. # @return The Screen and Java PIDs. # --------------------------------------------------------------------------- getProcessIDs() { local SCREEN_PID JAVA_PID SCREEN_PID=$(execute "$SCREEN -ls" $USER_NAME | $PERL -ne 'if ($_ =~ /^\t(\d+)\.minecraft-'$1'\s+/) { print $1; }') JAVA_PID=$(ps -a -u $USER_NAME -o pid,ppid,comm | $PERL -ne 'if ($_ =~ /^\s*(\d+)\s+'$SCREEN_PID'\s+java/) { print $1; }') echo "$SCREEN_PID $JAVA_PID" } # --------------------------------------------------------------------------- # Check to see if the world server is running. # # @param 1 The world server of interest. # @return A 1 if the server is thought to be running, a 0 otherwise. # --------------------------------------------------------------------------- serverRunning() { local PIDS PIDS=$(getProcessIDs $1) # Try to determine if the world is running. if [ -n "$(echo $PIDS | cut -d ' ' -f1)" ] && [ -n "$(echo $PIDS | cut -d ' ' -f2)" ]; then echo 1 else echo 0 fi } # --------------------------------------------------------------------------- # Send a command to the world server. # # @param 1 The world server of interest. # @param 2 The command to send. # --------------------------------------------------------------------------- sendCommand() { local COMMAND PID COMMAND=$(printf "$2\r") PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f1) execute "$SCREEN -S $PID.minecraft-$1 -p 0 -X stuff \"$COMMAND\"" $USER_NAME if [ $? -ne 0 ]; then printf "Error sending command to server $1.\n" exit 1 fi } # --------------------------------------------------------------------------- # Connect to the Screen of a world server. # # @param 1 The world server of interest. # --------------------------------------------------------------------------- displayScreen() { local PID TTY_PERMISSIONS PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f1) TTY_PERMISSIONS=$($PERL -e 'printf "%04o", ((stat(shift))[2] & 07777);' $(tty)) # Make sure that we have read/write access to the tty. execute "chmod o+rw $(tty)" > /dev/null 2>&1 if [ $? -ne 0 ]; then printf "Error changing the permissions of the tty.\n" printf "Try giving user '$USER_NAME' access to the tty with:\n" printf " chmod o+rw $(tty)\n" printf "\n" printf "Attempting to load the screen anyway.\n" fi # Connect to the screen of the world server. execute "$SCREEN -x $PID.minecraft-$1" $USER_NAME if [ $? -ne 0 ]; then printf "Error connecting to Screen.\n" execute "chmod $TTY_PERMISSIONS $(tty)" > /dev/null 2>&1 exit 1 fi execute "chmod $TTY_PERMISSIONS $(tty)" > /dev/null 2>&1 } # --------------------------------------------------------------------------- # Check whether the item is in the list. # # @param 1 The item being searched for. # @param 2 The list being searched. # @return A 1 if the list contains the item, a 0 otherwise. # --------------------------------------------------------------------------- listContains() { local MATCH ITEM MATCH=0 for ITEM in $2; do if [ "$ITEM" = "$1" ]; then MATCH=1 fi done echo $MATCH } # --------------------------------------------------------------------------- # Create a world. # # @param 1 The world server to create. # @param 2 The port of the world server. # @param 3 The IP address of the world server. # --------------------------------------------------------------------------- createWorld() { # Create a basic server.properties file. Values not supplied here # will use default values when the world server is first started. execute "mkdir -p $WORLDS_LOCATION/$1" $USER_NAME setWorldPropertiesValue "$1" "level-name" "$1" setWorldPropertiesValue "$1" "server-port" "$2" setWorldPropertiesValue "$1" "server-ip" "$3" setWorldPropertiesValue "$1" "enable-query" "true" setWorldPropertiesValue "$1" "query.port" "$2" } # --------------------------------------------------------------------------- # Delete a world. # # @param 1 The world server to delete. # --------------------------------------------------------------------------- deleteWorld() { # Delete the world directory. execute "rm -Rf $WORLDS_LOCATION/$1" $USER_NAME } # --------------------------------------------------------------------------- # Disable a world. # # @param 1 The world server to disable. # --------------------------------------------------------------------------- disableWorld() { # Disable the world. execute "mv $WORLDS_LOCATION/$1 $DISABLED_WORLDS_LOCATION/$1" $USER_NAME } # --------------------------------------------------------------------------- # Enable a world. # # @param 1 The world server to enable. # --------------------------------------------------------------------------- enableWorld() { # Enable the world. execute "mv $DISABLED_WORLDS_LOCATION/$1 $WORLDS_LOCATION/$1" $USER_NAME } # --------------------------------------------------------------------------- # Grab the first line of the Message of the Day file as a summary, and strip # any color codes from it. # # @param 1 The world server of interest. # @return The global message of the day or the specifued world # --------------------------------------------------------------------------- getMOTD() { local MOTD_SUMMARY WORLD_MOTD MOTD_SUMMARY="" WORLD_MOTD="$WORLDS_LOCATION/$1.motd" if [ -e $WORLD_MOTD ]; then MOTD_SUMMARY=$(head -n 1 $WORLD_MOTD | $PERL -ne '$_ =~ s/§[0-9a-fA-F]//g; print;') elif [ -e $MOTD ]; then MOTD_SUMMARY=$(head -n 1 $MOTD | $PERL -ne '$_ =~ s/§[0-9a-fA-F]//g; print;') fi echo $MOTD_SUMMARY } # --------------------------------------------------------------------------- # Grab the list of worlds. # # @return The list of worlds. # --------------------------------------------------------------------------- getWorlds() { local WORLD WORLDS WORLDS="" for WORLD in $(ls $WORLDS_LOCATION); do if [ -f $WORLDS_LOCATION/$WORLD/server.properties ]; then WORLDS="$WORLDS $WORLD" fi done echo $WORLDS } # --------------------------------------------------------------------------- # Get the value of a key in a world properties file. # # @param 1 The world server of interest. # @param 2 The key to get. # --------------------------------------------------------------------------- getWorldPropertiesValue() { local PROPERTY_FILE KEY VALUE PROPERTY_FILE=$WORLDS_LOCATION/$1/server.properties # Make sure the properties file exists if [ -e "$PROPERTY_FILE" ]; then # Find the key/value combo. KEY=$($PERL -ne 'if ($_ =~ /^('$2')=.*$/) { print "$1"; }' $PROPERTY_FILE) VALUE=$($PERL -ne 'if ($_ =~ /^'$2'=(.*)$/) { print "$1"; }' $PROPERTY_FILE) if [ -n "$KEY" ] && [ -n "$VALUE" ]; then echo "$VALUE" else echo "" fi fi } # --------------------------------------------------------------------------- # Modify the value of a key/value combo in a world properties file. # # @param 1 The world server of interest. # @param 2 The key to modify. # @param 3 The value to assign to the key. # --------------------------------------------------------------------------- setWorldPropertiesValue() { local PROPERTY_FILE KEY_VALUE PROPERTY_FILE=$WORLDS_LOCATION/$1/server.properties # Make sure that the properties file exists. execute "touch $PROPERTY_FILE" $USER_NAME # Replace the key/value combo if it already exists, otherwise just # append it to the end of the file. KEY_VALUE=$($PERL -ne 'if ($_ =~ /^('$2'=.*)$/) { print "$1"; }' $PROPERTY_FILE) if [ -n "$KEY_VALUE" ]; then execute "$PERL -i -ne 'if (\$_ =~ /^$2=.*$/) { print \"$2=$3\\n\"; } else { print; }' $PROPERTY_FILE" $USER_NAME else execute "printf \"$2=$3\\n\" >> $PROPERTY_FILE" $USER_NAME fi } # --------------------------------------------------------------------------- # Send a message to the desktop using lib-notify, if it is available. # # @param 1 The summary of the message to send. # @param 2 The body of the message to send. # --------------------------------------------------------------------------- libNotify() { local NOTIFY NOTIFY=$(which notify-send) if [ -e "$NOTIFY" ]; then execute "DISPLAY=$LIBNOTIFY_DISPLAY $NOTIFY \"$1\" \"$2\"" $LIBNOTIFY_USER_NAME > /dev/null 2>&1 fi } # --------------------------------------------------------------------------- # Send the contents of the Message Of The Day (MOTD) to the user. # # @param 1 The world server of interest. # @param 2 The user being told the contents of the motd file. # --------------------------------------------------------------------------- tellMOTD() { local LINE WORLD_MOTD WORLD_MOTD="$WORLDS_LOCATION/$1.motd" if [ -e $WORLD_MOTD ]; then while read LINE; do sendCommand $1 "tell $2 $LINE" done < $WORLD_MOTD elif [ -e $MOTD ]; then while read LINE; do sendCommand $1 "tell $2 $LINE" done < $MOTD fi } # --------------------------------------------------------------------------- # Check to see if the user is in the ops.txt file of the specified world. # # @param 1 The world server of interest. # @param 2 The user being checked. # --------------------------------------------------------------------------- checkUserIsAdmin() { local IS_ADMIN IS_ADMIN=$(cat $WORLDS_LOCATION/$1/ops.txt | $PERL -ne 'if ($_ =~ /^'$2'$/i) { print "1"; }') echo $IS_ADMIN } # --------------------------------------------------------------------------- # Check for the optional argument. If the argument is not supplied, return # the original list. If the argument is supplied, verify that it is a member # of the list, then modify the list to just contain that member. # # @param 1 The original list. # @param 2 The name of the script. # @param 3 The command line argument used. # @param 4 The optional command line argument. # @return Either the original list, or the optional command line argument. # --------------------------------------------------------------------------- checkOptionalArgument() { local LIST LIST="$1" # Check for the optional command line argument. if [ -n "$4" ] && [ $(listContains $4 "$1") -eq 1 ]; then LIST="$4" elif [ -n "$4" ]; then printf "Optional argument '$4' not recognized.\n" printf " Usage: $2 $3 \n" exit 1 fi echo "$LIST" } # --------------------------------------------------------------------------- # Rotates the world server log file. # # @param 1 The world server generating the log to rotate. # --------------------------------------------------------------------------- rotateLog() { local WORLD_DIR LOG_LIST LOG_LINES LOG_NUMBER WORLD_DIR="$WORLDS_LOCATION/$1" # Make sure that the server.log file exists. execute "touch $WORLD_DIR/server.log" $USER_NAME # Scan the log for entires and skip rotate is none are found. LOG_LINES="$(cat "$WORLD_DIR/server.log" | wc -l )" if [ $LOG_LINES -le 1 ]; then printf "\nNo new log entries to rotate. No changes made.\n" return 0 fi # Server logfiles in chronological order. LOGLIST=$(ls -r $WORLD_DIR/server.log* | grep -v lck) # Look at all the logfiles for i in $LOGLIST; do LOG_NUMBER=$(ls $i | cut -d "." -f 3) # If we're working with server.log, append .1 then compress # it. if [ -z $LOG_NUMBER ]; then LOG_NUMBER="1" execute "cp $WORLD_DIR/server.log $WORLD_DIR/server.log.$LOG_NUMBER" $USER_NAME execute "gzip $WORLD_DIR/server.log.$LOG_NUMBER" $USER_NAME # Otherwise, check if the file number is under $LOG_COUNT. elif [ $LOG_NUMBER -ge $LOG_COUNT ]; then # If so, delete it. execute "rm -f $i" $USER_NAME else # Otherwise, add one to the number. LOG_NUMBER=$(($LOG_NUMBER+1)) execute "mv -f $i $WORLD_DIR/server.log.$LOG_NUMBER.gz" $USER_NAME fi done # Blank the existing logfile to renew it. execute "cp /dev/null $WORLD_DIR/server.log" $USER_NAME } # --------------------------------------------------------------------------- # Watch the world server log file. # # @param 1 The world server generating the log to watch. # --------------------------------------------------------------------------- watchLog() { local PID WORLD_DIR WORLD_DIR="$WORLDS_LOCATION/$1" # Make sure that the server.log file exists. if [ -e "$WORLD_DIR/server.log" ]; then # Watch the log. PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f2) tail -n0 -f --pid=$PID $WORLD_DIR/server.log fi } # --------------------------------------------------------------------------- # Synchronizes the data stored in the mirror images. # # @param 1 The world server to sync. # --------------------------------------------------------------------------- syncMirrorImage() { # Sync the world server. execute "cp -Ru $WORLDS_LOCATION/$1/$1/* $WORLDS_LOCATION/$1/$1-original" $USER_NAME if [ $? -ne 0 ]; then printf "Error synchronizing mirror images for world $1.\n" exit 1 fi } # --------------------------------------------------------------------------- # Start the world server. Generate the appropriate environment for the # server if it doesn't already exist. # # @param 1 The world server to start. # --------------------------------------------------------------------------- start() { local PID WORLD_DIR # Make sure that the world's directory exists. WORLD_DIR="$WORLDS_LOCATION/$1" execute "mkdir -p $WORLD_DIR" $USER_NAME # Make a mirror image of the world directory if requested. if [ $ENABLE_MIRROR -eq 1 ]; then execute "mkdir -p $MIRROR_PATH/$1" $USER_NAME if [ $? -ne 0 ]; then printf "Error copying world data, path %s not found.\n" $MIRROR_PATH/$1 exit 1 fi # Check for a clean dismount from the previous server run. If we have a # -original directory within we didn't stop cleanly. if [ -d "WORLDS_LOCATION/$1/$1-original" ]; then # Remove the symlink to the world-file mirror image. execute "rm -r $WORLDS_LOCATION/$1/$1" $USER_NAME # Move the world files back to their original path name. execute "mv $WORLDS_LOCATION/$1/$1-original $WORLDS_LOCATION/$1/$1" $USER_NAME fi # Copy the world files over to the mirror. execute "cp -R $WORLDS_LOCATION/$1/$1/* $MIRROR_PATH/$1" $USER_NAME # Rename the original world file directory. execute "mv $WORLDS_LOCATION/$1/$1 $WORLDS_LOCATION/$1/$1-original" $USER_NAME # Create a symlink from the world file directory's original name to the mirrored files. execute "ln -s $MIRROR_PATH/$1 $WORLDS_LOCATION/$1/$1" $USER_NAME fi # Change to the world's directory. cd $WORLD_DIR # Start the server. execute "$SCREEN -dmS minecraft-$1 $SERVER_COMMAND" $USER_NAME if [ $? -ne 0 ]; then printf "Error starting the server.\n" exit 1 fi # Grab the Screen Process ID of the server. PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f1) if [ ! -n "$PID" ]; then printf "Error starting the server: SCREEN failed to create a screen for the server.\n" exit 1 fi # Grab the Java Process ID of the server. PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f2) if [ ! -n "$PID" ]; then printf "Error starting the server: couldn't retrieve the server's process ID.\n" exit 1 fi # Start a tail process to watch for changes to the query.dat file to pipe to # the Minecraft query server via netcat. The response from the query # server is piped into the response.dat file. execute "printf '' > $WORLD_DIR/query.dat" $USER_NAME execute "printf '' > $WORLD_DIR/response.dat" $USER_NAME execute "tail -f --pid=$PID $WORLD_DIR/query.dat | nc -q 1 -u 127.0.0.1 $(getWorldPropertiesValue $1 'server-port') > $WORLD_DIR/response.dat &" $USER_NAME # Create a lock file on RedHat and derivatives. if [ -d "/var/lock/subsys" ]; then touch /var/lock/subsys/minecraft_server fi } # --------------------------------------------------------------------------- # Stop the world server. # # @param 1 The world server to stop. # --------------------------------------------------------------------------- stop() { local WORLD NUM sendCommand $1 "stop" # Synchronize the mirror image of the world prior to closing, if # required. if [ $ENABLE_MIRROR -eq 1 ] && [ -d $MIRROR_PATH ]; then syncMirrorImage $1 # Remove the symlink to the world-file mirror image. execute "rm -r $WORLDS_LOCATION/$1/$1" $USER_NAME # Move the world files back to their original path name. execute "mv $WORLDS_LOCATION/$1/$1-original $WORLDS_LOCATION/$1/$1" $USER_NAME fi # Remove the lock file on Redhat and derivatives if all world servers # are stopped. if [ -e "/var/lock/subsys/minecraft_server" ]; then NUM=0 for WORLD in $ALL_WORLDS; do if [ "$1" != "$WORLD" ] && [ $(serverRunning $WORLD) -eq 1 ]; then NUM=$(($NUM + 1)) fi done if [ $NUM -eq 0 ]; then rm -f /var/lock/subsys/minecraft_server fi fi } # --------------------------------------------------------------------------- # Forcibly stop the world server. # # @param 1 The world server to forcibly stop. # --------------------------------------------------------------------------- forceStop() { local PIDS PIDS=$(getProcessIDs $1) # Try to stop the server cleanly first. stop $1 sleep 5 # Kill the process ids of the world server. kill -9 $PIDS > /dev/null 2>&1 # Remove the lock file on Redhat and derivatives if it is still # around. rm -f /var/lock/subsys/minecraft_server } # --------------------------------------------------------------------------- # Backup the world server. # # @param 1 The world server to backup. # --------------------------------------------------------------------------- worldBackup() { # Make sure that the backup location exists. execute "mkdir -p $BACKUP_LOCATION" $USER_NAME # Create the backup. execute "$RDIFF_BACKUP -v5 --print-statistics $WORLDS_LOCATION/$1 $BACKUP_LOCATION/$1 >> $BACKUP_LOG" $USER_NAME # Cleanup old backups. if [ $BACKUP_DURATION -gt 0 ]; then execute "$RDIFF_BACKUP --remove-older-than ${BACKUP_DURATION}D --force $BACKUP_LOCATION/$1 >> $BACKUP_LOG" $USER_NAME fi } # --------------------------------------------------------------------------- # update the Minecraft client software. # --------------------------------------------------------------------------- updateClientSoftware() { # Make sure the client software directory exists. execute "mkdir -p $CLIENT_LOCATION" $USER_NAME # Backup the old client jar. if [ -e "$CLIENT_LOCATION/$CLIENT_JAR" ]; then execute "mv -f \"$CLIENT_LOCATION/$CLIENT_JAR\" \"$CLIENT_LOCATION/$CLIENT_JAR.old\"" $USER_NAME fi # Download the new Minecraft client software execute "$WGET -qO \"$CLIENT_LOCATION/$CLIENT_JAR\" \"$CLIENT_URL\"" $USER_NAME # Check for error and restore backup if found. if [ $? -ne 0 ]; then printf "\nError updating the Minecraft client software.\n" if [ -e "$CLIENT_LOCATION/$CLIENT_JAR.old" ]; then execute "mv -f \"$CLIENT_LOCATION/$CLIENT_JAR.old\" \"$CLIENT_LOCATION/$CLIENT_JAR\"" $USER_NAME fi fi } # --------------------------------------------------------------------------- # Update the Minecraft server software. # --------------------------------------------------------------------------- updateServerSoftware() { execute "mkdir -p $SERVER_LOCATION" $USER_NAME # Backup the old jar file. if [ -e "$SERVER_LOCATION/$SERVER_JAR" ]; then execute "mv -f \"$SERVER_LOCATION/$SERVER_JAR\" \"$SERVER_LOCATION/$SERVER_JAR.old\"" $USER_NAME fi # Download the new minecraft server software. execute "$WGET -qO \"$SERVER_LOCATION/$SERVER_JAR\" \"$SERVER_URL\"" $USER_NAME # Check for error and restore backup if found. if [ $? -ne 0 ]; then printf "\nError updating the Minecraft server software.\n" if [ -e "$SERVER_LOCATION/$SERVER_JAR.old" ]; then execute "mv -f $SERVER_LOCATION/$SERVER_JAR.old $SERVER_LOCATION/$SERVER_JAR" $USER_NAME fi exit 1 fi } # --------------------------------------------------------------------------- # Run Minecraft Overviewer mapping software on the world. Generates an # index.html file using the Google Maps API. # # @param 1 The world server to map with Overviewer. # --------------------------------------------------------------------------- overviewer() { # Make sure the maps directory exists. execute "mkdir -p $MAPS_LOCATION/$1" $USER_NAME # Make sure the Minecraft client is available. if [ ! -e "$CLIENT_LOCATION/$CLIENT_JAR" ]; then updateClientSoftware fi # Make sure that the world files are actually there before mapping. if [ -e "$WORLDS_LOCATION/$1/server.properties" ]; then # Check for Overviewer settings file. if [ -e "$WORLDS_LOCATION/$1/overviewer-settings.py" ]; then # Generate map and POI with custom settings. execute "$OVERVIEWER_BIN --config=$WORLDS_LOCATION/$1/overviewer-settings.py" $USER_NAME execute "$OVERVIEWER_BIN --config=$WORLDS_LOCATION/$1/overviewer-settings.py --genpoi" $USER_NAME else # Generate map with default settings. execute "$OVERVIEWER_BIN --rendermodes=normal,lighting,cave --processes 1 $WORLDS_LOCATION/$1/$1 $MAPS_LOCATION/$1" $USER_NAME > /dev/null 2>&1 fi fi } # --------------------------------------------------------------------------- # Pack a hex string into a buffer file that is piped to the Minecraft query # server. # # @param 1 The world server of interest. # @param 2 The packet type. # @param 3 The packet ID. # @param 4 The packet payload. # @param 5 The response format. # @return The response from the Query server in the requested format. # --------------------------------------------------------------------------- querySendPacket() { local PACKET RESPONSE WORLD_DIR RETRY # The world's directory. WORLD_DIR="$WORLDS_LOCATION/$1" # Add the magic bytes to the incoming packet. PACKET=$(printf "FEFD%s%s%s" "$2" "$3" "$4") # Send the packet to the query server and wait for a response. RETRY=0 while [ -z "$RESPONSE" -a $RETRY -lt 5 ]; do # Increment the retry counter. RETRY=$(($RETRY+1)) # Pack the hex string packet and write it to the query.dat buffer file. execute "$PERL -e ' print map { pack (\"C\", hex(\$_)) } (\"'$PACKET'\" =~ /(..)/g); ' >> $WORLD_DIR/query.dat" $USER_NAME # Give the query server a moment to respond. Sleep longer if the retry # count is high. sleep $RETRY # Unpack the token from the response.dat buffer file. There are a # variable amount of null bytes at the start of the response string, so # find the start of the packet by searching for the packet type and ID. RESPONSE=$($PERL -ne ' $hex .= sprintf "%.2x", $_ foreach (unpack "C*", $_); $hex =~ s/^0*'$2$3'/'$2$3'/; print $hex; ' $WORLD_DIR/response.dat) done # Remove the response from the response.dat buffer file. execute "printf '' > $WORLD_DIR/response.dat" $USER_NAME # Return the response in the format requested. $PERL -e ' $packed = join "", map { pack ("C", hex($_)) } ("'$RESPONSE'" =~ /(..)/g); printf "%s\n", join "\t", unpack ("'$5'", $packed); ' } # --------------------------------------------------------------------------- # Send a challenge packet to the Minecraft query server. # # @param 1 The world server of interest. # @return Tab separated values: # type - The packet type. # id - The packet identifier. # token - The token. # --------------------------------------------------------------------------- querySendChallengePacket() { local ID PACKET RESPONSE # The packet identifier. ID="00000001" # Use an empty packet. PACKET="00000000" # Send the challenge packet to the Minecraft query server. RESPONSE=$(querySendPacket "$1" "09" "$ID" "$PACKET" "Cl>Z*") # Return the response. printf "$RESPONSE\n" } # --------------------------------------------------------------------------- # Send an information request packet to the Minecraft query server. # # @param 1 The world server of interest. # @param 2 The challenge token. # @return Tab separated values: # type - The packet type. # id - The packet identifier. # MOTD - The world's message of the day. # gametype - The world's game type, hardcoded to 'SMP'. # map - The world's name. # numplayers - The world's current number of players. # maxplayers - The world's maximum number of players. # hostport - The world's host port. # hostip - The world's host IP address. # --------------------------------------------------------------------------- querySendInformationPacket() { local ID PACKET RESPONSE # The packet identifier. ID="00000001" # Use the challenge token for the packet. PACKET=$(printf "%.8x" $2) # Send the information request packet to the Minecraft query server. RESPONSE=$(querySendPacket "$1" "00" "$ID" "$PACKET" "Cl>Z*Z*Z*Z*Z*sZ*CCZ*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*Z*CCZ*C(Z*)*") # Return the response. printf "$RESPONSE\n" } # --------------------------------------------------------------------------- # Send a status query to the Minecraft query server. # # @param 1 The world server of interest. # @return Tab separated values: # type - The packet type. # id - The packet identifier. # MOTD - The world's message of the day. # gametype - The world's game type. # map - The name of the world. # numplayers - The current number of players. # maxplayers - The maximum number of players. # hostport - The host's port # hostip - The host's IP address. # --------------------------------------------------------------------------- queryStatus() { local TOKEN RESPONSE # Send a challenge packet to the Minecraft query server. TOKEN=$(querySendChallengePacket $1 | cut -f 3) # Send an information request packet to the Minecraft query server. RESPONSE=$(querySendInformationPacket $1 $TOKEN) # Return the response. printf "$RESPONSE\n" } # --------------------------------------------------------------------------- # Send a detailed status query to the Minecraft query server. # # @param 1 The world server of interest. # @return Tab separated values: # type - The packet type. # id - The packet identifier. # * - The string 'splitnum'. # * - The value 128. # * - The value 0. # * - The string 'hostname'. # MOTD - The world's message of the day. # * - The string 'gametype'. # gametype - The world's game type, hardcoded to 'SMP'. # * - The string 'game_id'. # gameid - The world's game ID, hardcoded to 'MINECRAFT'. # * - The string 'version'. # version - The world's Minecraft version. # * - The string 'plugins'. # plugins - The world's plugins. # * - The string 'map'. # map - The world's name. # * - The string 'numplayers'. # numplayers - The world's current number of players. # * - The string 'maxplayers'. # maxplayers - The world's maximum number of players. # * - The string 'hostport'. # hostport - The world's host port. # * - The string 'hostip'. # hostip - The world's host IP address. # * - The value 0. # * - The value 1. # * - The string 'player_'. # * - The value 0. # players - The players currently logged onto the world. # --------------------------------------------------------------------------- queryDetailedStatus() { local TOKEN RESPONSE # Send a challenge packet to the Minecraft query server. TOKEN=$(querySendChallengePacket $1 | cut -f 3) # Send an information request packet to the Minecraft query server. RESPONSE=$(querySendDetailedInformationPacket $1 $TOKEN) # Return the response. printf "$RESPONSE\n" } # --------------------------------------------------------------------------- # Display the status of a Minecraft world server. # # @param 1 The world server of interest. # --------------------------------------------------------------------------- worldStatus() { local STATUS if [ $(serverRunning $1) -eq 1 ]; then STATUS=$(queryStatus $1) printf "running (%d of %d users online).\n" $(echo "$STATUS" | cut -f6) $(echo "$STATUS" | cut -f7) printf " Process IDs: Screen %d, Java %d.\n" $(getProcessIDs $1) else printf "not running.\n" fi } # --------------------------------------------------------------------------- # Begin. # --------------------------------------------------------------------------- # Make sure that Java, Perl, Python, Rsync, GNU Screen, and GNU Wget are # installed. if [ ! -e "$JAVA" ]; then printf "ERROR: Java not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install default-jre" exit 1 fi if [ ! -e "$PERL" ]; then printf "ERROR: Perl not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install perl\n" exit 1 fi if [ ! -e "$PYTHON" ]; then printf "ERROR: Python not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install python\n" exit 1 fi if [ ! -e "$RSYNC" ] && [ $ENABLE_MIRROR -eq 1 ]; then printf "ERROR: Rsync not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install rsync\n" exit 1 fi if [ ! -e "$SCREEN" ]; then printf "ERROR: GNU Screen not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install screen\n" exit 1 fi if [ ! -e "$WGET" ]; then printf "ERROR: GNU Wget not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install wget\n" exit 1 fi if [ ! -e "$RDIFF_BACKUP" ]; then printf "ERROR: rdiff-backup not found!\n" printf "Try installing this with:\n" printf "sudo apt-get install rdiff-backup\n" exit 1 fi # Make sure that the minecraft user exists. if [ ! -n "$(grep $USER_NAME /etc/passwd)" ]; then printf "ERROR: This script requires that a user account named " printf "$USER_NAME exist on this system.\nEither modify the " printf "USER_NAME variable in this script, or try adding this " printf "user:\n" printf "sudo adduser $USER_NAME\n" exit 1 fi # Warn if the script is running with the wrong user. if [ $(id -u) -ne 0 ] && [ "$(whoami)" != "$USER_NAME" ]; then printf "WARNING: This script appears to have been started by the " printf "wrong user.\n" printf "Expected to find the user: $USER_NAME. You can try to log " printf "on to this user:\n" printf "su $USER_NAME\n" exit 1 fi # Make sure that the server software exists. if [ ! -e "$SERVER_LOCATION/$SERVER_JAR" ]; then printf "Server software not found, downloading it...\n" updateServerSoftware fi # Grab the list of worlds. ALL_WORLDS=$(getWorlds) # Respond to the command line arguments. case "$1" in start) # Figure out which worlds to start. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Start each world requested, if not already running. printf "Starting Minecraft Server:" for WORLD in $WORLDS; do if [ $(serverRunning $WORLD) -eq 0 ]; then printf " $WORLD" start $WORLD fi done printf ".\n" ;; stop|force-stop) # Figure out which worlds to stop. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Stop each world requested, if running. printf "Stopping Minecraft Server:" for WORLD in $WORLDS; do # Try to stop the world cleanly. if [ $(serverRunning $WORLD) -eq 1 ]; then printf " $WORLD" if [ $(printf "%d" $(queryStatus $WORLD | cut -f6)) -gt 0 ]; then sendCommand $WORLD "say The server admin has initiated a server shut down." sendCommand $WORLD "say The server will shut down in 1 minute..." sleep 60 sendCommand $WORLD "say The server is now shutting down." fi sendCommand $WORLD "save-all" sendCommand $WORLD "save-off" if [ "$1" = "force-stop" ]; then forceStop $WORLD else stop $WORLD fi sleep 5 fi done printf ".\n" ;; restart|reload|force-restart|force-reload) # Figure out which worlds to restart. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Restart each world requested, start those not already # running. printf "Restarting Minecraft Server:" for WORLD in $WORLDS; do printf " $WORLD" if [ $(serverRunning $WORLD) -eq 1 ]; then if [ $(printf "%d" $(queryStatus $WORLD | cut -f6)) -gt 0 ]; then sendCommand $WORLD "say The server admin has initiated a server restart." sendCommand $WORLD "say The server will restart in 1 minute..." sleep 60 sendCommand $WORLD "say The server is now restarting." fi sendCommand $WORLD "save-all" sendCommand $WORLD "save-off" if [ "$(echo \"$1\" | cut -d '-' -f1)" = "force" ]; then forceStop $WORLD else stop $WORLD fi sleep 5 fi start $WORLD done printf ".\n" ;; create|new) if [ ! -n "$2" ]; then printf "A name for the new world must be supplied.\n" exit 1 fi if [ ! -n "$3" ]; then printf "A port for the new world must be supplied.\n" exit 1 fi printf "Creating Minecraft world: $2\n" createWorld "$2" "$3" "$4" ;; delete|remove) if [ ! -n "$2" ] || [ $(listContains "$2" "$ALL_WORLDS") -eq 0 ]; then printf "World not found, unable to delete world '$2'.\n" exit 1 fi printf "Deleting Minecraft world $2\n" if [ $(serverRunning "$2") -eq 1 ]; then # If the world server has users logged in, announce that the world is # being deleted. if [ $(queryStatus "$2" | cut -f6) -gt 0 ]; then sendCommand "$2" "say The server admin is deleting this world." sendCommand "$2" "say The server will be deleted in 1 minute..." sleep 60 sendCommand "$2" "say The server is now shutting down." fi # Stop the world server. stop "$2" sleep 5 fi # Delete the world. deleteWorld "$2" ;; disable) if [ ! -n "$2" ] || [ $(listContains "$2" "$ALL_WORLDS") -eq 0 ]; then printf "World not found, unable to disable world '$2'.\n" exit 1 fi printf "Disabling Minecraft world $2\n" if [ $(serverRunning "$2") -eq 1 ]; then # If the world server has users logged in, announce that the world is # being disabled. if [ $(queryStatus "$2" | cut -f6) -gt 0 ]; then sendCommand "$2" "say The server admin is disabling this world." sendCommand "$2" "say The server will be disabled in 1 minute..." sleep 60 sendCommand "$2" "say The server is now shutting down." fi # Stop the world server. stop "$2" sleep 5 fi # Disable the world. disableWorld "$2" ;; enable) if [ ! -n "$2" ] || [ ! -f "$DISABLED_WORLDS_LOCATION/$2/server.properties" ]; then printf "World not found, unable to enable world '$2'.\n" exit 1 fi # Enable the world. enableWorld "$2" # Start the world. start "$2" ;; status|show) # Figure out which worlds to show the status for. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Show the status of each world requested. printf "Minecraft Server Status:\n" for WORLD in $WORLDS; do printf " $WORLD: " worldStatus $WORLD done ;; sync|synchronize) # Figure out which worlds to synchronize. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Synchronize the images for each world. printf "Synchronizing Minecraft Server:" for WORLD in $WORLDS; do if [ $(serverRunning $WORLD) -eq 1 ]; then printf " $WORLD" sendCommand $WORLD "save-all" if [ $ENABLE_MIRROR -eq 1 ]; then sendCommand $WORLD "save-off" sleep 20 syncMirrorImage $WORLD sendCommand $WORLD "save-on" fi fi done printf ".\n" ;; send) # Check for the world command line argument. if [ -n "$2" ] && [ $(listContains $2 "$ALL_WORLDS") -eq 1 ] && [ -n "$3" ]; then WORLD=$2 shift 2 printf "Sending command to world: $WORLD - '$*'.\n" sendCommand $WORLD "$*" else printf "Usage: $0 $1 \n" printf " ie: $0 $1 world say Hello World!\n" exit 1 fi ;; screen) # Check for the world command line argument. if [ -n "$2" ] && [ $(listContains $2 "$ALL_WORLDS") -eq 1 ]; then printf "About to load the screen for world: $2.\n" printf "To exit the screen, hit Ctrl+A then type the letter d.\n" sleep 5 displayScreen $2 else if [ -n "$2" ]; then printf "Minecraft world $2 not found!\n" else printf "Minecraft world not provided!\n" fi printf " Usage: $0 $1 \n" exit 1 fi ;; watch) # Check for the world command line argument. if [ -n "$2" ] && [ $(listContains $2 "$ALL_WORLDS") -eq 1 ]; then printf "Monitoring Minecraft Server: $2.\n" watchLog $2 else if [ -n "$2" ]; then printf "Minecraft world $2 not found!\n" else printf "Minecraft world not provided!\n" fi printf " Usage: $0 $1 \n" exit 1 fi ;; logrotate) # Figure out which worlds to rotate the log. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Backup each world requested. printf "Rotating Minecraft Server Log:" for WORLD in $WORLDS; do printf " $WORLD" rotateLog $WORLD done printf ".\n" ;; backup) # Figure out which worlds to backup. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Backup each world requested. printf "Backing up Minecraft Server:" for WORLD in $WORLDS; do printf " $WORLD" if [ $(serverRunning $WORLD) -eq 1 ]; then sendCommand $WORLD "say Backing up the world." sendCommand $WORLD "save-all" sendCommand $WORLD "save-off" sleep 20 worldBackup $WORLD sendCommand $WORLD "save-on" sendCommand $WORLD "say Backup complete." else worldBackup $WORLD fi done printf ".\n" ;; update) # Stop all of the world servers and backup the worlds. printf "Stopping Minecraft Server:" for WORLD in $ALL_WORLDS; do if [ $(serverRunning $WORLD) -eq 1 ]; then printf " $WORLD" if [ $(printf "%d" $(queryStatus $WORLD | cut -f6)) -gt 0 ]; then sendCommand $WORLD "say The server admin has initiated a software update." sendCommand $WORLD "say The server will restart and update in 1 minute..." sleep 60 sendCommand $WORLD "say The server is now restarting." fi sendCommand $WORLD "save-all" sendCommand $WORLD "save-off" stop $WORLD fi done printf ".\n" printf "Backing up Minecraft Server:" for WORLD in $ALL_WORLDS; do printf " $WORLD" worldBackup $WORLD done printf ".\n" printf "Updating software package:" # Update the client software. printf " client" updateClientSoftware # Update the server software. printf " server" updateServerSoftware printf ".\n" printf "Restarting Minecraft Server:" for WORLD in $ALL_WORLDS; do printf " $WORLD" start $WORLD done printf ".\n" ;; map|overviewer) # Make sure that the Minecraft Overviewer software exists. if [ ! -e "$OVERVIEWER_BIN" ]; then printf "Mincraft Overviewer software not found.\n" exit 1 fi # Figure out which worlds to map. WORLDS=$(checkOptionalArgument "$ALL_WORLDS" $0 $1 $2) # Run Minecraft Overviewer on each world requested. printf "Running Minecraft Overviewer mapping:" for WORLD in $WORLDS; do printf " $WORLD" if [ $(serverRunning $WORLD) -eq 1 ]; then sendCommand $WORLD "say The world is about to be mapped with Minecraft Overviewer." sendCommand $WORLD "save-all" sendCommand $WORLD "save-off" sleep 20 worldBackup $WORLD overviewer $WORLD sendCommand $WORLD "save-on" sendCommand $WORLD "say Mapping is complete. You can access the maps at:" sendCommand $WORLD "say $MAPS_URL/$WORLD" else worldBackup $WORLD overviewer $WORLD fi done printf ".\n" ;; *) printf "Error, in command line usage.\n" printf "\n" printf "$USAGE\n" exit 1 ;; esac exit 0