aa2679162d
Two defects, both found by the guards misfiring rather than by reading: 1. BIN preferred target/debug and fell back to release only when debug was absent. `cargo build --release` therefore produced a correct binary while the script kept inspecting a stale debug one, and the build guard refused with a message naming a commit nobody was trying to run. The guard was right that something was stale — it just pointed at the wrong artifact. Disagreement between the two is now an explicit refusal naming both, with OPENFUT_REDIRECTOR_BIN as the deliberate override. The refusal is recorded at load and raised only by `verify` and `start`. `stop` and `status` must work in any build-tree state: rollback can never be blocked by a question about which artifact would have been started. 2. `verify-running` read the stamp file without checking the process still existed. The stamp outlives the process, so after a stop it reported on a corpse — either "identity OK" or a REFUSAL naming a commit, both implying something was running when nothing was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
7.4 KiB
Bash
Executable File
179 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Lifecycle for the Rust redirector host.
|
|
#
|
|
# redirector.sh start | stop | status | verify
|
|
#
|
|
# Mirrors sidecar.sh: refuses to start with an orphan present or the port busy,
|
|
# and stop PROVES the process is gone and the port free rather than assuming a
|
|
# signal worked.
|
|
#
|
|
# Additionally REFUSES TO START unless the binary's stamped commit equals HEAD
|
|
# and the migration crates are clean — evidence from an unidentifiable binary is
|
|
# not evidence.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
|
ROOT="$(cd "$HERE/.." && pwd)"
|
|
RUNDIR="${OPENFUT_REDIRECTOR_RUNDIR:-${TMPDIR:-/tmp}/openfut-redirector}"
|
|
PIDFILE="$RUNDIR/redirector.pid"
|
|
PORTFILE="$RUNDIR/redirector.port"
|
|
# Commit the RUNNING process was started from. `verify` alone inspects the
|
|
# on-disk binary, which a rebuild (even `cargo test`, which re-runs build.rs
|
|
# when the branch ref moves) can silently advance past the live process.
|
|
STAMPFILE="$RUNDIR/redirector.commit"
|
|
LOGFILE="${OPENFUT_REDIRECTOR_LOG:-$RUNDIR/redirector.log}"
|
|
# Which artifact is under test must never be ambiguous. Preferring debug and
|
|
# falling back to release meant `cargo build --release` could produce a fresh
|
|
# binary while this kept launching a stale debug one — the guard then compared
|
|
# the WRONG artifact against HEAD and refused with a message naming a commit
|
|
# nobody was trying to run. Both present and disagreeing is an error, not a
|
|
# preference. Set OPENFUT_REDIRECTOR_BIN to choose deliberately.
|
|
DEBUG_BIN="$ROOT/target/debug/openfut-redirector-host"
|
|
RELEASE_BIN="$ROOT/target/release/openfut-redirector-host"
|
|
BIN_ERR=""
|
|
if [[ -n "${OPENFUT_REDIRECTOR_BIN:-}" ]]; then
|
|
BIN="$OPENFUT_REDIRECTOR_BIN"
|
|
[[ -x "$BIN" ]] || { echo "redirector: OPENFUT_REDIRECTOR_BIN is not executable: $BIN" >&2; exit 1; }
|
|
elif [[ -x "$DEBUG_BIN" && -x "$RELEASE_BIN" ]]; then
|
|
d="$("$DEBUG_BIN" --identity 2>&1 | sed -nE 's/.*commit=([0-9a-f]+).*/\1/p' | head -1)"
|
|
r="$("$RELEASE_BIN" --identity 2>&1 | sed -nE 's/.*commit=([0-9a-f]+).*/\1/p' | head -1)"
|
|
if [[ "$d" != "$r" ]]; then
|
|
# Recorded, NOT fatal here: `stop` and `status` must keep working no matter
|
|
# what state the build tree is in. Rolling back can never be blocked by a
|
|
# question about which artifact would have been started.
|
|
BIN_ERR="REFUSING — two binaries exist and disagree.
|
|
debug $DEBUG_BIN commit=$d
|
|
release $RELEASE_BIN commit=$r
|
|
Rebuild both, delete one, or set OPENFUT_REDIRECTOR_BIN."
|
|
fi
|
|
BIN="$RELEASE_BIN"
|
|
elif [[ -x "$DEBUG_BIN" ]]; then
|
|
BIN="$DEBUG_BIN"
|
|
else
|
|
BIN="$RELEASE_BIN"
|
|
fi
|
|
|
|
die() { echo "redirector: $*" >&2; exit 1; }
|
|
pid_alive() { kill -0 "$1" 2>/dev/null; }
|
|
port_listening() { ss -ltn 2>/dev/null | grep -qE "[:.]${1}[[:space:]]"; }
|
|
|
|
# Match the resolved executable, not the command line: `pgrep -f` matches any
|
|
# shell whose arguments merely mention the name.
|
|
list_procs() {
|
|
local self=$$ pid exe
|
|
for d in /proc/[0-9]*; do
|
|
pid="${d#/proc/}"; [[ "$pid" == "$self" ]] && continue
|
|
# readlink, NOT readlink -f: once the binary is rebuilt the link reads
|
|
# "<path> (deleted)", and -f resolves that to something that matches
|
|
# nothing. The orphan check would then be blind to exactly the long-lived
|
|
# processes it exists to find — verified: two orphans (a stale-cert
|
|
# redirector and a Blaze sidecar) were both invisible to this until the
|
|
# suffix was stripped.
|
|
exe="$(readlink "$d/exe" 2>/dev/null)" || continue
|
|
exe="${exe% (deleted)}"
|
|
[[ "${exe##*/}" == "openfut-redirector-host" ]] && echo "$pid"
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# Only the commands that actually run a binary care which one it is.
|
|
need_bin() { [[ -z "$BIN_ERR" ]] || die "$BIN_ERR"; }
|
|
|
|
# The binary prints `commit=<sha>` in its banner; ask it rather than guessing.
|
|
stamped_commit() { "$BIN" --identity 2>&1 | sed -nE 's/.*commit=([0-9a-f]+).*/\1/p' | head -1; }
|
|
|
|
cmd_verify() {
|
|
need_bin
|
|
local c; c="$(stamped_commit)"
|
|
[[ -n "$c" ]] || die "could not read the binary's commit stamp"
|
|
"$ROOT/scripts/verify-build-identity.sh" "$c"
|
|
}
|
|
|
|
cmd_start() {
|
|
need_bin
|
|
[[ -x "$BIN" ]] || die "not built: cargo build -p openfut-redirector-host"
|
|
: "${OPENFUT_REDIRECTOR_HOST_PORT:?set OPENFUT_REDIRECTOR_HOST_PORT (no default: runs beside Python)}"
|
|
local strays; strays="$(list_procs)"
|
|
[[ -z "$strays" ]] || die "orphan redirector process(es): $strays"
|
|
port_listening "$OPENFUT_REDIRECTOR_HOST_PORT" && die "port $OPENFUT_REDIRECTOR_HOST_PORT in use"
|
|
|
|
cmd_verify || die "build identity check failed — refusing to start"
|
|
|
|
mkdir -p "$RUNDIR"; echo "$OPENFUT_REDIRECTOR_HOST_PORT" > "$PORTFILE"
|
|
stamped_commit > "$STAMPFILE"
|
|
"$BIN" >"$LOGFILE" 2>&1 &
|
|
local pid=$!; echo "$pid" > "$PIDFILE"
|
|
local w=0
|
|
while (( w < 100 )); do
|
|
pid_alive "$pid" || { echo "died during startup:" >&2; tail -20 "$LOGFILE" >&2; rm -f "$PIDFILE"; return 1; }
|
|
if port_listening "$OPENFUT_REDIRECTOR_HOST_PORT"; then
|
|
echo "redirector started: pid $pid, port $OPENFUT_REDIRECTOR_HOST_PORT"
|
|
grep -E 'SELF-TEST|openfut-redirector-host v' "$LOGFILE" | sed 's/^/ /'
|
|
return 0
|
|
fi
|
|
sleep 0.1; w=$((w+1))
|
|
done
|
|
echo "did not listen within 10s:" >&2; tail -20 "$LOGFILE" >&2
|
|
kill "$pid" 2>/dev/null; rm -f "$PIDFILE"; return 1
|
|
}
|
|
|
|
cmd_stop() {
|
|
local rc=0 pid="" port=""
|
|
[[ -f "$PIDFILE" ]] && pid="$(cat "$PIDFILE")"
|
|
[[ -f "$PORTFILE" ]] && port="$(cat "$PORTFILE")"
|
|
if [[ -n "$pid" ]] && pid_alive "$pid"; then
|
|
kill "$pid" 2>/dev/null
|
|
local w=0; while pid_alive "$pid" && (( w < 50 )); do sleep 0.1; w=$((w+1)); done
|
|
pid_alive "$pid" && kill -9 "$pid" 2>/dev/null
|
|
sleep 0.2
|
|
fi
|
|
[[ -n "$pid" ]] && pid_alive "$pid" && { echo "FAILED to stop $pid" >&2; rc=1; }
|
|
[[ -n "$port" ]] && port_listening "$port" && { echo "FAILED: port $port still listening" >&2; rc=1; }
|
|
local strays; strays="$(list_procs)"
|
|
[[ -n "$strays" ]] && { echo "FAILED: still running: $strays" >&2; rc=1; }
|
|
rm -f "$PIDFILE" "$PORTFILE"
|
|
[[ $rc -eq 0 ]] && echo "redirector stopped and verified gone${pid:+ (pid $pid)}"
|
|
return $rc
|
|
}
|
|
|
|
cmd_status() {
|
|
if [[ -f "$PIDFILE" ]] && pid_alive "$(cat "$PIDFILE")"; then
|
|
echo "running: pid $(cat "$PIDFILE"), port $(cat "$PORTFILE" 2>/dev/null || echo '?')"
|
|
else
|
|
echo "not running"
|
|
fi
|
|
local strays; strays="$(list_procs)"
|
|
[[ -n "$strays" ]] && echo "redirector processes: $strays"
|
|
return 0
|
|
}
|
|
|
|
cmd_verify_running() {
|
|
# The stamp outlives the process it describes. Without this check the command
|
|
# happily reports on a corpse — either "OK" or a REFUSAL naming a commit,
|
|
# both implying something is running when nothing is.
|
|
local pid=""
|
|
[[ -f "$PIDFILE" ]] && pid="$(cat "$PIDFILE" 2>/dev/null)"
|
|
if [[ -z "$pid" ]] || ! pid_alive "$pid"; then
|
|
echo "REFUSING: nothing is running — the stamp describes a process that has exited." >&2
|
|
return 1
|
|
fi
|
|
[[ -f "$STAMPFILE" ]] || die "no running-process stamp — was it started by this script?"
|
|
local running head
|
|
running="$(cat "$STAMPFILE")"; head="$(git -C "$ROOT" rev-parse --short=7 HEAD 2>/dev/null)"
|
|
if [[ "$running" != "$head" ]]; then
|
|
echo "REFUSING: the RUNNING process was started from $running but HEAD is $head" >&2
|
|
echo " Restart before treating this run as evidence." >&2
|
|
return 1
|
|
fi
|
|
echo "running-process identity OK: started from $running == HEAD"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
start) cmd_start ;;
|
|
stop) cmd_stop ;;
|
|
status) cmd_status ;;
|
|
verify) cmd_verify ;;
|
|
verify-running) cmd_verify_running ;;
|
|
*) sed -n '2,6p' "$0" | sed 's/^# \?//'; exit 2 ;;
|
|
esac
|