#!/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