fix(gateway): self-heal missing launchd plist on start (#3601)

When the plist is deleted (manual cleanup, failed upgrade),
hermes gateway start now regenerates it automatically instead of
failing. Also simplifies the returncode==3 error path since the
plist is guaranteed to exist at that point.

Co-authored-by: Bartok9 <Bartok9@users.noreply.github.com>
This commit is contained in:
Teknium
2026-03-28 14:48:55 -07:00
committed by GitHub
parent ee066b7be6
commit a641f20cac
2 changed files with 40 additions and 2 deletions

View File

@@ -948,13 +948,24 @@ def launchd_uninstall():
print("✓ Service uninstalled")
def launchd_start():
refresh_launchd_plist_if_needed()
plist_path = get_launchd_plist_path()
label = get_launchd_label()
# Self-heal if the plist is missing entirely (e.g., manual cleanup, failed upgrade)
if not plist_path.exists():
print("↻ launchd plist missing; regenerating service definition")
plist_path.parent.mkdir(parents=True, exist_ok=True)
plist_path.write_text(generate_launchd_plist(), encoding="utf-8")
subprocess.run(["launchctl", "load", str(plist_path)], check=True)
subprocess.run(["launchctl", "start", label], check=True)
print("✓ Service started")
return
refresh_launchd_plist_if_needed()
try:
subprocess.run(["launchctl", "start", label], check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 3 or not plist_path.exists():
if e.returncode != 3:
raise
print("↻ launchd job was unloaded; reloading service definition")
subprocess.run(["launchctl", "load", str(plist_path)], check=True)

View File

@@ -240,6 +240,33 @@ class TestLaunchdPlistRefresh:
assert any("unload" in s for s in cmd_strs)
assert any("start" in s for s in cmd_strs)
def test_launchd_start_recreates_missing_plist_and_loads_service(self, tmp_path, monkeypatch):
"""launchd_start self-heals when the plist file is missing entirely."""
plist_path = tmp_path / "ai.hermes.gateway.plist"
assert not plist_path.exists()
monkeypatch.setattr(gateway_cli, "get_launchd_plist_path", lambda: plist_path)
calls = []
def fake_run(cmd, check=False, **kwargs):
calls.append(cmd)
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run)
gateway_cli.launchd_start()
# Should have created the plist
assert plist_path.exists()
assert "--replace" in plist_path.read_text()
cmd_strs = [" ".join(c) for c in calls]
# Should load the new plist, then start
assert any("load" in s for s in cmd_strs)
assert any("start" in s for s in cmd_strs)
# Should NOT call unload (nothing to unload)
assert not any("unload" in s for s in cmd_strs)
class TestCmdUpdateLaunchdRestart:
"""cmd_update correctly detects and handles launchd on macOS."""