Files
EscapistsReintroduced/docs/evidence/terrain-takeover-feature-2026-10-01/pair-diagnostic-3/runner.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

81 lines
6.0 KiB
Python

from pathlib import Path
import os,sys,json,shutil,subprocess,time,signal,fcntl,hashlib
ROOT=Path('/home/jakub/devel/escapistsrereintroduced');sys.path.insert(0,str(ROOT/'tools'))
from external_addons import policy,prepare_profile
from headless_game import assert_prefix_idle,cleanup_owned_run
steam=Path.home()/'.local/share/Steam';compat=Path('/tmp/erri-headless-compat');runtime=Path('/tmp/erri-takeover-dedicated-runtime');source=Path('/tmp/erri-takeover-pair-source3');run=Path('/tmp/erri-takeover-pair3');run.mkdir()
lock=(compat/'.erri-workbench.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);assert_prefix_idle(compat)
shutil.copytree('/tmp/erri-takeover-ds-source6',source)
(source/'Scripts/Game/ERR/00_Core/ERR_TerrainValidationProbe.c').write_text('''modded class ArmaReforgerScripted
{
protected float m_ERRPairNext;
override void OnUpdate(BaseWorld world, float timeslice)
{
super.OnUpdate(world, timeslice);
m_ERRPairNext -= timeslice;
if (m_ERRPairNext > 0) return;
m_ERRPairNext = 5;
if (!GetGameMode()) return;
ERR_GameMode escape = ERR_GameMode.Get();
bool takeover = escape && escape.ERR_TerrainTakenOver();
PrintFormat("ERR TERRAIN PAIR: session=%1 server=%2 mode=%3 takeover=%4 controller=%5", RplSession.Mode(), Replication.IsServer(), GetGameMode().Type().ToString(), takeover, GetPlayerController() != null);
}
}
''')
addons=run/'addons';addons.mkdir()
prepare_profile(addons,steam/'steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons',policy(ROOT),run/'external-cache.json')
(addons/'ERR_Core').symlink_to('/tmp/source',target_is_directory=True)
(addons/'Nogova').symlink_to('/home/jakub/devel/gabreta/addons/NogovaReforger',target_is_directory=True)
for name in ('server','client','home','tmp','xdg'):(run/name).mkdir()
proton=steam/'steamapps/common/Proton - Experimental/proton';game=steam/'steamapps/common/Arma Reforger'
server=['/tmp/runtime/ArmaReforgerServer','-server','{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf','-addonsDir','/tmp/work/addons','-addons','2528466DE220D91C,70C981A53E02B64F','-profile','/tmp/work/server','-noBackend','-bindIP','127.0.0.1','-bindPort','2001','-maxFPS','30','-errTerrainTakeover','1','-errAirfields','1525 5075','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter']
client=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(game/'ArmaReforgerSteamDiag.exe'),'-client','127.0.0.1','-addonsDir','Z:/tmp/work/addons','-addons','2528466DE220D91C,70C981A53E02B64F','-profile','Z:/tmp/work/client','-noBackend','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter']
env=os.environ.copy();env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx'),PULSE_SINK='erri_tests_silent')
(run/'launch.json').write_text(json.dumps({'server':server,'client':client,'client_cwd':str(game),'env':{k:env[k] for k in ('STEAM_COMPAT_DATA_PATH','STEAM_COMPAT_CLIENT_INSTALL_PATH','WINEPREFIX','PULSE_SINK')}},indent=2))
(run/'inner.py').write_text('''from pathlib import Path
import json,subprocess,time,os,signal
root=Path('/tmp/work');spec=json.loads((root/'launch.json').read_text());children=[];handles=[];started=time.time();reason='deadline'
def start(key,cwd):
log=(root/(key+'-stdout.log')).open('w');handles.append(log)
env=os.environ.copy();env.update(spec['env'])
if key=='client':
for variable in ('WAYLAND_DISPLAY','DISPLAY','WLR_BACKENDS','SDL_VIDEODRIVER'):env.pop(variable,None)
p=subprocess.Popen(spec[key],cwd=cwd,env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True);children.append(p);return p
def logs(key):
files=sorted((root/key/'logs').glob('logs_*/console.log'));return files[-1].read_text(errors='replace') if files else ''
try:
server=start('server','/tmp/runtime');client=None;client_start=0
while time.time()-started<240:
s=logs('server')
if server.poll() is not None:reason='server-exit';break
if "Can't compile" in s or 'terrain takeover failed' in s:reason='server-failed';break
if client is None and 'Reintroduced [mission]: phase 0 -> 1' in s:
client=start('client',spec['client_cwd']);client_start=time.time()
if client:
c=logs('client')
if client.poll() is not None:reason='client-exit';break
if time.time()-client_start>90:reason='client-observation-complete';break
time.sleep(1)
finally:
for p in reversed(children):
if p.poll() is None:
os.killpg(p.pid,signal.SIGTERM)
try:p.wait(timeout=8)
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait(timeout=5)
for f in handles:f.close()
(root/'inner-result.json').write_text(json.dumps({'reason':reason,'codes':[p.returncode for p in children],'seconds':round(time.time()-started,2)},indent=2))
''')
from dedicated_native import PROBE
(run/'network_probe.py').write_text(PROBE)
cmd=['bwrap','--unshare-net','--unshare-pid','--unshare-ipc','--unshare-uts','--die-with-parent','--cap-drop','ALL','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev-bind','/dev','/dev','--ro-bind',str(runtime),'/tmp/runtime','--bind',str(source),'/tmp/source','--bind',str(run),'/tmp/work','--bind',str(compat),str(compat),'--setenv','HOME','/tmp/work/home','--setenv','XDG_RUNTIME_DIR','/tmp/work/xdg','--setenv','TMPDIR','/tmp/work/tmp','--chdir','/tmp/runtime','/usr/bin/python3','/tmp/work/network_probe.py',os.readlink('/proc/self/ns/net'),'/usr/bin/python3','/tmp/work/inner.py']
(run/'command.json').write_text(json.dumps(cmd,indent=2));(run/'source-before.json').write_text(json.dumps({str(p.relative_to(source)):hashlib.sha256(p.read_bytes()).hexdigest() for p in source.rglob('*') if p.is_file() and p.name!='resourceDatabase.rdb'},indent=2))
child=None
with (run/'supervisor.log').open('w') as log:
try:
child=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
child.wait(timeout=290)
finally:
if child:
report=cleanup_owned_run(child,proton,env,compat);(run/'cleanup.json').write_text(json.dumps(report,indent=2));print(report)
print(run)