#!/usr/bin/env bash # 轻闸探针正主——headless 安全段固化,禁再手拼同款 Godot 命令。 # 三闸:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 Godot # (按命令行精确匹配本 game 路径,不误杀其他 worktree 会话的合法进程)③末尾残留清点=批末闸(必须 0)。 # 段:ui_input_probe=默认必跑;ui_struct_probe=默认必跑( # 全 UI 屏结构断言:按钮互压/等宽/出屏/零尺寸/标题屏双列);ui_contrast_probe=默认必跑( # UI 文字对比度机检 ≥4.5:1 无区域例外,测点表随改动维护);--click=加跑 ui_click_probe 行为走查 # (自退出+退出码闸;超时多半=探针未适配新停点层,需做探针适配)。 # layout_probe=默认必跑(分辨率几何断言,档位表是数据/断言是一份函数, # 增删档位只改表内一行;用 content_scale_size 改逻辑视口,headless 下真生效—— # get_window().size 在 headless 下不改变视口,其「1920 档恒定」类断言是 # 永远为真的假断言,禁用)。 # 判定口径(三硬闸):每段走 verdict 函数——①退出码为唯一权威判据(PASS 串降级为日志) # ②脚本错误扫描命中即红(tools/error_scan.py,标记与 run_smoke.py 同源)③PASS 串保留为附加判据。 # --full 段(重闸件):ui_probe/ui_tune_probe/t17_defer_probe/fx_probe # 四件带断言探针;轻闸默认段断言面零删减零改动(--full 只增不改)。 # 渲染面归 render_gate.sh 正主(窗口化:shot_probe 七屏+fx_shot/board_shot 取证+棋盘 golden 逐像素)。 # 用法: tools/probe_gate.sh [game目录] [--click] [--full] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree) # 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell;Godot 路径可用环境变量 GODOT_BIN 覆盖。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" GAME=""; WITH_CLICK=0; WITH_FULL=0 for a in "$@"; do case "$a" in --click) WITH_CLICK=1 ;; --full) WITH_FULL=1 ;; *) GAME="$a" ;; esac done GAME="${GAME:-$REPO/game}" GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式 TMP="$(mktemp -d /tmp/probegate_XXXXXX)" trap 'rm -rf "$TMP"' EXIT _rc=0 # 错误扫描闸件用纯标准库 python(不依赖 PIL),优先带 PIL 的 gamedev venv 以保持与重闸同解释器。 PY="" for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "python" "python3"; do if "$cand" -c "import sys" >/dev/null 2>&1; then PY="$cand"; break; fi done if [ -z "$PY" ]; then echo "[probe_gate] 警告:未找到 python,脚本错误扫描闸不生效(三硬闸缺一)" fi # 判定三硬闸——各探针段统一走本函数,禁再手拼判定: # ①退出码为唯一权威判据(PASS 串降级为日志,不得再当判据) # ②脚本错误扫描命中即红(SCRIPT ERROR/Parse Error 不改退出码,只能外部扫) # ③签名标记保留为附加判据(退出码 0 却无该探针的签名行=静默退出,仍判红)。 # 标记默认 PASS;ui_click_probe 本就走退出码语义、日志不写 PASS,传它自己的摘要标记。 verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记] local name="$1" log="$2" st="$3" mark="${4:-PASS}" if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then echo "[probe_gate] FAIL — $name 超时挂死(exit=$st),孤儿已清"; return 1 fi if [ "$st" -ne 0 ]; then echo "[probe_gate] FAIL — $name 退出码=$st(退出码为唯一权威判据,PASS 串不抵)"; return 1 fi if [ -n "$PY" ] && ! "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1; then echo "[probe_gate] FAIL — $name 脚本错误扫描命中:" sed -n '1,6p' "$TMP/err.txt" | sed 's/^/ /' return 1 fi if ! grep -q "$mark" "$log"; then echo "[probe_gate] FAIL — $name 未见签名标记「$mark」(exit=$st——退出码 0 但零输出亦判红)"; return 1 fi return 0 } orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算) printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r' } kill_orphans() { local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done } # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 kill_orphans fi pre="$(orphans | wc -l | tr -d ' ')" if [ "$pre" -gt 0 ]; then echo "[probe_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi echo "======== ui_input_probe ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_input_probe.tscn >"$TMP/uip.log" 2>&1 st=$?; kill_orphans grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12 verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1 echo "======== ui_struct_probe ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_struct_probe.tscn >"$TMP/stp.log" 2>&1 st=$?; kill_orphans grep -E "PASS|FAIL" "$TMP/stp.log" | head -12 verdict "ui_struct_probe" "$TMP/stp.log" "$st" || _rc=1 echo "======== layout_probe(分辨率几何·两档) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/layout_probe.tscn >"$TMP/lay.log" 2>&1 st=$?; kill_orphans grep -E "PASS|FAIL" "$TMP/lay.log" | head -12 verdict "layout_probe" "$TMP/lay.log" "$st" || _rc=1 echo "======== ui_contrast_probe ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_contrast_probe.tscn >"$TMP/ctp.log" 2>&1 st=$?; kill_orphans grep -E "PASS|FAIL" "$TMP/ctp.log" | head -12 verdict "ui_contrast_probe" "$TMP/ctp.log" "$st" || _rc=1 echo "======== screen_flow_probe(L3 屏壳切换·迭代 29 T4) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/screen_flow_probe.tscn >"$TMP/sfp.log" 2>&1 st=$?; kill_orphans grep -E "screen_flow_probe" "$TMP/sfp.log" | tail -3 verdict "screen_flow_probe" "$TMP/sfp.log" "$st" "screen_flow_probe" || _rc=1 if [ "$WITH_CLICK" -eq 1 ]; then echo "======== ui_click_probe 行为走查 ========" timeout 240 "$G" --headless --path "$GAME_WIN" res://smoke/ui_click_probe.tscn >"$TMP/clk.log" 2>&1 st=$?; kill_orphans grep -E "click_probe" "$TMP/clk.log" | tail -4 verdict "ui_click_probe" "$TMP/clk.log" "$st" "click_probe" || _rc=1 fi # ── --full 段(四件带断言探针入闸;实测时长 ui_probe 17.4s/ # ui_tune_probe 49.6s/t17_defer_probe 63.0s/fx_probe 21.9s,timeout 120=2-7×余量) ── if [ "$WITH_FULL" -eq 1 ]; then echo "======== ui_probe(--full 收编) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_probe.tscn >"$TMP/flp.log" 2>&1 st=$?; kill_orphans grep -E "\[ui_probe\]" "$TMP/flp.log" | tail -2 verdict "ui_probe" "$TMP/flp.log" "$st" || _rc=1 echo "======== ui_tune_probe(--full 收编) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_tune_probe.tscn >"$TMP/flt.log" 2>&1 st=$?; kill_orphans grep -E "\[tune_probe\]" "$TMP/flt.log" | tail -2 verdict "ui_tune_probe" "$TMP/flt.log" "$st" || _rc=1 echo "======== t17_defer_probe(--full 收编) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/t17_defer_probe.tscn >"$TMP/fld.log" 2>&1 st=$?; kill_orphans grep -E "\[t17_defer\]" "$TMP/fld.log" | tail -2 verdict "t17_defer_probe" "$TMP/fld.log" "$st" || _rc=1 echo "======== fx_probe(--full 收编) ========" timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/fx_probe.tscn >"$TMP/flx.log" 2>&1 st=$?; kill_orphans grep -E "\[fx_probe\]" "$TMP/flx.log" | tail -2 verdict "fx_probe" "$TMP/flx.log" "$st" || _rc=1 fi echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)" echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)" if [ "$left" -ne 0 ]; then echo "[probe_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi if [ "$_rc" -eq 0 ]; then echo "[probe_gate] 全绿"; else echo "[probe_gate] 存在 FAIL,见上"; fi exit "$_rc"