Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
96 lines
8.1 KiB
Python
96 lines
8.1 KiB
Python
from pathlib import Path
|
|
import os,sys,json,shutil,subprocess,time,signal,fcntl,hashlib,socket,selectors
|
|
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,assert_no_game,cleanup_owned_run
|
|
from dedicated_native import PROBE
|
|
steam=Path.home()/'.local/share/Steam';compat=Path('/tmp/erri-takeover-client-compat');runtime=Path('/tmp/erri-takeover-dedicated-runtime');source=Path('/tmp/erri-terrain-name-source2');run=Path('/tmp/erri-terrain-name-native2');run.mkdir()
|
|
compat.mkdir(exist_ok=True)
|
|
lock=(compat/'.erri-workbench.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);assert_prefix_idle(compat);assert_no_game()
|
|
from source_manifest import addon_manifest
|
|
source.mkdir()
|
|
for rel in addon_manifest(ROOT):
|
|
original=ROOT/rel;dest=source/Path(rel).relative_to('addons/core')
|
|
dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(original,dest)
|
|
shutil.copy2(Path('/tmp/erri-takeover-pair-source32/resourceDatabase.rdb'),source/'resourceDatabase.rdb')
|
|
for name in ['ERR_TerrainValidationProbe.c','ERR_ZTerrainManagerProof.c']:
|
|
shutil.copy2(Path('/tmp/erri-takeover-pair-source32/Scripts/Game/ERR/00_Core')/name,source/'Scripts/Game/ERR/00_Core'/name)
|
|
probe=source/'Scripts/Game/ERR/00_Core/ERR_ZTerrainManagerProof.c'
|
|
proof=probe.read_text().replace(' PrintFormat("ERR FACTION CACHE PROOF:', ' SCR_MissionHeader header = SCR_MissionHeader.Cast(GetMissionHeader());\n if (header) PrintFormat("ERR NAME PROOF: server=%1 name=%2 header=%3 mode=%4 resource=%5", Replication.IsServer(), GetMissionName(), header.m_sName, header.m_sGameMode, header.GetHeaderResourceName());\n PrintFormat("ERR FACTION CACHE PROOF:')
|
|
proof=proof.replace(' PrintFormat("ERR PAIR CLEANUP:', ' SCR_MissionHeader header = SCR_MissionHeader.Cast(GetMissionHeader());\n if (header) PrintFormat("ERR NAME CLEANUP: server=%1 header=%2 mode=%3", Replication.IsServer(), header.m_sName, header.m_sGameMode);\n PrintFormat("ERR PAIR CLEANUP:')
|
|
probe.write_text(proof)
|
|
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(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'):(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','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter']
|
|
client=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(game/'ArmaReforgerSteam.exe'),'-client','127.0.0.1','-addonsDir','Z:'+str(addons),'-addons','2528466DE220D91C,70C981A53E02B64F','-profile','Z:'+str(run/'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',PROTON_LOG='1',PROTON_LOG_DIR=str(run),SteamGameId='1874880',SteamAppId='1874880')
|
|
(run/'launch.json').write_text(json.dumps({'server':server,'client':client,'transport':'Only host UDP 127.0.0.1:2001 -> private filesystem Unix socket -> isolated namespace UDP 127.0.0.1:2001. Single client, no external server interface.'},indent=2))
|
|
(run/'inner.py').write_text('''from pathlib import Path
|
|
import json,subprocess,time,os,signal,socket,selectors
|
|
root=Path('/tmp/work');spec=json.loads((root/'launch.json').read_text());p=None
|
|
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(root/'inner.sock'))
|
|
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',0));udp.connect(('127.0.0.1',2001))
|
|
sel=selectors.DefaultSelector();sel.register(unix,selectors.EVENT_READ);sel.register(udp,selectors.EVENT_READ)
|
|
try:
|
|
with (root/'server-stdout.log').open('w') as log:
|
|
p=subprocess.Popen(spec['server'],cwd='/tmp/runtime',stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
|
while p.poll() is None and not (root/'stop').exists():
|
|
for key,_ in sel.select(.2):
|
|
if key.fileobj is unix:
|
|
packet,_=unix.recvfrom(65535);udp.send(packet)
|
|
else:
|
|
packet=udp.recv(65535);unix.sendto(packet,str(root/'outer.sock'))
|
|
finally:
|
|
if p and 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)
|
|
(root/'server-result.json').write_text(json.dumps({'exit':p.returncode if p else None}))
|
|
''')
|
|
(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),str(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'),'/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))
|
|
def logs(key):
|
|
files=sorted((run/key/'logs').glob('logs_*/console.log'));return files[-1].read_text(errors='replace') if files else ''
|
|
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',2001))
|
|
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(run/'outer.sock'))
|
|
sel=selectors.DefaultSelector();sel.register(udp,selectors.EVENT_READ);sel.register(unix,selectors.EVENT_READ)
|
|
server_process=None;client_process=None;client_log=None;peer=None;forwarded=0;replies=0;started=time.time();client_start=0;reason='deadline'
|
|
with (run/'supervisor.log').open('w') as log:
|
|
try:
|
|
server_process=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
|
while time.time()-started<330:
|
|
s=logs('server')
|
|
if server_process.poll() is not None:reason='server-exit';break
|
|
if "Can't compile" in s:reason='compile-failed';break
|
|
if client_process is None and 'Reintroduced [mission]: phase 0 -> 1' in s:
|
|
client_log=(run/'client-stdout.log').open('w');client_process=subprocess.Popen(client,cwd=game,env=env,stdout=client_log,stderr=subprocess.STDOUT,start_new_session=True);client_start=time.time()
|
|
for key,_ in sel.select(.2):
|
|
if key.fileobj is udp:
|
|
data,address=udp.recvfrom(65535)
|
|
if peer is None:peer=address
|
|
if address!=peer:continue
|
|
unix.sendto(data,str(run/'inner.sock'));forwarded+=1
|
|
else:
|
|
data,_=unix.recvfrom(65535)
|
|
if peer:udp.sendto(data,peer);replies+=1
|
|
if client_process:
|
|
if client_process.poll() is not None:reason='client-exit';break
|
|
if time.time()-client_start>165:reason='client-observation-complete';break
|
|
finally:
|
|
if client_process:
|
|
report=cleanup_owned_run(client_process,proton,env,compat);(run/'cleanup.json').write_text(json.dumps(report,indent=2));print(report)
|
|
if client_log:client_log.close()
|
|
(run/'stop').touch()
|
|
if server_process:
|
|
try:server_process.wait(timeout=12)
|
|
except subprocess.TimeoutExpired:os.killpg(server_process.pid,signal.SIGKILL);server_process.wait(timeout=5)
|
|
udp.close();unix.close()
|
|
result={'reason':reason,'server_reaped':server_process is not None and server_process.poll() is not None,'forwarded':forwarded,'replies':replies,'seconds':round(time.time()-started,2)}
|
|
(run/'result.json').write_text(json.dumps(result,indent=2));print(result)
|
|
print(run)
|