summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/pin_supervisor.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tools/guest-diag/pin_supervisor.sh')
-rwxr-xr-xtools/guest-diag/pin_supervisor.sh169
1 files changed, 169 insertions, 0 deletions
diff --git a/tools/guest-diag/pin_supervisor.sh b/tools/guest-diag/pin_supervisor.sh
new file mode 100755
index 0000000..b87d5c1
--- /dev/null
+++ b/tools/guest-diag/pin_supervisor.sh
@@ -0,0 +1,169 @@
+#!/bin/bash
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Open-IOV contributors
+# pin_supervisor.sh - supervise the host-side pin tools (pin_gr_stream.py and
+# semfix_uvmt.py) so a silent death cannot strand every later CUDA client at
+# first cudaMalloc.
+#
+# Both tools write a heartbeat file; this script checks, every 5 s, that each
+# process is alive (exact script path) and that its heartbeat mtime is < 75 s.
+# A startup grace (120 s, skipped early once the tool log shows it has done
+# first useful work) keeps a slow cold start from being restarted. On failure
+# it kills the tool (exact path match) and restarts it, appending RESTART
+# lines to the supervisor log. After 10 restarts in one hour for a tool it
+# logs GIVEUP and exits 1. Only one semfix_uvmt instance is ever started
+# (QEMU's QMP socket accepts a single client).
+#
+# Liveness policy (do not tighten without re-checking the soak evidence):
+# * the tools refresh their heartbeats from dedicated loops/threads even
+# while idle (pin_gr_stream: 5 s thread; semfix: per loop + around every
+# blocking guest-agent/dmesg call), so 75 s never trips on a healthy
+# client between phases;
+# * a DEAD process is restarted on the next 5 s tick regardless of grace;
+# * dead-pid/state recovery (the kill cascade) is handled INSIDE the tools
+# (in-band #nopid + idle liveness probe + stall watchdog), NOT by this
+# supervisor - a log-based restart here could fire mid-run of a healthy
+# client (the m178 lesson).
+#
+# Usage: pin_supervisor.sh LOGDIR TAG
+# Logs: LOGDIR/12-pin-supervisor-TAG.log (supervisor)
+# LOGDIR/12-pin_gr_stream-TAG.log, LOGDIR/12-semfix-TAG.log (tools)
+set -uo pipefail
+
+LOGDIR="${1:?usage: pin_supervisor.sh LOGDIR TAG}"
+TAG="${2:?usage: pin_supervisor.sh LOGDIR TAG}"
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+PIN="$HERE/pin_gr_stream.py"
+SEMFIX="$HERE/semfix_uvmt.py"
+PIN_HB="${PIN_GR_HEARTBEAT:-/tmp/opencode/pin_gr_heartbeat}"
+SEMFIX_HB="${SEMFIX_HEARTBEAT_FILE:-/tmp/opencode/semfix_heartbeat}"
+SUPLOG="$LOGDIR/12-pin-supervisor-$TAG.log"
+PIN_LOG="$LOGDIR/12-pin_gr_stream-$TAG.log"
+SEMFIX_LOG="$LOGDIR/12-semfix-$TAG.log"
+WINDOW=3600
+CAP=10
+STALE=75
+GRACE=120
+mkdir -p "$LOGDIR" /tmp/opencode
+
+log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$SUPLOG"; }
+
+tool_alive() { pgrep -f "python3 $1" >/dev/null 2>&1; }
+
+hb_age() { # age in seconds of the heartbeat file, 9999 if missing
+ local f="$1" m
+ m=$(stat -c %Y "$f" 2>/dev/null) || { echo 9999; return; }
+ echo $(( $(date +%s) - m ))
+}
+
+kill_tool() {
+ pkill -f "python3 $1" 2>/dev/null
+ sleep 1
+ if pgrep -f "python3 $1" >/dev/null 2>&1; then
+ pkill -9 -f "python3 $1" 2>/dev/null
+ sleep 1
+ fi
+ return 0
+}
+
+start_pin() {
+ if tool_alive "$PIN"; then return 0; fi
+ touch "$PIN_HB" 2>/dev/null || true
+ ( setsid nohup env SEMFIX_COUNTER_RETRY=10 \
+ python3 "$PIN" >> "$PIN_LOG" 2>&1 & )
+ pin_started=$(date +%s)
+ log "START pin_gr_stream"
+}
+
+start_semfix() {
+ # pgrep exact-count check before start: if an instance already exists
+ # (ours or a leftover), never start a second one.
+ if tool_alive "$SEMFIX"; then
+ log "semfix already running - not starting another"
+ return 0
+ fi
+ touch "$SEMFIX_HB" 2>/dev/null || true
+ ( setsid nohup env SEMFIX_SKIP_PB=0 SEMFIX_COUNTER_RETRY=10 \
+ python3 "$SEMFIX" 0.5 >> "$SEMFIX_LOG" 2>&1 & )
+ semfix_started=$(date +%s)
+ log "START semfix_uvmt"
+}
+
+prune() { # $1 now, $2 list -> list with entries older than WINDOW dropped
+ local now="$1" t out=""
+ for t in $2; do
+ [ $(( now - t )) -lt "$WINDOW" ] && out="$out $t"
+ done
+ printf '%s' "$out"
+}
+
+count_list() { # $1 list -> element count
+ local n=0 t
+ for t in $1; do n=$(( n + 1 )); done
+ echo "$n"
+}
+
+restarts_pin=""
+restarts_semfix=""
+pin_started=$(date +%s)
+semfix_started=$(date +%s)
+
+restart() { # $1 label, $2 script path, $3 start function
+ local now list n
+ now=$(date +%s)
+ if [ "$1" = pin_gr_stream ]; then
+ restarts_pin=$(prune "$now" "$restarts_pin")
+ n=$(count_list "$restarts_pin")
+ else
+ restarts_semfix=$(prune "$now" "$restarts_semfix")
+ n=$(count_list "$restarts_semfix")
+ fi
+ if [ "$n" -ge "$CAP" ]; then
+ log "GIVEUP $1: $n restarts within ${WINDOW}s"
+ exit 1
+ fi
+ kill_tool "$2"
+ if [ "$1" = pin_gr_stream ]; then
+ restarts_pin="$restarts_pin $now"
+ else
+ restarts_semfix="$restarts_semfix $now"
+ fi
+ log "RESTART $(( n + 1 )) $1 $(date +%Y-%m-%dT%H:%M:%S)"
+ "$3"
+}
+
+check_tool() { # $1 label, $2 path, $3 heartbeat file, $4 start function,
+ # $5 tool start TS, $6 tool log file, $7 first-work pattern
+ local age runtime
+ age=$(hb_age "$3")
+ if tool_alive "$2" && [ "$age" -le "$STALE" ]; then
+ return 0
+ fi
+ if tool_alive "$2"; then
+ # Startup grace: a cold tool can spend its first ~2 min installing the
+ # guest scanner or connecting QMP with a lagging heartbeat; only
+ # enforce staleness once the grace has passed or the log shows the
+ # tool has already done successful work.
+ runtime=$(( $(date +%s) - $5 ))
+ if [ "$runtime" -lt "$GRACE" ] && \
+ ! grep -q -- "$7" "$6" 2>/dev/null; then
+ return 0
+ fi
+ log "$1 heartbeat stale (${age}s) - restarting"
+ else
+ log "$1 not running (heartbeat age ${age}s) - restarting"
+ fi
+ restart "$1" "$2" "$4"
+}
+
+log "pin_supervisor up (LOGDIR=$LOGDIR TAG=$TAG)"
+start_pin
+start_semfix
+while true; do
+ sleep 5
+ check_tool pin_gr_stream "$PIN" "$PIN_HB" start_pin "$pin_started" \
+ "$PIN_LOG" "listening on vsock"
+ check_tool semfix_uvmt "$SEMFIX" "$SEMFIX_HB" start_semfix \
+ "$semfix_started" "$SEMFIX_LOG" "semfix_uvmt: watching"
+done