Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
49 lines
3.6 KiB
Python
49 lines
3.6 KiB
Python
"""Owned subprocess lifecycle and read-only per-phase admission. No game code."""
|
|
import os,re,shutil,subprocess,time
|
|
BANNER='Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org'
|
|
MINIMUM_FREE_BYTES=300*1024*1024
|
|
LEAK=re.compile(r'(?im)^\s*(?:(?:WARNING:|ERROR:)[^\r\n]*(?:ObjectDB instances leaked|resources? still in use at exit)|Leaked instance:)[^\r\n]*$')
|
|
def engine_banner_valid(raw):
|
|
lines=[line for line in raw.splitlines(keepends=True)if line.startswith('Godot Engine v')]
|
|
return len(lines)==1 and lines[0]in[BANNER+'\n',BANNER+'\r\n']
|
|
def leak_lines(raw):return LEAK.findall(raw)
|
|
def strict_engine_raw(raw):
|
|
return engine_banner_valid(raw)and not leak_lines(raw)and not any(t in raw for t in ['ERROR:','SCRIPT ERROR:','Parse Error:'])and not re.search(r'(?m)^\s*(?:FAIL:|FAILED:|FAIL )',raw)
|
|
def live_engines():
|
|
return[s for s in subprocess.check_output(['ps','-axo','pid=,comm='],text=True).splitlines()if 'godot'in s.lower()]
|
|
def admission(root,scan=None,free=None):
|
|
observed=(scan or live_engines)();available=(free or(lambda p:shutil.disk_usage(p).free))(root)
|
|
if observed:raise RuntimeError('external/previous Godot present before owned phase: '+str(observed))
|
|
if type(available)is not int or available<MINIMUM_FREE_BYTES:raise RuntimeError('fresh free disk must be >=300MiB before every phase')
|
|
return {'live_engines_before':observed,'free_bytes_before':available,'minimum_free_bytes':MINIMUM_FREE_BYTES}
|
|
def run_owned(command,root,environment,stream,context,timeout,write_start,write_end,popen=None,monotonic_ns=None):
|
|
process=None;started=None;end=None;exit_code=127;error=None;timed_out=False;start_written=False;end_written=False
|
|
now=monotonic_ns or time.monotonic_ns
|
|
try:
|
|
process=(popen or subprocess.Popen)(command,cwd=root,env=environment,stdout=stream,stderr=subprocess.STDOUT)
|
|
started=dict(context,event='ENGINE_STARTED',actual_pid=process.pid,owner_pid=os.getpid(),command=command,monotonic_ns=now())
|
|
write_start(started);start_written=True
|
|
try:exit_code=process.wait(timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
timed_out=True;process.kill();process.wait();exit_code=124
|
|
except BaseException as caught:
|
|
error=type(caught).__name__+': '+str(caught);exit_code=130 if isinstance(caught,KeyboardInterrupt)else 127
|
|
finally:
|
|
if process is not None:
|
|
# A second interrupt during cleanup still retries exactly this owned child.
|
|
# Persistent OS kill/wait errors fail closed and are exposed, never accepted.
|
|
for attempt in range(3):
|
|
if process.poll()is not None:break
|
|
try:process.kill();process.wait(timeout=5)
|
|
except BaseException as caught:
|
|
error=(error+'; 'if error else'')+'owned cleanup '+type(caught).__name__+': '+str(caught)
|
|
ended=process is not None and process.poll()is not None
|
|
end=dict(context,event='ENGINE_ENDED',actual_pid=process.pid if process is not None else None,owner_pid=os.getpid(),command=command,monotonic_ns=now(),exit_code=exit_code,actual_process_exit_code=process.poll()if process is not None else None,engine_ended=ended,timed_out=timed_out,lifecycle_error=error,start_receipt_written=start_written)
|
|
try:write_end(end);end_written=True
|
|
except BaseException as caught:
|
|
error=(error+'; 'if error else'')+'end receipt '+type(caught).__name__+': '+str(caught)
|
|
if error:
|
|
try:stream.write('RUNNER_LIFECYCLE_ERROR: '+error+'\n')
|
|
except OSError:pass
|
|
return {'actual_pid':process.pid if process is not None else None,'exit_code':exit_code,'actual_process_exit_code':process.poll()if process is not None else None,'engine_ended':ended,'timed_out':timed_out,'lifecycle_error':error,'start_receipt_written':start_written,'end_receipt_written':end_written,'start':started,'end':end}
|