Files
TheTree_mock/tools/regression/p09_owned_engine_process.py
T
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
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.
2026-10-11 15:58:00 +09:00

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}