#!/bin/bash
# /etc/init.d/minecraft
# version 2013-04-19 (YYYY-MM-DD)
### 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: Starts the CraftBukkit Minecraft server
### END INIT INFO
# minecraft-init-script - An initscript to start Minecraft or CraftBukkit
# Copyright (C) 2011 - Super Jamie <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# Source function library
## CentOS/Fedora
if [ -f /etc/rc.d/init.d/functions ]
then
. /etc/rc.d/init.d/functions
fi
## Ubuntu
if [ -f /lib/lsb/init-functions ]
then
. /lib/lsb/init-functions
fi
## Settings
# Nice looking name of service for script to report back to users
SERVERNAME="obscurity"
# Filename of server binary
SERVICE="FTBServer-1.7.10-1448.jar"
# URL of server executable for update checking - http://cbukk.it/craftbukkit.jar is the recommended latest Craftbukkit URL
SERVER_URL="http://cbukk.it/craftbukkit.jar"
# Username of non-root user who will run the server
USERNAME="minecraft"
# Path of server binary and world
MCPATH="/home/minecraft/servers/obscurity"
# Number of CPU cores to thread across if using multithreaded garbage collection
CPU_COUNT="4"
# Where backups go
BACKUPPATH="/home/minecraft/Dropbox/ovh_backup/obscurity"
# Find the world name from the existing server file
WORLDNAME="$(cat $MCPATH/server.properties | grep -E 'level-name' | sed -e s/.*level-name=//)"
# Find the port number from the existing server file
SERVERPORT="$(cat $MCPATH/server.properties | grep -E 'server-port' | sed -e s/.*server-port=//)"
# Name of Screen session
SCRNAME="obscurity"
## The Java command to run the server
# Nothing special, just start the server with 1Gb RAM
# INVOCATION="java -Xms2048M -Xmx1024M -Djava.net.preferIPv4Stack=true -jar $SERVICE nogui"
# This is what I run my server with. Tune your RAM usage accordingly
# Tested fastest GC - Default parallel new gen collector, plus parallel old gen collector
#INVOCATION="java -Xmx12288M -Xms12288M -XX:PermSize=256m -XX:NewRatio=3 -XX:SurvivorRatio=3 -XX:TargetSurvivorRatio=80 -XX:MaxTenuringThreshold=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=10 -XX:GCPauseIntervalMillis=50 -XX:MaxGCMinorPauseMillis=7 -XX:+ExplicitGCInvokesConcurrent -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+BindGCTaskThreadsToCPUs -Xnoclassgc -jar $SERVICE nogui"
#INVOCATION="java -Xms8G -Xmx8G -XX:PermSize=256m -XX:NewRatio=3 -XX:SurvivorRatio=3 -XX:TargetSurvivorRatio=80 -XX:MaxTenuringThreshold=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=10 -XX:GCPauseIntervalMillis=50 -XX:MaxGCMinorPauseMillis=7 -XX:+ExplicitGCInvokesConcurrent -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+BindGCTaskThreadsToCPUs -Xnoclassgc -jar $SERVICE nogui"
INVOCATION="java -Xms512M -Xmx4096M -jar $SERVICE nogui"
# I removed these "performance" commands as I don't see any difference with them
# -XX:+UseFastAccessorMethods -XX:+UseAdaptiveGCBoundary
# I've had a suggestion from a Java tuning engineer to use these
# -XX:+AggressiveOpts -XX:+UseCompressedStrings
# Add HugePage support if you have it configured on the OS
# -XX:+UseLargePages
# I have no idea if it helps, but you can add SSE support depending on your CPU (check /proc/cpuinfo):
# -XX:UseSSE=3
## Runs all commands as the non-root user
as_user() {
  ME="$(whoami)"
  if [ "$ME" == "$USERNAME" ]
  then
    bash -c "$1"
  else
    su - "$USERNAME" -c "$1"
  fi
}
## Check if the server is running or not, and get the Java Process ID if it is
server_running() {
  # Get the PID of the running Screen session:
  # ps, remove grep, look for Screen, look for the Screen session named $SCRNAME to differentiate between multiple servers, awk out everything but the PID of Screen
  SCREENPID=""
  SCREENPID="$(ps -ef | grep -v grep | grep -i screen | grep $SCRNAME | awk '{print $2}')"
  # If the Screen session with $SCRNAME is not running, then the server is not running, so we return 1 and exit the function
  if [ -z "$SCREENPID" ]
  then
   return 1
  fi
  JAVAPID=""
  # PID="$(ps -ef | grep -v grep | grep -i screen | grep $SCRNAME | awk '{print $2}' | xargs ps -f --ppid | grep $SERVICE | awk '{print $2}')"
  # Use the Screen session PID to see what processes it is parent to, which is the actual PID of the Java process, check that that PID is actually $SERVICE
  JAVAPID="$(ps -f --ppid $SCREENPID | grep $SERVICE | awk '{print $2}')"
  # If the Java process is not running, then the server is not running, so we return 1 to exit the function
  if [ -z "$JAVAPID" ]
  then
    return 1
  fi
  # If we haven't failed those two tests, we must have a running server, so we return success
  return 0
}
## Start the server executable as a service
mc_start() {
  if server_running
  then
    echo " * [ERROR] $SERVERNAME was already running (pid $JAVAPID). Not starting!"
    exit 1
  else
    echo " * $SERVERNAME was not already running. Starting..."
    echo " * Using map named \"$WORLDNAME\"..."
    as_user "cd \"$MCPATH\" && screen -c /dev/null -dmS $SCRNAME $INVOCATION"
    sleep 10
    echo " * Checking $SERVERNAME is running..."
    if server_running
    then
      echo " * [OK] $SERVERNAME is now running (pid $JAVAPID)."
    else
      echo " * [ERROR] Could not start $SERVERNAME."
      exit 1
    fi
  fi
}
## Stop the executable
mc_stop() {
  if server_running
  then
    echo " * $SERVERNAME is running (pid $JAVAPID). Commencing shutdown..."
    echo " * Notifying users of shutdown..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"say SERVER SHUTTING DOWN IN 10 SECONDS. Saving map...\"\015'"
    echo " * Saving map named \"$WORLDNAME\" to disk..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"save-all\"\015'"
    sleep 10
    echo " * Stopping $SERVERNAME..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"stop\"\015'"
    sleep 10
  else
    echo " * [ERROR] $SERVERNAME was not running. Unable to stop!"
    ##exit 1
  fi
  if server_running
  then
    echo " * [ERROR] $SERVERNAME is still running (pid $JAVAPID). Could not be shutdown!"
    exit 1
  else
    echo " * [OK] $SERVERNAME is shut down."
  fi
}
## Set the server read-only, save the map, and have Linux sync filesystem buffers to disk
mc_saveoff() {
  if server_running
  then
    echo " * $SERVERNAME is running. Commencing save..."
    echo " * Notifying users of save..."
    as_user "screen -p 0 -S $SCRNAME  -X eval 'stuff \"say SERVER BACKUP STARTING. Server going read-only...\"\015'"
    echo " * Setting server read-only..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"save-off\"\015'"
    echo " * Saving map named \"$WORLDNAME\" to disk..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"save-all\"\015'"
    sync
    sleep 10
    echo " * [OK] Map saved."
  else
    echo " * [INFO] $SERVERNAME was not running. Not suspending saves."
  fi
}
## Set the server read-write
mc_saveon() {
  if server_running
  then
    echo " * $SERVERNAME is running. Re-enabling saves..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"say SERVER BACKUP ENDED. Server going read-write...\"\015'"
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"save-on\"\015'"
  else
    echo " * [INFO] $SERVERNAME was not running. Not resuming saves."
  fi
}
## Notify everyone that the server will be rebooting in 5 minutes
mc_rbnotify() {
  if server_running
  then
    echo " * $SERVERNAME is running.  Notifying users of pending reboot..."
    as_user "screen -p 0 -S $SCRNAME -X eval 'stuff \"say SERVER REBOOT in 5 minutes...\"\015'"
  else
    echo " * [INFO] $SERVERNAME was not running. No notification sent."
  fi
}
## Checks for update, exits if update not required, updates if the server is not running
mc_updatecheck() {
  echo " * Downloading latest $SERVERNAME executable..."
  as_user "cd \"$MCPATH\" && curl -# -L -o \"$MCPATH/$SERVICE.update\" \"$SERVER_URL\""
  if [ -f "$MCPATH/$SERVICE.update" ]
  then
    echo " * Checking downloaded update against existing server..."
    if $(diff "$MCPATH/$SERVICE" "$MCPATH/$SERVICE.update" >/dev/null)
    then
      echo " * You are already running the latest version of $SERVERNAME!"
      exit 0; # keep this exit in as we don't need to do anything
    fi
  else
    echo " * [ERROR] $SERVERNAME update could not be downloaded."
    exit 1
  fi
  if server_running
  then
    echo " * $SERVERNAME is running (pid $JAVAPID). Shutting down for update..."
  else
    as_user "/bin/cp \"$MCPATH/$SERVICE.update\" \"$MCPATH/$SERVICE\""
    echo " * [OK] $SERVERNAME successfully updated."
  fi
}
## Actually do the executable update
mc_updatedo() {
  if server_running
  then
    echo " * [ERROR] $SERVICE is still running (pid $JAVAPID). Cannot update!"
    exit 1
  else
    as_user "/bin/cp \"$MCPATH/$SERVICE.update\" \"$MCPATH/$SERVICE\""
    echo " * [OK] $SERVERNAME successfully updated."
  fi
}
## Check and see if a worldname was given to the backup command. Use the default world, or check the optional world exists and exit if it doesn't.
mc_checkbackup() {
  if [ -n "$1" ]
    then
    WORLDNAME="$1"
      if [ -d "$MCPATH/$WORLDNAME" ]
        then
        echo " * Found world named \"$MCPATH/$WORLDNAME\""
      else
        echo " * Could not find world named \"$MCPATH/$WORLDNAME\""
        exit 1
      fi
  fi
}
## Backs up map by rsyncing current world to backup location, creates tar.gz with datestamp
mc_backupmap() {
  echo " * Backing up $SERVERNAME map named \"$WORLDNAME\"..."
  echo " * Syncing \"$MCPATH/$WORLDNAME\" to \"$BACKUPPATH/$WORLDNAME\""
  as_user "rsync --checksum --group --human-readable --copy-links --owner --perms --recursive --times --update --delete \"$MCPATH/$WORLDNAME\" \"$BACKUPPATH\""
  sleep 10
  echo " * Creating compressed backup..."
  NOW="$(date +%Y-%m-%d.%H-%M-%S)"
  # Create a compressed backup file and background it so we can get back to restarting the server
  # You can tell when the compression is done as it makes an md5sum file of the backup
  as_user "cd "$BACKUPPATH" && tar cfz \""$WORLDNAME"_backup_"$NOW".tar.gz\" \"$WORLDNAME\" && md5sum \""$WORLDNAME"_backup_"$NOW".tar.gz\" > \""$WORLDNAME"_backup_"$NOW".tar.gz.md5\" &"
  # we can safely background the above command and get back to restarting the server
  echo " * [OK] Backed up map \"$WORLDNAME\"."
}
## Backs up executable by copying it to backup location
mc_backupexe() {
  echo " * Backing up the $SERVERNAME server executable..."
  NOW="$(date +%Y-%m-%d.%H-%M-%S)"
  as_user "cp \""$MCPATH"/"$SERVICE"\" \""$BACKUPPATH"/"$SERVICE"_backup_"$NOW".jar\""
  echo " * [OK] Backed up executable."
}
## Removes any backups older than 7 days, designed to be called by daily cron job
mc_removeoldbackups() {
  echo " * Removing backups older than 7 days..."
  as_user "cd \"$BACKUPPATH\" && find . -name \"*backup*\" -type f -mtime +7 | xargs rm -fv"
  echo " * Removed old backups."
}
## Rotates logfile to server.1 through server.7, designed to be called by daily cron job
mc_logrotate() {
  # Server logfiles in chronological order
  LOGLIST="$(ls -r $MCPATH/server.log* | grep -v lck)"
  # How many logs to keep
  COUNT="6"
  # Look at all the logfiles
  for logfile in $LOGLIST; do
    LOGTMP="$(basename $logfile | cut -d '.' -f 3)"
    # If we're working with server.log then append .1
    if [ -z "$LOGTMP" ]
    then
      LOGNEW="server.log.1"
      as_user "/bin/cp $logfile $MCPATH/$LOGNEW"
    # Otherwise, check if the file number is greater than $COUNT
    elif [ "$LOGTMP" -gt "$COUNT" ]
    then
      # If so, delete it
      as_user "rm -f $logfile"
    else
      # Otherwise, add one to the number
      LOGBASE="$(basename $logfile | cut -d '.' -f 1-2)"
      LOGNEW="$LOGBASE.$(($LOGTMP+1))"
      as_user "/bin/cp $logfile $MCPATH/$LOGNEW"
    fi
  done
  # Blank the existing logfile to renew it
  as_user "echo -n \"\" > $MCPATH/server.log"
}
## Check if server is running and display PID
mc_status() {
  if server_running
  then
    echo " * $SERVERNAME status: Running (pid $JAVAPID)."
  else
    echo " * $SERVERNAME status: Not running."
    exit 1
  fi
}
## Display some extra environment informaton
mc_info() {
  if server_running
  then
    RSS="$(ps --pid $JAVAPID --format rss | grep -v RSS)"
    echo " - Java Path          : $(readlink -f $(which java))"
    echo " - Start Command      : $INVOCATION"
    echo " - Server Path        : $MCPATH"
    echo " - World Name         : $WORLDNAME"
    echo " - Process ID         : $JAVAPID"
    echo " - Screen Session     : $SCRNAME"
    echo " - Memory Usage       : $[$RSS/1024] Mb ($RSS kb)"
  # Check for HugePages support in kernel, display statistics if HugePages are in use, otherwise skip
  if [ -n "$(cat /proc/meminfo | grep HugePages_Total | awk '{print $2}')" -a "$(cat /proc/meminfo | grep HugePages_Total | awk '{print $2}')" -gt 0 ]
  then
    HP_SIZE="$(cat /proc/meminfo | grep Hugepagesize | awk '{print $2}')"
    HP_TOTAL="$(cat /proc/meminfo | grep HugePages_Total | awk '{print $2}')"
    HP_FREE="$(cat /proc/meminfo | grep HugePages_Free | awk '{print $2}')"
    HP_RSVD="$(cat /proc/meminfo | grep HugePages_Rsvd | awk '{print $2}')"
    HP_USED="$[$HP_TOTAL-$HP_FREE+$HP_RSVD]"
    TOTALMEM="$[$RSS+$[$HP_USED*$HP_SIZE]]"
    echo " - HugePage Usage     : $[$HP_USED*$[$HP_SIZE/1024]] Mb ($HP_USED HugePages)"
    echo " - Total Memory Usage : $[$TOTALMEM/1024] Mb ($TOTALMEM kb)"
  fi
    echo " - Active Connections : "
    netstat --inet -tna | grep -E "Proto|$SERVERPORT"
  else
    echo " * $SERVERNAME is not running. Unable to give info."
    exit 1
  fi
}
## Connect to the active Screen session, disconnect with Ctrl+a then d
mc_console() {
  if server_running
  then
    as_user "screen -S $SCRNAME -dr"
  else
    echo " * [ERROR] $SERVERNAME was not running! Unable to console."
    exit 1
  fi
}
## These are the parameters passed to the script
case "$1" in
  start)
mc_start
;;
  stop)
mc_stop
;;
  restart)
mc_stop
sleep 1
mc_start
;;
  update)
mc_updatecheck
mc_stop
mc_backupmap
mc_backupexe
mc_updatedo
mc_start
;;
  backup)
mc_checkbackup "$2"
mc_saveoff
mc_backupmap
mc_backupexe
mc_saveon
;;
  status)
mc_status
;;
  info)
mc_info
;;
  console)
mc_console
;;
# These are intended for cron usage, not regular users.
  removeoldbackups)
mc_removeoldbackups
;;
  logrotate)
mc_logrotate
;;
  rbnotify)
mc_rbnotify
;;
# Debug usage only
  justbackup) # don't use this while the server is running!!!
mc_checkbackup
mc_backupmap
mc_backupexe
;;
  *)
echo " * Usage: minecraft {start|stop|restart|backup (worldname)|update|status|info|console}"
exit 1
;;
esac
exit 0