Retain aircraft through native landing and qualify bounded input diagnostics

This commit is contained in:
2026-09-19 20:34:18 +02:00
parent f870443b9b
commit 2fba12f987
129 changed files with 50369 additions and 88 deletions
+32 -14
View File
@@ -18,7 +18,7 @@ import uuid
import zipfile
from source_bundle import build, inventory, PROJECTS
from headless_game import assert_no_game, assert_prefix_idle, cleanup
from headless_game import assert_no_game, assert_prefix_idle, cleanup_owned_run
from native_report import analyze
ROOT = Path(__file__).resolve().parents[1]
@@ -109,7 +109,9 @@ def copied_authored_unchanged(source, authored):
return copied_authored_snapshot(source, authored) == authored
def command_for(steam, session, project, silent_workbench=False):
def command_for(steam, session, project, silent_workbench=False, exit_after_init=False):
if silent_workbench and exit_after_init:
raise ValueError("Choose only one Workbench exit mode")
if project not in PROJECTS:
raise ValueError('Unknown successor project')
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
@@ -125,6 +127,8 @@ def command_for(steam, session, project, silent_workbench=False):
'-packAddonCacheDir', 'Z:'+str(session/'cache'/project)]
if silent_workbench:
command.append('-wbSilent')
elif exit_after_init:
command.append('-exitAfterInit')
return command
@@ -195,8 +199,8 @@ def quiet_streams(application_id, sink, compat):
subprocess.run(['pactl', *args], check=True, timeout=5, stdout=subprocess.DEVNULL)
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False):
command = command_for(steam, session, project, silent_workbench)
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False, exit_after_init=False):
command = command_for(steam, session, project, silent_workbench, exit_after_init)
proton = Path(command[command.index('--')+1])
executable = Path(command[command.index('--')+3])
profile = session/'profiles'/project
@@ -218,7 +222,8 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
save(evidence/'launch.json', {'command': command, 'native_command': native_command, 'authored': authored, 'started_unix': time.time(),
'executable_sha256': digest(executable), 'proton_sha256': digest(proton),
'child_recorder_sha256': digest(recorder), 'native_environment_sha256': digest(environment_path),
'silent_workbench_experiment': silent_workbench})
'silent_workbench_experiment': silent_workbench,
'exit_after_init_experiment': exit_after_init})
run_id = uuid.uuid4().hex
sink, application_id = 'erri_pack_'+run_id, 'org.escapistsrereintroduced.pack.'+run_id
env = os.environ.copy()
@@ -231,6 +236,7 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
module = None
exit_code = None
result, cleanup_ok, runner_error = 'timeout', True, None
cleanup_report = None
launch_log = evidence/'launch.log'
deadline = time.monotonic()+timeout
launcher_exited_at = None
@@ -273,12 +279,19 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
finally:
if child is not None:
try:
# Reuse guarded ordinary runner cleanup, only after owning the
# shared prefix lock and proving that prefix was initially idle.
cleanup(child, proton, env)
assert_prefix_idle(compat)
# Shared post-run adapter: normal shutdown first, then only its
# proven canonical adopted helpers, followed by strict idle proof.
# This never upgrades the completion result observed before cleanup.
cleanup_report = cleanup_owned_run(child, proton, env, compat)
cleanup_ok = isinstance(cleanup_report, dict) and cleanup_report.get('cleanup_confirmed') is True
if not cleanup_ok:
detail = 'Owned packaging cleanup unconfirmed'
if isinstance(cleanup_report, dict):
detail += ': ' + '; '.join(str(item) for item in cleanup_report.get('errors', []))
runner_error = (runner_error + '; ' if runner_error else '') + detail
except (OSError, ValueError, subprocess.SubprocessError) as error:
cleanup_ok, runner_error = False, str(error)
cleanup_ok = False
runner_error = (runner_error + '; ' if runner_error else '') + 'Owned packaging cleanup: ' + str(error)
if module is not None:
try:
subprocess.run(['pactl', 'unload-module', module], check=True, timeout=5, stdout=subprocess.DEVNULL)
@@ -306,8 +319,9 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
'candidate_created': candidate, 'native_exit_code': native_exit, 'launcher_exit_code': exit_code,
'completion_observed': result, 'display_cleanup_after_native_exit': result == 'native-child-exited',
'silent_workbench_experiment': silent_workbench,
'exit_after_init_experiment': exit_after_init,
'native_status_sha256': digest(status_path) if status_path.is_file() and not status_path.is_symlink() else None,
'cleanup_confirmed': cleanup_ok,
'cleanup_confirmed': cleanup_ok, 'cleanup_report': cleanup_report,
'copied_authored_unchanged': unchanged, 'runner_error': runner_error,
'logs': logs, 'errors': errors, 'packages': packages,
'package_contents_verified': False, 'native_consumption_verified': False,
@@ -323,7 +337,9 @@ def run(a, root=ROOT):
validate_session(root, steam, compat, session)
if not 30 <= a.timeout <= 1800:
raise ValueError('--timeout must be 30..1800 seconds per addon')
command = command_for(steam, session, a.project)
silent_workbench = getattr(a, 'silent_workbench', False)
exit_after_init = getattr(a, 'exit_after_init', False)
command = command_for(steam, session, a.project, silent_workbench, exit_after_init)
separator = command.index('--')
for path in (Path(command[separator+1]), Path(command[separator+3]), steam/'steamapps/common/Arma Reforger/addons/data/ArmaReforger.gproj'):
if not path.is_file():
@@ -344,7 +360,7 @@ def run(a, root=ROOT):
continue_on_failure = getattr(a, 'continue_on_failure', False)
stopped_reason = None
for project in order:
report = run_project(steam, compat, session, project, a.timeout, authored, getattr(a, 'silent_workbench', False))
report = run_project(steam, compat, session, project, a.timeout, authored, silent_workbench, exit_after_init)
reports.append(report)
# Diagnostic continuation never grants authority to reuse an unsafe
# prefix or a changed source copy. Timeout/interruption also stop.
@@ -392,7 +408,9 @@ def main(argv=None):
parser.add_argument('--compat', type=Path, required=True, help='Idle disposable cloned Tools prefix under /tmp')
parser.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
parser.add_argument('--timeout', type=int, default=300, help='30..1800 seconds per addon')
parser.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle = parser.add_mutually_exclusive_group()
lifecycle.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle.add_argument('--exit-after-init', action='store_true', help='Opt-in documented -exitAfterInit experiment; natural exit and all native error gates remain mandatory')
parser.add_argument('--continue-on-failure', action='store_true', help='Diagnostic only: continue completed, safely cleaned addon attempts after validation failures; any failure keeps overall exit nonzero and unqualified')
args = parser.parse_args(argv)
try: