"""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=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}