"""Owned child lifecycle reused from exact c007e helper, strict raw diagnostics. No external process/group signals. A tracked child Popen is finally killed/waited by the vendored helper even if a START/END receipt cannot be written. """ from pathlib import Path import hashlib, json, os, re, subprocess, time HERE = Path(__file__).resolve().parent HELPER_SHA = 'c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037' _helper_file = HERE / 'p09_owned_engine_process.py' if hashlib.sha256(_helper_file.read_bytes()).hexdigest() != HELPER_SHA: raise RuntimeError('Exact previously reviewed owned-child helper changed') helper = {'__name__': 'owned_diagnostic_vendored_helper', '__file__': str(_helper_file)} exec(compile(_helper_file.read_bytes(), str(_helper_file), 'exec'), helper) BANNER = 'Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org' # Exact finite diagnostic families from the preserved strict collector r06. KNOWN_EXIT_LEAK = re.compile(r'^\s*(?:(?:WARNING:|ERROR:)[^\r\n]*(?:ObjectDB instances leaked|(?:[0-9]+ )?ObjectDB instances were leaked at exit|resources? still in use at exit)|Leaked instance:)[^\r\n]*$') DIGEST_SCRIPT = 'res://tests/performance/test_digest_performance.gd' DIGEST_SOURCE_SHA = '363772fc43975f12c9e2056e2b01beb4cd8b2d7544db7528f80c6bd127710c1f' DIGEST_UNICODE_LINE = 'Unicode parsing error, some characters were replaced with � (U+FFFD): Unexpected NUL character' def process_probe(pid): try: raw = subprocess.check_output(['ps', '-p', str(pid), '-o', 'pid=,ppid=,pgid=,args='], text=True).strip() except subprocess.CalledProcessError: return None parts = raw.split(None, 3) if len(parts) != 4: return None return {'pid': int(parts[0]), 'ppid': int(parts[1]), 'pgid': int(parts[2]), 'args': parts[3]} def native_diagnostics(stdout, stderr, argv): lines = stdout.splitlines(keepends=True) + stderr.splitlines(keepends=True) banners = [line for line in lines if line.startswith('Godot Engine v')] errors = [] if len(banners) != 1 or banners[0] not in [BANNER + '\n', BANNER + '\r\n']: errors.append('Missing, duplicate, wrong, or unterminated exact native engine banner') for raw in lines: line = raw.rstrip('\r\n') if any(token in line for token in ['ERROR:', 'SCRIPT ERROR:', 'Parse Error:']) or re.match(r'^\s*(?:FAIL:|FAILED:|FAIL )', line): errors.append(line) if KNOWN_EXIT_LEAK.fullmatch(line): errors.append(line) unicode_lines = [line.rstrip('\r\n') for line in lines if 'Unicode parsing error' in line] allowed_unicode = False if unicode_lines and '--script' in argv and '--path' in argv: script = argv[argv.index('--script') + 1] stage = Path(argv[argv.index('--path') + 1]) source = stage / DIGEST_SCRIPT.removeprefix('res://') allowed_unicode = script == DIGEST_SCRIPT and source.is_file() and hashlib.sha256(source.read_bytes()).hexdigest() == DIGEST_SOURCE_SHA and all(line == DIGEST_UNICODE_LINE for line in unicode_lines) if unicode_lines and not allowed_unicode: errors.append('Unrecognized Unicode diagnostic or source pin') return {'errors': errors, 'banner_count': len(banners), 'known_unicode_diagnostic_count': len(unicode_lines) if allowed_unicode else 0, 'unrecognized_unicode_diagnostic_count': 0 if allowed_unicode else len(unicode_lines), 'exit_leak_diagnostic_lines': [line.rstrip('\r\n') for line in lines if KNOWN_EXIT_LEAK.fullmatch(line.rstrip('\r\n'))]} def cleanup_actual_owned_child(process): """Extra bounded cleanup of exactly the factory's actual Popen object. The old helper's bytes/three tries remain unchanged. These are at most three additional poll/kill/wait attempts, each explicit wait at most 5s. Receipt completeness is independent of actual child-ended observation. """ rows = [] code = None if process is None: return {'owned_child_created': False, 'engine_ended': False, 'actual_process_exit_code': None, 'attempts': [], 'errors': []} errors = [] for attempt in range(3): row = {'attempt': attempt + 1, 'kill_attempted': False, 'wait_timeout_seconds': 5, 'wait_completed': False, 'errors': []} try: code = process.poll() row['poll_before'] = code except BaseException as caught: code = None row['errors'].append('poll before ' + type(caught).__name__ + ': ' + str(caught)) if code is None: row['kill_attempted'] = True try: process.kill() except BaseException as caught: row['errors'].append('kill ' + type(caught).__name__ + ': ' + str(caught)) try: waited = process.wait(timeout=5) row['wait_completed'] = True row['wait_returned_code'] = waited if type(waited) is int: code = waited except BaseException as caught: row['errors'].append('wait ' + type(caught).__name__ + ': ' + str(caught)) try: polled = process.poll() row['poll_after'] = polled if polled is not None: code = polled except BaseException as caught: row['errors'].append('poll after ' + type(caught).__name__ + ': ' + str(caught)) rows.append(row) errors.extend(row['errors']) if code is not None: break return {'owned_child_created': True, 'engine_ended': code is not None, 'actual_process_exit_code': code, 'attempts': rows, 'errors': errors} def run_owned_diagnostic(argv, folder, timeout, environment, *, now_fn, dump_fn, sha_fn, live_engines_fn, popen_fn=None, probe_fn=None, monotonic_fn=None): if live_engines_fn(): raise RuntimeError('Another Godot process owns CPU; diagnostic did not start') folder = Path(folder) folder.mkdir(exist_ok=False) started = now_fn() clock = monotonic_fn or time.monotonic tick = clock() owned = {'process': None, 'start': None, 'end': None, 'start_written': False, 'end_written': False} probe = probe_fn or process_probe actual_popen = popen_fn or subprocess.Popen cwd = Path(argv[argv.index('--path') + 1]) if '--path' in argv else folder with (folder / 'stdout.log').open('wb') as stdout, (folder / 'stderr.log').open('wb') as stderr: def spawn(command, **options): # No receipt, probe, or other failure-prone work before returning # the actual Popen to the helper's owned try/finally. process = actual_popen(command, cwd=options['cwd'], env=options['env'], stdout=stdout, stderr=stderr, start_new_session=True) owned['process'] = process class HelperPopenView: # Preserve the old helper's actual object identity externally. # Only its unbounded wait(None) is mapped to a 5s wait; normal # explicit import/probe/BUILD timeout arguments pass unchanged. @property def pid(self): return process.pid def poll(self): return process.poll() def kill(self): return process.kill() def wait(self, timeout=None): return process.wait(timeout=5 if timeout is None else timeout) return HelperPopenView() def write_start(receipt): process = owned['process'] if process is None or receipt['actual_pid'] != process.pid or receipt['command'] != argv or receipt['owner_pid'] != os.getpid(): raise RuntimeError('START binding differs from actual owned Popen/argv/owner') current = probe(process.pid) exact_current = current is not None and current['pid'] == process.pid and current['ppid'] == os.getpid() and current['pgid'] == process.pid and current['args'] == ' '.join(argv) ended_before_probe = current is None and process.poll() is not None record = dict(receipt, pid=process.pid, argv=argv, started_utc=started, current_process_identity=current, live_identity_exact=exact_current, tracked_child_ended_before_initial_probe=ended_before_probe, signal_scope='tracked Popen child only; no external group signal') owned['start'] = record if not exact_current and not ended_before_probe: raise RuntimeError('Actual owned child live PID/PPID/PGID/argv identity mismatch') dump_fn(folder / 'started.json', record) owned['start_written'] = True print('RUNTIME_SCALE_PROCESS_START ' + json.dumps({'name': folder.name, 'pid': process.pid}), flush=True) def write_end(receipt): process = owned['process'] if receipt['command'] != argv or receipt['owner_pid'] != os.getpid() or receipt['actual_pid'] != (process.pid if process is not None else None): raise RuntimeError('END binding differs from actual tracked Popen/argv/owner') record = dict(receipt, pid=process.pid if process is not None else None, argv=argv, ended_utc=now_fn(), signal_scope='tracked Popen child only; no external group signal') owned['end'] = record dump_fn(folder / 'ended.json', record) owned['end_written'] = True class LifecycleTextSink: def write(self, text): return stdout.write(text.encode('utf-8')) observed = None adapter_error = None cleanup = None try: observed = helper['run_owned'](argv, cwd, environment, LifecycleTextSink(), {'scope': 'isolated diagnostic owned child'}, timeout, write_start, write_end, popen=spawn) except BaseException as caught: # Includes a second interrupt in the old helper's unguarded poll. adapter_error = type(caught).__name__ + ': ' + str(caught) finally: # Keep the actual factory object, never discover or signal a PID # or group from ps/receipts. All cleanup operations are bounded and # catch BaseException individually, even after helper START failure. cleanup = cleanup_actual_owned_child(owned['process']) if observed is None: observed = {'actual_pid': owned['process'].pid if owned['process'] is not None else None, 'exit_code': 130 if adapter_error and adapter_error.startswith('KeyboardInterrupt') else 127, 'actual_process_exit_code': cleanup['actual_process_exit_code'], 'engine_ended': cleanup['engine_ended'], 'timed_out': None, 'lifecycle_error': 'Helper escaped before returning: ' + str(adapter_error), 'start_receipt_written': owned['start_written'], 'end_receipt_written': owned['end_written']} dump_fn(folder / 'adapter-owned-cleanup.json', cleanup) raw = native_diagnostics((folder / 'stdout.log').read_text(errors='replace'), (folder / 'stderr.log').read_text(errors='replace'), argv) errors = list(raw['errors']) if adapter_error is not None or cleanup['errors'] or not cleanup['engine_ended']: errors.append('Additional owned cleanup failed/interrupted: ' + str(adapter_error) + '; ' + str(cleanup['errors'])) if observed['lifecycle_error'] is not None or not observed['engine_ended'] or not observed['start_receipt_written'] or not observed['end_receipt_written']: errors.append('Owned lifecycle/START/END receipt incomplete: ' + str(observed['lifecycle_error'])) lifecycle_errors = [str(value) for value in [observed['lifecycle_error'], adapter_error] if value is not None] + cleanup['errors'] result = {'argv': argv, 'pid': owned['process'].pid if owned['process'] is not None else None, 'started_utc': started, 'ended_utc': now_fn(), 'wall_seconds': clock() - tick, 'exit_code': cleanup['actual_process_exit_code'] if cleanup['actual_process_exit_code'] is not None else observed['exit_code'], 'helper_exit_code': observed['exit_code'], 'timed_out': observed['timed_out'], 'stdout_sha256': sha_fn(folder / 'stdout.log'), 'stderr_sha256': sha_fn(folder / 'stderr.log'), 'error_lines': errors, 'owned_popen_ended': cleanup['engine_ended'], 'old_helper_engine_ended': observed['engine_ended'], 'start_receipt_written': observed['start_receipt_written'], 'end_receipt_written': observed['end_receipt_written'], 'lifecycle_error': '; '.join(lifecycle_errors) if lifecycle_errors else None, 'old_helper_lifecycle_error': observed['lifecycle_error'], 'actual_start_binding': owned['start'], 'actual_end_binding': owned['end'], 'strict_native_diagnostics': raw, 'external_process_or_group_signals': 0, 'owned_child_process_helper_sha256': HELPER_SHA, 'adapter_owned_cleanup': cleanup, 'adapter_error': adapter_error, 'old_helper_bytes_preserved': True, 'helper_wait_None_maps_to_actual_timeout_seconds': 5, 'explicit_helper_wait_timeouts_preserved': True, 'additional_cleanup_wait_timeout_seconds': 5, 'additional_cleanup_max_attempts': 3} dump_fn(folder / 'process.json', result) return result def process_gate(result, require_fn): require_fn(result['exit_code'] == 0 and result['timed_out'] is False and not result['error_lines'] and result['owned_popen_ended'] is True and result['start_receipt_written'] is True and result['end_receipt_written'] is True and result['lifecycle_error'] is None, 'Owned exit/timeout/START/END/exact banner/diagnostic gate failed')