from pathlib import Path import sys,os,subprocess,json,fcntl,time,hashlib,re ROOT=Path('/home/jakub/devel/escapistsrereintroduced') sys.path.insert(0,str(ROOT/'tools')) from headless_game import assert_no_game,assert_prefix_idle,cleanup_owned_run from external_addons import policy,prepare_profile,verify_profile base=Path('/tmp/erri-ai-19'); target=base/sys.argv[1]; run=Path(sys.argv[2]); run.mkdir(exist_ok=False) compat=base/'compat'; steam=Path.home()/'.local/share/Steam'; proton=steam/'steamapps/common/Proton - Experimental/proton'; wb=steam/'steamapps/common/Arma Reforger Tools/Workbench' lock=(compat/'.erri-workbench.lock').open('a'); fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB); assert_no_game(); assert_prefix_idle(compat) profile=run/'profile'; addons=profile/'addons'; addons.mkdir(parents=True); (addons/'ArmaReforger').symlink_to(steam/'steamapps/common/Arma Reforger/addons/data',target_is_directory=True) settings={'configVersion':1,'factions':{'escapists':'fia','enemy':'ussr','independent':'us'}} (profile/'profile').mkdir(); (profile/'profile/Escapists.json').write_text(json.dumps(settings,indent=2)+'\n'); (run/'input-settings.json').write_text(json.dumps(settings,indent=2)+'\n') source=steam/'steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons'; rules=policy(ROOT); cache=run/'external-cache.json'; records=prepare_profile(addons,source,rules,cache) source_hashes={str(p.relative_to(target)):hashlib.sha256(p.read_bytes()).hexdigest() for p in target.rglob('*') if p.is_file() and p.suffix in {'.c','.gproj','.conf','.ent','.meta','.json'}} (run/'source-hashes.json').write_text(json.dumps(source_hashes,indent=2)+'\n') projects=run/'projects'; projects.mkdir(); (projects/'core').symlink_to(target,target_is_directory=True) cmd=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'),'-gproj','Z:'+str(target/'addon.gproj'),'-addonsDir','Z:'+str(projects),'-profile','Z:'+str(profile),'-logconsole','-wbModule=WorldEditor','-load','Worlds/ERR_Everon.ent','-plugin=ERR_ValidateWorldPlugin','-errPlay','1','-errSmoke','1','-errSmokeOutcome','supply-convoys','-noThrow','-noSound','-disableCrashReporter'] (run/'command.json').write_text(json.dumps(cmd,indent=2)+'\n'); env=os.environ.copy(); env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx')) marker='ERR SUPPLY PASS: production auto-started three five-vehicle convoys, exact ledger admission, no troop transport, finite native cargo' result={'scope':'startup phase only, not full supply-convoys smoke','deadline_seconds':900,'reason':'timeout','source':str(target),'ready_elapsed':None,'ready_perf_windows':None,'cleanup':None} started=time.monotonic(); previous=-1; ready_windows=None with (run/'launch.log').open('w') as log: child=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,env=env,cwd=wb,start_new_session=True) result['wrapper_pid']=child.pid try: while time.monotonic()-started < 900: logs=sorted(profile.glob('logs/logs_*/console.log')); content=''; perf='' if logs: content=logs[-1].read_text(errors='replace'); result['console']=str(logs[-1]); pp=logs[-1].parent/'rereintroduced-performance.log' if pp.exists(): perf=pp.read_text(errors='replace') windows=set(re.findall(r'ERR PERF run=\S+ window=(\d+) ',perf)) if ready_windows is None and marker in content: ready_windows=windows; result['ready_elapsed']=time.monotonic()-started; result['ready_perf_windows']=sorted(windows); print('STARTUP_READY',result['ready_elapsed'],flush=True) if re.search(r'ERR (?:SMOKE|SUPPLY) FAIL:|scenario preparation failed|Virtual Machine Exception|Can.t compile|Cannot initialize game project settings',content): result['reason']='native-failure'; break if ready_windows is not None and windows-ready_windows: result['reason']='startup-ready-plus-flush'; result['final_perf_windows']=sorted(windows); break if child.poll() is not None: result['reason']='unexpected-exit'; break elapsed=int(time.monotonic()-started) if elapsed//30 != previous: previous=elapsed//30; print(json.dumps({'elapsed':elapsed,'log_bytes':len(content),'perf_windows':sorted(windows),'ready':ready_windows is not None,'tail':content.splitlines()[-1:] }),flush=True) time.sleep(1) finally: result['elapsed_seconds']=time.monotonic()-started; result['cleanup']=cleanup_owned_run(child,proton,env,compat) # Preserve the initial verdict; permit only natural process teardown, no new signals. for retry in range(10): try: assert_prefix_idle(compat); result['cleanup_followup_idle']=True; break except (OSError,ValueError,subprocess.SubprocessError): time.sleep(0.5) result['external_sources_unchanged']=verify_profile(records,source,rules,cache,addons) result['source_unchanged']=all((target/name).is_file() and hashlib.sha256((target/name).read_bytes()).hexdigest()==value for name,value in source_hashes.items()) (run/'result.json').write_text(json.dumps(result,indent=2)+'\n') print(json.dumps(result,indent=2),flush=True) sys.exit(0 if result['reason']=='startup-ready-plus-flush' and result.get('cleanup_followup_idle') and result['source_unchanged'] and result['external_sources_unchanged'] else 1)