#!/bin/bash

### BEGIN INIT INFO
# Provides:   minecraft
# Required-Start: $local_fs $remote_fs
# Required-Stop:  $local_fs $remote_fs
# Should-Start:   $network
# Should-Stop:    $network
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description:    Minecraft server
# Description:    Init script for minecraft/bukkit server, with rolling logs and use of ramdisk for less lag. 
### END INIT INFO

# Minecraft/Bukkit init script by Marcus Whybrow
# - Modified from a script created by Ahtenus (https://github.com/Ahtenus/minecraft-init)
# - Which was apparently based upon http://www.minecraftwiki.net/wiki/Server_startup_script

# The location of the configuration file for this script
CONFIG="/opt/minecraft/server1/manager.config"


### Load configuration variables
source $CONFIG


### General Utility functions

# Returns the current time as a UNIX timestamp (in seconds since 1970)
now() {
	date +%s
}

# This function is used to quite the stdout of another function
# like this: quite $(other_func)
quite() {
    return $(true)
}

as_user() {
	if [ $(whoami) == $SERVER_USER ] ; then
		bash -c "$1"
	else
		su - $SERVER_USER -s /bin/bash -c "$1"
	fi
}


### Log Utility Functions

# Gets the time UNIX timestamp for a server log line
# $1: A server log line
# returns: Time in seconds since 1970-01-01 00:00:00 UTC
log_line_get_time() {
	time_string=$(echo $1 | awk '{print $1 " " $2}')
	date -d "$time_string" "+%s" 2> /dev/null
}

# Watches the log
# $1: The line in the log to wait for
# $2: A UNIX timestamp (seconds since 1970) which the $1 line must be after
# returns: When the line is found
log_wait_for_line() {
    # Make sure there is a server log to check
    as_user "touch $SERVER_LOG"

	regex="^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} \[.*\] ${1}"

	while read LINE
	do
		time=$(log_line_get_time "$LINE")

		# If the entry is old enough and matches the regular expression
		if [[ $time -ge $2 && $LINE =~ $regex ]]
		then
			echo $LINE
			break
		fi
	done < <(as_user "tail --follow --lines=100 --sleep-interval=0.1 $SERVER_LOG")
}


### Server Utility Functions

server_is_running() {
	if ps ax | grep -v grep | grep "$SCREEN_NAME $INVOCATION" > /dev/null
	then
		return $(true)
	else
		return $(false)
	fi
}

# Gets the process ID for the server if running, otherwise it outputs nothing
server_pid() {
	ps ax | grep -v grep | grep "$SCREEN_NAME $INVOCATION" | awk '{print $1}'
}

server_wait_for_stop() {
	pid=$(server_pid)

	while ps -p $pid > /dev/null
	do
		sleep 0.1
	done
}

# $1: A line of text to enter into the server console
server_eval() {
	as_user "screen -p 0 -S $SCREEN_NAME -X eval 'stuff \"$1\"\015'"
}

# $1: A line of text to enter into the server console
# $2: The line of text in the log to wait for
server_eval_and_wait() {
	time=$(now)
	server_eval $1
	log_wait_for_line "$2" "$time"
}


### World Handling Functions

worlds_get() {
	a=1
	for NAME in $(ls ${WORLD_STORAGE_PATH})
	do
		if [ -d ${WORLD_STORAGE_PATH}/$NAME ]
		then
			WORLDNAME[$a]=$NAME
			if [ -e ${WORLD_STORAGE_PATH}/$NAME/ramdisk ]
			then
				WORLDRAM[$a]=true
			else
				WORLDRAM[$a]=false
			fi
			a=$a+1
		fi
	done
}

