Files
mtgodot-poc/script/beta_probe.sh
T

105 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run the exported native 40250 macOS client from an unrelated working directory.
# Credentials come only from MT_LIVE_LOGIN / MT_LIVE_PASSWORD in the environment.
set -euo pipefail
set +x
cd "$(dirname "$0")/.."
repo="$PWD"
app="${1:-}"
output="${2:-$repo/build/beta-probe-$(date +%Y%m%d-%H%M%S)-$$}"
if [ -z "$app" ] || [ ! -d "$app" ]; then echo 'BETA_PROBE: BLOCKED provide an exported .app' >&2; exit 2; fi
if [ -z "${MT_LIVE_HOST:-}" ] || [ -z "${MT_LIVE_LOGIN:-}" ] || [ -z "${MT_LIVE_PASSWORD:-}" ]; then
echo 'BETA_PROBE: BLOCKED MT_LIVE_HOST, MT_LIVE_LOGIN and MT_LIVE_PASSWORD are required' >&2
exit 2
fi
app="$(cd "$app" && pwd)"
if [ -e "$output" ] && [ -n "$(find "$output" -mindepth 1 -print -quit)" ]; then
echo 'BETA_PROBE: BLOCKED output directory is not empty' >&2
exit 2
fi
mkdir -p "$output"
output="$(cd "$output" && pwd)"
engine="$app/Contents/MacOS/mtgodot-poc"
client="$app/Contents/Resources/40250/Client"
if [ ! -x "$engine" ] || [ ! -s "$client/pack/Index" ] || ! codesign --verify --strict "$app" >/dev/null 2>&1; then
echo 'BETA_PROBE: BLOCKED bundle executable, 40250 pack or signature is invalid' >&2
exit 2
fi
bash script/playable_build_info.sh "$app" >"$output/candidate.json"
python3 - "$client" "$app/Contents/Resources/beta-build.json" "$output/candidate.json" <<'PY'
import hashlib, json, pathlib, sys
client, build_manifest, output = map(pathlib.Path, sys.argv[1:])
digest = hashlib.sha256()
for path in sorted(p for p in client.rglob('*') if p.is_file()):
relative = path.relative_to(client).as_posix().encode()
digest.update(len(relative).to_bytes(4, 'big'))
digest.update(relative)
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
digest.update(chunk)
candidate = json.loads(output.read_text())
candidate.update(json.loads(build_manifest.read_text()))
candidate['client_resources_sha256'] = digest.hexdigest()
output.write_text(json.dumps(candidate, indent=2) + '\n')
PY
# Python owns exactly this child, enforces the wall-clock deadline, and writes only the process
# code and status to its report. The password is inherited by the child, never placed in argv.
python3 - "$engine" "$output" <<'PY'
import datetime, json, os, pathlib, platform, re, subprocess, sys, tempfile
engine, output = sys.argv[1:]
root = pathlib.Path(output)
env = os.environ.copy()
for key in ('MT_40250_CLIENT', 'MT_ASSETS', 'MT_ASSETS_ZIP', 'MT_PYTHON_STDLIB'):
env.pop(key, None)
env['MT_TEST_MODE'] = 'beta_probe'
env['MT_BETA_OUTPUT'] = output
env['MT_BETA_REQUIRE_BUNDLE'] = '1'
timed_out = False
with tempfile.TemporaryDirectory(prefix='mt-beta-cwd-') as cwd, (root / 'client.log').open('wb') as log:
proc = subprocess.Popen([engine], cwd=cwd, env=env, stdout=log, stderr=subprocess.STDOUT)
try:
code = proc.wait(timeout=180)
except subprocess.TimeoutExpired:
timed_out = True
proc.terminate()
try:
code = proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
code = proc.wait()
client = None
try:
client = json.loads((root / 'client-report.json').read_text())
except (OSError, ValueError):
pass
log_path = root / 'client.log'
log_text = log_path.read_text(errors='replace')
# Native trace output is outside the probe's control; keep account credentials out of artifacts.
for secret in (env.get('MT_LIVE_PASSWORD', ''), env.get('MT_LIVE_LOGIN', '')):
if secret:
log_text = log_text.replace(secret, '[REDACTED]')
log_path.write_text(log_text)
errors = []
if timed_out: errors.append('wall_clock_timeout')
if code != 0: errors.append('client_exit_nonzero')
if not isinstance(client, dict) or client.get('route') != 'native_40250' or client.get('status') != 'PASS':
errors.append('native_client_report_missing_or_failed')
if not (root / 'game.png').is_file(): errors.append('screenshot_missing')
if re.search(r'SCRIPT ERROR|Parse Error|^ERROR:|leaked at exit|shaders of type .* were never freed', log_text, re.M):
errors.append('client_log_error')
if re.search(r'Cannot open property pack|CArea::LoadObject Property\(\d+\) Load ERROR', log_text):
errors.append('map_property_load_error')
candidate = json.loads((root / 'candidate.json').read_text())
report = {'schema_version': 1, 'status': 'PASS' if not errors else 'FAIL',
'tested_at_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'device': {'macos': platform.mac_ver()[0], 'arch': platform.machine()},
'candidate': candidate, 'process_code': code, 'timed_out': timed_out,
'cases': client.get('cases', []) if isinstance(client, dict) else [],
'map': client.get('map', '') if isinstance(client, dict) else '',
'errors': errors}
(root / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
print('BETA_PROBE: ' + report['status'] + ' report=' + str(root / 'report.json'))
sys.exit(0 if not errors else 1)
PY