105 lines
4.9 KiB
Bash
Executable File
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
|