# Creates symbolic links in the server directory (SERVER_PATH) for each
# of the Minecraft worlds located in the world storage directory (WORD_STORAGE_PATH).
worlds_ensure_links() {
	echo "Updating world symbolic links..."

	worlds_get
	for INDEX in ${!WORLDNAME[@]}
	do
		# -L checks for the path being a link rather than a file
		# ! -a, since it is within double square brackets means: the negation of
		# the existence of the file. In other words: true if does not exist
		if [[ -L ${SERVER_PATH}/${WORLDNAME[$INDEX]} || ! -a ${SERVER_PATH}/${WORLDNAME[$INDEX]} ]]
		then
			# If there is a symbolic link in the SERVER_PATH to this world or there is not
			# a directory in the SERVER_PATH for this world.
			
			# Get the original file path the symbolic link is pointing to
			# If there is no link, link_target will contain nothing
			link_target=$(readlink ${SERVER_PATH}/${WORLDNAME[$INDEX]})
			
			if ${WORLDRAM[$INDEX]}
			then
				# If this world is marked as loaded into RAM

				if [ "${link_target}" != "${RAMDISK_PATH}/${WORLDNAME[$INDEX]}" ]
				then
					# If the symbolic link does not point to the RAM version of the world

					# Remove the symbolic link if it exists
					as_user "rm -f ${SERVER_PATH}/${WORLDNAME[$INDEX]}"

					# Create a new symbolic link pointing to the RAM version of the world
					as_user "ln -s ${RAMDISK_PATH}/${WORLDNAME[$INDEX]} ${SERVER_PATH}/${WORLDNAME[$INDEX]}"
					echo "Created a link for ${WORLDNAME[$INDEX]} which is in RAM: ${RAMDISK_PATH}/${WORLDNAME[$INDEX]}"
				fi
			else
				# Otherwise the world is not loaded into RAM, and is just on disk

				if [ "${link_target}" != "${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}" ]
				then
					# If the symbolic link does not point to the disk version of the world

					# Remove the symbolic link if it exists
					as_user "rm -f ${SERVER_PATH}/${WORLDNAME[$INDEX]}"

					# Create a new symbolic link pointing to the disk version of the world
					as_user "ln -s ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]} ${SERVER_PATH}/${WORLDNAME[$INDEX]}"

					echo "Created a link for ${WORLDNAME[$INDEX]} which on disk: ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}"
				fi
			fi
		else
			# There was no symbolic link, and there was a directory, meaning a world
			# directory has been placed in SERVER_PATH, this is not allowed:
			echo "Could not process ${WORLDNAME[$INDEX]}. please move all worlds to ${WORLD_STORAGE_PATH}."
			exit 1
		fi
	done
	echo "Finished updating world symbolic links."
}

world_to_ram() {
	as_user "mkdir -p ${RAMDISK_PATH}/$1 && rsync -rt --exclude 'ramdisk' ${WORLD_STORAGE_PATH}/$1/ ${RAMDISK_PATH}/$1"
}

worlds_to_ram() {
	echo "Synchronising worlds to RAM..."

	worlds_get
	for INDEX in ${!WORLDNAME[@]}
	do
		if ${WORLDRAM[$INDEX]}
		then
			if [ -L ${SERVER_PATH}/${WORLDNAME[$INDEX]} ]
			then
				printf "[RAM] \"${WORLDNAME[$INDEX]}\": Synchronising to RAM... "
				world_to_ram "${WORLDNAME[$INDEX]}"
				echo "Done."
			fi
		else
			echo "[DSK] \"${WORLDNAME[$INDEX]}\": Nothing to do."
		fi
	done

	echo "Finished synchronising worlds to RAM."
}

worlds_to_disk() {
	echo "Synchronising worlds in RAM to disk..."

	worlds_get
	for INDEX in ${!WORLDNAME[@]}
	do
		if [ -e ${RAMDISK_PATH}/${WORLDNAME[$INDEX]} ]
		then
			printf "[RAM] \"${WORLDNAME[$INDEX]}\": Synchronising to disk... "
			as_user "rsync -rt --exclude 'ramdisk' ${RAMDISK_PATH}/${WORLDNAME[$INDEX]}/ ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]}"
			echo "Done."
		else
			echo "[DSK] \"${WORLDNAME[$INDEX]}\": Nothing to do."			
		fi
	done

	echo "Finished synchronising worlds in RAM to disk."
}

