Files
EscapistsReintroduced/docs/evidence/terrain-takeover-feature-2026-10-01/dedicated-restart/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

43 lines
5.1 KiB
Python

from pathlib import Path
import sys,os,json,subprocess,time,shutil,signal,hashlib
ROOT=Path('/home/jakub/devel/escapistsrereintroduced');sys.path.insert(0,str(ROOT/'tools'))
from external_addons import policy,prepare_profile
runtime=Path('/tmp/erri-takeover-dedicated-runtime');source=Path('/tmp/erri-takeover-ds-source7');run=Path('/tmp/erri-takeover-ds7');run.mkdir()
shutil.copytree('/tmp/erri-takeover-feature-source4',source)
(source/'Scripts/Game/ERR/00_Core/ERR_TerrainValidationProbe.c').write_text('class ERR_DedicatedRestartProof\n{\n static bool Requested;\n}\nmodded class ArmaReforgerScripted\n{\n protected float m_ERRRestartDelay = 5;\n override void OnUpdate(BaseWorld world, float timeslice)\n {\n super.OnUpdate(world, timeslice);\n ERR_GameMode mode = ERR_GameMode.Get();\n if (!Replication.IsServer() || !mode || !mode.Active() || ERR_DedicatedRestartProof.Requested) return;\n m_ERRRestartDelay -= timeslice;\n if (m_ERRRestartDelay > 0) return;\n ERR_DedicatedRestartProof.Requested = true;\n PrintFormat("ERR DEDICATED RESTART: accepted=%1", GameStateTransitions.RequestScenarioRestart());\n }\n override protected void OnBeforeWorldCleanup()\n {\n super.OnBeforeWorldCleanup();\n PrintFormat("ERR DEDICATED CLEANUP: stage=%1 worldClear=%2 oldModeClear=%3 elapsed=%4 admissions=%5 airports=%6 slots=%7", m_ERRTerrainStage, m_ERRTerrainWorld == null, m_ERRTerrainOriginal == null, m_ERRTerrainElapsed, m_ERRTerrainAdmissions.Count(), m_ERRTerrainAirfields.Count(), m_ERRTerrainMarkerSlots.Count());\n }\n}\n')
shutil.copy2(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c',source/'Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c')
steam=Path.home()/'.local/share/Steam';game=steam/'steamapps/common/Arma Reforger';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)
if not (runtime/'addons').exists():(runtime/'addons').symlink_to(game/'addons',target_is_directory=True)
for name in ('profile','home','tmp'):(run/name).mkdir()
config={'bindAddress':'127.0.0.1','bindPort':22991,'game':{'name':'Private terrain takeover test','password':'private-local-probe','scenarioId':'{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf','maxPlayers':8,'visible':False,'supportedPlatforms':['PLATFORM_PC'],'gameProperties':{'battlEye':False,'missionHeader':{'m_bERR_EnableEscape':True,'m_sERR_Airfields':'1525 5075','m_eSaveTypes':0}}}}
(run/'server.json').write_text(json.dumps(config,indent=2))
exe=['/tmp/runtime/ArmaReforgerServer','-server','{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf','-errTerrainTakeover','1','-errAirfields','1525 5075','-profile','/tmp/work/profile','-addonsDir','/tmp/work/addons','-addons','2528466DE220D91C,70C981A53E02B64F','-noBackend','-bindIP','127.0.0.1','-bindPort','22991','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter']
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','--clearenv','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--ro-bind',str(runtime),'/tmp/runtime','--bind',str(source),'/tmp/source','--bind',str(run),'/tmp/work','--setenv','HOME','/tmp/work/home','--setenv','TMPDIR','/tmp/work/tmp','--setenv','PATH','/usr/bin:/bin','--setenv','LANG','C.UTF-8','--chdir','/tmp/runtime','/usr/bin/python3','/tmp/work/network_probe.py',os.readlink('/proc/self/ns/net'),*exe]
(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;started=time.time();reason='deadline'
with (run/'stdout.log').open('w') as log:
try:
child=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
while time.time()-started<180:
if child.poll() is not None:reason='process-exit';break
logs=sorted((run/'profile/logs').glob('logs_*/console.log'))
if logs:
s=logs[-1].read_text(errors='replace')
if s.count('Reintroduced [mission]: phase 0 -> 1') >= 2:reason='mission-started';break
if 'ERR DEDICATED RESTART: accepted=0' in s or 'terrain takeover failed' in s or "Can't compile" in s or 'Unable to initialize the game' in s:reason='startup-failed';break
time.sleep(1)
finally:
if child and child.poll() is None:
os.killpg(child.pid,signal.SIGTERM)
try:child.wait(timeout=10)
except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);child.wait(timeout=5)
result={'reason':reason,'exit_code':child.returncode if child else None,'supervisor_reaped':child is not None and child.poll() is not None,'seconds':round(time.time()-started,2)}
(run/'result.json').write_text(json.dumps(result,indent=2));print(result)
print(run)