From 14d931a8d7ff638f1ac031d22a25a3f087fe7b11 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Wed, 21 Oct 2026 06:40:00 +0800 Subject: [PATCH] perf(delivery): share byte proofs across bounded setup --- backend/tests/test_delivery_io.py | 234 ++++++++++++++++++++++++ deploy/release/README.md | 32 ++++ scripts/release/__main__.py | 4 +- scripts/release/dependencies.py | 4 +- scripts/release/dependency_install.py | 4 +- scripts/release/factory_service.py | 4 +- scripts/release/install.py | 2 +- scripts/release/journal.py | 4 +- scripts/release/manifest.py | 5 + scripts/release/proxy_install.py | 4 +- scripts/release/restore.py | 4 +- scripts/release/service_install.py | 4 +- scripts/release/services.py | 4 +- scripts/release/setup_session.py | 248 ++++++++++++++++++++++++++ 14 files changed, 538 insertions(+), 19 deletions(-) create mode 100644 scripts/release/setup_session.py diff --git a/backend/tests/test_delivery_io.py b/backend/tests/test_delivery_io.py index eea3c4d..65438e6 100644 --- a/backend/tests/test_delivery_io.py +++ b/backend/tests/test_delivery_io.py @@ -20,6 +20,7 @@ policy = importlib.import_module("scripts.release.policy") file_copy = importlib.import_module("scripts.release.file_copy") factory_service = importlib.import_module("scripts.release.factory_service") proxy_install = importlib.import_module("scripts.release.proxy_install") +setup_session = importlib.import_module("scripts.release.setup_session") @pytest.fixture @@ -350,3 +351,236 @@ def test_expired_copy_deadline_refuses_before_dispatch(): with pytest.raises(policy.DeliveryError, match="copy deadline exceeded"): file_copy.bounded_copies(list(range(8)), calls.append, deadline=time.monotonic() - 1) assert calls == [] + + +def test_setup_session_runs_real_cli_and_releases_complete_proofs(ordinary_bundle, monkeypatch): + # Ordinary owned data and a modeled operator identity, not installation or + # isolation acceptance. The real CLI and verifier still read fixture bytes. + bundle, expected = ordinary_bundle + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + reads = observed_digests(monkeypatch) + arguments = ["verify", str(bundle), "--manifest-sha256", expected] + with setup_session.SetupSession(maximum_seconds=5) as operation: + first = operation.invoke("scripts.release", arguments) + initial = list(reads) + second = operation.invoke("scripts.release", arguments) + assert first.exit_code == second.exit_code == 0 + assert first.stdout == second.stdout and json.loads(first.stdout)["bundle_valid"] + assert initial and reads == initial and not operation.completed + assert operation.completed and len(operation.records) == 2 + assert manifest._READ_SCOPE.get() is None + with setup_session.SetupSession(maximum_seconds=5) as another: + another.invoke("scripts.release", arguments) + assert reads == initial * 2 + with pytest.raises(policy.DeliveryError, match="single-use"): + with operation: + pass + + +def test_setup_session_does_not_remember_renamed_staging_tree(ordinary_bundle, monkeypatch): + bundle, expected = ordinary_bundle + reads = observed_digests(monkeypatch) + published = bundle.with_name(bundle.name + "-published") + with manifest.verification_scope(): + manifest.verify_staged(bundle, expected) + first = list(reads) + assert not manifest._READ_SCOPE.get().entries + bundle.rename(published) + manifest.verify(published, expected) + assert len(reads) == 2 * len(first) + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_checks_metadata_before_completing(ordinary_bundle, monkeypatch): + bundle, expected = ordinary_bundle + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + signature = manifest._bundle_signature + changed = False + + def drift(path): + value = signature(path) + if changed: + stamp = list(value["manifest.json"]) + stamp[-1] += 1 + value["manifest.json"] = tuple(stamp) + return value + + monkeypatch.setattr(manifest, "_bundle_signature", drift) + operation = setup_session.SetupSession(maximum_seconds=5) + with pytest.raises(policy.DeliveryError, match="verified bundle changed"): + with operation: + result = operation.invoke( + "scripts.release", ["verify", str(bundle), "--manifest-sha256", expected] + ) + assert result.exit_code == 0 and not operation.completed + changed = True + assert not operation.completed and manifest._READ_SCOPE.get() is None + + +def test_setup_session_keeps_cli_failure_and_stops_following_calls(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + operation = setup_session.SetupSession(maximum_seconds=5) + with pytest.raises(policy.DeliveryError, match="failed setup session"): + with operation: + result = operation.invoke("scripts.release", ["verify"]) + assert result.exit_code == 2 and result.stderr + with pytest.raises(policy.DeliveryError, match="active successful"): + operation.invoke("scripts.release", ["verify"]) + assert not operation.completed and operation.records[0]["actual_exit_code"] == 2 + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_excludes_runtime_execution_and_inside_override(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + for module, argv in [ + ("scripts.release.lifecycle", ["--action", "start"]), + ("scripts.release", ["build"]), + ("scripts.release.restore", ["--inside"]), + ("scripts.release.services", ["check-binding"]), + ("scripts.release.journal", ["--help"]), + ("scripts.release.dependencies", ["--check-service", "postgres"]), + ("scripts.release.dependencies", ["--check-s=postgres"]), + ("scripts.release.proxy_install", ["--check-binding"]), + ("scripts.release.proxy_install", ["--check-b"]), + ]: + with pytest.raises(policy.DeliveryError): + with setup_session.SetupSession(maximum_seconds=5) as operation: + operation.invoke(module, argv) + assert operation.records == [] and manifest._READ_SCOPE.get() is None + + +def test_setup_session_rejects_operator_identity_and_invalid_deadline(monkeypatch): + monkeypatch.setattr(setup_session.os, "geteuid", lambda: 1001) + with pytest.raises(policy.DeliveryError, match="trusted root operator"): + with setup_session.SetupSession(): + pass + for value in [False, 0, -1, 2401, float("inf"), float("nan"), "1800"]: + with pytest.raises(policy.DeliveryError, match="finite setup deadline"): + setup_session.SetupSession(maximum_seconds=value) + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_preserves_existing_earlier_alarm(monkeypatch): + import signal + + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + handler = signal.getsignal(signal.SIGALRM) + timer = signal.getitimer(signal.ITIMER_REAL) + + def caller_deadline(*args): + raise AssertionError("the earlier caller alarm must remain installed") + + try: + signal.signal(signal.SIGALRM, caller_deadline) + signal.setitimer(signal.ITIMER_REAL, 10) + with setup_session.SetupSession(maximum_seconds=20) as operation: + assert signal.getsignal(signal.SIGALRM) is caller_deadline + assert 0 < signal.getitimer(signal.ITIMER_REAL)[0] <= 10 + assert operation.completed and signal.getsignal(signal.SIGALRM) is caller_deadline + assert 0 < signal.getitimer(signal.ITIMER_REAL)[0] <= 10 + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, handler) + if timer[0]: + signal.setitimer(signal.ITIMER_REAL, *timer) + + +def test_setup_session_own_deadline_interrupts_call_and_clears_scope(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + entry = importlib.import_module("scripts.release.__main__") + + def wait_for_deadline(argv): + time.sleep(3) + + monkeypatch.setattr(entry, "main", wait_for_deadline) + operation = setup_session.SetupSession(maximum_seconds=0.1) + with pytest.raises(policy.DeliveryError, match="deadline exceeded"): + with operation: + operation.invoke("scripts.release", ["verify"]) + assert not operation.completed and manifest._READ_SCOPE.get() is None + + +def test_setup_session_call_count_is_finite(ordinary_bundle, monkeypatch): + bundle, expected = ordinary_bundle + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + operation = setup_session.SetupSession(maximum_seconds=5) + arguments = ["verify", str(bundle), "--manifest-sha256", expected] + with pytest.raises(policy.DeliveryError, match="oversized setup"): + with operation: + for _ in range(32): + assert operation.invoke("scripts.release", arguments).exit_code == 0 + operation.invoke("scripts.release", arguments) + assert len(operation.records) == 32 and not operation.completed + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_unexpected_exception_is_not_an_exit_success(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + entry = importlib.import_module("scripts.release.__main__") + + def refuse(argv): + print("ordinary progress before refusal") + raise OSError("ordinary call failure model") + + monkeypatch.setattr(entry, "main", refuse) + operation = setup_session.SetupSession(maximum_seconds=5) + with pytest.raises(OSError, match="ordinary call failure"): + with operation: + operation.invoke("scripts.release", ["verify"]) + assert operation.records[0]["actual_exit_code"] is None + assert operation.records[0]["actual_exception_type"] == "OSError" + assert operation.records[0]["stdout_bytes"] > 0 and not operation.completed + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_output_is_bounded_in_bytes(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + entry = importlib.import_module("scripts.release.__main__") + + def oversized(argv): + print("界" * 10923) + + monkeypatch.setattr(entry, "main", oversized) + operation = setup_session.SetupSession(maximum_seconds=5) + with pytest.raises(policy.DeliveryError, match="output exceeds bound"): + with operation: + operation.invoke("scripts.release", ["verify"]) + assert operation.records[0]["actual_exit_code"] is None + assert not operation.completed and manifest._READ_SCOPE.get() is None + + +def test_setup_session_rejects_private_option_abbreviations(monkeypatch): + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + for argument in ["--ins", "--inside=1", "--he", "--help=true", "-hfoo", "--"]: + with pytest.raises(policy.DeliveryError, match="setup CLI call"): + with setup_session.SetupSession(maximum_seconds=5) as operation: + operation.invoke("scripts.release.restore", [argument]) + assert operation.records == [] and not operation.completed + assert manifest._READ_SCOPE.get() is None + + +def test_setup_session_failed_alarm_installation_restores_handler(monkeypatch): + import signal + + monkeypatch.setattr(setup_session, "_require_operator", lambda: None) + original_handler = signal.getsignal(signal.SIGALRM) + original_timer = signal.getitimer(signal.ITIMER_REAL) + real = signal.setitimer + + def refused_alarm(which, seconds, interval=0): + if seconds: + raise OSError("ordinary timer-installation refusal model") + return real(which, seconds, interval) + + signal.setitimer(signal.ITIMER_REAL, 0) + monkeypatch.setattr(setup_session.signal, "setitimer", refused_alarm) + try: + with pytest.raises(OSError, match="timer-installation refusal"): + with setup_session.SetupSession(): + pass + assert signal.getsignal(signal.SIGALRM) is original_handler + assert manifest._READ_SCOPE.get() is None + finally: + monkeypatch.setattr(setup_session.signal, "setitimer", real) + if original_timer[0]: + signal.setitimer(signal.ITIMER_REAL, *original_timer) diff --git a/deploy/release/README.md b/deploy/release/README.md index 95bc01d..ff8a906 100644 --- a/deploy/release/README.md +++ b/deploy/release/README.md @@ -126,6 +126,38 @@ volumes, then follow this order: The tools use offline materials and do not contact a package index. +Installer automation can run the same setup CLI entry points in one bounded +Python operation. Use the protected delivery interpreter and its normal Python +environment, as the trusted Root operator: + +```python +from scripts.release.setup_session import SetupSession + +with SetupSession(maximum_seconds=1800) as setup: + for command in ("verify", "preflight", "install-files", "install-files"): + result = setup.invoke( + "scripts.release", + [command, "/path/to/bundle", "--manifest-sha256", trusted_manifest_sha256], + ) + if result.exit_code: + raise RuntimeError("setup CLI refused; inspect its bounded result") + # Continue the configuration/dependency/unit steps above with the same CLI + # module names and explicit argument lists through setup.invoke(). +assert setup.completed +``` + +The session permits at most 32 calls and a deadline up to 2400 seconds. It preserves +an earlier caller alarm. Calls retain their argument parsing, permission checks +and individual exit codes; service startup, judging and private `--inside` +helpers are excluded. Completed steps are not rolled back after a failure. + +The first verification of each unchanged bundle reads every byte. Reuse stays +within the same single-threaded process, with complete topology and metadata +checks on every reuse and before the session completes. Staging directories get +an uncached full byte check before publication. Results are provisional until +the context exits successfully; a new session or process starts fresh. Ordinary +standalone CLI invocations keep their existing behavior. + See the [operator command reference](../../scripts/release/README.md) for service, HTTPS, backup and recovery operations. Upgrades and restores must preserve immutable problem versions, submitted source, audit and original judging reports. diff --git a/scripts/release/__main__.py b/scripts/release/__main__.py index 32666b3..7dbad17 100644 --- a/scripts/release/__main__.py +++ b/scripts/release/__main__.py @@ -8,7 +8,7 @@ from pathlib import Path from . import build, configuration, database, install, manifest, policy, preflight, proxy -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="KF bounded offline delivery") sub = parser.add_subparsers(dest="command", required=True) check = sub.add_parser("contract") @@ -50,7 +50,7 @@ def main() -> None: check.add_argument("--manifest-sha256", required=True) check.add_argument("--configuration-id", required=True) sub.add_parser("check-active") - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.command == "contract": policy.contract(args.path) diff --git a/scripts/release/dependencies.py b/scripts/release/dependencies.py index 0f4b6da..ff07f3d 100644 --- a/scripts/release/dependencies.py +++ b/scripts/release/dependencies.py @@ -437,12 +437,12 @@ def generate(connections_path: Path, output: Path) -> dict: return result -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Bounded local dependency composition") parser.add_argument("--connections", type=Path) parser.add_argument("--output", type=Path) parser.add_argument("--check-service", choices=STORAGE) - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.check_service is not None: if args.connections is not None or args.output is not None: diff --git a/scripts/release/dependency_install.py b/scripts/release/dependency_install.py index 3e33fb2..5dd04b9 100644 --- a/scripts/release/dependency_install.py +++ b/scripts/release/dependency_install.py @@ -510,13 +510,13 @@ def install_snapshot(source: Path, expected: str, cookie: Path, cookie_sha: str) } -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Protected dependency snapshot installation") parser.add_argument("directory", type=Path) parser.add_argument("--receipt-sha256", required=True) parser.add_argument("--erlang-cookie", required=True, type=Path) parser.add_argument("--cookie-sha256", required=True) - args = parser.parse_args() + args = parser.parse_args(argv) try: print( json.dumps( diff --git a/scripts/release/factory_service.py b/scripts/release/factory_service.py index a5e4929..cf23c1d 100644 --- a/scripts/release/factory_service.py +++ b/scripts/release/factory_service.py @@ -305,13 +305,13 @@ def install_unit(source: Path, expected: str) -> dict: } -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Protected Root factory unit composition") action = parser.add_mutually_exclusive_group(required=True) action.add_argument("--output", type=Path) action.add_argument("--install", type=Path) parser.add_argument("--receipt-sha256") - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.output is not None: if args.receipt_sha256 is not None: diff --git a/scripts/release/install.py b/scripts/release/install.py index 163d690..6d05b62 100644 --- a/scripts/release/install.py +++ b/scripts/release/install.py @@ -307,7 +307,7 @@ def files(bundle: Path, expected: str) -> dict: # `frozen_copy` accepts names through the same bounded allowlist. frozen_copy(bundle / "payload", stage / "payload", verified_records=data["files"]) copy_verified(bundle / "manifest.json", stage / "manifest.json", expected, 0o444) - manifest.verify(stage, expected) + manifest.verify_staged(stage, expected) stage.chmod(0o555) os.rename(stage, release) sync_directory(RELEASES) diff --git a/scripts/release/journal.py b/scripts/release/journal.py index 6971d08..237857e 100644 --- a/scripts/release/journal.py +++ b/scripts/release/journal.py @@ -355,9 +355,9 @@ def install_profile() -> dict: } -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Install only the fixed KF log namespace profile") - parser.parse_args() + parser.parse_args(argv) try: print(json.dumps(install_profile(), sort_keys=True)) except (OSError, ValueError, subprocess.SubprocessError) as error: diff --git a/scripts/release/manifest.py b/scripts/release/manifest.py index 99c28a6..36a96e9 100644 --- a/scripts/release/manifest.py +++ b/scripts/release/manifest.py @@ -350,6 +350,11 @@ def verify(bundle: Path, expected_sha256: str) -> dict: return data +def verify_staged(bundle: Path, expected_sha256: str) -> dict: + """Read every byte of a staging tree without remembering its temporary name.""" + return _verify_contents(bundle, expected_sha256) + + def _verify_contents(bundle: Path, expected_sha256: str) -> dict: """The manifest digest comes from a separate trusted operator receipt.""" expected_sha256 = sha256(expected_sha256) diff --git a/scripts/release/proxy_install.py b/scripts/release/proxy_install.py index a39b4fe..489f7d5 100644 --- a/scripts/release/proxy_install.py +++ b/scripts/release/proxy_install.py @@ -450,7 +450,7 @@ def install_proxy(source: Path, expected: str, certificate: Path, key: Path) -> } -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Fixed protected HTTPS installation") parser.add_argument("--check-binding", action="store_true") parser.add_argument("--release-id") @@ -459,7 +459,7 @@ def main() -> None: parser.add_argument("--receipt-sha256") parser.add_argument("--certificate", type=Path) parser.add_argument("--private-key", type=Path) - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.check_binding: if ( diff --git a/scripts/release/restore.py b/scripts/release/restore.py index c3dcfa6..5161a12 100644 --- a/scripts/release/restore.py +++ b/scripts/release/restore.py @@ -428,13 +428,13 @@ def launch(recovery_id: str, backup_id: str, expected: str) -> dict: return json.loads(result.stdout) -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Private bounded cold data restoration") parser.add_argument("--recovery-id", required=True) parser.add_argument("--backup-id", required=True) parser.add_argument("--manifest-sha256", required=True) parser.add_argument("--inside", action="store_true", help=argparse.SUPPRESS) - args = parser.parse_args() + args = parser.parse_args(argv) try: if os.getuid() != 0 or os.geteuid() != 0: raise DeliveryError("explicit root cold recovery operator required") diff --git a/scripts/release/service_install.py b/scripts/release/service_install.py index 1dfcc64..a9362b7 100644 --- a/scripts/release/service_install.py +++ b/scripts/release/service_install.py @@ -276,11 +276,11 @@ def install_units(source: Path, expected: str) -> dict: } -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Root ordinary-unit installation") parser.add_argument("directory", type=Path) parser.add_argument("--receipt-sha256", required=True) - args = parser.parse_args() + args = parser.parse_args(argv) try: print(json.dumps(install_units(args.directory, args.receipt_sha256), sort_keys=True)) except (OSError, ValueError, subprocess.SubprocessError) as error: diff --git a/scripts/release/services.py b/scripts/release/services.py index a7f9b23..ded7b33 100644 --- a/scripts/release/services.py +++ b/scripts/release/services.py @@ -181,7 +181,7 @@ def generate( return result -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Bounded ordinary service composition") sub = parser.add_subparsers(dest="command", required=True) check = sub.add_parser("check-binding") @@ -193,7 +193,7 @@ def main() -> None: check.add_argument("--configuration", type=Path, required=True) check.add_argument("--configuration-receipt-sha256", required=True) check.add_argument("--output", type=Path, required=True) - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.command == "check-binding": if os.geteuid() != 0: diff --git a/scripts/release/setup_session.py b/scripts/release/setup_session.py new file mode 100644 index 0000000..0690259 --- /dev/null +++ b/scripts/release/setup_session.py @@ -0,0 +1,248 @@ +"""Finite operator automation using the existing setup CLI entry points.""" + +import contextlib +import hashlib +import importlib +import io +import os +import signal +import threading +import time +from dataclasses import dataclass + +from . import manifest +from .policy import DeliveryError + +_ENTRIES = { + "scripts.release": "scripts.release.__main__", + **{ + "scripts.release." + name: "scripts.release." + name + for name in ( + "journal", + "dependencies", + "dependency_install", + "restore", + "services", + "service_install", + "factory_service", + "proxy_install", + ) + }, +} +_DELIVERY_COMMANDS = { + "verify", + "preflight", + "install-files", + "generate-configuration", + "install-configuration", + "activate", + "generate-proxy", +} + + +def _require_operator() -> None: + if (os.getuid(), os.geteuid(), os.getgid(), os.getegid()) != ( + 0, + 0, + 0, + 0, + ) or not set(os.getgroups()) <= {0}: + raise DeliveryError("setup session requires the trusted root operator") + + +class _Output(io.StringIO): + def __init__(self, maximum: int): + super().__init__() + self.maximum = maximum + self.bytes_written = 0 + + def write(self, value: str) -> int: + size = len(value.encode("utf-8")) + if self.bytes_written + size > self.maximum: + raise DeliveryError("setup CLI output exceeds bound") + self.bytes_written += size + return super().write(value) + + +@dataclass(frozen=True, slots=True) +class CommandResult: + exit_code: int + stdout: str + stderr: str + + +class SetupSession: + """Run at most 32 setup calls in one synchronous, bounded operator operation. + + Individual CLI results are provisional until the context exits successfully. + This does not roll back completed installation steps or certify execution. + """ + + def __init__(self, *, maximum_seconds: float = 1800): + if ( + isinstance(maximum_seconds, bool) + or not isinstance(maximum_seconds, (int, float)) + or not 0 < maximum_seconds <= 2400 + ): + raise DeliveryError("finite setup deadline up to 2400 seconds required") + self.maximum_seconds = maximum_seconds + self.completed = False + self.records: list[dict] = [] + self._entered = False + self._used = False + self._failed = False + self._owns_alarm = False + + def _owner(self) -> None: + _require_operator() + if ( + os.getpid() != self._pid + or threading.get_ident() != self._thread + or threading.current_thread() is not threading.main_thread() + or threading.active_count() != 1 + ): + raise DeliveryError("same single-threaded setup caller required") + + def _deadline(self) -> None: + if time.monotonic() >= self._deadline_at: + self._failed = True + raise DeliveryError("setup operation deadline exceeded") + + def _expired(self, _signum, _frame) -> None: + self._failed = True + raise DeliveryError("setup operation deadline exceeded") + + def __enter__(self): + if self._used: + raise DeliveryError("setup session is single-use") + self._pid, self._thread = os.getpid(), threading.get_ident() + self._owner() + self._used = True + self._started = time.monotonic() + self._deadline_at = self._started + self.maximum_seconds + self._old_handler = signal.getsignal(signal.SIGALRM) + self._old_timer = signal.getitimer(signal.ITIMER_REAL) + try: + # An earlier caller deadline retains its original handler and timer. + if not self._old_timer[0] or self._old_timer[0] > self.maximum_seconds: + self._owns_alarm = True + signal.signal(signal.SIGALRM, self._expired) + signal.setitimer(signal.ITIMER_REAL, self.maximum_seconds) + self._scope = manifest.verification_scope() + self._scope.__enter__() + self._entered = True + except BaseException: + self._restore_alarm() + raise + return self + + def _restore_alarm(self) -> None: + if self._owns_alarm: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, self._old_handler) + if self._old_timer[0]: + remaining = self._old_timer[0] - (time.monotonic() - self._started) + signal.setitimer(signal.ITIMER_REAL, max(0.000001, remaining), self._old_timer[1]) + self._owns_alarm = False + + def __exit__(self, kind, value, traceback): + try: + if kind is None: + try: + self._owner() + self._deadline() + if self._failed: + raise DeliveryError("failed setup session cannot complete") + except BaseException as error: + self._scope.__exit__(type(error), error, error.__traceback__) + raise + self._scope.__exit__(kind, value, traceback) + self.completed = kind is None and not self._failed + finally: + self._entered = False + self._restore_alarm() + return False + + def invoke(self, module: str, argv: list[str]) -> CommandResult: + if not self._entered or self._failed: + raise DeliveryError("active successful setup session required") + try: + self._owner() + self._deadline() + if ( + not isinstance(module, str) + or module not in _ENTRIES + or not isinstance(argv, list) + or len(argv) > 64 + or any( + not isinstance(arg, str) + or not 0 < len(arg.encode("utf-8")) <= 4096 + or "\x00" in arg + or arg.startswith("-h") + or ( + arg.startswith("--") + and any( + private.startswith(arg.split("=", 1)[0]) + for private in ("--help", "--inside") + ) + ) + for arg in argv + ) + or len(self.records) >= 32 + ): + raise DeliveryError("unknown or oversized setup CLI call") + if module == "scripts.release" and (not argv or argv[0] not in _DELIVERY_COMMANDS): + raise DeliveryError("command is outside offline setup") + if module == "scripts.release.services" and (not argv or argv[0] != "generate"): + raise DeliveryError("only service composition belongs to setup") + if module == "scripts.release.journal" and argv: + raise DeliveryError("fixed journal setup takes no overrides") + runtime_option = { + "scripts.release.dependencies": "--check-service", + "scripts.release.proxy_install": "--check-binding", + }.get(module) + if runtime_option and any( + arg.startswith("--") and runtime_option.startswith(arg.split("=", 1)[0]) + for arg in argv + ): + raise DeliveryError("runtime checks are outside setup") + entry = importlib.import_module(_ENTRIES[module]).main + stdout, stderr = _Output(32768), _Output(8192) + started = time.monotonic() + record = {"module": module, "actual_exit_code": None} + self.records.append(record) + try: + with ( + contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr), + ): + try: + entry(list(argv)) + except SystemExit as error: + code = ( + error.code + if type(error.code) is int + else (0 if error.code is None else 1) + ) + else: + code = 0 + record["actual_exit_code"] = code + except BaseException as error: + record["actual_exception_type"] = type(error).__name__ + raise + finally: + record.update( + seconds=time.monotonic() - started, + stdout_bytes=stdout.bytes_written, + stdout_sha256=hashlib.sha256(stdout.getvalue().encode()).hexdigest(), + stderr_bytes=stderr.bytes_written, + stderr_sha256=hashlib.sha256(stderr.getvalue().encode()).hexdigest(), + ) + result = CommandResult(code, stdout.getvalue(), stderr.getvalue()) + if code: + self._failed = True + self._deadline() + return result + except BaseException: + self._failed = True + raise