cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

release/portal-packaging
goq / scripts / loopback-proof.sh
loopback-proof.sh 46.0K executable
1#!/usr/bin/env bash23set -euo pipefail45script_dir="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"6workspace_dir="$(CDPATH='' cd -- "$script_dir/.." && pwd)"78profile="debug"9primary_frames=60010reconnect_cycles=311reconnect_frames=3012probe_timeout_seconds=1513command_timeout_seconds=4514readiness_timeout_seconds=3015host_runtime_seconds=12016media_transport="iroh-moq"17media_alpn="moq-lite-04"18media_accept_log="authorized MoQ media attachment accepted"19media_release_log="MoQ control client released"20keyframe_request_log="accepted MoQ keyframe request"21session_gate_name="control"22keyframe_recovery="ok"23media_v3=false24relay_only=false25expected_path_mode=direct26control_v2=false27viewers=128assert_shared_generation=false29replace_viewer=030legacy_exclusive=false31expect_second_rejected=false32focus_handoffs=033assert_neutral_before_successor=false34stall_viewer=035assert_survivor_recovery=false36stress=false37host_rss_samples=""3839usage() {40 cat <<'EOF'41Usage: scripts/loopback-proof.sh [options]4243Build and exercise the exact sigil and sigil-probe binaries over real loopback44Iroh and upstream MoQ connections. The ordinary proof covers a configured IDR,45native group bounds and recovery, input acknowledgment, single-active-client46rejection, and clean reconnects.4748Options:49 --profile debug|release Cargo profile to build (default: debug)50 --primary-frames COUNT Frames in the held primary session (default: 600)51 --reconnect-cycles COUNT Fresh sessions after the primary (default: 3)52 --reconnect-frames COUNT Frames per reconnect session (default: 30)53 --probe-timeout-seconds SEC Probe per-operation timeout (default: 15)54 --media-v3 Exercise custom grouped media v3 compatibility55 instead of upstream MoQ56 --relay-only Remove direct probe transports and require the57 control, media, and input paths to stay relayed58 --control-v2 Exercise explicit control/2 and input/2 focus59 --viewers COUNT Concurrent viewers (control-v2 supports 1..8)60 --assert-shared-generation Require host-scoped generation naming, one61 producer start, and final-viewer cleanup62 --replace-viewer INDEX Replace this 1-based control-v2 viewer identity63 --legacy-exclusive Exercise the unchanged exclusive legacy path64 --expect-second-rejected Require explicit legacy second-client rejection65 --focus-handoffs COUNT Run bounded holder-approved control transfers66 --assert-neutral-before-successor67 Require host ordering evidence for every transfer68 --stall-viewer INDEX Drive this control-v2 viewer persistently below69 the shared bitrate floor70 --assert-survivor-recovery Require clean viewers to recover after isolation71 --stress Require the release-profile eight-viewer churn,72 pressure isolation, and resource-bound gate73 --help Show this help74EOF75}7677die() {78 printf 'loopback proof failed: %s\n' "$*" >&279 exit 180}8182require_positive_integer() {83 local name="$1"84 local value="$2"85 case "$value" in86 ''|*[!0-9]*) die "$name must be a positive integer" ;;87 esac88 if [[ "$value" -eq 0 ]]; then89 die "$name must be greater than zero"90 fi91}9293while [[ $# -gt 0 ]]; do94 case "$1" in95 --profile)96 [[ $# -ge 2 ]] || die "--profile requires a value"97 profile="$2"98 shift 299 ;;100 --primary-frames)101 [[ $# -ge 2 ]] || die "--primary-frames requires a value"102 primary_frames="$2"103 shift 2104 ;;105 --reconnect-cycles)106 [[ $# -ge 2 ]] || die "--reconnect-cycles requires a value"107 reconnect_cycles="$2"108 shift 2109 ;;110 --reconnect-frames)111 [[ $# -ge 2 ]] || die "--reconnect-frames requires a value"112 reconnect_frames="$2"113 shift 2114 ;;115 --probe-timeout-seconds)116 [[ $# -ge 2 ]] || die "--probe-timeout-seconds requires a value"117 probe_timeout_seconds="$2"118 shift 2119 ;;120 --relay-only)121 relay_only=true122 expected_path_mode=relay123 shift124 ;;125 --media-v3)126 media_transport="grouped-v3"127 media_alpn="sigil/media/3"128 media_accept_log="media v3 client accepted"129 media_release_log="media v3 client released"130 keyframe_request_log="accepted media v3 keyframe request"131 session_gate_name="media"132 media_v3=true133 shift134 ;;135 --control-v2)136 control_v2=true137 media_accept_log="MoQ control v2 client accepted"138 media_release_log="MoQ control v2 client released"139 session_gate_name="control v2"140 shift141 ;;142 --viewers)143 [[ $# -ge 2 ]] || die "--viewers requires a value"144 viewers="$2"145 shift 2146 ;;147 --assert-shared-generation)148 assert_shared_generation=true149 shift150 ;;151 --replace-viewer)152 [[ $# -ge 2 ]] || die "--replace-viewer requires a value"153 replace_viewer="$2"154 shift 2155 ;;156 --legacy-exclusive)157 legacy_exclusive=true158 shift159 ;;160 --expect-second-rejected)161 expect_second_rejected=true162 shift163 ;;164 --focus-handoffs)165 [[ $# -ge 2 ]] || die "--focus-handoffs requires a value"166 focus_handoffs="$2"167 shift 2168 ;;169 --assert-neutral-before-successor)170 assert_neutral_before_successor=true171 shift172 ;;173 --stall-viewer)174 [[ $# -ge 2 ]] || die "--stall-viewer requires a value"175 stall_viewer="$2"176 shift 2177 ;;178 --assert-survivor-recovery)179 assert_survivor_recovery=true180 shift181 ;;182 --stress)183 stress=true184 shift185 ;;186 --help|-h)187 usage188 exit 0189 ;;190 *) die "unknown argument: $1" ;;191 esac192done193194case "$profile" in195 debug)196 cargo_profile="dev"197 profile_dir="debug"198 ;;199 release)200 cargo_profile="release"201 profile_dir="release"202 ;;203 *) die "--profile must be debug or release" ;;204esac205require_positive_integer "--primary-frames" "$primary_frames"206require_positive_integer "--reconnect-cycles" "$reconnect_cycles"207require_positive_integer "--reconnect-frames" "$reconnect_frames"208require_positive_integer "--probe-timeout-seconds" "$probe_timeout_seconds"209require_positive_integer "--viewers" "$viewers"210case "$focus_handoffs" in211 ''|*[!0-9]*) die "--focus-handoffs must be a non-negative integer" ;;212esac213if [[ "$focus_handoffs" -gt 0 && ( "$control_v2" != true || "$viewers" -lt 2 ) ]]; then214 die "--focus-handoffs requires --control-v2 with at least two viewers"215fi216if [[ "$assert_neutral_before_successor" == true && "$focus_handoffs" -eq 0 ]]; then217 die "--assert-neutral-before-successor requires --focus-handoffs"218fi219if [[ "$control_v2" == true && ( "$viewers" -lt 1 || "$viewers" -gt 8 ) ]]; then220 die "this bounded control-v2 proof supports --viewers 1 through 8"221fi222case "$replace_viewer" in223 ''|*[!0-9]*) die "--replace-viewer must be a non-negative integer" ;;224esac225if [[ "$replace_viewer" -gt "$viewers" ]]; then226 die "--replace-viewer must identify one of the configured viewers"227fi228case "$stall_viewer" in229 ''|*[!0-9]*) die "--stall-viewer must be a non-negative integer" ;;230esac231if [[ "$stall_viewer" -gt "$viewers" ]]; then232 die "--stall-viewer must identify one of the configured viewers"233fi234if [[ "$stall_viewer" -gt 0 && ( "$control_v2" != true || "$viewers" -lt 2 ) ]]; then235 die "--stall-viewer requires --control-v2 with at least two viewers"236fi237if [[ "$assert_survivor_recovery" == true && "$stall_viewer" -eq 0 ]]; then238 die "--assert-survivor-recovery requires --stall-viewer"239fi240if [[ "$stall_viewer" -gt 0 && "$command_timeout_seconds" -lt 75 ]]; then241 command_timeout_seconds=75242fi243if [[ "$control_v2" == true && "$media_v3" == true ]]; then244 die "--control-v2 conflicts with --media-v3"245fi246if [[ "$legacy_exclusive" == true && "$control_v2" == true ]]; then247 die "--legacy-exclusive conflicts with --control-v2"248fi249if [[ "$legacy_exclusive" == true && "$viewers" -ne 2 ]]; then250 die "--legacy-exclusive currently proves exactly --viewers 2"251fi252if [[ "$legacy_exclusive" == true && "$expect_second_rejected" != true ]]; then253 die "--legacy-exclusive requires --expect-second-rejected"254fi255if [[ "$stress" == true ]]; then256 [[ "$control_v2" == true && "$profile" == release && "$viewers" -eq 8 ]] \257 || die "--stress requires --profile release --control-v2 --viewers 8"258 assert_shared_generation=true259 stall_viewer=8260 assert_survivor_recovery=true261fi262if [[ "$control_v2" == true && "$viewers" -gt 1 && "$reconnect_cycles" -gt 0 \263 && "$replace_viewer" -eq 0 ]]; then264 if [[ "$focus_handoffs" -gt 0 ]]; then265 # Viewers 1 and 2 are the focus-handoff participants. Replacing one tears266 # down its control session mid-exchange, so it can never observe the267 # required handoffs. Replace a viewer outside the participant set, and268 # leave replacement off entirely when the roster has no spare viewer.269 if [[ "$viewers" -gt 2 ]]; then270 replace_viewer=3271 fi272 else273 replace_viewer=2274 fi275fi276if [[ "$focus_handoffs" -gt 0 && "$replace_viewer" -gt 0 && "$replace_viewer" -le 2 ]]; then277 die "--replace-viewer must target a viewer above 2 when --focus-handoffs is requested"278fi279[[ "$primary_frames" -ge 4 ]] \280 || die "--primary-frames must be at least 4 for keyframe recovery"281[[ "$reconnect_frames" -ge 4 ]] \282 || die "--reconnect-frames must be at least 4 for keyframe recovery"283284# A fresh Iroh endpoint performs relay discovery and path establishment on285# every reconnect. Keep the default three-cycle proof fast, but scale the host286# watchdog for deliberate soak runs such as --reconnect-cycles 100. The bound287# remains finite and deliberately conservative at eight seconds per cycle.288estimated_primary_seconds=$(( (primary_frames + 59) / 60 ))289scaled_host_runtime_seconds=$((120 + estimated_primary_seconds + reconnect_cycles * 8))290if [[ "$scaled_host_runtime_seconds" -gt "$host_runtime_seconds" ]]; then291 host_runtime_seconds="$scaled_host_runtime_seconds"292fi293scaled_command_timeout_seconds=$((estimated_primary_seconds + 45))294if [[ "$scaled_command_timeout_seconds" -gt "$command_timeout_seconds" ]]; then295 command_timeout_seconds="$scaled_command_timeout_seconds"296fi297298if [[ -f "${HOME}/.cargo/env" ]]; then299 # The project pins its Rust toolchain, but Cargo may not yet be on PATH in a300 # non-interactive shell.301 # shellcheck source=/dev/null302 source "${HOME}/.cargo/env"303fi304305command -v cargo >/dev/null 2>&1 || die "cargo is required"306ffmpeg_bin="$(command -v ffmpeg || true)"307[[ -n "$ffmpeg_bin" ]] || die "ffmpeg is required"308# Capture the encoder list before matching. Piping it straight into `grep -q`309# lets grep exit on the first hit, which hands ffmpeg a SIGPIPE, and `pipefail`310# then reports the whole probe as failed. A full ffmpeg build emits enough311# encoders to lose that race every time, so the check rejected exactly the312# builds that do provide libx264.313ffmpeg_encoders="$("$ffmpeg_bin" -hide_banner -encoders 2>/dev/null || true)"314if ! grep -q '[[:space:]]libx264[[:space:]]' <<<"$ffmpeg_encoders"; then315 die "ffmpeg does not provide the required libx264 encoder"316fi317318tmp_root="$(mktemp -d "${TMPDIR:-/tmp}/sigil-loopback.XXXXXX")"319case "$tmp_root" in320 */sigil-loopback.??????) ;;321 *) die "mktemp returned an unexpected path: $tmp_root" ;;322esac323324host_pid=""325host_watchdog_pid=""326primary_pid=""327primary_watchdog_pid=""328bounded_pid=""329bounded_watchdog_pid=""330watchdog_pid=""331viewer_pids=()332resource_sampler_pid=""333334stop_pid() {335 local pid="$1"336 local killer_pid337 if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then338 kill -TERM "$pid" 2>/dev/null || true339 (340 for _ in {1..50}; do341 if ! kill -0 "$pid" 2>/dev/null; then342 exit 0343 fi344 sleep 0.1345 done346 kill -KILL "$pid" 2>/dev/null || true347 ) >/dev/null 2>&1 &348 killer_pid=$!349 else350 killer_pid=""351 fi352 if [[ -n "$pid" ]]; then353 wait "$pid" 2>/dev/null || true354 fi355 if [[ -n "$killer_pid" ]]; then356 wait "$killer_pid" 2>/dev/null || true357 fi358}359360cleanup() {361 local exit_status=$?362 trap - EXIT INT TERM HUP363364 stop_pid "$bounded_pid"365 [[ -z "$bounded_watchdog_pid" ]] || wait "$bounded_watchdog_pid" 2>/dev/null || true366 stop_pid "$primary_pid"367 [[ -z "$primary_watchdog_pid" ]] || wait "$primary_watchdog_pid" 2>/dev/null || true368 for viewer_pid in "${viewer_pids[@]-}"; do369 [[ -n "$viewer_pid" ]] || continue370 stop_pid "$viewer_pid"371 done372 stop_pid "$resource_sampler_pid"373 stop_pid "$host_pid"374 [[ -z "$host_watchdog_pid" ]] || wait "$host_watchdog_pid" 2>/dev/null || true375376 case "$tmp_root" in377 */sigil-loopback.??????) rm -rf -- "$tmp_root" ;;378 esac379 exit "$exit_status"380}381trap cleanup EXIT INT TERM HUP382383sha256_file() {384 local path="$1"385 if command -v sha256sum >/dev/null 2>&1; then386 sha256sum "$path" | awk '{print $1}'387 else388 shasum -a 256 "$path" | awk '{print $1}'389 fi390}391392wait_for_log_line() {393 local log_path="$1"394 local literal="$2"395 local watched_pid="$3"396 local label="$4"397 local deadline=$((SECONDS + readiness_timeout_seconds))398399 while (( SECONDS < deadline )); do400 if grep -Fq -- "$literal" "$log_path" 2>/dev/null; then401 return 0402 fi403 if ! kill -0 "$watched_pid" 2>/dev/null; then404 printf '%s exited before %s readiness. Log follows:\n' "$label" "$literal" >&2405 sed -n '1,240p' "$log_path" >&2 || true406 return 1407 fi408 sleep 0.1409 done410 printf 'timed out waiting for %s readiness marker %s. Log follows:\n' "$label" "$literal" >&2411 sed -n '1,240p' "$log_path" >&2 || true412 return 1413}414415wait_for_log_count() {416 local log_path="$1"417 local literal="$2"418 local expected_count="$3"419 local watched_pid="$4"420 local label="$5"421 local deadline=$((SECONDS + readiness_timeout_seconds))422 local actual_count423424 while (( SECONDS < deadline )); do425 actual_count="$(grep -Fc -- "$literal" "$log_path" 2>/dev/null || true)"426 if [[ "$actual_count" -ge "$expected_count" ]]; then427 return 0428 fi429 if ! kill -0 "$watched_pid" 2>/dev/null; then430 printf '%s exited while waiting for %s occurrence %s. Log follows:\n' \431 "$label" "$literal" "$expected_count" >&2432 sed -n '1,240p' "$log_path" >&2 || true433 return 1434 fi435 sleep 0.1436 done437 printf 'timed out waiting for %s occurrence %s in %s. Log follows:\n' \438 "$literal" "$expected_count" "$label" >&2439 sed -n '1,240p' "$log_path" >&2 || true440 return 1441}442443host_accepted_keyframe_request() {444 local log_path="$1"445 local request_id="$2"446 sed $'s/\033\\[[0-9;]*m//g' "$log_path" 2>/dev/null \447 | awk -v request_id="$request_id" -v request_log="$keyframe_request_log" '448 index($0, request_log) \449 && $0 ~ ("request_id=" request_id "([^[:digit:]]|$)") \450 && index($0, "coalesced=false") { found=1 }451 END { exit !found }452 '453}454455host_keyframe_request_count() {456 local log_path="$1"457 local request_id="$2"458 sed $'s/\033\\[[0-9;]*m//g' "$log_path" 2>/dev/null \459 | awk -v request_id="$request_id" -v request_log="$keyframe_request_log" '460 index($0, request_log) \461 && $0 ~ ("request_id=" request_id "([^[:digit:]]|$)") \462 && index($0, "coalesced=false") { count++ }463 END { print count + 0 }464 '465}466467wait_for_keyframe_request() {468 local log_path="$1"469 local request_id="$2"470 local watched_pid="$3"471 local label="$4"472 local deadline=$((SECONDS + readiness_timeout_seconds))473474 while (( SECONDS < deadline )); do475 if host_accepted_keyframe_request "$log_path" "$request_id"; then476 return 0477 fi478 if ! kill -0 "$watched_pid" 2>/dev/null; then479 printf '%s exited before accepting keyframe request %s. Log follows:\n' \480 "$label" "$request_id" >&2481 sed -n '1,240p' "$log_path" >&2 || true482 return 1483 fi484 sleep 0.1485 done486 printf 'timed out waiting for %s to accept keyframe request %s. Log follows:\n' \487 "$label" "$request_id" >&2488 sed -n '1,240p' "$log_path" >&2 || true489 return 1490}491492start_watchdog() {493 local pid="$1"494 local seconds="$2"495 local timeout_marker="$3"496 (497 local deadline=$((SECONDS + seconds))498 while kill -0 "$pid" 2>/dev/null; do499 if (( SECONDS >= deadline )); then500 : > "$timeout_marker"501 kill -TERM "$pid" 2>/dev/null || true502 for _ in {1..20}; do503 if ! kill -0 "$pid" 2>/dev/null; then504 exit 0505 fi506 sleep 0.1507 done508 kill -KILL "$pid" 2>/dev/null || true509 exit 0510 fi511 sleep 0.1512 done513 ) >/dev/null 2>&1 &514 watchdog_pid=$!515}516517run_bounded() {518 local seconds="$1"519 local output="$2"520 shift 2521 local status522523 rm -f -- "${output}.timeout"524 "$@" >"$output" 2>&1 &525 bounded_pid=$!526 start_watchdog "$bounded_pid" "$seconds" "${output}.timeout"527 bounded_watchdog_pid="$watchdog_pid"528 if wait "$bounded_pid"; then529 status=0530 else531 status=$?532 fi533 bounded_pid=""534 wait "$bounded_watchdog_pid" 2>/dev/null || true535 bounded_watchdog_pid=""536 if [[ -e "${output}.timeout" ]]; then537 return 124538 fi539 return "$status"540}541542validate_probe_log() {543 local log_path="$1"544 local expected_frames="$2"545 local expected_request_id="$3"546 grep -Fxq 'probe=ok' "$log_path" || return 1547 grep -Fxq "frames=${expected_frames}" "$log_path" || return 1548 grep -Fxq 'dimensions=1280x800' "$log_path" || return 1549 grep -Fxq 'sequence_gaps=0' "$log_path" || return 1550 grep -Fxq "transport=${media_transport}" "$log_path" || return 1551 grep -Fxq "transport_alpn=${media_alpn}" "$log_path" || return 1552 grep -Fxq 'first_configured_idr=ok' "$log_path" || return 1553 grep -Fxq 'frame_sequence=monotonic' "$log_path" || return 1554 grep -Fxq "keyframe_recovery=${keyframe_recovery}" "$log_path" || return 1555 grep -Fxq "keyframe_request_id=${expected_request_id}" "$log_path" || return 1556 if [[ "$expected_request_id" == not-requested ]]; then557 grep -Fxq 'keyframe_recovery_micros=not-requested' "$log_path" || return 1558 else559 awk -F= '$1 == "keyframe_recovery_micros" && $2 ~ /^[0-9]+$/ { found=1 } END { exit !found }' \560 "$log_path" || return 1561 fi562 awk -F= '$1 == "media_objects_dropped" && $2 ~ /^[0-9]+$/ { found=1 } END { exit !found }' \563 "$log_path" || return 1564 awk -F= '$1 == "media_objects_late" && $2 ~ /^[0-9]+$/ { found=1 } END { exit !found }' \565 "$log_path" || return 1566 awk -F= '$1 == "keyframes" && $2 ~ /^[0-9]+$/ && $2 > 0 { found=1 } END { exit !found }' \567 "$log_path" || return 1568 awk -F= '$1 == "input_ack_micros" && $2 ~ /^[0-9]+$/ { found=1 } END { exit !found }' \569 "$log_path" || return 1570 grep -Fxq "path_mode=${expected_path_mode}" "$log_path" || return 1571 grep -Fxq "media_path_mode=${expected_path_mode}" "$log_path" || return 1572 grep -Fxq "input_path_mode=${expected_path_mode}" "$log_path" || return 1573 if [[ "$media_transport" == "iroh-moq" ]]; then574 grep -Fxq "control_path_mode=${expected_path_mode}" "$log_path" || return 1575 grep -Fxq 'control_alpn=sigil/control/1' "$log_path" || return 1576 grep -Fxq 'group_sequence=monotonic' "$log_path" || return 1577 grep -Fxq 'moq_group_capacity=1' "$log_path" || return 1578 grep -Fxq 'moq_catalog=goq-v1' "$log_path" || return 1579 grep -Fxq 'moq_unrecovered_group_gaps=0' "$log_path" || return 1580 grep -Fxq 'moq_historical_suffix_frames=0' "$log_path" || return 1581 grep -Fxq 'recovery_barrier=configured-idr' "$log_path" || return 1582 awk -F= '$1 == "moq_group_gaps" && $2 ~ /^[0-9]+$/ { found=1 } END { exit !found }' \583 "$log_path" || return 1584 awk -F= '$1 == "moq_cancelled_groups" && $2 ~ /^[0-9]+$/ && $2 > 0 { found=1 } END { exit !found }' \585 "$log_path" || return 1586 awk -F= '$1 == "moq_maximum_group_objects" && $2 ~ /^[0-9]+$/ && $2 > 0 && $2 <= 256 { found=1 } END { exit !found }' \587 "$log_path" || return 1588 awk -F= '$1 == "moq_maximum_group_bytes" && $2 ~ /^[0-9]+$/ && $2 > 0 && $2 <= 33554432 { found=1 } END { exit !found }' \589 "$log_path" || return 1590 fi591 if [[ "$media_transport" == "iroh-moq" && "$expected_request_id" == 1 ]]; then592 grep -Fxq 'slow_consumer=ok' "$log_path" || return 1593 grep -Fxq 'slow_consumer_stall_ms=1500' "$log_path" || return 1594 grep -Fxq 'slow_consumer_first_post_stall=configured-idr' "$log_path" || return 1595 grep -Fxq 'slow_consumer_historical_suffix_frames=0' "$log_path" || return 1596 awk -F= '$1 == "slow_consumer_recovery_micros" && $2 ~ /^[0-9]+$/ && $2 <= 2000000 { found=1 } END { exit !found }' \597 "$log_path" || return 1598 awk -F= '$1 == "slow_consumer_cancellation_delta" && $2 ~ /^[0-9]+$/ && $2 > 0 { found=1 } END { exit !found }' \599 "$log_path" || return 1600 awk -F= '$1 == "slow_consumer_group_advance" && $2 ~ /^[0-9]+$/ && $2 > 0 { found=1 } END { exit !found }' \601 "$log_path" || return 1602 awk -F= '$1 == "slow_consumer_sequence_advance" && $2 ~ /^[0-9]+$/ && $2 > 0 { found=1 } END { exit !found }' \603 "$log_path" || return 1604 awk -F= '$1 == "slow_consumer_capture_advance_micros" && $2 ~ /^[0-9]+$/ && $2 >= 750000 { found=1 } END { exit !found }' \605 "$log_path" || return 1606 awk -F= '$1 == "slow_consumer_input_ack_micros" && $2 ~ /^[0-9]+$/ && $2 <= 2000000 { found=1 } END { exit !found }' \607 "$log_path" || return 1608 else609 grep -Fxq 'slow_consumer=not-requested' "$log_path" || return 1610 fi611 grep -Fxq "forced_relay=$([[ "$relay_only" == true ]] && printf ok || printf not-requested)" \612 "$log_path" || return 1613}614615run_probe() {616 if [[ "$relay_only" == true ]]; then617 set -- --relay-only "$@"618 fi619 if [[ "$media_v3" == true ]]; then620 "$probe_bin" --media-v3 "$@"621 else622 "$probe_bin" "$@"623 fi624}625626run_proof_probe() {627 local request_id="$1"628 shift629 run_probe --keyframe-smoke --keyframe-request-id "$request_id" "$@"630}631632cd "$workspace_dir"633cargo build --locked --profile "$cargo_profile" -p sigil-host --bins634635target_root="${CARGO_TARGET_DIR:-$workspace_dir/target}"636case "$target_root" in637 /*) ;;638 *) target_root="$workspace_dir/$target_root" ;;639esac640host_bin="$target_root/$profile_dir/sigil"641probe_bin="$target_root/$profile_dir/sigil-probe"642[[ -x "$host_bin" ]] || die "built host binary is missing: $host_bin"643[[ -x "$probe_bin" ]] || die "built probe binary is missing: $probe_bin"644645identity_path="$tmp_root/host.key"646runtime_path="$tmp_root/runtime"647identity_log="$tmp_root/identity.log"648host_log="$tmp_root/host.log"649primary_log="$tmp_root/primary.log"650secondary_log="$tmp_root/secondary.log"651652"$host_bin" identity init --output "$identity_path" >"$identity_log" 2>&1653node_id="$(sed -n 's/^node_id=//p' "$identity_log" | tail -n 1)"654[[ -n "$node_id" ]] || die "identity initialization did not print a node ID"655656RUST_LOG='info,sigil::server=debug' "$host_bin" serve \657 --identity "$identity_path" \658 --source test-pattern \659 --state-path "$runtime_path" \660 --width 1280 \661 --height 800 \662 --framerate 60 \663 --max-viewers "$viewers" \664 --ffmpeg "$ffmpeg_bin" \665 >"$host_log" 2>&1 &666host_pid=$!667start_watchdog "$host_pid" "$host_runtime_seconds" "$tmp_root/host.timeout"668host_watchdog_pid="$watchdog_pid"669wait_for_log_line "$host_log" 'status=ready' "$host_pid" 'sigil' || die "host did not become ready"670host_node_id="$(sed -n 's/^node_id=//p' "$host_log" | tail -n 1)"671[[ "$host_node_id" == "$node_id" ]] || die "ready host node ID does not match its identity"672if [[ "$stress" == true ]]; then673 host_rss_samples="$tmp_root/host-rss-kib.txt"674 (675 while kill -0 "$host_pid" 2>/dev/null; do676 ps -o rss= -p "$host_pid" 2>/dev/null | tr -d ' ' >>"$host_rss_samples" || true677 sleep 0.25678 done679 ) &680 resource_sampler_pid=$!681fi682683if [[ "$control_v2" == true ]]; then684 if [[ "$viewers" -gt 1 ]]; then685 viewer_logs=()686 viewer_identities=()687 viewer_pids=()688 replacement_log=""689 replacement_logs=()690 viewer=1691 while [[ "$viewer" -le "$viewers" ]]; do692 viewer_identity="$tmp_root/viewer-${viewer}.key"693 "$host_bin" identity init --output "$viewer_identity" \694 >"$tmp_root/viewer-${viewer}-identity.log" 2>&1695 viewer_identities+=("$viewer_identity")696 viewer_log="$tmp_root/control-v2-viewer-${viewer}.log"697 viewer_logs+=("$viewer_log")698 if [[ "$stall_viewer" -gt 0 ]]; then699 adaptive_profile="clean"700 if [[ "$viewer" -eq "$stall_viewer" ]]; then adaptive_profile="floor-breach"; fi701 "$probe_bin" --control-v2 --spectator --adaptive-profile "$adaptive_profile" \702 --identity "$viewer_identity" --node-id "$node_id" --frames "$primary_frames" \703 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800 \704 >"$viewer_log" 2>&1 &705 elif [[ "$focus_handoffs" -gt 0 && "$viewer" -le 2 ]]; then706 "$probe_bin" --control-v2 --focus-handoffs "$focus_handoffs" \707 --identity "$viewer_identity" --node-id "$node_id" --frames "$primary_frames" \708 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800 \709 >"$viewer_log" 2>&1 &710 elif [[ "$viewer" -gt 1 ]]; then711 "$probe_bin" --control-v2 --spectator \712 --identity "$viewer_identity" --node-id "$node_id" --frames "$primary_frames" \713 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800 \714 >"$viewer_log" 2>&1 &715 else716 "$probe_bin" --control-v2 --disconnect-with-focus \717 --identity "$viewer_identity" --node-id "$node_id" --frames "$primary_frames" \718 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800 \719 >"$viewer_log" 2>&1 &720 fi721 viewer_pids+=("$!")722 if [[ "$viewer" -eq 1 && "$focus_handoffs" -eq 0 && "$stall_viewer" -eq 0 ]]; then723 wait_for_log_count "$host_log" 'input v2 client accepted' 1 "$host_pid" 'sigil' \724 || die "slot-0 holder did not acquire focused input before spectators joined"725 fi726 viewer=$((viewer + 1))727 done728729 wait_for_log_count "$host_log" 'MoQ control v2 client accepted' "$viewers" "$host_pid" 'sigil' \730 || die "not all control-v2 viewers were admitted"731 wait_for_log_count "$host_log" 'authorized MoQ media attachment accepted' "$viewers" "$host_pid" 'sigil' \732 || die "not all control-v2 viewers attached to native MoQ"733734 if [[ "$replace_viewer" -gt 0 ]]; then735 cycle=1736 while [[ "$cycle" -le "$reconnect_cycles" ]]; do737 replacement_log="$tmp_root/control-v2-replacement-${replace_viewer}-${cycle}.log"738 replacement_logs+=("$replacement_log")739 replacement_role=(--spectator)740 if [[ "$stress" == true && "$cycle" -eq 1 ]]; then741 replacement_role=(--disconnect-with-focus)742 fi743 if ! run_bounded "$command_timeout_seconds" "$replacement_log" \744 "$probe_bin" --control-v2 "${replacement_role[@]}" \745 --identity "${viewer_identities[$((replace_viewer - 1))]}" \746 --node-id "$node_id" --frames "$reconnect_frames" \747 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800; then748 sed -n '1,240p' "$replacement_log" >&2 || true749 die "same-peer replacement cycle $cycle failed"750 fi751 grep -Fxq 'probe=ok' "$replacement_log" \752 || die "replacement viewer cycle $cycle did not progress"753 if [[ "$stress" == true ]]; then754 grep -Eq '^roster_viewers=(7|8)$' "$replacement_log" \755 || die "replacement cycle $cycle exceeded the bounded stress roster"756 else757 observed_roster="$(sed -n 's/^roster_viewers=\([0-9][0-9]*\)$/\1/p' \758 "$replacement_log" | head -1)"759 [[ -n "$observed_roster" ]] \760 || die "replacement cycle $cycle did not report a roster cardinality"761 # Cycle 1 runs immediately after every viewer attached and long762 # before any of them exhausts its frame budget, so the full roster is763 # deterministic there. Later cycles race the primaries' own exit, so764 # assert only what same-peer replacement actually governs: the roster765 # never grows past the configured bound, and the replacement joined a766 # generation that still has at least one survivor.767 if [[ "$cycle" -eq 1 ]]; then768 [[ "$observed_roster" -eq "$viewers" ]] \769 || die "replacement cycle 1 observed roster $observed_roster instead of $viewers"770 else771 [[ "$observed_roster" -ge 2 && "$observed_roster" -le "$viewers" ]] \772 || die "replacement cycle $cycle observed unbounded roster $observed_roster"773 fi774 fi775 grep -Fxq 'media_progress=monotonic' "$replacement_log" \776 || die "replacement cycle $cycle did not report monotonic progress"777 if [[ "$stress" == true && "$cycle" -eq 1 ]]; then778 grep -Fxq 'slot_0_input=ok' "$replacement_log" \779 || die "stress replacement did not prove the single slot-0 input holder"780 fi781 cycle=$((cycle + 1))782 done783 fi784785 viewer=1786 for viewer_pid in "${viewer_pids[@]}"; do787 if wait "$viewer_pid"; then viewer_status=0; else viewer_status=$?; fi788 if [[ "$viewer" -eq "$replace_viewer" ]]; then789 viewer=$((viewer + 1))790 continue791 fi792 [[ "$viewer_status" -eq 0 ]] || {793 sed -n '1,240p' "${viewer_logs[$((viewer - 1))]}" >&2 || true794 die "control-v2 viewer $viewer failed"795 }796 grep -Fxq 'probe=ok' "${viewer_logs[$((viewer - 1))]}" \797 || die "control-v2 viewer $viewer did not complete"798 viewer=$((viewer + 1))799 done800 viewer_pids=()801 if [[ "$stall_viewer" -gt 0 ]]; then802 viewer=1803 while [[ "$viewer" -le "$viewers" ]]; do804 if [[ "$viewer" -eq "$replace_viewer" ]]; then805 viewer=$((viewer + 1))806 continue807 elif [[ "$viewer" -eq "$stall_viewer" ]]; then808 grep -Fxq 'adaptive_profile=floor-breach' "${viewer_logs[$((viewer - 1))]}" \809 || die "stalled viewer did not run the floor-breach profile"810 grep -Fxq 'adaptive_floor_detach=ok' "${viewer_logs[$((viewer - 1))]}" \811 || die "stalled viewer was not detached after bounded isolated recovery"812 elif [[ "$assert_survivor_recovery" == true ]]; then813 grep -Fxq 'adaptive_profile=clean' "${viewer_logs[$((viewer - 1))]}" \814 || die "survivor did not run the clean adaptive profile"815 grep -Fxq 'adaptive_survivor_recovery=ok' "${viewer_logs[$((viewer - 1))]}" \816 || die "survivor did not recover after the stalled viewer was isolated"817 fi818 viewer=$((viewer + 1))819 done820 grep -Fq 'detached persistently below-floor viewer after bounded isolated recovery' "$host_log" \821 || die "host did not record terminal slow-viewer isolation"822 if [[ "$stress" == true ]]; then823 grep -Fq 'input v2 client accepted' "$host_log" \824 || die "stress run did not admit a focused input path"825 fi826 elif [[ "$focus_handoffs" -gt 0 ]]; then827 participant=1828 while [[ "$participant" -le 2 ]]; do829 grep -Fxq "focus_handoffs_observed=${focus_handoffs}" "${viewer_logs[$((participant - 1))]}" \830 || die "focus participant $participant did not observe all handoffs"831 participant=$((participant + 1))832 done833 else834 grep -Fxq 'slot_0_input=ok' "${viewer_logs[0]}" || die "slot-0 holder input failed"835 fi836 spectator=$([[ "$focus_handoffs" -gt 0 ]] && printf 3 || printf 2)837 if [[ "$stall_viewer" -gt 0 ]]; then spectator=1; fi838 while [[ "$spectator" -le "$viewers" ]]; do839 if [[ "$spectator" -eq "$replace_viewer" ]]; then840 spectator=$((spectator + 1))841 continue842 fi843 grep -Fxq 'spectator=ok' "${viewer_logs[$((spectator - 1))]}" \844 || die "viewer $spectator did not remain a spectator"845 spectator=$((spectator + 1))846 done847 roster_logs=("${viewer_logs[@]}")848 if [[ "${#replacement_logs[@]}" -gt 0 ]]; then roster_logs+=("${replacement_logs[@]}"); fi849 grep -Fhq "roster_viewers=${viewers}" "${roster_logs[@]}" 2>/dev/null \850 || die "no viewer observed the complete roster"851 wait_for_log_count "$host_log" 'shared media generation stopped with all sources cleaned up' \852 1 "$host_pid" 'sigil' || die "shared generation did not stop after the final viewer"853 [[ "$(grep -Fc -- 'shared media generation started' "$host_log" || true)" -eq 1 ]] \854 || die "multi-viewer control-v2 started more than one generation"855 [[ "$(grep -Fc -- 'shared media generation stopped with all sources cleaned up' "$host_log" || true)" -eq 1 ]] \856 || die "multi-viewer control-v2 stopped the generation more than once"857 clean_resource_log="$tmp_root/host-shared-resources.log"858 sed $'s/\033\[[0-9;]*m//g' "$host_log" >"$clean_resource_log"859 for resource_field in \860 'video_sources=1' 'video_encoders=1' 'audio_sources=0' 'audio_encoders=0' \861 'publishers=1' 'adaptive_actuators=1'; do862 grep -F 'shared media generation started' "$clean_resource_log" \863 | grep -Fq -- "$resource_field" \864 || die "shared generation resource-count evidence is missing $resource_field"865 done866 expected_membership_changes=$((viewers + reconnect_cycles))867 [[ "$(grep -Fc -- 'multi-viewer session membership changed' "$host_log" || true)" -ge "$expected_membership_changes" ]] \868 || die "host did not record bounded connect/replacement/disconnect audit events"869 for viewer_log in "${viewer_logs[@]}" "${replacement_logs[@]}"; do870 [[ -f "$viewer_log" ]] || continue871 grep -Fxq 'probe=ok' "$viewer_log" || continue872 grep -Fxq 'resource_summary_version=1' "$viewer_log" \873 || die "viewer resource summary is missing"874 grep -Fxq 'probe_media_object_capacity=4' "$viewer_log" \875 || die "viewer media-object queue bound is missing"876 done877 if [[ "$assert_neutral_before_successor" == true ]]; then878 clean_focus_log="$tmp_root/host-focus-transitions.log"879 sed $'s/\033\[[0-9;]*m//g' "$host_log" >"$clean_focus_log"880 if ! awk -v required="$focus_handoffs" '881 function transition_id(line, value) {882 if (match(line, /transition_id=[0-9]+/)) {883 value = substr(line, RSTART + 14, RLENGTH - 14)884 return value + 0885 }886 return 0887 }888 /focus transition entered no-inject neutralizing state/ {889 id = transition_id($0); if (id > 0) entered[id] = NR890 }891 /focus transition virtual input neutralized/ {892 id = transition_id($0); if (id > 0) neutral[id] = NR893 }894 /focus transition successor committed after neutralization/ {895 id = transition_id($0)896 if (id > 0) {897 committed[id] = NR898 count += 1899 }900 }901 END {902 if (count < required) exit 1903 for (id in committed) {904 if (!(id in entered) || !(id in neutral) || entered[id] >= neutral[id] || neutral[id] >= committed[id]) exit 1905 }906 }907 ' "$clean_focus_log"; then908 grep -F 'focus transition' "$clean_focus_log" >&2 || true909 die "focus successor was published before complete neutralization evidence"910 fi911 fi912913 kill -TERM "$host_pid" 2>/dev/null || true914 if wait "$host_pid"; then host_status=0; else host_status=$?; fi915 host_pid=""916 wait "$host_watchdog_pid" 2>/dev/null || true917 host_watchdog_pid=""918 [[ "$host_status" -eq 0 ]] || die "host exited with status $host_status"919 maximum_rss_kib=0920 if [[ "$stress" == true ]]; then921 stop_pid "$resource_sampler_pid"922 resource_sampler_pid=""923 maximum_rss_kib="$(awk 'NF && $1 > maximum { maximum = $1 } END { print maximum + 0 }' "$host_rss_samples")"924 [[ "$maximum_rss_kib" -gt 0 && "$maximum_rss_kib" -le 2097152 ]] \925 || die "stress RSS exceeded the bounded 2048 MiB gate"926 fi927 printf 'loopback_proof=ok\n'928 printf 'profile=%s\n' "$profile"929 printf 'host_sha256=%s\n' "$(sha256_file "$host_bin")"930 printf 'control_v2=ok\n'931 printf 'viewers=%s\n' "$viewers"932 printf 'same_peer_replacement=%s\n' "$([[ "$replace_viewer" -gt 0 ]] && printf ok || printf not-requested)"933 printf 'reconnect_cycles=%s\n' "$reconnect_cycles"934 printf 'shared_generation=ok\n'935 printf 'slot_0_single_holder=ok\n'936 printf 'focus_handoffs=%s\n' "$focus_handoffs"937 printf 'neutral_before_successor=%s\n' "$([[ "$assert_neutral_before_successor" == true ]] && printf ok || printf not-requested)"938 printf 'stalled_viewer=%s\n' "$stall_viewer"939 printf 'survivor_recovery=%s\n' "$([[ "$assert_survivor_recovery" == true ]] && printf ok || printf not-requested)"940 printf 'spectator_progress=ok\n'941 printf 'resource_summary_version=1\n'942 printf 'shared_video_sources=1\n'943 printf 'shared_video_encoders=1\n'944 printf 'shared_audio_sources=0\n'945 printf 'shared_audio_encoders=0\n'946 printf 'shared_publishers=1\n'947 printf 'shared_adaptive_actuators=1\n'948 printf 'maximum_host_rss_kib=%s\n' "$maximum_rss_kib"949 printf 'bounded_queues=ok\n'950 printf 'revocation_isolation=not-applicable-direct-proof-mode\n'951 printf 'final_cleanup=ok\n'952 printf 'stress=%s\n' "$([[ "$stress" == true ]] && printf ok || printf not-requested)"953 exit 0954 fi955 v2_log="$tmp_root/control-v2.log"956 v1_log="$tmp_root/control-v1-compatibility.log"957 if ! run_bounded "$command_timeout_seconds" "$v2_log" \958 "$probe_bin" --control-v2 --node-id "$node_id" --frames "$primary_frames" \959 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800; then960 sed -n '1,240p' "$v2_log" >&2 || true961 die "control-v2 probe failed"962 fi963 grep -Fxq 'probe=ok' "$v2_log" || die "control-v2 probe did not complete"964 grep -Fxq 'control_alpn=sigil/control/2' "$v2_log" || die "control-v2 ALPN evidence is missing"965 grep -Fxq 'input_alpn=sigil/input/2' "$v2_log" || die "input-v2 ALPN evidence is missing"966 grep -Fxq 'slot_0_input=ok' "$v2_log" || die "slot-0 input evidence is missing"967 grep -Fxq 'focus_release=ok' "$v2_log" || die "focus release evidence is missing"968 if [[ "$assert_shared_generation" == true ]]; then969 grep -Fxq 'media_generation_scope=host' "$v2_log" \970 || die "host-scoped media generation evidence is missing"971 grep -Eq '^media_generation_id=[1-9][0-9]*$' "$v2_log" \972 || die "media generation ID evidence is missing"973 grep -Eq '^media_broadcast_name=sigil/generation/[1-9][0-9]*/media$' "$v2_log" \974 || die "generation-scoped broadcast evidence is missing"975 fi976 awk -F= '$1 == "initial_snapshot_revision" && $2 ~ /^[1-9][0-9]*$/ { found=1 } END { exit !found }' \977 "$v2_log" || die "initial snapshot evidence is missing"978 awk -F= '$1 == "focus_generation" && $2 ~ /^[1-9][0-9]*$/ { found=1 } END { exit !found }' \979 "$v2_log" || die "focus generation evidence is missing"980 wait_for_log_count "$host_log" 'MoQ control v2 client released' 1 "$host_pid" 'sigil' \981 || die "control-v2 session was not released"982 wait_for_log_count "$host_log" 'input v2 client released' 1 "$host_pid" 'sigil' \983 || die "input-v2 session was not neutralized and released"984 if [[ "$assert_shared_generation" == true ]]; then985 clean_host_log="$tmp_root/host-shared-generation.log"986 sed $'s/\033\[[0-9;]*m//g' "$host_log" >"$clean_host_log"987 wait_for_log_count "$host_log" 'shared media generation stopped with all sources cleaned up' \988 1 "$host_pid" 'sigil' || die "shared generation did not clean up after the final viewer"989 [[ "$(grep -Fc -- 'shared media generation started' "$host_log" || true)" -eq 1 ]] \990 || die "control-v2 started more than one shared media generation"991 [[ "$(grep -Fc -- 'shared media generation stopped with all sources cleaned up' "$host_log" || true)" -eq 1 ]] \992 || die "control-v2 stopped the shared media generation more than once"993 for resource_field in \994 'video_sources=1' 'video_encoders=1' 'audio_sources=0' 'audio_encoders=0' 'publishers=1'; do995 grep -F 'shared media generation started' "$clean_host_log" | grep -Fq -- "$resource_field" \996 || die "shared generation resource-count evidence is missing $resource_field"997 done998 fi9991000 # The unchanged v1 path remains explicitly usable after a v2 generation.1001 if ! run_bounded "$command_timeout_seconds" "$v1_log" \1002 "$probe_bin" --node-id "$node_id" --frames 4 \1003 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800; then1004 sed -n '1,160p' "$v1_log" >&2 || true1005 die "v1 compatibility probe failed after control-v2"1006 fi1007 grep -Fxq 'probe=ok' "$v1_log" || die "v1 compatibility probe did not complete"1008 grep -Fxq 'control_alpn=sigil/control/1' "$v1_log" || die "v1 compatibility ALPN evidence is missing"10091010 kill -TERM "$host_pid" 2>/dev/null || true1011 if wait "$host_pid"; then host_status=0; else host_status=$?; fi1012 host_pid=""1013 wait "$host_watchdog_pid" 2>/dev/null || true1014 host_watchdog_pid=""1015 [[ "$host_status" -eq 0 ]] || die "host exited with status $host_status"1016 printf 'loopback_proof=ok\n'1017 printf 'control_v2=ok\n'1018 printf 'viewers=1\n'1019 printf 'initial_snapshot=ok\n'1020 printf 'slot_0_focus=grant-release\n'1021 printf 'input_v2=ok\n'1022 printf 'neutralization=reset-before-input-lease-release\n'1023 printf 'v1_compatibility=ok\n'1024 printf 'shared_generation=%s\n' "$([[ "$assert_shared_generation" == true ]] && printf ok || printf not-requested)"1025 exit 01026fi10271028primary_probe_args=(1029 --node-id "$node_id"1030 --frames "$primary_frames"1031 --timeout-seconds "$probe_timeout_seconds"1032 --expect-size 1280x8001033)1034if [[ "$media_v3" == false ]]; then1035 primary_probe_args+=(--slow-consumer-ms 1500)1036fi1037run_proof_probe 1 "${primary_probe_args[@]}" >"$primary_log" 2>&1 &1038primary_pid=$!1039start_watchdog "$primary_pid" "$command_timeout_seconds" "$tmp_root/primary.timeout"1040primary_watchdog_pid="$watchdog_pid"10411042if ! wait_for_log_count "$host_log" "$media_accept_log" 1 "$primary_pid" 'primary probe'; then1043 sed -n '1,240p' "$primary_log" >&2 || true1044 die "primary session did not become active"1045fi1046if ! kill -0 "$primary_pid" 2>/dev/null; then1047 die "primary probe ended before the concurrent rejection check"1048fi10491050if run_bounded "$probe_timeout_seconds" "$secondary_log" \1051 run_probe --node-id "$node_id" --frames 1 \1052 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800; then1053 die "secondary client was accepted while the primary was active"1054fi1055if [[ -e "${secondary_log}.timeout" ]]; then1056 die "secondary client timed out instead of receiving the active-client rejection"1057fi1058if ! grep -Fxq "Error: host rejected ${session_gate_name} stream: host already has an active client" "$secondary_log"; then1059 printf 'secondary probe did not return the expected rejection. Log follows:\n' >&21060 sed -n '1,120p' "$secondary_log" >&2 || true1061 die "unexpected secondary-client failure"1062fi10631064if wait "$primary_pid"; then1065 primary_status=01066else1067 primary_status=$?1068fi1069primary_pid=""1070wait "$primary_watchdog_pid" 2>/dev/null || true1071primary_watchdog_pid=""1072[[ ! -e "$tmp_root/primary.timeout" ]] || die "primary probe exceeded its deadline"1073[[ "$primary_status" -eq 0 ]] || {1074 sed -n '1,240p' "$primary_log" >&2 || true1075 die "primary probe exited with status $primary_status"1076}1077validate_probe_log "$primary_log" "$primary_frames" 1 || {1078 sed -n '1,240p' "$primary_log" >&2 || true1079 die "primary probe evidence is incomplete"1080}1081wait_for_keyframe_request "$host_log" 1 "$host_pid" 'sigil' \1082 || die "primary keyframe request was not accepted by the host"10831084wait_for_log_count "$host_log" "$media_release_log" 1 "$host_pid" 'sigil' \1085 || die "primary session was not released"1086wait_for_log_count "$host_log" 'input client released' 1 "$host_pid" 'sigil' \1087 || die "primary input session was not drained"10881089cycle=11090while [[ "$cycle" -le "$reconnect_cycles" ]]; do1091 reconnect_log="$tmp_root/reconnect-${cycle}.log"1092 reconnect_request_id=$((cycle + 1))1093 if ! run_bounded "$command_timeout_seconds" "$reconnect_log" \1094 run_proof_probe "$reconnect_request_id" --node-id "$node_id" --frames "$reconnect_frames" \1095 --timeout-seconds "$probe_timeout_seconds" --expect-size 1280x800; then1096 sed -n '1,240p' "$reconnect_log" >&2 || true1097 die "reconnect cycle $cycle failed"1098 fi1099 validate_probe_log "$reconnect_log" "$reconnect_frames" "$reconnect_request_id" || {1100 sed -n '1,240p' "$reconnect_log" >&2 || true1101 die "reconnect cycle $cycle evidence is incomplete"1102 }1103 wait_for_keyframe_request "$host_log" "$reconnect_request_id" "$host_pid" 'sigil' \1104 || die "reconnect cycle $cycle keyframe request was not accepted by the host"1105 wait_for_log_count "$host_log" "$media_release_log" "$((cycle + 1))" "$host_pid" 'sigil' \1106 || die "reconnect cycle $cycle was not released"1107 wait_for_log_count "$host_log" 'input client released' "$((cycle + 1))" "$host_pid" 'sigil' \1108 || die "reconnect input cycle $cycle was not drained"1109 cycle=$((cycle + 1))1110done11111112request_id=11113while [[ "$request_id" -le $((reconnect_cycles + 1)) ]]; do1114 request_count="$(host_keyframe_request_count "$host_log" "$request_id")"1115 [[ "$request_count" -eq 1 ]] \1116 || die "keyframe request $request_id was accepted $request_count times; expected exactly once"1117 request_id=$((request_id + 1))1118done11191120kill -TERM "$host_pid" 2>/dev/null || true1121if wait "$host_pid"; then1122 host_status=01123else1124 host_status=$?1125fi1126host_pid=""1127wait "$host_watchdog_pid" 2>/dev/null || true1128host_watchdog_pid=""1129[[ ! -e "$tmp_root/host.timeout" ]] || die "host exceeded its bounded runtime"1130[[ "$host_status" -eq 0 ]] || {1131 sed -n '1,240p' "$host_log" >&2 || true1132 die "host exited with status $host_status"1133}1134grep -Fq 'shutdown signal received' "$host_log" || die "host did not record a graceful shutdown"11351136printf 'loopback_proof=ok\n'1137printf 'profile=%s\n' "$profile"1138printf 'host_binary=%s\n' "$host_bin"1139printf 'host_sha256=%s\n' "$(sha256_file "$host_bin")"1140printf 'probe_binary=%s\n' "$probe_bin"1141printf 'probe_sha256=%s\n' "$(sha256_file "$probe_bin")"1142printf 'node_id=%s\n' "$node_id"1143printf 'primary_frames=%s\n' "$primary_frames"1144grep -E '^(transport|control_alpn|transport_alpn|first_configured_idr|frame_sequence|group_sequence|keyframe_recovery|keyframe_recovery_micros|keyframe_request_id|keyframes|sequence_gaps|media_objects_dropped|media_objects_late|moq_catalog|moq_group_capacity|moq_cancelled_groups|moq_group_gaps|moq_unrecovered_group_gaps|moq_maximum_group_objects|moq_maximum_group_bytes|moq_historical_suffix_frames|recovery_barrier|slow_consumer|slow_consumer_stall_ms|slow_consumer_first_post_stall|slow_consumer_historical_suffix_frames|slow_consumer_recovery_micros|slow_consumer_cancellation_delta|slow_consumer_group_advance|slow_consumer_sequence_advance|slow_consumer_capture_advance_micros|slow_consumer_input_ack_micros|input_ack_micros|path_mode|path_rtt_ms|control_path_mode|control_path_rtt_ms|media_path_mode|media_path_rtt_ms|input_path_mode|input_path_rtt_ms|forced_relay)=' "$primary_log"1145printf 'keyframe_request_correlation=unique\n'1146printf 'active_client_rejection=ok\n'1147printf 'reconnect_cycles=%s\n' "$reconnect_cycles"1148printf 'cleanup=ok\n'