365 lines
7.6 KiB
Bash
365 lines
7.6 KiB
Bash
|
#!/usr/bin/env bash
|
||
|
set -euo pipefail
|
||
|
|
||
|
|
||
|
## CONSTANTS ##
|
||
|
readonly DATA_DIR="/opt/spigot-data"
|
||
|
readonly SERVER_JAR_PATH="${DATA_DIR}/server.jar"
|
||
|
readonly BUILD_META_DIR="${DATA_DIR}/.build-meta"
|
||
|
readonly BUILD_DIR="/opt/spigotbuild"
|
||
|
readonly SERVER_FD_DIR="/opt/spigot-fd"
|
||
|
|
||
|
|
||
|
## PUBLIC VARIABLES ##
|
||
|
declare spigot_pid
|
||
|
declare proc_pid
|
||
|
|
||
|
|
||
|
## HANDLE EXIT SIGNALS ##
|
||
|
trap trap_exit EXIT SIGINT SIGTERM
|
||
|
trap_exit(){
|
||
|
# try stopping server first
|
||
|
if [[ "${spigot_pid:+x}" && "$spigot_pid" -gt 0 ]]; then
|
||
|
cmd "stop"
|
||
|
local w=0
|
||
|
while proc_running "$spigot_pid"; do
|
||
|
w=$(( w + 1 ))
|
||
|
snore 1
|
||
|
if [[ "$w" -ge 30 ]]; then
|
||
|
echo "[WARN ] Spigot did not react to stop command after 30s; using normal process shutdown actions" 1>&2
|
||
|
proc_pid="$spigot_pid"
|
||
|
break
|
||
|
fi
|
||
|
done
|
||
|
fi
|
||
|
|
||
|
# normal process
|
||
|
if [[ "${proc_pid:+x}" && "$proc_pid" -gt 0 ]]; then
|
||
|
kill -- $proc_pid 2> /dev/null || true
|
||
|
local q=0
|
||
|
while proc_running "$proc_pid"; do
|
||
|
q=$(( q + 1 ))
|
||
|
snore 1
|
||
|
if [[ "$q" -ge 15 ]]; then
|
||
|
echo "[WARN ] Sending kill to running process $proc_pid" 1>&2
|
||
|
kill -s KILL -- $proc_pid 2> /dev/null || true
|
||
|
break
|
||
|
fi
|
||
|
done
|
||
|
fi
|
||
|
exit 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# LIB: Efficient sleep (does not create a new process).
|
||
|
#
|
||
|
snore(){
|
||
|
local IFS
|
||
|
[[ -n "${_snore_fd:-}" ]] || exec {_snore_fd}<> <(:)
|
||
|
read ${1:+-t "$1"} -u $_snore_fd || true
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Check whether given pid is running.
|
||
|
#
|
||
|
# @param $1 Process ID.
|
||
|
#
|
||
|
# @exit 0: Process is running
|
||
|
# 1: Process is not running.
|
||
|
#
|
||
|
proc_running(){
|
||
|
# try reading state
|
||
|
local state_path="/proc/$1/stat"
|
||
|
[[ ! -f "$state_path" ]] && return 1
|
||
|
local state
|
||
|
state=$(cat "$state_path" 2> /dev/null | cut -d ' ' -f3) || return 1
|
||
|
|
||
|
# parse state
|
||
|
case "$state" in
|
||
|
R|S|D|Z|W|W|P|I) return 0;;
|
||
|
*) return 1;;
|
||
|
esac
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Check whether spigot can be built for a given MC Version.
|
||
|
#
|
||
|
# @param $1 MC Version.
|
||
|
#
|
||
|
# @exit 0: Valid MC Version for a spigot build
|
||
|
# 1: Invalid MC Version for a spigot build.
|
||
|
#
|
||
|
mc_version_is_valid(){
|
||
|
local version="$1"
|
||
|
|
||
|
## CHECK FORMAT ##
|
||
|
if [[ ! "$version" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||
|
return 1
|
||
|
fi
|
||
|
|
||
|
|
||
|
## CHECK AVAILABILITY ##
|
||
|
local version_refs
|
||
|
version_refs=$(spigot_version_refs "$version") || return 1
|
||
|
|
||
|
|
||
|
## RETURN ##
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Request spigot version refs from server.
|
||
|
#
|
||
|
# @param $1 MC Version.
|
||
|
#
|
||
|
# @return Refs JSON.
|
||
|
# @exit 0: Refs lookup successful
|
||
|
# 1: Refs lookup failed.
|
||
|
#
|
||
|
spigot_version_refs(){
|
||
|
## FETCH DATA ##
|
||
|
# make request
|
||
|
local api_response
|
||
|
local curl_exit=0
|
||
|
api_response=$(curl -fsm 10 "https://hub.spigotmc.org/versions/${1}.json") || curl_exit="$?"
|
||
|
if [[ "$curl_exit" -gt 0 ]]; then
|
||
|
echo "[ERROR] Failed to fetch refs" 1>&2
|
||
|
return 1
|
||
|
fi
|
||
|
|
||
|
# validate returned data
|
||
|
local refs
|
||
|
local jq_exit=0
|
||
|
refs=$(echo -n "$api_response" | jq -r ".refs" 2> /dev/null) || jq_exit="$?"
|
||
|
if [[ "$jq_exit" -gt 0 ]]; then
|
||
|
echo "[ERROR] Failed to parse refs api response" 1>&2
|
||
|
return 1
|
||
|
fi
|
||
|
|
||
|
|
||
|
## RETURN ##
|
||
|
echo -n "$refs"
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: (Re-)build server jar if required.
|
||
|
#
|
||
|
build_check(){
|
||
|
## CHECK FOR MISSING BUILD ARTIFACTS ##
|
||
|
for f in $SERVER_JAR_PATH ${BUILD_META_DIR}/{mc_version,spigot,craftbukkit}; do
|
||
|
if [[ ! -f "$f" ]]; then
|
||
|
echo "[WARN ] Missing build artifact detected: '$f'" 1>&2
|
||
|
build_run; return 0
|
||
|
fi
|
||
|
done
|
||
|
|
||
|
|
||
|
## CHECK WHETHER MC VERSION WAS CHANGED ##
|
||
|
local mc_version_curr=$(cat "${BUILD_META_DIR}/mc_version") || exit 1
|
||
|
if [[ "$MC_VERSION" != "$mc_version_curr" ]]; then
|
||
|
echo "[INFO ] MC Version changed" 1>&2
|
||
|
build_run; return 0
|
||
|
fi
|
||
|
|
||
|
|
||
|
## IGNORE REST IF AUTO UPDATE IS OFF ##
|
||
|
[[ "$AUTO_UPDATE" -gt 0 ]] && return 0
|
||
|
|
||
|
|
||
|
## CHECK FOR REFS CHANGE ##
|
||
|
# get all refs
|
||
|
local version_refs
|
||
|
version_refs=$(spigot_version_refs "$MC_VERSION") || exit 1
|
||
|
|
||
|
# compare
|
||
|
for r in CraftBukkit Spigot; do
|
||
|
# parse remote ref
|
||
|
local ref_remote
|
||
|
local jq_exit=0
|
||
|
ref_remote=$(echo -n "$version_refs" | jq -r ".${r}" 2> /dev/null) || jq_exit="$?"
|
||
|
if [[ "$jq_exit" -gt 0 || ! "$ref_remote" =~ ^[0123456789abcdef]{40}$ ]]; then
|
||
|
echo "[ERROR] Failed to parse remote ref '$r': $ref_remote" 1>&2
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
# load local ref
|
||
|
local ref_local_path="${BUILD_META_DIR}/$(echo -n "$r" | tr A-Z a-z)"
|
||
|
local ref_local
|
||
|
ref_local=$(cat "$ref_local_path")
|
||
|
|
||
|
# compare
|
||
|
if [[ "$ref_local" != "$ref_remote" ]]; then
|
||
|
echo "[INFO ] Remote ref '$r' has changed: '$ref_remote'" 1>&2
|
||
|
build_run; return 0
|
||
|
fi
|
||
|
done
|
||
|
|
||
|
|
||
|
## NO BUILD REQUIRED ##
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Run a server build.
|
||
|
#
|
||
|
build_run(){
|
||
|
echo "[INFO ] (Re-)building server" 1>&2
|
||
|
|
||
|
## CLEAN UP BUILD AREA ##
|
||
|
rm -rf "$BUILD_DIR"
|
||
|
mkdir -p "$BUILD_DIR"
|
||
|
pushd "$BUILD_DIR" > /dev/null
|
||
|
|
||
|
|
||
|
## BUILD ##
|
||
|
# download build tools
|
||
|
wget -O BuildTools.jar https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar & proc_pid="$!"
|
||
|
wait $proc_pid; proc_pid="0"
|
||
|
|
||
|
# run build tools
|
||
|
java -jar BuildTools.jar --rev "$MC_VERSION" --final-name "server.jar" & proc_pid="$!"
|
||
|
wait $proc_pid; proc_pid="0"
|
||
|
|
||
|
# verify
|
||
|
if [[ ! -f "server.jar" ]]; then
|
||
|
echo "[ERROR] Build did not produce a server jar" 1>&2
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
|
||
|
## STORE BUILD METADATA ##
|
||
|
# mc version
|
||
|
echo -n "$MC_VERSION" > "${BUILD_META_DIR}/mc_version"
|
||
|
|
||
|
# git rev hashes
|
||
|
for d in CraftBukkit Spigot; do
|
||
|
pushd "$d" > /dev/null
|
||
|
local git_rev
|
||
|
git_rev=$(git rev-parse HEAD)
|
||
|
echo -n "$git_rev" > "${BUILD_META_DIR}/$(echo -n "$d" | tr A-Z a-z)"
|
||
|
popd > /dev/null
|
||
|
done
|
||
|
|
||
|
|
||
|
## FINISH ##
|
||
|
# return to previous working directory
|
||
|
popd > /dev/null
|
||
|
|
||
|
# switch to new server jar
|
||
|
build_switch
|
||
|
|
||
|
# print info
|
||
|
echo "[INFO ] (Re-)build successful" 1>&2
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Switch spigot jar to build output.
|
||
|
#
|
||
|
build_switch(){
|
||
|
## MAYBE SET ASIDE OLD JAR ##
|
||
|
[[ -f "$SERVER_JAR_PATH" ]] && mv "$SERVER_JAR_PATH" "${SERVER_JAR_PATH}.old"
|
||
|
|
||
|
|
||
|
if ! mv "${BUILD_DIR}/server.jar" "$SERVER_JAR_PATH"; then
|
||
|
echo "[ERROR] Failed to switch to new server jar" 1>&2
|
||
|
[[ -f "${SERVER_JAR_PATH}.old" ]] && echo "[INFO ] Old jar file retained as '${SERVER_JAR_PATH}.old'" 1>&2
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
|
||
|
## MAYBE DELETE OLD JAR ##
|
||
|
[[ -f "${SERVER_JAR_PATH}.old" ]] && rm "${SERVER_JAR_PATH}.old"
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#
|
||
|
# HELPER: Initialize spigot server files.
|
||
|
#
|
||
|
spigot_init(){
|
||
|
## MAYBE WRITE EULA ##
|
||
|
if [[ ! -f "${DATA_DIR}/eula.txt" ]]; then
|
||
|
echo "eula=true" > "${DATA_DIR}/eula.txt"
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
## VALIDATE ENV ##
|
||
|
# eula
|
||
|
if [[ ! "${AGREE_MINECRAFT_EULA:-}" =~ ^(on|yes|true|1)$ ]]; then
|
||
|
echo "[ERROR] You must agree to the Minecraft EULA (https://aka.ms/MinecraftEULA)" 1>&2
|
||
|
echo "[INFO ] To agree, set AGREE_MINECRAFT_EULA environment variable to 'yes'" 1>&2
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
# mc version
|
||
|
if [[ -z "${MC_VERSION:+x}" ]]; then
|
||
|
MC_VERSION="latest"
|
||
|
echo "[INFO ] Using default mc version: '$MC_VERSION'" 1>&2
|
||
|
fi
|
||
|
if ! mc_version_is_valid "$MC_VERSION"; then
|
||
|
echo "[ERROR] Not a valid MC Version for a spigot build (yet): '$MC_VERSION'" 1>&2
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
# auto update
|
||
|
if [[ -z "${AUTO_UPDATE:+x}" ]]; then
|
||
|
AUTO_UPDATE="yes"
|
||
|
echo "[INFO ] Auto update enabled by default" 1>&2
|
||
|
fi
|
||
|
case "$AUTO_UPDATE" in
|
||
|
"on"|"yes"|"true"|1) AUTO_UPDATE="0";;
|
||
|
"off"|"no"|"false"|0) AUTO_UPDATE="1";;
|
||
|
|
||
|
*)
|
||
|
echo "[ERROR] Invalid option for auto update: '$AUTO_UPDATE'" 1>&2
|
||
|
exit 1
|
||
|
;;
|
||
|
esac
|
||
|
|
||
|
|
||
|
## SET UP USED DIRECTORIES ##
|
||
|
mkdir -p "$DATA_DIR"
|
||
|
mkdir -p "$BUILD_META_DIR"
|
||
|
mkdir -p "$SERVER_FD_DIR"
|
||
|
|
||
|
|
||
|
## MAYBE (RE-)BUILD SERVER JAR ##
|
||
|
build_check
|
||
|
|
||
|
|
||
|
## MAYBE INITIALIZE SERVER FILES ##
|
||
|
spigot_init
|
||
|
|
||
|
|
||
|
## RUN SERVER JAR ##
|
||
|
# prepare fds
|
||
|
echo -n "" > "${SERVER_FD_DIR}/stdin"
|
||
|
|
||
|
# switch into data directory
|
||
|
pushd "$DATA_DIR" > /dev/null
|
||
|
|
||
|
# start server process and stream its output
|
||
|
java -jar server.jar < <(tail -fn 0 "${SERVER_FD_DIR}/stdin") | while IFS= read -r line; do
|
||
|
echo "[LOG ] $line" 1>&2
|
||
|
done & spigot_pid="$!"
|
||
|
wait $spigot_pid
|