Files
mtgodot-poc/script/playable_soak.sh
T

199 lines
9.1 KiB
Bash
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
# STB-01 public entry: 2 h wall-clock soak plus independent normal-exit runs.
#
# Layout under --output (must be empty):
# exit-<n>/ N independent client processes, suite playable, each through
# run_client_gate.sh (normal quit, exit 0, no leak/RID warnings)
# soak-1/ one client process, suite soak: move/combat/pickup rounds until the
# wall-clock duration, active reconnects, resolution switches, and —
# only when the scenario confirms them — portal warps and real
# transport faults through the 127.0.0.1 fault proxy; RSS is sampled
# for that child PID and sealed as STB-MEMORY-01
# release-manifest.json listing exactly these runs; aggregated with
# validate_playable_report.mjs --release-dir (no historical reports are scanned).
#
# Exit runs go first; if one is not PASS the remaining runs (including the 2 h soak)
# are not started and are recorded as BLOCKED so the release cannot pass.
# Credentials only from MT_ACCOUNT/MT_PASSWORD or one hidden TTY prompt.
# Do not add `set -x` to this script. Never pkill; never touch routes/firewall/caches.
#
# exit: 0 PASS, 1 FAIL, 2 BLOCKED (config/precondition/unconfirmed environment), 124 timeout.
set -euo pipefail
set +x
cd "$(dirname "$0")/.."
repo="$PWD"
process_cleanup_grace_seconds=15 # Allow the child runner's 10-second cleanup to finish.
source script/playable_process_cleanup.sh
app="${MT_PLAYABLE_APP:-}"
config=""
output=""
duration=""
exits=""
timeout=""
exit_timeout="900"
allow=0
assets=""
serverlist="${MT_PLAYABLE_SERVERLIST:-}"
usage() {
cat <<'EOF'
用法: script/playable_soak.sh --app APP --config CONFIG --allow-gameplay [选项]
--app APP 候选导出包(所有 run 必须同一哈希)
--config CONFIG 场景 JSON(含 soak 块;不含凭据)
--output DIR 本批次根目录,必须不存在或为空(默认 build/playable/soak-<时间>-<pid>
--duration-seconds N 墙钟时长,必须等于配置 soak.duration_seconds(默认取配置,>= 7200
--exits N 独立正常退出运行次数(默认配置 soak.exits>= 10
--timeout-seconds N soak 进程墙钟超时(默认 duration+900,不得更小)
--exit-timeout-seconds N 每个退出运行的墙钟超时,默认 900
--assets DIR 资源预检根目录(透传 run_client_gate.sh
--serverlist FILE 测试模式 serverlist 覆盖(透传)
--allow-gameplay 允许专用测试账号发送移动/战斗/拾取请求(不给则只校验配置,返回 2,不启动客户端)
环境变量:
MT_ACCOUNT / MT_PASSWORD 测试账号(未设置且 stdin 是终端时隐藏输入一次)
说明:
切图(soak.warp)和真实断网(soak.faults)须由环境负责人确认后在配置里标 confirmed;
未确认时对应用例为 BLOCKED,不用主动重连或直接传送替代。
退出码: 0 PASS / 1 FAIL / 2 BLOCKED / 124 超时
EOF
}
blocked() { echo "PLAYABLE SOAK: BLOCKED $*" >&2; exit 2; }
positive() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; }
while [ "$#" -gt 0 ]; do
case "$1" in
--app) app="${2:-}"; shift 2 ;;
--config) config="${2:-}"; shift 2 ;;
--output) output="${2:-}"; shift 2 ;;
--duration-seconds) duration="${2:-}"; shift 2 ;;
--exits) exits="${2:-}"; shift 2 ;;
--timeout-seconds) timeout="${2:-}"; shift 2 ;;
--exit-timeout-seconds) exit_timeout="${2:-}"; shift 2 ;;
--assets) assets="${2:-}"; shift 2 ;;
--serverlist) serverlist="${2:-}"; shift 2 ;;
--allow-gameplay) allow=1; shift ;;
--help) usage; exit 0 ;;
*) echo "未知选项: $1" >&2; usage >&2; exit 2 ;;
esac
done
[ -n "$app" ] && [ -d "$app" ] || blocked "需要 --app 指向候选导出包"
[ -n "$config" ] && [ -f "$config" ] || blocked "需要 --config CONFIG"
config="$(cd "$(dirname "$config")" && pwd)/$(basename "$config")"
for pair in "duration-seconds:$duration" "exits:$exits" "timeout-seconds:$timeout"; do
value="${pair#*:}"
if [ -n "$value" ] && ! positive "$value"; then blocked "--${pair%%:*} 必须是正整数"; fi
done
positive "$exit_timeout" || blocked "--exit-timeout-seconds 必须是正整数"
# Config validation before any prompt or process: the same gate the runner uses.
if [ -z "$assets" ]; then
if [ -d "$app/Contents/Resources/assets" ]; then assets="$app/Contents/Resources/assets"; else assets="$repo/assets"; fi
fi
if ! config_log="$(env MT_PLAYABLE_VALIDATE_CONFIG="$config" MT_PLAYABLE_SUITE=soak MT_PLAYABLE_ASSETS="$assets" \
MT_PLAYABLE_SERVERLIST="$serverlist" "${MT_GODOT:-godot}" --headless --path project --script playable_config_gate.gd 2>&1)"; then
printf '%s\n' "$config_log" | grep '^CONFIG:' >&2 || true
blocked "配置/资源预检失败(suite soak"
fi
read -r cfg_duration cfg_exits <<<"$(node -e '
const s = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).soak;
process.stdout.write(`${s.duration_seconds} ${s.exits}`);
' "$config")"
if [ -z "$duration" ]; then duration="$cfg_duration"; fi
[ "$duration" -eq "$cfg_duration" ] || blocked "--duration-seconds $duration 与配置 soak.duration_seconds $cfg_duration 不一致;请改配置而不是覆盖"
if [ -z "$exits" ]; then exits="$cfg_exits"; fi
[ "$exits" -ge 10 ] || blocked "--exits 至少 10"
minimum_timeout=$((duration + 900))
if [ -z "$timeout" ]; then timeout="$minimum_timeout"; fi
[ "$timeout" -ge "$minimum_timeout" ] || blocked "--timeout-seconds 不得小于 duration+900${minimum_timeout}"
# Without explicit consent only the configuration is checked; no client, no gameplay request.
[ "$allow" -eq 1 ] || blocked "配置校验通过;soak 需要 --allow-gameplay(专用测试账号上的移动/战斗/拾取),未启动客户端"
root="$output"
if [ -z "$root" ]; then root="$repo/build/playable/soak-$(date +%Y%m%d-%H%M%S)-$$"; fi
if [ -e "$root" ] && [ -n "$(find "$root" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
blocked "输出目录已有内容,拒绝混入历史运行:$root"
fi
mkdir -p "$root"
root="$(cd "$root" && pwd)"
if [ -z "${MT_ACCOUNT:-}" ] || [ -z "${MT_PASSWORD:-}" ]; then
if [ -t 0 ]; then
if [ -z "${MT_ACCOUNT:-}" ]; then read -r -s -p "测试账号: " MT_ACCOUNT; echo >&2; fi
if [ -z "${MT_PASSWORD:-}" ]; then read -r -s -p "测试密码: " MT_PASSWORD; echo >&2; fi
fi
fi
[ -n "${MT_ACCOUNT:-}" ] && [ -n "${MT_PASSWORD:-}" ] || blocked "缺少 MT_ACCOUNT/MT_PASSWORD,且不是交互终端"
export MT_ACCOUNT MT_PASSWORD
bash script/playable_build_info.sh "$app" >"$root/candidate.json"
runs_json="[]"
timed_out=0
stop_reason=""
list_run() { # name suite
runs_json="$(node -e '
const fs = require("node:fs");
const [runs, dir, name, suite] = process.argv.slice(1);
let runId = `${name}-unsealed`;
try { runId = JSON.parse(fs.readFileSync(`${dir}/report.json`, "utf8")).run_id || runId; } catch {}
const list = JSON.parse(runs);
list.push({ run_id: runId, suite, path: name });
process.stdout.write(JSON.stringify(list));
' "$runs_json" "$root/$1" "$1" "$2")"
}
run_one() { # name suite timeout
local name="$1" run_suite="$2" run_timeout="$3" code
if [ -n "$stop_reason" ]; then
mkdir -p "$root/$name"
echo "BLOCKED not started: $stop_reason" >"$root/$name/gate.log"
list_run "$name" "$run_suite"
return
fi
local args=(--app "$app" --config "$config" --output "$root/$name" --suite "$run_suite" --timeout-seconds "$run_timeout" --allow-gameplay)
if [ -n "$assets" ]; then args+=(--assets "$assets"); fi
if [ -n "$serverlist" ]; then args+=(--serverlist "$serverlist"); fi
set +e
bash script/run_client_gate.sh "${args[@]}" </dev/null &
child_pid=$!
wait "$child_pid"
code=$?
child_pid=""
set -e
if [ "$code" -eq 124 ]; then timed_out=1; fi
if [ "$code" -ne 0 ] && [ "$run_suite" = "playable" ]; then stop_reason="$name exit $code"; fi
list_run "$name" "$run_suite"
}
for index in $(seq 1 "$exits"); do
run_one "exit-$index" playable "$exit_timeout"
done
run_one soak-1 soak "$timeout"
node -e '
const fs = require("node:fs");
const [root, exits, runs] = process.argv.slice(1);
const candidate = JSON.parse(fs.readFileSync(`${root}/candidate.json`, "utf8"));
fs.writeFileSync(`${root}/release-manifest.json`, JSON.stringify({
schema_version: 1, candidate, required_runs: { playable: Number(exits), soak: 1 }, runs: JSON.parse(runs),
}, null, 2) + "\n");
' "$root" "$exits" "$runs_json"
set +e
node script/validate_playable_report.mjs --release-dir "$root"
release_code=$?
set -e
if [ -f "$root/soak-1/report.json" ]; then
node -e '
const r = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
for (const c of [...(r.cases || []).filter((c) => c.id.startsWith("STB-")), ...(r.runner_cases || [])]) console.log(` ${c.id} ${c.status} ${c.reason || ""}`);
' "$root/soak-1/report.json" || true
fi
if [ "$timed_out" -eq 1 ]; then echo "PLAYABLE SOAK: TIMEOUT root=$root" >&2; exit 124; fi
case "$release_code" in
0) echo "PLAYABLE SOAK: PASS exits=$exits duration=${duration}s root=$root" ;;
2) echo "PLAYABLE SOAK: BLOCKED root=$root" >&2 ;;
*) echo "PLAYABLE SOAK: FAIL root=$root" >&2; release_code=1 ;;
esac
exit "$release_code"