From 711767fa25f3733b7412ead717f720c03ece2544 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 11:51:19 -0400 Subject: [PATCH] Add per-instance stop.sh so one person's shutdown cannot kill another's game Stopping an instance meant `pkill -f spike-webswing/start.sh`, which matches every instance on the machine - so shutting down a test run also killed a live game someone else was playing. start.sh now publishes results/.pid recording the script PID and the process group ids of its two children (both started with setsid, so PID == PGID and they can be signalled as a group). The file is removed on clean shutdown. stop.sh takes an instance name and signals only those groups. It stops the script first so its EXIT trap does not race, then reaps the children directly in case the script had already died. With no matching PID file it lists the instances that do have one. Verified: with default (8080/8850) and claude (8090/8860) both running, `stop.sh claude` left the default instance serving HTTP 200 and untouched. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 + README.md | 6 +++++ spike-webswing/start.sh | 13 ++++++++++ spike-webswing/stop.sh | 55 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100755 spike-webswing/stop.sh diff --git a/.gitignore b/.gitignore index f6fcba1..20bd53e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ spike-webswing/fontconfig.properties # behind the numbers in README "Findings". results/watch/ results/*.log +results/*.pid diff --git a/README.md b/README.md index fecccea..48f2d23 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ Two people can run side by side by picking different ports and an instance name, which namespaces the generated configs and logs: INSTANCE=claude PORT_WS=8090 PORT_MM=8860 ./spike-webswing/start.sh + ./spike-webswing/stop.sh claude + +**Stop instances with `stop.sh`, never `pkill -f spike-webswing/start.sh`** — that +pattern matches every instance on the box and will kill someone else's live game. +`start.sh` publishes `results/.pid` with the process group ids of its +children, and `stop.sh` signals exactly those. Still shared between instances: MegaMek's `mmconf/` (both seed the same `clientsettings.xml`, so identical content) and `logs/`. diff --git a/spike-webswing/start.sh b/spike-webswing/start.sh index f232508..50e96db 100755 --- a/spike-webswing/start.sh +++ b/spike-webswing/start.sh @@ -29,6 +29,7 @@ INSTANCE="${INSTANCE:-default}" WS_CONFIG="$WS/webswing-$INSTANCE.config" WS_JETTY="$WS/jetty-$INSTANCE.properties" WS_LOG="logs/ws-$INSTANCE.log" +PID_FILE="$ROOT/results/$INSTANCE.pid" RAW_LOG="$ROOT/results/host-$INSTANCE-raw.log" [ -x "$JAVA" ] || { echo "ERROR: JDK missing; run scripts/fetch-deps.sh" >&2; exit 1; } @@ -93,6 +94,7 @@ cleanup() { [ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null || true kill_group "$HOST_PID" kill_group "$WS_PID" # its group includes any MegaMek client it spawned + rm -f "$PID_FILE" echo "done." } trap cleanup INT TERM EXIT @@ -159,5 +161,16 @@ grep --line-buffered -vE "^(ERROR|WARN|INFO) " < <(tail -f -n +1 "$RAW_LOG") \ | tee "$ROOT/results/host-$INSTANCE.log" & TAIL_PID=$! +# Both children were started with setsid, so their PID is also their process +# group id and can be signalled as a group. +cat > "$PID_FILE" <.pid holding the process group ids of its two children, so we +# can signal exactly those. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTANCE="${1:-${INSTANCE:-default}}" +PID_FILE="$ROOT/results/$INSTANCE.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "No PID file for instance '$INSTANCE' ($PID_FILE)." + echo "Running instances:" + shopt -s nullglob + found=0 + for f in "$ROOT"/results/*.pid; do + echo " $(basename "${f%.pid}")" + found=1 + done + [ "$found" = "0" ] && echo " (none)" + exit 1 +fi + +# shellcheck disable=SC1090 +. "$PID_FILE" + +kill_group() { + local pid="${1:-}" what="$2" + [ -n "$pid" ] || return 0 + kill -0 "$pid" 2>/dev/null || return 0 + echo " stopping $what (pgid $pid)" + kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true + for _ in $(seq 20); do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.25; done + kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true +} + +echo "stopping instance '$INSTANCE' (web :${PORT_WS:-?}, game :${PORT_MM:-?})" + +# The script's own EXIT trap would normally do this, but stop it first so it does +# not race us, then clean up the children directly in case it was already gone. +if [ -n "${SCRIPT_PID:-}" ] && kill -0 "$SCRIPT_PID" 2>/dev/null; then + kill -TERM "$SCRIPT_PID" 2>/dev/null || true + for _ in $(seq 20); do kill -0 "$SCRIPT_PID" 2>/dev/null || break; sleep 0.25; done +fi +kill_group "${HOST_PGID:-}" "MegaMek host" +kill_group "${WEBSWING_PGID:-}" "Webswing" + +rm -f "$PID_FILE" +echo "done." -- 2.51.2