fix(gateway): boot out stale launchd registration before restart bootstrap
launchd restart can leave the gateway job stopped but still registered after update-time drain logic, so a direct bootstrap hits exit 5 and falls back to a detached process. Booting the stale registration out before bootstrap keeps the launchd-managed restart path intact and locks it with a regression test. Constraint: Keep upstream-facing conventional commit style while preserving local decision context Rejected: Treat bootstrap exit 5 as expected | Leaves macOS launchd restart outside launchd supervision after update Confidence: high Scope-risk: narrow Directive: Keep launchd start/restart recovery flows aligned when changing launchctl handling Tested: pytest -q tests/hermes_cli/test_gateway_service.py -k "launchd_restart_boots_out_stale_registration_before_bootstrap or launchd_restart_falls_back_to_detached_on_error_5 or launchd_restart_drains_running_gateway_before_kickstart or launchd_restart_self_requests_graceful_restart_without_kickstart" Tested: pytest -q tests/hermes_cli/test_gateway_service.py -k launchd Not-tested: Manual macOS launchctl restart after hermes update
This commit is contained in:
parent
52a853f5c3
commit
c4719aa51c
2 changed files with 45 additions and 0 deletions
|
|
@ -4082,6 +4082,11 @@ def launchd_restart():
|
|||
print("↻ launchd job was unloaded; reloading")
|
||||
plist_path = get_launchd_plist_path()
|
||||
try:
|
||||
subprocess.run(
|
||||
["launchctl", "bootout", target],
|
||||
check=False,
|
||||
timeout=90,
|
||||
)
|
||||
subprocess.run(
|
||||
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
||||
check=True,
|
||||
|
|
|
|||
|
|
@ -1077,6 +1077,46 @@ class TestLaunchdServiceRecovery:
|
|||
assert spawned == [True]
|
||||
assert gateway_cli._launchd_unsupported_marker_exists()
|
||||
|
||||
def test_launchd_restart_boots_out_stale_registration_before_bootstrap(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
plist_path = tmp_path / "ai.hermes.gateway.plist"
|
||||
plist_path.write_text(gateway_cli.generate_launchd_plist(), encoding="utf-8")
|
||||
label = gateway_cli.get_launchd_label()
|
||||
domain = gateway_cli._launchd_domain()
|
||||
target = f"{domain}/{label}"
|
||||
|
||||
monkeypatch.setattr(gateway_cli, "get_launchd_plist_path", lambda: plist_path)
|
||||
monkeypatch.setattr(gateway_cli, "_get_restart_drain_timeout", lambda: 5.0)
|
||||
monkeypatch.setattr(gateway_cli, "_request_gateway_self_restart", lambda pid: False)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_wait_for_gateway_exit", lambda timeout, force_after=None: True
|
||||
)
|
||||
monkeypatch.setattr(gateway_cli, "terminate_pid", lambda pid, force=False: None)
|
||||
monkeypatch.setattr("gateway.status.get_running_pid", lambda: 321)
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, check=False, **kwargs):
|
||||
if cmd and cmd[0] == "launchctl":
|
||||
calls.append(cmd)
|
||||
if cmd == ["launchctl", "kickstart", "-k", target]:
|
||||
raise gateway_cli.subprocess.CalledProcessError(
|
||||
3, cmd, stderr="Could not find service"
|
||||
)
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run)
|
||||
|
||||
gateway_cli.launchd_restart()
|
||||
|
||||
assert calls == [
|
||||
["launchctl", "kickstart", "-k", target],
|
||||
["launchctl", "bootout", target],
|
||||
["launchctl", "bootstrap", domain, str(plist_path)],
|
||||
["launchctl", "kickstart", target],
|
||||
]
|
||||
|
||||
def test_launchd_stop_tolerates_domain_unsupported_bootout(self, monkeypatch, capsys):
|
||||
"""bootout exit 125 (macOS 26) must fall through to PID-based kill, not raise."""
|
||||
def fake_run(cmd, check=False, **kwargs):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue