Add isolated Classic data staging with integrity checks

This commit is contained in:
Codex
2026-09-25 22:05:17 +02:00
parent 1f9ad52d07
commit 58b2019015
2 changed files with 97 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Finish isolated Classic prerequisites. Never starts a realm or edits live data."""
import argparse
import hashlib
import json
import pathlib
import shutil
import subprocess
import time
parser = argparse.ArgumentParser()
parser.add_argument('--root', required=True)
args = parser.parse_args()
root = pathlib.Path(args.root).resolve()
def status(stage, **extra):
value = {'at': time.time(), 'stage': stage, 'cutover': False, **extra}
temporary = root / 'prerequisites-status.tmp'
temporary.write_text(json.dumps(value, indent=2))
temporary.replace(root / 'prerequisites-status.json')
print(stage, flush=True)
def run(*command):
return subprocess.check_output(command, text=True).strip()
try:
status('waiting for Classic compilation and client transfer')
deadline = time.monotonic() + 6 * 3600
while True:
state = json.loads(run('docker', 'inspect', 'azerothai-classic-build'))[0]['State']
if not state['Running']:
if state['ExitCode'] != 0:
raise RuntimeError('Classic build failed; inspect docker logs azerothai-classic-build')
break
if time.monotonic() > deadline:
raise RuntimeError('Classic build exceeded six-hour observation window')
time.sleep(30)
manifest = json.loads((root / 'client-manifest.json').read_text())
while not all((root / 'client' / f).exists() and (root / 'client' / f).stat().st_size == v['size'] for f, v in manifest.items()):
if time.monotonic() > deadline:
raise RuntimeError('Client transfer incomplete after six hours')
time.sleep(30)
status('verifying transferred client archives')
for filename, expected in manifest.items():
with (root / 'client' / filename).open('rb') as stream:
actual = hashlib.file_digest(stream, 'sha256').hexdigest()
if actual != expected['sha256']:
raise RuntimeError('Client archive checksum mismatch: ' + filename)
if shutil.disk_usage(root).free < 8 * 1024**3:
raise RuntimeError('Less than 8 GiB free for extracted Vanilla data')
output = root / 'extracted'; output.mkdir(exist_ok=True)
status('extracting Vanilla maps and navigation data; realm not switched')
with (root / 'data-extraction.log').open('w') as log:
subprocess.run([
'docker', 'run', '--name', 'azerothai-classic-extract', '--cpus', '2', '--memory', '4g',
'--user', '1000:1000', '-i', '-v', str(root) + ':/work', '-w', '/work/run/bin/tools',
'azerothai-classic-builder:local', 'bash', './ExtractResources.sh', '/work/client', '/work/extracted'
], input='y\n2\n\nn\nn\n\n', text=True, stdout=log, stderr=subprocess.STDOUT, check=True)
counts = {name: sum(1 for f in (output / name).rglob('*') if f.is_file()) for name in ('dbc', 'maps', 'vmaps', 'mmaps')}
if not all(counts.values()):
raise RuntimeError('An extracted data directory is empty: ' + str(counts))
status('prerequisites extracted; database, connector, roster and gameplay validation remain', files=counts)
except Exception as error:
status('blocked', error=str(error))
raise