world_toggle_ramdisk_state() {
	if [ ! -e ${WORLD_STORAGE_PATH}/$1 ]
	then
		echo "World \"$1\" not found."
		exit 1
	fi
	
	if [ -e ${WORLD_STORAGE_PATH}/$1/ramdisk ]
	then
		as_user "rm ${WORLD_STORAGE_PATH}/$1/ramdisk"
		as_user "rm -r ${RAMDISK_PATH}/$1"
		echo "Removed the RAM flag from \"$1\", and deleted it from RAM."
	else
		as_user "touch ${WORLD_STORAGE_PATH}/$1/ramdisk"
		echo "Added the RAM flag to \"$1\"."
		printf "Copying world to RAM... "
		quite $(world_to_ram "$1")
		echo "Done."
	fi
	echo "Changes will only take effect after server is restarted."	
}


### Server Control Functions

server_start() {
	if server_is_running
	then
		echo "$JAR is already running!"
	else
		worlds_ensure_links
		worlds_to_ram

		time=$(now)

		printf "Starting server... "
		as_user "cd $SERVER_PATH && screen -dmS $SCREEN_NAME $INVOCATION"
		quite $(log_wait_for_line "$CONFIRMATIONS_START" "$time")
		echo "Done."
	fi
}

server_saveall() {
	if server_is_running
	then
		# Send the "save-all" command and wait for it to finish
		printf "Forcing save... "
		quite $(server_eval_and_wait "save-all" "$CONFIRMATIONS_SAVE_ALL")
		echo "Done."
	else
		echo "$JAR was not running. Not forcing save."
	fi
}

server_saveoff() {
	if server_is_running
	then
		# Send the "save-off" command and wait for it to finish
		printf "Disabling level saving... "
		quite $(server_eval_and_wait "save-off" "$CONFIRMATIONS_SAVE_OFF")
		echo "Done."

		# Also save all
		server_saveall

		# Not sure what this does, might be important
		sync
	else
		echo "$JAR was not running. Not suspending saves."
	fi
}

server_saveon() {
	if server_is_running
	then
		# Send the "save-on" command and wait for it to finish
		printf "Enabling level saving... "
		quite $(server_eval_and_wait "save-on" "$CONFIRMATIONS_SAVE_ON")
		echo "Done."
	else
		echo "$JAR was not running. Not resuming saves."
	fi
}

server_stop() {
    if server_is_running
    then
        server_saveall
        
        printf "Stopping the server... "
        server_eval "stop"
        
        server_wait_for_stop
		echo "Done."
	else
	    echo "$JAR was not running."
	fi
}

server_restart() {
	# Restarts the server if it is already running
	if server_is_running
	then
		# Stop the server
		server_stop

		# Synchronise all worlds in RAM to disk
		worlds_to_disk
		# Ensure world symbolic links are up to date
		worlds_ensure_links
		# Synchronise worlds back to RAM
		worlds_to_ram

		# Start the server again
		server_start
	else
		echo "$JAR is not running. Only running servers may be restarted."
	fi
}

# Not really tested this
server_update_jars() {
	if server_is_running
	then
		echo "$JAR is running! Will not start update."
	else
		# Update minecraft_server.jar
		echo "Updating minecraft_server.jar...."
		MC_SERVER_URL=http://minecraft.net/$(wget -q -O - http://www.minecraft.net/download.jsp | grep minecraft_server.jar\</a\> | cut -d \" -f 2)
		as_user "cd ${SERVER_PATH} && wget -q -O ${SERVER_PATH}/minecraft_server.jar.update $MC_SERVER_URL"
		if [ -f ${SERVER_PATH}/minecraft_server.jar.update ]
		then
			if $(diff ${SERVER_PATH}/minecraft_server.jar ${SERVER_PATH}/minecraft_server.jar.update >/dev/null)
			then
				echo "You are already running the latest version of the Minecraft server."
				as_user "rm ${SERVER_PATH}/minecraft_server.jar.update"
			else
				as_user "mv ${SERVER_PATH}/minecraft_server.jar.update $MCPATH/minecraft_server.jar"
				echo "Minecraft successfully updated."
			fi
		else
			echo "Minecraft update could not be downloaded."
		fi

		# Update craftbukkit

		echo "Updating craftbukkit...."
		as_user "cd ${SERVER_PATH} && wget -q -O ${SERVER_PATH}/craftbukkit.jar.update http://dl.bukkit.org/latest-rb/craftbukkit.jar"
		if [ -f ${SERVER_PATH}/craftbukkit.jar.update ]
		then
			if $(diff ${SERVER_PATH}/craftbukkit-0.0.1-SNAPSHOT.jar $MCPATH/craftbukkit.jar.update > /dev/null)
			then
				echo "You are already running the latest version of CraftBukkit."
				as_user "rm ${SERVER_PATH}/craftbukkit.jar.update"
			else
				as_user "mv ${SERVER_PATH}/craftbukkit.jar.update $MCPATH/craftbukkit-0.0.1-SNAPSHOT.jar"
				echo "CraftBukkit successfully updated."
			fi
		else
			echo "CraftBukkit update could not be downloaded."
		fi
	fi
}


