games-development-ai/tier2/harness/serve-and-play-phaser.sh
lili 8d4654ab63 fix(tier2): serve-and-play chrome cleanup 杀整树(治泄漏)+ 切片二 U5 执行发现
chrome 泄漏根因:cleanup() 只 kill $CHROME_PID(父)→ renderer/gpu 子进程 reparent 给 init 成孤儿、
跨款累积(实测 U5 Round1 撞 wall_timeout、杀批组仍残留)。修=cleanup 加 pkill -f "$USER_DATA_DIR"
杀本实例整棵 chrome 树(user-data-dir mktemp 唯一、不误伤别的 chrome/live worker)。

plan 执行发现:① env 闸缺口(fresh worktree 无 node_modules/esbuild → build 失败 → M3 假性不收敛,
已补 U1 S0 + Risks);② M3 已证能收敛(smoke 面包+水果 clean accept、九门全过);
③ U5 Round1 被 confound(chrome 泄漏 + live worker:9501/@8200 争 4 核)、需隔离干净重跑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:54:17 -07:00

102 lines
5.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# serve-and-play-phaser.sh —— tier2 富游戏线真玩取证一键编排(fork 自 wg1 serve-and-play.sh,Phaser 化)
# owner:tier2 引擎线(M2 harness)+ 集成线(run.run_gates 调它)| cwd 必须 = 仓根(games-tier2-engine/)
#
# 【与 wg1 版的硬差异】
# ① serve【仓根】而非 game-runtime:宿主页 index.html 经绝对路径 /tier2/engine/boot-phaser-host.js 引 host,
# 而 tier2/ 是 game-runtime/ 的兄弟目录、在它外面 → 必须 serve 仓根才同时够到 game-runtime/games/ 与 tier2/engine/。
# ② 端口段 4330/9322(避开 wg1 的 4320/9222,可并行)。
# ③ gameId 形参 = 仓内相对路径段 _tier2-gen/<id>;URL = http://localhost:<port>/game-runtime/games/_tier2-gen/<id>/index.html。
# ④ 真玩驱动 = play-phaser.cdp.cjs(Phaser 探针重写版,非 wg1 的 play.cdp.cjs)。
#
# 用法:bash tier2/harness/serve-and-play-phaser.sh <gameRel> [port] [cdpPort]
# <gameRel> 例:_tier2-gen/feie-001
# 流程:净场 → 起 static-serve(仓根) → 起 headless Chrome → 等就绪 → play-phaser.cdp.cjs → trap 清理,返 play 退出码。
# 真跑必须在 mini-desktop(6c6g 禁 chrome)。本机只 node --check 静态校验本脚本无法做(bash),用 bash -n。
set -u
GAME_REL="${1:?用法: serve-and-play-phaser.sh <gameRel 如 _tier2-gen/feie-001> [port] [cdpPort]}"
PORT="${2:-4330}"
CDP_PORT="${3:-9322}"
# 仓根 = 本脚本所在目录上溯两级(tier2/harness/ → tier2/ → 仓根)。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# 生成游戏目录(绝对)+ 证据目录(显式给 play harness 的 --out,避免它内部 path.resolve('games',...) 依赖 cwd)。
GAME_DIR="$REPO_ROOT/game-runtime/games/$GAME_REL"
EV_DIR="$GAME_DIR/evidence"
[ -d "$GAME_DIR" ] || { echo "❌ 游戏目录不存在: $GAME_DIR"; exit 6; }
[ -f "$GAME_DIR/index.html" ] || { echo "❌ 宿主页缺失: $GAME_DIR/index.html(run.scaffold 应已拷模板)"; exit 6; }
[ -f "$GAME_DIR/bundle.iife.js" ] || { echo "❌ 构建产物缺失: $GAME_DIR/bundle.iife.js(先跑 build-phaser.mjs)"; exit 7; }
mkdir -p "$EV_DIR"
# Chrome 路径探测(跨平台:CHROME_BIN 显式覆盖 > Linux(mini-desktop x86 e2e门) > macOS)。
CHROME=""
if [ -n "${CHROME_BIN:-}" ] && [ -x "${CHROME_BIN}" ]; then
CHROME="${CHROME_BIN}"
else
for p in "/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/opt/google/chrome/chrome" \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium"; do
[ -x "$p" ] && CHROME="$p" && break
done
[ -z "$CHROME" ] && CHROME="$(command -v google-chrome chromium chromium-browser 2>/dev/null | head -1)"
fi
[ -z "$CHROME" ] && { echo "❌ 未找到 Chrome(探测 /usr/bin、/Applications、PATH、CHROME_BIN 均无)"; exit 3; }
USER_DATA_DIR="$(mktemp -d -t tier2-chrome-XXXX)"
SERVE_PID=""; CHROME_PID=""
cleanup() {
# 杀整棵 chrome 进程树:只 kill $CHROME_PID(父)会把 renderer/gpu 子进程 reparent 给 init 成孤儿、泄漏
# (实测串行批跑跨款累积、撞 wall_timeout)。用唯一的 $USER_DATA_DIR 精确匹配本实例【全部】chrome 进程,
# 不误伤别的 chrome 实例 / live worker 的 chrome(它们 user-data-dir 不同)。
[ -n "$USER_DATA_DIR" ] && pkill -f "$USER_DATA_DIR" 2>/dev/null
[ -n "$CHROME_PID" ] && kill "$CHROME_PID" 2>/dev/null
[ -n "$SERVE_PID" ] && kill "$SERVE_PID" 2>/dev/null
rm -rf "$USER_DATA_DIR" 2>/dev/null
}
trap cleanup EXIT INT TERM
# 净场:杀掉占用端口的旧进程(保守,仅杀本脚本端口相关)。
lsof -ti tcp:"$PORT" 2>/dev/null | xargs kill 2>/dev/null || true
lsof -ti tcp:"$CDP_PORT" 2>/dev/null | xargs kill 2>/dev/null || true
sleep 0.3
# 1) 静态服务器:serve【仓根】(复用 game-runtime 的零依赖 static-serve.cjs;它 serve 任意 root 子树,拒路径穿越)。
node "$REPO_ROOT/game-runtime/test/harness/static-serve.cjs" "$REPO_ROOT" "$PORT" \
>/tmp/tier2-serve-$PORT.log 2>&1 &
SERVE_PID=$!
# 2) Chrome headless(CDP 必备旗标:--remote-allow-origins=* + --autoplay-policy=...)。
"$CHROME" --headless=new --no-sandbox --no-zygote --disable-gpu \
--remote-debugging-port="$CDP_PORT" --remote-allow-origins='*' \
--autoplay-policy=no-user-gesture-required \
--user-data-dir="$USER_DATA_DIR" about:blank >/tmp/tier2-chrome-$PORT.log 2>&1 &
CHROME_PID=$!
# 3) 就绪等待(serve 仓根 → 宿主页 URL 含 /game-runtime/ 前缀)。
GAME_URL_PATH="/game-runtime/games/$GAME_REL/index.html"
ok_serve=0; ok_cdp=0
for i in $(seq 1 20); do
curl -sf "http://localhost:$PORT$GAME_URL_PATH" -o /dev/null 2>/dev/null && ok_serve=1
curl -sf "http://localhost:$CDP_PORT/json/version" -o /dev/null 2>/dev/null && ok_cdp=1
[ "$ok_serve" = 1 ] && [ "$ok_cdp" = 1 ] && break
sleep 0.5
done
[ "$ok_serve" != 1 ] && { echo "❌ static-serve 未就绪(见 /tmp/tier2-serve-$PORT.log)"; cat /tmp/tier2-serve-$PORT.log; exit 4; }
[ "$ok_cdp" != 1 ] && { echo "❌ Chrome CDP 未就绪(见 /tmp/tier2-chrome-$PORT.log)"; tail -5 /tmp/tier2-chrome-$PORT.log; exit 5; }
# 4) 真玩驱动(play-phaser.cdp.cjs):
# --base = serve 根 + /game-runtime(harness 内部 URL = ${base}/games/${gameId}/index.html,故 gameId=_tier2-gen/<id>);
# --out = 显式证据目录(绝对,避免依赖 cwd);
# --spec = 生成游戏目录下的 play-spec.json(run.run_gates 已写)。
node "$SCRIPT_DIR/play-phaser.cdp.cjs" "$GAME_REL" \
--base="http://localhost:$PORT/game-runtime" \
--cdp="http://localhost:$CDP_PORT" \
--spec="$GAME_DIR/play-spec.json" \
--out="$EV_DIR"
PLAY_RC=$?
exit "$PLAY_RC"