242 lines
9.6 KiB
Bash
Executable File
242 lines
9.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# mc-refresh-restart.sh (adjusted for the /minecraft ZFS + HTTP-mirror build)
|
|
#
|
|
# 1. Syncs the packwiz pack against the live mods/ folder on the share
|
|
# (adds new jars, re-pins changed ones, drops orphaned metadata)
|
|
# 2. Refreshes index.toml
|
|
# 3. Takes a ZFS snapshot of the share
|
|
# 4. Restarts the Minecraft server via systemd (minecraft.service)
|
|
# 5. Prunes snapshots to keep only the 8 most recent
|
|
#
|
|
# Requires: passwordless sudo for `systemctl restart minecraft` and `zfs`,
|
|
# or run as root. packwiz must be in PATH.
|
|
|
|
set -euo pipefail
|
|
|
|
### CONFIG ###############################################
|
|
SHARE="/minecraft" # ZFS dataset mountpoint
|
|
PACK_DIR="${SHARE}/packs/mypack" # packwiz project root (has pack.toml)
|
|
MODS_DIR="${SHARE}/mods" # server+client mods, loaded by the server
|
|
CLIENT_MODS_DIR="${SHARE}/client-mods" # client-only mods, never loaded server-side
|
|
BASE_URL="https://games.abdelbaki.eu/mods" # mirror base URL, no trailing slash
|
|
PACK_URL="https://games.abdelbaki.eu/packs/mypack" # where PACK_DIR is served; "" to skip the check
|
|
SERVICE_NAME="minecraft.service"
|
|
START_TIMEOUT=60 # seconds to wait for the service to report active
|
|
KEEP_SNAPSHOTS=8 # how many snapshots to retain
|
|
SNAP_PREFIX="packwiz" # snapshot name prefix
|
|
REMOVE_ORPHANS=1 # drop .pw.toml whose jar is gone
|
|
###########################################################
|
|
|
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
|
|
# Run privileged. Root runs directly; anyone else goes through sudo.
|
|
priv() { if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi; }
|
|
|
|
BASE_URL="${BASE_URL%/}"
|
|
CLIENT_URL="${CLIENT_URL%/}"
|
|
|
|
### 0. Preflight #########################################
|
|
command -v systemctl >/dev/null 2>&1 || die "systemctl not found."
|
|
command -v packwiz >/dev/null 2>&1 || die "packwiz not found in PATH"
|
|
|
|
# An unmounted dataset leaves /minecraft as an empty dir on the root
|
|
# filesystem. Refuse rather than snapshot nothing and archive nothing.
|
|
mountpoint -q "${SHARE}" || die "${SHARE} is not a mountpoint — the ZFS dataset is not mounted.
|
|
Check 'zpool status' and 'zfs mount -a'."
|
|
|
|
[ -f "${PACK_DIR}/pack.toml" ] || die "no pack.toml in ${PACK_DIR}"
|
|
[ -d "${MODS_DIR}" ] || die "mods dir not found: ${MODS_DIR}"
|
|
[ -d "${CLIENT_MODS_DIR}" ] || mkdir -p "${CLIENT_MODS_DIR}"
|
|
|
|
DATASET="$(findmnt -no SOURCE "${SHARE}" 2>/dev/null || true)"
|
|
[ -n "${DATASET}" ] || die "could not determine the dataset backing ${SHARE}"
|
|
|
|
### 1. Sync pack metadata against the live mods folders ###
|
|
# Mods are sourced by URL with hashes pinned in the index, so `packwiz refresh`
|
|
# alone will NOT notice a new or replaced jar — refresh only reindexes existing
|
|
# .pw.toml files. Each jar has to be (re-)registered with `packwiz url add`.
|
|
#
|
|
# Two directories: MODS_DIR is what the server loads and clients also get
|
|
# (side=both); CLIENT_MODS_DIR reaches clients only (side=client). Rendering
|
|
# mods like Sodium link against LWJGL and crash a dedicated server on boot, so
|
|
# they must never sit in MODS_DIR.
|
|
log "Syncing pack metadata..."
|
|
cd "${PACK_DIR}"
|
|
|
|
added=0; updated=0; unchanged=0; FAILED=()
|
|
declare -A SEEN=()
|
|
|
|
# packwiz url add always writes side = "both"; force the right one.
|
|
set_side() {
|
|
local meta="$1" side="$2"
|
|
[ -f "${meta}" ] || return 0
|
|
if grep -q '^side *=' "${meta}"; then
|
|
sed -i "s|^side *=.*|side = \"${side}\"|" "${meta}"
|
|
else
|
|
sed -i "0,/^filename *=/s|^\(filename *=.*\)$|\1\nside = \"${side}\"|" "${meta}"
|
|
fi
|
|
}
|
|
|
|
sync_dir() {
|
|
local dir="$1" url="$2" side="$3" label="$4"
|
|
local jars=() jar file modname meta enc sha256 sha512 action probe
|
|
|
|
shopt -s nullglob
|
|
jars=("${dir}"/*.jar)
|
|
shopt -u nullglob
|
|
|
|
if (( ${#jars[@]} == 0 )); then
|
|
log " no ${label} jars in ${dir}"
|
|
return 0
|
|
fi
|
|
|
|
probe="$(curl -so /dev/null -w '%{http_code}' --max-time 10 "${url}/" || true)"
|
|
probe="${probe:-000}"
|
|
[ "${probe}" = "000" ] && die "cannot reach ${url} — packwiz needs it to hash each mod"
|
|
|
|
log " ${#jars[@]} ${label} jar(s) in ${dir}"
|
|
for jar in "${jars[@]}"; do
|
|
file="$(basename "${jar}")"
|
|
modname="$(printf '%s' "${file%.jar}" | sed -E 's/-[0-9][0-9A-Za-z.+_-]*$//')"
|
|
[ -n "${modname}" ] || modname="${file%.jar}"
|
|
SEEN["${modname}"]=1
|
|
|
|
meta="mods/${modname}.pw.toml"
|
|
enc="$(printf '%s' "${file}" | sed -e 's/%/%25/g' -e 's/ /%20/g' -e 's/+/%2B/g')"
|
|
|
|
# Re-add only when the jar is new or its content changed. packwiz
|
|
# downloads the URL to hash it, so skipping unchanged mods keeps this
|
|
# quick. The side is re-asserted either way, in case it drifted.
|
|
if [ -f "${meta}" ]; then
|
|
sha256="$(sha256sum "${jar}" | cut -d' ' -f1)"
|
|
sha512="$(sha512sum "${jar}" | cut -d' ' -f1)"
|
|
if grep -qF -e "${sha256}" -e "${sha512}" "${meta}"; then
|
|
set_side "${meta}" "${side}"
|
|
(( unchanged++ )) || true
|
|
continue
|
|
fi
|
|
action="updated"
|
|
else
|
|
action="added"
|
|
fi
|
|
|
|
if packwiz url add "${modname}" "${url}/${enc}" >/dev/null 2>&1; then
|
|
set_side "${meta}" "${side}"
|
|
log " ${action}: ${modname} (${side})"
|
|
[ "${action}" = "added" ] && (( added++ )) || (( updated++ ))
|
|
else
|
|
log " FAILED: ${modname}"
|
|
FAILED+=("${file}")
|
|
fi
|
|
done
|
|
}
|
|
|
|
sync_dir "${MODS_DIR}" "${BASE_URL}" both "server+client"
|
|
sync_dir "${CLIENT_MODS_DIR}" "${CLIENT_URL}" client "client-only"
|
|
|
|
### 1b. Drop metadata for jars that are gone ##############
|
|
if (( REMOVE_ORPHANS )); then
|
|
shopt -s nullglob
|
|
for meta in mods/*.pw.toml; do
|
|
modname="$(basename "${meta}" .pw.toml)"
|
|
[ -n "${SEEN[${modname}]:-}" ] && continue
|
|
log " orphaned, removing: ${modname}"
|
|
packwiz remove "${modname}" >/dev/null 2>&1 || rm -f -- "${meta}"
|
|
done
|
|
shopt -u nullglob
|
|
fi
|
|
|
|
log "Sync complete: ${added} added, ${updated} updated, ${unchanged} unchanged."
|
|
|
|
if (( ${#FAILED[@]} > 0 )); then
|
|
log "WARNING: ${#FAILED[@]} mod(s) failed to register:"
|
|
printf ' %s\n' "${FAILED[@]}"
|
|
die "refusing to restart with an incomplete pack — fix these first"
|
|
fi
|
|
|
|
### 2. Refresh packwiz index #############################
|
|
log "Refreshing packwiz index..."
|
|
packwiz refresh
|
|
log "index.toml refreshed."
|
|
|
|
### 2b. Verify clients can actually see the new pack ######
|
|
# Clients pull pack.toml -> index.toml -> mods/*.pw.toml over HTTP. If PACK_DIR
|
|
# isn't served, or a proxy is caching it, the server restarts onto a pack no
|
|
# client can fetch — so compare what the mirror hands back against what's on
|
|
# disk before touching the service.
|
|
PACK_URL="${PACK_URL%/}"
|
|
if [ -n "${PACK_URL}" ]; then
|
|
log "Verifying ${PACK_URL}/pack.toml ..."
|
|
remote="$(curl -fsS --max-time 10 -H 'Cache-Control: no-cache' "${PACK_URL}/pack.toml" 2>/dev/null || true)"
|
|
|
|
if [ -z "${remote}" ]; then
|
|
die "cannot fetch ${PACK_URL}/pack.toml — ${PACK_DIR} is not being served.
|
|
Point the web root at it, or set PACK_URL=\"\" to skip this check."
|
|
fi
|
|
|
|
if [ "$(printf '%s' "${remote}" | sha256sum | cut -d' ' -f1)" \
|
|
= "$(sha256sum pack.toml | cut -d' ' -f1)" ]; then
|
|
log "Mirror is serving the current pack.toml."
|
|
else
|
|
die "${PACK_URL}/pack.toml differs from ${PACK_DIR}/pack.toml.
|
|
Usually a caching proxy. Serve pack.toml and index.toml with
|
|
'Cache-Control: no-cache', or purge the cache, then re-run."
|
|
fi
|
|
|
|
# index.toml is fetched as a separate request and cached separately.
|
|
idx="$(curl -fsS --max-time 10 -o /dev/null -w '%{http_code}' "${PACK_URL}/index.toml" || true)"
|
|
idx="${idx:-000}"
|
|
[ "${idx}" = "200" ] || die "${PACK_URL}/index.toml returned HTTP ${idx} — clients cannot resolve the pack"
|
|
fi
|
|
|
|
### 3. Snapshot the share ################################
|
|
# Taken BEFORE the restart so a server that fails to come up can be rolled
|
|
# back to the exact state that last worked.
|
|
TIMESTAMP="$(date '+%Y%m%d%H%M')"
|
|
SNAPSHOT="${DATASET}@${SNAP_PREFIX}-${TIMESTAMP}"
|
|
log "Snapshotting ${SNAPSHOT}..."
|
|
if priv zfs snapshot "${SNAPSHOT}"; then
|
|
log "Snapshot complete: ${SNAPSHOT}"
|
|
else
|
|
die "zfs snapshot failed"
|
|
fi
|
|
|
|
### 4. Restart the server via systemd #####################
|
|
log "Restarting ${SERVICE_NAME}..."
|
|
priv systemctl restart "${SERVICE_NAME}"
|
|
|
|
waited=0
|
|
until systemctl is-active --quiet "${SERVICE_NAME}" || (( waited >= START_TIMEOUT )); do
|
|
sleep 1
|
|
(( waited++ )) || true
|
|
done
|
|
|
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
|
log "${SERVICE_NAME} is active."
|
|
else
|
|
log "WARNING: ${SERVICE_NAME} did not report active within ${START_TIMEOUT}s."
|
|
log " Check: journalctl -u ${SERVICE_NAME} -n 50"
|
|
log " Roll back: sudo systemctl stop ${SERVICE_NAME} && sudo zfs rollback ${SNAPSHOT}"
|
|
fi
|
|
|
|
### 5. Prune old snapshots, keep newest N #################
|
|
log "Pruning snapshots, keeping newest ${KEEP_SNAPSHOTS}..."
|
|
mapfile -t SNAPSHOTS < <(
|
|
zfs list -H -o name -t snapshot -s creation "${DATASET}" 2>/dev/null \
|
|
| grep -- "@${SNAP_PREFIX}-" || true
|
|
)
|
|
|
|
if (( ${#SNAPSHOTS[@]} > KEEP_SNAPSHOTS )); then
|
|
# Sorted oldest-first by creation, so the excess is at the front.
|
|
for old in "${SNAPSHOTS[@]:0:$(( ${#SNAPSHOTS[@]} - KEEP_SNAPSHOTS ))}"; do
|
|
log "Destroying old snapshot: ${old}"
|
|
priv zfs destroy -- "${old}"
|
|
done
|
|
else
|
|
log "Nothing to prune (${#SNAPSHOTS[@]}/${KEEP_SNAPSHOTS})."
|
|
fi
|
|
|
|
log "Done."
|