Something went wrong. Try again.
Backend environment for match hosting for lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399#!/usr/bin/env bash# PID 1 for a match container.## init/*.sh in lexical order - manifest, identity, assets, config# suramadu Suramadu, serving the real MegaMek Swing client at :8080# host arena.MatchHost - the MegaMek server, Princess, one game# watcher arena.MatchWatcher - observer client spooling turn reports# watch/ uploads what the watcher spools, while the match is still running,# and shuts an unjoined lobby down before it bills a full lifetime# exit/ runs once the host is finished, whether by victory or by signal## The host is the process whose exit decides the container's. Suramadu and# the watcher are support: if either dies the match is degraded, not over, and# taking the container down would cost players a game in progress.set -euo pipefail# shellcheck disable=SC2034 # read by common.sh's log helpersARENA_LOG_TAG=entrypoint# shellcheck source=lib/common.sh. "${ARENA_HOME:-/opt/arena}/container/lib/common.sh"# common.sh defaulted ARENA_HOME; export it so child scripts see the same root.export ARENA_HOME
MM_HOME="$ARENA_HOME/megamek"JAVA="$ARENA_HOME/jre/bin/java"WS_HOME="$ARENA_HOME/suramadu"
# java.util.prefs writes under user.home, which the JVM reads from the passwd# entry - not from $HOME, which it ignores on Linux. The image therefore gives# the arena user a home on the tmpfs (see the Dockerfile); this creates it.## Without it every JVM logs "Couldn't create user preferences directory" twice# at startup, including the Swing client, which Suramadu spawns and which# would not see a -D we put on our own command lines.export HOME="$ARENA_RUN/home"mkdir -p "$HOME"
WS_PID=""HOST_PID=""WATCHER_PID=""RESULTS_PID=""IDLE_PID=""FINALIZED=0
# --- phase timing ------------------------------------------------------------# Milliseconds since this script started, logged at each boot phase and sent# with the readiness callback.## This is the only place the boot phases can be told apart. ECS reports when it# pulled the image and when it started the container, and headquarters sees the# callbacks - but nothing between those two says where the seconds went, which# is why "the pull is ~16s" is the only start-time number we have ever had.T0_MS="$(date +%s%3N)"elapsed_ms() { echo $(( $(date +%s%3N) - T0_MS )); }phase() { log "phase=$1 at=+$(elapsed_ms)ms"; }
# There is no ss or netstat in a slim image, so readiness is read from# /proc/net/tcp, whose columns are: sl local_address rem_address st ...## Both the column and the state matter. Grepping the whole line also matches the# *remote* address, so an outbound connection to the same port would look like a# server; and without st=0A a socket in any state at all counts as listening.port_listening() { local hex hex="$(printf '%04X' "$1")" awk -v want=":$hex" '$2 ~ (want "$") && $4 == "0A" { found = 1 } END { exit !found }' /proc/net/tcp /proc/net/tcp6 2>/dev/null}
# Both JVMs are running by the time either is waited on, so a startup failure# has to take the other down with it rather than leave it orphaned under a PID 1# that is about to exit.## The callback is what stops the waiting screen sitting through the whole# provisioning timeout: headquarters otherwise learns nothing until it notices# the task has stopped, which is several polls later.startup_failed() { emit error "$1" for pid in "$HOST_PID" "$WS_PID"; do [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true done die "$1"}
# await_port <what> <port> <pid> <tries> [logfile]# Polls twice a second. A process that dies while being waited on fails# immediately rather than burning the whole timeout.await_port() { local what="$1" port="$2" pid="$3" tries="$4" logfile="${5:-}" local _ for _ in $(seq "$tries"); do if ! kill -0 "$pid" 2>/dev/null; then [ -n "$logfile" ] && { tail -40 "$logfile" >&2 || true; } startup_failed "$what exited during startup" fi port_listening "$port" && return 0 sleep 0.5 done # Falling through to "it is listening" would report a server that never came # up as ready, and leave the failure to show up as an unexplained connection # refused in someone's browser. [ -n "$logfile" ] && { tail -40 "$logfile" >&2 || true; } startup_failed "$what did not open :$port within $((tries / 2))s"}
# Fargate sends SIGTERM and then waits. That window is the only chance to upload# a result, so finalize runs on the signal path too, not just on clean victory.finalize_once() { [ "$FINALIZED" = "1" ] && return 0 FINALIZED=1 log "finalizing ..." "$ARENA_HOME/container/exit/finalize.sh" || warn "finalize reported errors"}
# Whether this match is being profiled. Off unless the task sets ARENA_PROFILE# to something other than 0; see the agent block under "match host".profiling() { [ -n "${ARENA_PROFILE:-}" ] && [ "$ARENA_PROFILE" != "0" ]; }
# async-profiler writes its output when the JVM exits, and finalize collects it,# so on a profiled run the JVMs have to be stopped before finalize rather than# after. Suramadu is signalled too: it owns the client JVM and terminates it# with SIGTERM, which is what lets that JVM's agent flush as well.## Only for profiled runs. An ordinary match keeps finalize first, because a# SIGTERM window is the one chance to upload a result and no diagnostic is worth# spending it on.flush_profiles() { log "stopping the JVMs so their profiles flush" for pid in "$HOST_PID" "$WS_PID"; do [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true done for _ in $(seq 40); do kill -0 "$HOST_PID" 2>/dev/null || break sleep 0.25 done # The client JVM is a grandchild, so there is nothing to wait on; give it the # same 10s ceiling and stop as soon as the file lands. for _ in $(seq 40); do [ -s "$ARENA_STATE/profile-client.collapsed" ] && break sleep 0.25 done}
shutdown() { log "signal received; shutting down" if profiling; then flush_profiles; fi finalize_once for pid in "$IDLE_PID" "$RESULTS_PID" "$WATCHER_PID" "$HOST_PID" "$WS_PID"; do [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true done # Give the JVMs a moment to close their sockets before the container dies. for _ in $(seq 40); do kill -0 "$HOST_PID" 2>/dev/null || break sleep 0.25 done exit 143}trap shutdown INT TERM
# --- init -------------------------------------------------------------------for step in "$ARENA_HOME"/container/init/*.sh; do log "running $(basename "$step")" ARENA_HOME="$ARENA_HOME" "$step"donephase init-done
# shellcheck source=/dev/null. "$ARENA_RUN/players.env"# shellcheck source=/dev/null. "$ARENA_RUN/scenario.env"
LINGER="$(mq '.limits.lingerSeconds' 300)"AUTO_READY="$(mq '.limits.autoReadySeconds' 0)"OBSERVER_ENABLED="$(mq '.observer.enabled' true)"
# --- Suramadu ---------------------------------------------------------------# Launched first because it is the slower of the two to open its port, but only# launched here - the wait for it moved below the host, so the two JVMs warm up# at the same time instead of end to end. Neither needs the other to start: the# host is a MegaMek server, and Suramadu only dials it once a browser arrives.## The flag list is the one Suramadu's toolkit requires to reach into# java.desktop internals; it is not tunable.log "starting Suramadu on :$PORT_WS"cd "$WS_HOME"env -u DISPLAY -u XAUTHORITY "$JAVA" -server -Xmx"${ARENA_WS_HEAP:-1G}" -Xms256m \ --add-modules=java.desktop \ --add-exports=java.desktop/sun.awt=ALL-UNNAMED \ --add-exports=java.desktop/sun.awt.dnd=ALL-UNNAMED \ --add-exports=java.desktop/sun.awt.datatransfer=ALL-UNNAMED \ --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED \ --add-exports=java.desktop/sun.java2d=ALL-UNNAMED \ --add-exports=java.desktop/sun.java2d.pipe=ALL-UNNAMED \ --add-exports=java.desktop/sun.java2d.loops=ALL-UNNAMED \ --add-exports=java.desktop/sun.font=ALL-UNNAMED \ --add-exports=java.desktop/sun.print=ALL-UNNAMED \ --add-exports=java.desktop/java.awt.peer=ALL-UNNAMED \ --add-exports=java.desktop/java.awt.dnd=ALL-UNNAMED \ --add-exports=java.base/sun.nio.cs=ALL-UNNAMED \ --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED \ -Dwebswing.websocketMessageSizeLimit=1024000 \ -Dlog4j2.configurationFile="$ARENA_HOME/config/log4j2-server.xml" \ -jar "$WS_HOME/$ARENA_SURAMADU_WAR" \ -h 0.0.0.0 \ -j "$ARENA_RUN/suramadu/jetty.properties" \ -c "$ARENA_RUN/suramadu/suramadu.config" \ -pf "$WS_HOME/suramadu.properties" \ > "$ARENA_RUN/suramadu.log" 2>&1 &WS_PID=$!
# Mirror Suramadu's log onto stdout so the task's log stream carries every# process - session lifecycle (and disconnect reasons) live only here. The# file stays authoritative: startup failure still tails it directly.tail -n +1 -F "$ARENA_RUN/suramadu.log" 2>/dev/null | sed -u 's/^/[suramadu] /' &phase suramadu-launched
# --- match host -------------------------------------------------------------CP="$ARENA_HOME/arena.jar:$ARENA_HOME/MegaMek-patched.jar"for jar in "$MM_HOME"/lib/*.jar; do CP="$CP:$jar"; done
# Written by 30-assets.sh, and absent when nothing was staged - a manifest with# no camo, or every camo having failed its digest. The host takes no flag then# and every slot keeps the colours MegaMek assigns it.CAMO_ARGS=()if [ -s "$ARENA_RUN/camo/index.tsv" ]; then CAMO_ARGS=(--camo-index "$ARENA_RUN/camo/index.tsv")fi
# Written by 20-identity.sh, and empty when no slot in the manifest carries a# team. The host takes no flag then and the scenario's own `team_` lines stand.TEAM_ARGS=()if [ -s "$ARENA_RUN/teams.tsv" ]; then TEAM_ARGS=(--teams "$ARENA_RUN/teams.tsv")fi
# --- the profiling agent, if this match is being profiled ---------------------# Empty unless ARENA_PROFILE is set to something other than 0, so an unprofiled# match runs the command line it ran before this existed. The client JVM gets# the same treatment through vmArgs; see container/init/40-render-config.sh.## ctimer, not cpu: `cpu` means perf_events, and Docker's default seccomp profile# blocks perf_event_open while Fargate gives no way to relax it. 4.x does fall# back to ctimer on its own, but a profiler that silently degrades is not what# you want to discover after the match. ctimer is timer_create(CLOCK_THREAD_# CPUTIME_ID), needs no privileges and no file descriptor per thread; the price# is no kernel stacks, which we could not have had here anyway.## CPU rather than wall clock, because the question this JVM answers is whether# Princess's pathfinding takes cores away from rendering on a 2-vCPU task. Wall# clock here would mostly report threads sitting in accept().PROFILE_ARGS=()if profiling; then AP_SO="$ARENA_HOME/async-profiler/lib/libasyncProfiler.so" AP_EVENT="${ARENA_PROFILE_HOST_EVENT:-ctimer}" AP_INTERVAL="${ARENA_PROFILE_INTERVAL:-10ms}" AP_OUT="$ARENA_STATE/profile-host.collapsed" [ -r "$AP_SO" ] || die "ARENA_PROFILE is set but $AP_SO is missing" PROFILE_ARGS=( -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints "-agentpath:$AP_SO=start,event=$AP_EVENT,interval=$AP_INTERVAL,memlimit=128m,collapsed,file=$AP_OUT" ) log "profiling the match host: event=$AP_EVENT interval=$AP_INTERVAL -> $AP_OUT"fi
# cwd must be MegaMek's install dir: it resolves data/ relative to it.cd "$MM_HOME"log "starting match host on :$PORT_MM (humans: $ARENA_HUMAN_SLOTS)"# A ctimer profile of this JVM over a whole 4v4 put 52.9% of its samples in the# JIT compiler and 10.6% in class loading - it spends more CPU compiling# MegaMek than doing anything else, on a task that has two vCPUs for Princess# and the frame path as well. ARENA_HOST_JIT_ARGS is how that gets experimented# with; empty is stock, and `-XX:TieredStopAtLevel=1` is the interesting end.read -r -a HOST_JIT_ARGS <<< "${ARENA_HOST_JIT_ARGS:-}""$JAVA" -Xmx"${ARENA_HOST_HEAP:-1g}" \ ${HOST_JIT_ARGS[@]+"${HOST_JIT_ARGS[@]}"} \ "${PROFILE_ARGS[@]}" \ -Dlog4j2.configurationFile="$ARENA_HOME/config/log4j2-quiet.xml" \ -Dsentry.dsn= \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ -cp "$CP" \ arena.MatchHost \ --scenario "$ARENA_SCENARIO" \ --scenario-source "${ARENA_SCENARIO_SOURCE:-}" \ --scenario-library "${ARENA_SCENARIO_LIBRARY:-}" \ --port "$PORT_MM" \ --humans "$ARENA_HUMAN_SLOTS" \ --names "$ARENA_RUN/names.tsv" \ --state-dir "$ARENA_STATE" \ --linger "$LINGER" \ --auto-ready "$AUTO_READY" \ "${CAMO_ARGS[@]}" \ "${TEAM_ARGS[@]}" &HOST_PID=$!phase host-launched
emit running "host started"
# --- observer + uploader ----------------------------------------------------# Beside the host, not after readiness. This is a full MegaMek client JVM and# its cold start is not free on two cores. Started after `ready` it competed# with the thing a player is actually watching: headquarters redirects on that# callback, Suramadu spawns the client JVM, and the board paints for the first# time - all while this was still loading classes. Started here it overlaps the# host's own boot, which is the long pole anyway, and is settled before anyone# is told the match exists.## It gates nothing either way: it joins as an ordinary client and the host flags# it observer, and `allHumansPresent` in MatchHost counts --humans slots, which# never include it.## Its dumps are not uploaded. They were, to `derived/turns/`, for an AppView# that would show a match in progress - and nothing has ever read one. What# they stood in for is now kept properly: `raw/game_actions_N.tsv` is the same# match in a thirtieth of the bytes and structured, and `raw/gamelog.html` is# MegaMek's own account of it. The spool is still written, because a local run# keeps it and it is the readable form when something needs debugging by eye,# and it dies with the container like anything else nobody collects.## The client itself is not optional. Until every human seat is filled MatchHost# leaves it unflagged, so an all-human match has one not-done player holding# the game shut; without it the game starts empty.if [ "$OBSERVER_ENABLED" != "false" ]; then log "starting observer" "$JAVA" -Xmx"${ARENA_WATCHER_HEAP:-512m}" \ -Dlog4j2.configurationFile="$ARENA_HOME/config/log4j2-quiet.xml" \ -Dsentry.dsn= \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ -cp "$CP" \ arena.MatchWatcher --host 127.0.0.1 --port "$PORT_MM" --spool "$ARENA_SPOOL" & WATCHER_PID=$!else log "observer disabled by manifest"fi
# --- the result, sent when it exists rather than when we stop -----------------# Outside the observer block on purpose: a match with no observer still has a# result and end-of-game images, and those belong to its players either way."$ARENA_HOME/container/watch/results.sh" &RESULTS_PID=$!
# --- readiness --------------------------------------------------------------# Both ports, not just Suramadu's. headquarters called a match ready as soon as# the web server answered, which - with the host started after it - was always# before the host was listening, so a player could arrive at a client that had# nothing to connect to. Waiting for both is what lets the `ready` callback mean# "a browser sent here now can play".await_port Suramadu "$PORT_WS" "$WS_PID" 120 "$ARENA_RUN/suramadu.log"phase suramadu-readyWS_READY_MS="$(elapsed_ms)"
await_port "match host" "$PORT_MM" "$HOST_PID" 120phase host-ready
emit ready "suramadu=${WS_READY_MS}ms host=$(elapsed_ms)ms"log "ready: Suramadu on :$PORT_WS, match host on :$PORT_MM"
# --- idle timeout -----------------------------------------------------------# Started once the lobby exists, since that is what it watches - which is now# after the readiness wait above rather than after the host launch, because# that is where "the lobby exists" became true. It reads# limits.idleTimeoutSeconds and exits immediately when the manifest omits it, so# this costs a manifest without one nothing but a process that starts and stops.## It shuts the match down by signalling us, not by killing anything: TERM here# runs shutdown() above, which finalizes first. Hence the exported PID - we are# PID 1 in the image but not when run from a checkout.export ARENA_SUPERVISOR_PID=$$"$ARENA_HOME/container/watch/idle.sh" &IDLE_PID=$!
# --- wait -------------------------------------------------------------------# `wait` on a specific PID so a support process exiting does not unblock us.set +ewait "$HOST_PID"HOST_RC=$?set -elog "match host exited rc=$HOST_RC"
# The idle timeout has nothing left to guard once the host is gone, and it is# stopped before finalize rather than with the others after it: a TERM landing# mid-finalize would run the trap on the way out and replace the host's exit# status with 143.[ -n "$IDLE_PID" ] && kill -TERM "$IDLE_PID" 2>/dev/null || trueIDLE_PID=""
# Suramadu is normally stopped after finalize, below. The client JVM only writes# its profile as it exits, so on a profiled run it has to go first.if profiling; then flush_profiles; fi
finalize_once
for pid in "$RESULTS_PID" "$WATCHER_PID" "$WS_PID"; do [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || truedone
emit exiting "rc=$HOST_RC"exit "$HOST_RC"