Something went wrong. Try again.
Backend environment for match hosting for lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227#!/usr/bin/env bash# The idle timeout is a script that kills a match, so the interesting cases are# the ones where it must not: a manifest that asks for no timeout, a game that# keeps showing signs of play, and a game that is over. Each is covered here,# along with the ones where it must - a lobby nobody joins and a game that# stalls mid-round.## How it shuts down matters as much as whether. idle.sh signals the supervisor# rather than the host, because the entrypoint's TERM trap finalizes - uploading# the result, the screenshots and the diagnostics - and a hard kill would throw# all of that away silently. The stub supervisor below has that trap shape and# nothing else, so a marker file written from its trap is the proof that what# arrived was a signal a trap can catch.set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"TMP="$(mktemp -d)"trap 'rm -rf "$TMP"' EXIT
fail=0check() { # <description> <test-command...> if "${@:2}"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi}
# The image collects container/ under ARENA_HOME; idle.sh sources lib/common.sh# from there.mkdir -p "$TMP/home" "$TMP/run/state"ln -s "$ROOT/container" "$TMP/home/container"STATE="$TMP/run/state"MARK="$TMP/finalized"
# Each case starts from a container that has just come up.reset_state() { rm -f "$STATE/host.status" "$STATE/host.activity" "$STATE/game-over"; }
# --- a stand-in for entrypoint.sh ---------------------------------------------# The real one wants a JRE, Suramadu and the MegaMek tree, none of which exist# outside the image. What it contributes to this test is one line - `trap# shutdown INT TERM`, where shutdown finalizes - and that is what this is.cat > "$TMP/supervisor.sh" <<'SUPERVISOR'finalize() { echo finalized > "$MARK"; }trap 'finalize; exit 143' TERMwhile true; do sleep 0.1; doneSUPERVISOR
SUP=""start_supervisor() { rm -f "$MARK" MARK="$MARK" bash "$TMP/supervisor.sh" & SUP=$!}stop_supervisor() { [ -n "$SUP" ] && kill -KILL "$SUP" 2>/dev/null wait "$SUP" 2>/dev/null SUP=""}
# The trap runs after idle.sh has already exited, so both of these wait.finalized() { for _ in $(seq 30); do [ -f "$MARK" ] && return 0; sleep 0.1; done return 1}supervisor_gone() { for _ in $(seq 30); do kill -0 "$SUP" 2>/dev/null || return 0; sleep 0.1; done return 1}untouched() { # nothing was signalled: still running, never finalized sleep 0.4 kill -0 "$SUP" 2>/dev/null && [ ! -f "$MARK" ]}
manifest() { # <limits-object-or-'null'> [callback-url] jq -n --argjson limits "$1" --arg cb "${2:-}" \ '{matchId: "test-match"} + (if $limits == null then {} else {limits: $limits} end) + (if $cb == "" then {} else {callback: {url: $cb}} end)' \ > "$TMP/run/manifest.json"}
# 1s rather than the manifest's 3600, and a fast poll, so the whole file runs in# a couple of seconds.run_idle() { ARENA_HOME="$TMP/home" ARENA_RUN="$TMP/run" \ ARENA_SUPERVISOR_PID="$SUP" ARENA_IDLE_POLL=0.2 \ bash "$ROOT/container/watch/idle.sh" 2>&1}
# --- nobody joins -------------------------------------------------------------# host.status stays at what MatchHost writes when it is waiting for its humans,# and host.activity stays at the one line the host wrote coming up.reset_stateecho 'listening' > "$STATE/host.status"echo "$(date +%s) round 0 LOUNGE turn -1 joined []" > "$STATE/host.activity"manifest '{"idleTimeoutSeconds": 1}'start_supervisorout="$(run_idle)"; status=$?
check "an unjoined lobby times out" grep -q 'nobody joined within 1s' <<<"$out"check "and idle.sh itself exits clean" test "$status" -eq 0check "and the shutdown finalizes" finalizedcheck "and the supervisor is gone" supervisor_gonestop_supervisor
# --- the manifest asks for no timeout -----------------------------------------# Two spellings of "no timeout", because the difference between them is a jq# default and it would be easy to honour only one.for limits in 'null' '{"idleTimeoutSeconds": 0}' '{"lingerSeconds": 300}'; do manifest "$limits" start_supervisor out="$(run_idle)"; status=$? check "no timeout for limits=$limits" grep -q 'no idle timeout' <<<"$out" check " exits clean" test "$status" -eq 0 check " and signals nothing" untouched stop_supervisordone
# A value that is not a number is a manifest bug, and shooting a match down on a# guess is worse than not having the timeout.manifest '{"idleTimeoutSeconds": "soon"}'start_supervisorout="$(run_idle)"check "a non-numeric value stands down" grep -q 'not a whole number' <<<"$out"check " and signals nothing" untouchedstop_supervisor
# --- the game stalled ---------------------------------------------------------# Joined, started, and then nothing: host.activity stops moving mid-round. The# old watcher disarmed for good on the first join, and this bill is what that# reading missed.reset_stateecho 'round 3 MOVEMENT' > "$STATE/host.status"echo "$(date +%s) round 3 MOVEMENT turn 4 joined [alice]" > "$STATE/host.activity"manifest '{"idleTimeoutSeconds": 1}'start_supervisorout="$(run_idle)"; status=$?check "a stalled game times out" grep -q 'no sign of play' <<<"$out"check " naming where it stalled" grep -q 'round 3 MOVEMENT' <<<"$out"check " exits clean" test "$status" -eq 0check " and the shutdown finalizes" finalizedstop_supervisor
# A host that never wrote host.activity - dead before its first tick, or an# image mixing an old host with this watcher - is held to the same ceiling,# counted from arming. The other reading, exempt forever, is the 83-minute# container again.reset_stateecho 'lobby-ready' > "$STATE/host.status"manifest '{"idleTimeoutSeconds": 1}'start_supervisorout="$(run_idle)"check "no activity file still times out" grep -q 'no sign of play' <<<"$out"check " and finalizes" finalizedstop_supervisor
# --- the game is being played -------------------------------------------------# host.activity refreshed faster than the timeout holds the watcher off for as# long as the signs keep coming; game-over then stands it down. The refresher# here is MatchHost on a good day. 2s rather than 1: the stamps are whole# seconds, so a fresh write can read as a full second stale at a 1s ceiling -# real ceilings are hours, where the truncation is noise.reset_stateecho 'round 3 MOVEMENT' > "$STATE/host.status"manifest '{"idleTimeoutSeconds": 2}'start_supervisor( for turn in $(seq 8); do echo "$(date +%s) round 3 MOVEMENT turn $turn joined [alice]" > "$STATE/host.activity" sleep 0.25 done : > "$STATE/game-over") &REFRESHER=$!out="$(run_idle)"; status=$?wait "$REFRESHER"check "a played game never fires" grep -q 'idle timeout disarmed' <<<"$out"check " exits clean" test "$status" -eq 0check " and signals nothing" untouchedstop_supervisor
# --- the game is over ---------------------------------------------------------# Both spellings the host leaves behind: the status line and the marker file.# The host is deliberately lingering then, and must be left alone.manifest '{"idleTimeoutSeconds": 1}'reset_stateecho 'game-over' > "$STATE/host.status"start_supervisorout="$(run_idle)"; status=$?check "disarmed by host.status 'game-over'" grep -q 'idle timeout disarmed' <<<"$out"check " exits clean" test "$status" -eq 0check " and signals nothing" untouchedstop_supervisor
reset_stateecho 'listening' > "$STATE/host.status": > "$STATE/game-over"start_supervisorout="$(run_idle)"check "disarmed by the game-over marker" grep -q 'idle timeout disarmed' <<<"$out"check " and signals nothing" untouchedstop_supervisor
# --- the callback -------------------------------------------------------------# Port 1 refuses at once, so `emit` fails and names the event it was sending in# the warning. That is the assertion: headquarters has to be able to tell an# idle timeout from a crash, and it can only do that by the event name.reset_stateecho 'listening' > "$STATE/host.status"manifest '{"idleTimeoutSeconds": 1}' 'http://127.0.0.1:1/events'start_supervisorout="$(run_idle)"
check "the event is idle-timeout" grep -q "callback 'idle-timeout' failed" <<<"$out"check "an unreachable callback still shuts down" finalizedstop_supervisor
# --- wiring -------------------------------------------------------------------# The half of this that cannot run outside the image: that the entrypoint starts# the watcher, tells it which process to signal, and traps that signal into the# path that finalizes.ENTRY="$ROOT/container/entrypoint.sh"check "the entrypoint starts the watcher" grep -q 'watch/idle.sh' "$ENTRY"check "and exports its own pid" grep -q 'ARENA_SUPERVISOR_PID=\$\$' "$ENTRY"check "and traps the signal it will send" grep -qE '^trap shutdown( .*)? TERM' "$ENTRY"check "and that trap finalizes" bash -c \ "sed -n '/^shutdown()/,/^}/p' \"\$1\" | grep -q finalize_once" _ "$ENTRY"
exit "$fail"