|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Stale-log watchdog for game-ci Unity test containers. |
| 3 | +# |
| 4 | +# game-ci/unity-test-runner starts the Unity editor with |
| 5 | +# `-logFile <workspace>/artifacts/<mode>.log`, so while the step runs the |
| 6 | +# editor log grows on the host but the step's own console stays silent. When |
| 7 | +# the editor deadlocks mid-run the job burns the whole `timeout-minutes` |
| 8 | +# budget with no output (run 29613116640 attempt 1: console silent from |
| 9 | +# 21:04:21 right after the "Testing in editmode" banner until manually |
| 10 | +# cancelled at 22:14:52 — 70 minutes). |
| 11 | +# |
| 12 | +# This script polls the editor log's byte size from the host and kills the |
| 13 | +# Unity container when the log stops growing, failing the test step within |
| 14 | +# ~STALL_THRESHOLD seconds instead. Arming rules mirror the build-phase |
| 15 | +# watchdog in scripts/cloudbuild/build.py (LOG_STALL_THRESHOLD): |
| 16 | +# - the stall clock arms only after one observed size *increase* — a log |
| 17 | +# that never grows reads as "watchdog inactive", never as "stalled"; |
| 18 | +# - a size *decrease* (log replaced or truncated) resets the clock. |
| 19 | +# If the log stalls but no container matches the image, the step is already |
| 20 | +# tearing down — the watchdog exits without reporting a stall. |
| 21 | +# |
| 22 | +# Usage: test-stale-log-watchdog.sh <editor-log> <container-image> <status-file> |
| 23 | +# <editor-log> host path of the Unity editor log to watch |
| 24 | +# <container-image> image the Unity container runs (docker ancestor filter) |
| 25 | +# <status-file> written (JSON) only when a stall kill actually happened |
| 26 | +# Env overrides: |
| 27 | +# STALL_THRESHOLD seconds without log growth before the kill (default 900) |
| 28 | +# POLL_INTERVAL seconds between size probes (default 30) |
| 29 | +# MAX_LIFETIME hard cap on the watchdog's own runtime (default 7200) |
| 30 | +# |
| 31 | +# Deliberately no `set -e`: the poll loop must survive transient stat/docker |
| 32 | +# failures; every external call is individually guarded instead. |
| 33 | +set -u |
| 34 | + |
| 35 | +LOG_FILE=$1 |
| 36 | +IMAGE=$2 |
| 37 | +STATUS_FILE=$3 |
| 38 | +STALL_THRESHOLD=${STALL_THRESHOLD:-900} |
| 39 | +POLL_INTERVAL=${POLL_INTERVAL:-30} |
| 40 | +MAX_LIFETIME=${MAX_LIFETIME:-7200} |
| 41 | + |
| 42 | +start=$(date +%s) |
| 43 | +last_size=-1 |
| 44 | +last_growth=$start |
| 45 | +growth_observed=false |
| 46 | + |
| 47 | +echo "Watching $LOG_FILE for container image $IMAGE (stall threshold ${STALL_THRESHOLD}s, poll ${POLL_INTERVAL}s)" |
| 48 | + |
| 49 | +while :; do |
| 50 | + sleep "$POLL_INTERVAL" |
| 51 | + now=$(date +%s) |
| 52 | + |
| 53 | + if (( now - start > MAX_LIFETIME )); then |
| 54 | + echo "Max lifetime (${MAX_LIFETIME}s) reached - exiting without a verdict." |
| 55 | + exit 0 |
| 56 | + fi |
| 57 | + |
| 58 | + size=$(stat -c%s "$LOG_FILE" 2>/dev/null) || continue |
| 59 | + |
| 60 | + if (( last_size < 0 || size < last_size )); then |
| 61 | + # First observation, or the log was replaced/truncated. |
| 62 | + last_size=$size |
| 63 | + last_growth=$now |
| 64 | + continue |
| 65 | + fi |
| 66 | + |
| 67 | + if (( size > last_size )); then |
| 68 | + if [ "$growth_observed" = false ]; then |
| 69 | + echo "Log is growing ($size bytes) - stall detection armed." |
| 70 | + fi |
| 71 | + last_size=$size |
| 72 | + last_growth=$now |
| 73 | + growth_observed=true |
| 74 | + continue |
| 75 | + fi |
| 76 | + |
| 77 | + if [ "$growth_observed" = true ] && (( now - last_growth > STALL_THRESHOLD )); then |
| 78 | + stalled_for=$(( now - last_growth )) |
| 79 | + # Results on disk mean the editor already exited: the log went quiet |
| 80 | + # during game-ci's post-run work, which is not a hang. |
| 81 | + if compgen -G "$(dirname "$LOG_FILE")/*.xml" > /dev/null; then |
| 82 | + echo "Log stalled for ${stalled_for}s but test results are already written - exiting without a verdict." |
| 83 | + exit 0 |
| 84 | + fi |
| 85 | + containers=$(docker ps -q --filter "ancestor=$IMAGE" 2>/dev/null || true) |
| 86 | + if [ -z "$containers" ]; then |
| 87 | + echo "Log stalled for ${stalled_for}s but no container is running $IMAGE - step is tearing down, exiting." |
| 88 | + exit 0 |
| 89 | + fi |
| 90 | + echo "Log has not grown for ${stalled_for}s (size $size bytes). Killing Unity container(s): $containers" |
| 91 | + jq -n --argjson stalled "$stalled_for" --argjson size "$size" --arg log "$LOG_FILE" \ |
| 92 | + '{stalledSeconds: $stalled, logSizeBytes: $size, log: $log}' > "$STATUS_FILE" |
| 93 | + # container ids are newline/space separated words |
| 94 | + # shellcheck disable=SC2086 |
| 95 | + docker kill $containers || true |
| 96 | + exit 0 |
| 97 | + fi |
| 98 | +done |
0 commit comments