### Backup Functions

backup_server() {
	path=${COMPLETE_BACKUP_PATH}/$(date "+%Y-%m-%d-%H-%M-%S").zip
	as_user "mkdir -p $COMPLETE_BACKUP_PATH"

	zip_flags="-rq"

	# Add the "y" flag if symbolic links should not be followed
	if [ "$COMPLETE_BACKUP_FOLLOW_SYMLINKS" != "true" ]
	then
		zip_flags="${zip_flags}y"
	fi
	
	# Zip up the server directory
	printf "Backing up the entire server directory... "
	as_user "mkdir -p ${COMPLETE_BACKUP_PATH} && cd ${SERVER_PATH} && zip ${zip_flags} $path ."
	echo "Done."
}

backup_worlds() {
	worlds_get
	echo "Backing up worlds..."
	for INDEX in ${!WORLDNAME[@]}
	do
		printf "Backing up world \"${WORLDNAME[$INDEX]}\"... "

		dir="${WORLD_SNAPSHOT_PATH}/${WORLDNAME[$INDEX]}"
		file_name="$(date "+%Y-%m-%d-%H-%M-%S").zip"
		as_user "mkdir -p ${dir} && cd ${WORLD_STORAGE_PATH}/${WORLDNAME[$INDEX]} && zip -rq ${dir}/${file_name} ."

		echo "Done."
	done
	echo "Finished backing up worlds."
}

# An experimental function which backs up config files for all
# Bukkit plugins. Only recognises .yml files currently
# I use the backup_server function instead to get everything
# for sure.
backup_configs() {
	dir=${CONFIG_BACKUP_PATH}
	file_name="$(date "+%Y-%m-%d-%H-%M-%S").zip"

	printf "Backing up plugin config files... "
	as_user "mkdir -p ${dir} && cd ${SERVER_PLUGIN_PATH} && zip -Rq ${dir}/${file_name} '${CONFIG_BACKUP_PATTERN}'"
	echo "Done."
}


### Maintenance Functions

log_roll() {
	# Moves and Gzips the logfile, a big log file slows down the
	# server A LOT (what was notch thinking?)

	path=${LOG_ARCHIVE_PATH}/${SERVER_NAME}-$(date +%F-%H-%M-%S).log

	printf "Rolling server logs... "
	as_user "mkdir -p $LOG_ARCHIVE_PATH && cp $SERVER_LOG $path && gzip $path"
	
	if [ $? -eq 0 ]
	then
	    as_user "cp /dev/null $SERVER_LOG && echo \"Previous logs rolled to $path\" > $SERVER_LOG"
	else
	    echo "Failed to rotate logs to $path.gz"
	fi
	
	echo "Done."
}


### Script switch statement

