mscs/minecraft_server
2013-06-25 22:27:19 -06:00

1220 lines
41 KiB
Bash
Executable File

#!/bin/sh
### BEGIN INIT INFO
# Provides: minecraft_server
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 3 4 5
# Default-Stop: 0 1 2 6
# chkconfig: 345 50 50
# Description: Minecraft Server Control Script
### END INIT INFO
# ---------------------------------------------------------------------------
# Copyright (c) 2013, Jason M. Wood <sandain@hotmail.com>
#
# 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 <<EOF
Usage: $0 <option>
Options:
start <world> - Start the Minecraft world server. Start all
world servers by default.
stop <world> - Stop the Minecraft world server. Stop all world
servers by default.
force-stop <world> - Forcibly stop the Minecraft world server.
Forcibly stop all world servers by default.
restart <world> - Restart the Minecraft world server. Restart all
world servers by default.
force-restart <world> - Forcibly restart the Minecraft world server.
Forcibly restart all world servers by default.
status <world> - Display the status of the Minecraft world server.
Display the status of all world servers by
default.
sync <world> - 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 <world> <cmd> - Send a command to a Minecraft world server.
screen <world> - Display the Screen for the Minecraft world
server.
watch <world> - Watch the log file for the Minecraft world
server.
logrotate <world> - Rotate the server.log file. Rotate the
server.log file for all worlds by default.
backup <world> - Backup the Minecraft world. Backup all worlds by
default.
map <world> - 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
# ---------------------------------------------------------------------------
# Automatically restart the Minecraft server when a SEVERE error is caught.
# 0 - Do not automatically restart when a SEVERE error is caught.
# 1 - Automatically restart the server on a SEVERE error.
AUTO_RESTART_ON_ERROR=0
# Minecraft Server Settings
# ---------------------------------------------------------------------------
# Choose only one server distribution, leave the other commented out.
# Default Mojang server distribution.
SERVER_JAR="minecraft_server.jar"
SERVER_URL="http://www.minecraft.net/download/minecraft_server.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/MinecraftDownload/minecraft.jar"
CLIENT_LOCATION="$LOCATION/.minecraft/bin"
# World (Server Instance) Configuration
# ---------------------------------------------------------------------------
# The location to store files for each world server.
WORLDS_LOCATION="$LOCATION/worlds"
# List of worlds and the ports they are running on. This file will
# be generated if missing.
#
# Note: The world name should not contain a space. Leave the ip
# address blank if not needed.
#
# # Minecraft world configuration file
# # <world> <port> <ip>
# alpha 25565
# beta 25566
# gamma 25567
# delta 25568
# epsilon 25569
WORLDS_CONF="$LOCATION/worlds.conf"
# 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=31
# 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.
#
# 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
printf "About to load the screen for world $1.\n"
printf "To exit the screen, hit Ctrl+A then type the letter d.\n"
sleep 5
# 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
}
# ---------------------------------------------------------------------------
# Grab the port for the given world.
#
# @param 1 The world server of interest.
# @return The port that the world is configured to use.
# ---------------------------------------------------------------------------
getPort() {
local PORT
PORT=$(execute "cat $WORLDS_CONF" $USER_NAME | $PERL -ne 'if ($_ =~ /^'$1'\s+(\d+)/) { print "$1"; }')
echo $PORT
}
# ---------------------------------------------------------------------------
# Grab the IP address for the given world.
#
# @param 1 The world server of interest.
# @return The IP address that the world is configured to run on.
# ---------------------------------------------------------------------------
getIP() {
local IP
IP=$(execute "cat $WORLDS_CONF" $USER_NAME | $PERL -ne 'if ($_ =~ /^'$1'\s+\d+\s+([\d\.]+)/) { print "$1"; }')
echo $IP
}
# ---------------------------------------------------------------------------
# Grab the first line of the Message of the Day file as a summary, and strip
# any color codes from it.
# ---------------------------------------------------------------------------
getMOTD() {
local MOTD_SUMMARY
MOTD_SUMMARY=""
if [ -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 WORLDS
WORLDS=$(execute "cat $WORLDS_CONF" $USER_NAME | $PERL -ne 'if ($_ =~ /^(\w+)\s+(\d+)/) { print "$1 "; }')
echo $WORLDS
}
# ---------------------------------------------------------------------------
# Modify the value of a key/value combo in a properties file.
#
# @param 1 The properties file of interest.
# @param 2 The key to modify.
# @param 3 The value to assign to the key.
# ---------------------------------------------------------------------------
setPropertiesValue() {
local KEY_VALUE
# Make sure that the properties file exists.
execute "touch $1" $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"; }' $1)
if [ -n "$KEY_VALUE" ]; then
execute "$PERL -i -ne 'if (\$_ =~ /^$2=.*$/) { print \"$2=$3\\n\"; } else { print; }' $1" $USER_NAME
else
execute "printf \"$2=$3\\n\" >> $1" $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
if [ -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 <optional argument>\n"
exit 1
fi
echo "$LIST"
}
# ---------------------------------------------------------------------------
# Check for users logging into a world. If a user logs in, perform
# login functions.
#
# @param 1 The world server of interest.
# @param 2 The message to check for users logging in.
# ---------------------------------------------------------------------------
checkForLogin() {
local LOGIN PLAYER_NAME
LOGIN=$(echo "$2" | $PERL -ne 'if ($_ =~ /(\w+)\s*\[\/([0-9\.]+)\:(\d+)\] logged in with entity id (\d+)/) { print "$1\t$2\t$3\t$4"; }')
if [ -n "$LOGIN" ]; then
PLAYER_NAME=$(printf "$LOGIN" | cut -f1)
# Add the user to the world.users file.
execute "printf \"$LOGIN\n\" >> \"$WORLDS_LOCATION/$1.users\"" $USER_NAME
# Announce the user logging in via lib-notify.
if [ $USE_LIBNOTIFY ]; then
libNotify "Minecraft - $1" "$PLAYER_NAME has logged into world."
fi
# Whisper the MOTD to the user logging in.
tellMOTD $1 $PLAYER_NAME
fi
}
# ---------------------------------------------------------------------------
# Check for users logging out of a world. If a user logs out, perform the
# logout functions.
#
# @param 1 The world server of interest.
# @param 2 The message to check for users logging out.
# ---------------------------------------------------------------------------
checkForLogout() {
local LOGOUT BAN PLAYER_NAME
LOGOUT=$(echo "$2" | $PERL -ne 'if ($_ =~ /(\w+) lost connection\: (.+)/) { print "$1\t$2"; }')
BAN=$(echo "$2" | $PERL -ne 'if ($_ =~ /Disconnecting (\w+)\s*\[\/([0-9\.\:]+)\]\: You are banned/) { print "$1\t$2"; }')
if [ -n "$LOGOUT" ]; then
PLAYER_NAME=$(printf "$LOGOUT" | cut -f1)
# Remove the user from the world.users file.
execute "$PERL -i -ne 'print unless /^$PLAYER_NAME\t[0-9\.]+\t\d+\d+/;' $WORLDS_LOCATION/$1.users" $USER_NAME
# Announce the user logging out via lib-notify.
if [ $USE_LIBNOTIFY ]; then
libNotify "Minecraft - $1" "$PLAYER_NAME has logged out of world."
fi
elif [ -n "$BAN" ]; then
PLAYER_NAME=$(printf "$BAN" | cut -f1)
# Remove the user from the world.users file.
execute "$PERL -i -ne 'print unless /^$PLAYER_NAME\t[0-9\.]+\t\d+\d+/;' $WORLDS_LOCATION/$1.users" $USER_NAME
# Announce the user ban via lib-notify.
if [ $USE_LIBNOTIFY ]; then
libNotify "Minecraft - $1" "$PLAYER_NAME has been banned from the world."
fi
fi
}
# ---------------------------------------------------------------------------
# Parse through the log file for the given world. Uses checkFor methods to
# find events such as users logging in or out.
#
# @param 1 The world server generating the log to parse.
# ---------------------------------------------------------------------------
parseLog() {
local LINE DATE TIME TYPE MESSAGE
while read LINE; do
LINE=$(echo "$LINE" | $PERL -ne 'if ($_ =~ /(.+) (.+) \[(\w+)\] (.+)/) { print "$1\t$2\t$3\t$4"; }')
DATE=$(echo "$LINE" | cut -f1)
TIME=$(echo "$LINE" | cut -f2)
TYPE=$(echo "$LINE" | cut -f3)
MESSAGE=$(echo "$LINE" | cut -f4)
case "$TYPE" in
INFO)
checkForLogin $1 "$MESSAGE"
checkForLogout $1 "$MESSAGE"
;;
SEVERE)
if [ $AUTO_RESTART_ON_ERROR -eq 1 ]; then
sendCommand $1 "say The server is experiencing issues, restarting in 5 seconds..."
sleep 5
stop $1
sleep 5
start $1
fi
;;
WARNING)
;;
*)
;;
esac
done
}
# ---------------------------------------------------------------------------
# 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"
# Use the mirror copy of the world directory if enabled.
if [ $ENABLE_MIRROR -eq 1 ] && [ -d $MIRROR_PATH ]; then
WORLD_DIR="$MIRROR_PATH/$1"
fi
# 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"
# Use the mirror copy of the world directory if enabled.
if [ $ENABLE_MIRROR -eq 1 ] && [ -d $MIRROR_PATH ]; then
WORLD_DIR="$MIRROR_PATH/$1"
fi
# 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 "$RSYNC -rt $MIRROR_PATH/$1/* $WORLDS_LOCATION/$1" $USER_NAME
if [ $? -ne 0 ]; then
printf "Error synchronizing mirror images for world $1.\n"
exit 1
fi
}
# ---------------------------------------------------------------------------
# Start the world server and the log processor. 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 sure that the server.properties file holds the correct values.
setPropertiesValue $WORLDS_LOCATION/$1/server.properties "level-name" "$1"
setPropertiesValue $WORLDS_LOCATION/$1/server.properties "server-port" "$(getPort $1)"
setPropertiesValue $WORLDS_LOCATION/$1/server.properties "server-ip" "$(getIP $1)"
setPropertiesValue $WORLDS_LOCATION/$1/server.properties "motd" "$(getMOTD)"
# Make a mirror image of the world directory if requested.
if [ $ENABLE_MIRROR -eq 1 ] && [ -d $MIRROR_PATH ]; then
execute "mkdir -p $MIRROR_PATH/$1" $USER_NAME
execute "cp -R $WORLDS_LOCATION/$1/* $MIRROR_PATH/$1" $USER_NAME
WORLD_DIR="$MIRROR_PATH/$1"
elif [ $ENABLE_MIRROR -eq 1 ]; then
printf "Error copying the world data to the mirror location, path not found.\n"
exit 1
fi
# Change to the world's directory.
cd $WORLD_DIR
# Make sure that the server.log file exists.
execute "touch server.log" $USER_NAME
# Erase the world's users file before starting up the world, in
# case it is not already empty for some reason.
execute "printf \"\" > \"$WORLDS_LOCATION/$1.users\"" $USER_NAME
# 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 Process ID of the world server.
PID=$(echo $(getProcessIDs $1) | cut -d ' ' -f2)
if [ ! -n "$PID" ]; then
printf "Error starting the server, the Process ID was not found.\n"
exit 1
fi
# Start the log processor.
tail -n0 -f --pid=$PID $WORLD_DIR/server.log | parseLog $1 &
# 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"
# Erase the world's users file since we won't be able to catch
# anyone logging off.
execute "printf \"\" > \"$WORLDS_LOCATION/$1.users\"" $USER_NAME
# Synchronize the mirror image of the world prior to closing, if
# required.
if [ $ENABLE_MIRROR -eq 1 ] && [ -d $MIRROR_PATH ]; then
syncMirrorImage $1
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() {
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
}
# ---------------------------------------------------------------------------
# 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 openjdk-6-jdk"
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
# Generate a default worlds.conf file if it does not already exist.
if [ ! -e "$WORLDS_CONF" ]; then
execute "printf \"# Minecraft world configuration file\n\" > $WORLDS_CONF" $USER_NAME
execute "printf \"# <world>\t<port>\t<ip>\n\" >> $WORLDS_CONF" $USER_NAME
execute "printf \"$DEFAULT_WORLD\t$DEFAULT_PORT\t$DEFAULT_IP\n\" >> $WORLDS_CONF" $USER_NAME
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
# Update old worlds.conf files.
if [ -e "$WORLDS_CONF" ] && [ "$(grep '<eport>' $WORLDS_CONF)" != "" ]; then
execute "cp $WORLDS_CONF $WORLDS_CONF.old" $USER_NAME
execute "$PERL -i -ne 'if (\$_ =~ /#\s*<world>\s+<eport>\s+<iport>\s+<ip>/) { print \"# <world>\t<port>\t<ip>\n\"; } else { print; }' $WORLDS_CONF" $USER_NAME
execute "$PERL -i -ne 'if (\$_ =~ /(\w+)\s+(\d+)\s+\d+\s*([\d\.]*)/) { printf \"%s\t%d\t%s\n\",\$1,\$2,\$3; } else { print; }' $WORLDS_CONF" $USER_NAME
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"
sendCommand $WORLD "say The server is about to go down."
sendCommand $WORLD "save-all"
sendCommand $WORLD "save-off"
sendCommand $WORLD "say The server is going down in 5 seconds..."
sleep 5
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
sendCommand $WORLD "say The server is about to restart."
sendCommand $WORLD "save-all"
sendCommand $WORLD "save-off"
sendCommand $WORLD "say Restarting in 5 seconds..."
sleep 5
if [ "$(echo \"$1\" | cut -d '-' -f1)" = "force" ]; then
forceStop $WORLD
else
stop $WORLD
fi
sleep 5
fi
start $WORLD
done
printf ".\n"
;;
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: "
if [ $(serverRunning $WORLD) -eq 1 ]; then
printf "running (%d users online).\n" $(cat $WORLDS_LOCATION/$WORLD.users | wc -l)
else
printf "not running.\n"
fi
done
;;
sync|synchronize)
# Make sure the Mirror image option is enabled.
if [ $ENABLE_MIRROR -ne 1 ]; then
printf "Mirror image option not enabled, unable to "
printf "synchronize.\n";
exit 1
fi
# 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-off"
syncMirrorImage $WORLD
sendCommand $WORLD "save-on"
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 <world> <command>\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
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 <world>\n"
exit 1
fi
;;
watch)
# Check for the world command line argument.
if [ -n "$2" ] && [ $(listContains $2 "$ALL_WORLDS") -eq 1 ]; then
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 <world>\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 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)
# If the server software is being updated, 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"
sendCommand $WORLD "say The server software is being updated."
sendCommand $WORLD "say Server restart is imminent."
sendCommand $WORLD "save-all"
sendCommand $WORLD "save-off"
sendCommand $WORLD "say Restarting in 5 seconds."
sleep 5
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 "Starting 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