case "$1" in
	start)
		# Starts the server
		server_start
		;;
	stop)
		# Stops the server

		if server_is_running
		then
		    if [[ $2 != "now" ]]
		    then
			    server_eval "say ${MESSAGE_SERVER_STOP_WARNING}"
			    
			    echo "Issued the warning \"${MESSAGE_SERVER_STOP_WARNING}\" to players."
			    echo "Shutting down in ${CONFIG_SERVER_STOP_DELAY} seconds..."
			
			    sleep ${CONFIG_SERVER_STOP_DELAY}
			fi
			
			server_stop
			worlds_to_disk
		else
		    echo "$JAR was not running."
		fi
		;;
	restart)
		if server_is_running
		then
		    if [[ $2 != "now" ]]
		    then
			    server_eval "say ${MESSAGE_SERVER_RESTART_WARNING}"
                
			    echo "Issued the warning \"${MESSAGE_SERVER_RESTART_WARNING}\" to players."
			    echo "Restarting in ${CONFIG_SERVER_RESTART_DELAY} seconds..."
			    
			    sleep ${CONFIG_SERVER_RESTART_DELAY}
			fi
			
			server_restart
		else
		    echo "$JAR was not running. Cannot restart a stopped server. Try \"start\" instead."
		fi
		;;
	backup)
		# Backs up worlds

		if server_is_running
		then
			server_eval "say ${MESSAGE_SERVER_WORLDS_BACKUP_STARTED}"
			server_saveoff
			worlds_to_disk

			backup_worlds

			server_saveon
			server_eval "say ${MESSAGE_SERVER_WORLDS_BACKUP_FINISHED}"
		else
			backup_worlds
		fi
		;;
	complete-backup)
		# Backup everything
		if server_is_running
		then
			server_eval "say ${MESSAGE_SERVER_COMPLETE_BACKUP_STARTED}"
			server_saveoff

			backup_server

			server_saveon
			server_eval "say ${MESSAGE_SERVER_COMPLETE_BACKUP_FINISHED}"
		fi
		;;
	update)
		echo "Not supported yet."
		# Update minecraft_server.jar and craftbukkit.jar (thanks karrth)
		#server_eval "say SERVER UPDATE IN 10 SECONDS."
		#server_stop
		#worlds_to_disk
		#backup_server
		#server_update_jars
		#check_links
		#server_start
		;;
	to-disk)
		# Writes from the ramdisk to disk, in case the server crashes. 
		# Using ramdisk speeds things up a lot, especially if you allow
		# teleportation on the server.
		server_saveoff
		worlds_to_disk
		server_saveon
		;;
	connected)
		# Lists connected users

		# Send the list command to the server, and wait for the response
		server_eval_and_wait "list" "Connected players:"
		;;
	log-roll)
		log_roll
		;;
	last)
	    echo "Not yet supported."
		# greps for recently logged in users
		#echo Recently logged in users:
		#cat $SERVER_LOG | awk '/entity|conn/ {sub(/lost/,"disconnected");print $1,$2,$4,$5}'
		;;
	status)
		# Shows server status
		if server_is_running
		then
			echo "$JAR is running."
		else
			echo "$JAR is not running."
		fi
		;;
	version)
		echo "Not supported yet."
		;;
	links)
		worlds_ensure_links
		;;
	ramdisk)
		world_toggle_ramdisk_state $2
		;;
	worlds)
		worlds_get
		for INDEX in ${!WORLDNAME[@]}
		do
			if ${WORLDRAM[$INDEX]}
			then
				echo "[RAM] ${WORLDNAME[$INDEX]}"
			else
				echo "[DSK] ${WORLDNAME[$INDEX]}"
			fi
		done
		;;
	console)
		# This only works if the terminal invoking this script
		# is logged in as the user $USER_NAME, this is a security
		# measure enforced by screen.
		as_user "screen -r ${SCREEN_NAME}"
		;;
	help)
		echo "Usage: $0 command"
		echo 
		echo "start - Starts the server"
		echo "stop - stops the server gracefully, after warning players"
		echo "stop now - stops the server gracefully, right now!"
		echo "restart - restarts the server gracefully, after warning players"
		echo "restart now - restarts the server gracefully, right now!"
		echo "console - opens the screen process (Press Ctr+A then D to exit)"
		echo "backup - backs up worlds in \"world storage\""
		echo "complete-backup - backups the entire server folder"
		echo "log-roll - Moves and gzips the logfile"
		echo "to-disk - copies any worlds in RAM to disk"
		echo "connected - lists connected users"
		echo "status - Shows server status"
		echo "links - creates nessesary symlinks"
		echo "worlds - shows a list of available worlds"
		echo "ramdisk WORLD - toggles ramdisk configuration for WORLD"
		echo "update - Not yet supported"
		echo "version - Not yet supported"
		echo "last - Not yet supported"
		;;
	*)
		echo "No such command see $0 help"
		exit 1
		;;
esac

exit 0