mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(gateway): replace deprecated launchctl start/stop with kickstart/kill
launchctl load/unload/start/stop are deprecated on macOS since 10.10 and fail silently on modern versions. This replaces them with the current equivalents: - load -> bootstrap gui/<uid> <plist> - unload -> bootout gui/<uid>/<label> - start -> kickstart gui/<uid>/<label> - stop -> kill SIGTERM gui/<uid>/<label> Adds _launchd_domain() helper returning the gui/<uid> target domain. Updates test assertions to match the new command signatures. Fixes #4820
This commit is contained in:
parent
abf1be564b
commit
aab74b582c
2 changed files with 31 additions and 20 deletions
|
|
@ -873,6 +873,11 @@ def get_launchd_label() -> str:
|
|||
return f"ai.hermes.gateway-{suffix}" if suffix else "ai.hermes.gateway"
|
||||
|
||||
|
||||
def _launchd_domain() -> str:
|
||||
import os
|
||||
return f"gui/{os.getuid()}"
|
||||
|
||||
|
||||
def generate_launchd_plist() -> str:
|
||||
python_path = get_python_path()
|
||||
working_dir = str(PROJECT_ROOT)
|
||||
|
|
@ -963,18 +968,19 @@ def launchd_plist_is_current() -> bool:
|
|||
def refresh_launchd_plist_if_needed() -> bool:
|
||||
"""Rewrite the installed launchd plist when the generated definition has changed.
|
||||
|
||||
Unlike systemd, launchd picks up plist changes on the next ``launchctl stop``/
|
||||
``launchctl start`` cycle — no daemon-reload is needed. We still unload/reload
|
||||
to make launchd re-read the updated plist immediately.
|
||||
Unlike systemd, launchd picks up plist changes on the next ``launchctl kill``/
|
||||
``launchctl kickstart`` cycle — no daemon-reload is needed. We still bootout/
|
||||
bootstrap to make launchd re-read the updated plist immediately.
|
||||
"""
|
||||
plist_path = get_launchd_plist_path()
|
||||
if not plist_path.exists() or launchd_plist_is_current():
|
||||
return False
|
||||
|
||||
plist_path.write_text(generate_launchd_plist(), encoding="utf-8")
|
||||
# Unload/reload so launchd picks up the new definition
|
||||
subprocess.run(["launchctl", "unload", str(plist_path)], check=False)
|
||||
subprocess.run(["launchctl", "load", str(plist_path)], check=False)
|
||||
label = get_launchd_label()
|
||||
# Bootout/bootstrap so launchd picks up the new definition
|
||||
subprocess.run(["launchctl", "bootout", f"{_launchd_domain()}/{label}"], check=False)
|
||||
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=False)
|
||||
print("↻ Updated gateway launchd service definition to match the current Hermes install")
|
||||
return True
|
||||
|
||||
|
|
@ -996,7 +1002,7 @@ def launchd_install(force: bool = False):
|
|||
print(f"Installing launchd service to: {plist_path}")
|
||||
plist_path.write_text(generate_launchd_plist())
|
||||
|
||||
subprocess.run(["launchctl", "load", str(plist_path)], check=True)
|
||||
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True)
|
||||
|
||||
print()
|
||||
print("✓ Service installed and loaded!")
|
||||
|
|
@ -1008,7 +1014,8 @@ def launchd_install(force: bool = False):
|
|||
|
||||
def launchd_uninstall():
|
||||
plist_path = get_launchd_plist_path()
|
||||
subprocess.run(["launchctl", "unload", str(plist_path)], check=False)
|
||||
label = get_launchd_label()
|
||||
subprocess.run(["launchctl", "bootout", f"{_launchd_domain()}/{label}"], check=False)
|
||||
|
||||
if plist_path.exists():
|
||||
plist_path.unlink()
|
||||
|
|
@ -1025,25 +1032,25 @@ def launchd_start():
|
|||
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)
|
||||
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True)
|
||||
subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True)
|
||||
print("✓ Service started")
|
||||
return
|
||||
|
||||
refresh_launchd_plist_if_needed()
|
||||
try:
|
||||
subprocess.run(["launchctl", "start", label], check=True)
|
||||
subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 3:
|
||||
raise
|
||||
print("↻ launchd job was unloaded; reloading service definition")
|
||||
subprocess.run(["launchctl", "load", str(plist_path)], check=True)
|
||||
subprocess.run(["launchctl", "start", label], check=True)
|
||||
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True)
|
||||
subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True)
|
||||
print("✓ Service started")
|
||||
|
||||
def launchd_stop():
|
||||
label = get_launchd_label()
|
||||
subprocess.run(["launchctl", "stop", label], check=True)
|
||||
subprocess.run(["launchctl", "kill", "SIGTERM", f"{_launchd_domain()}/{label}"], check=True)
|
||||
print("✓ Service stopped")
|
||||
|
||||
def _wait_for_gateway_exit(timeout: float = 10.0, force_after: float = 5.0):
|
||||
|
|
|
|||
|
|
@ -171,10 +171,12 @@ class TestLaunchdServiceRecovery:
|
|||
|
||||
gateway_cli.launchd_install()
|
||||
|
||||
label = gateway_cli.get_launchd_label()
|
||||
domain = gateway_cli._launchd_domain()
|
||||
assert "--replace" in plist_path.read_text(encoding="utf-8")
|
||||
assert calls[:2] == [
|
||||
["launchctl", "unload", str(plist_path)],
|
||||
["launchctl", "load", str(plist_path)],
|
||||
["launchctl", "bootout", f"{domain}/{label}"],
|
||||
["launchctl", "bootstrap", domain, str(plist_path)],
|
||||
]
|
||||
|
||||
def test_launchd_start_reloads_unloaded_job_and_retries(self, tmp_path, monkeypatch):
|
||||
|
|
@ -183,10 +185,12 @@ class TestLaunchdServiceRecovery:
|
|||
label = gateway_cli.get_launchd_label()
|
||||
|
||||
calls = []
|
||||
domain = gateway_cli._launchd_domain()
|
||||
target = f"{domain}/{label}"
|
||||
|
||||
def fake_run(cmd, check=False, **kwargs):
|
||||
calls.append(cmd)
|
||||
if cmd == ["launchctl", "start", label] and calls.count(cmd) == 1:
|
||||
if cmd == ["launchctl", "kickstart", target] and calls.count(cmd) == 1:
|
||||
raise gateway_cli.subprocess.CalledProcessError(3, cmd, stderr="Could not find service")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
|
|
@ -196,9 +200,9 @@ class TestLaunchdServiceRecovery:
|
|||
gateway_cli.launchd_start()
|
||||
|
||||
assert calls == [
|
||||
["launchctl", "start", label],
|
||||
["launchctl", "load", str(plist_path)],
|
||||
["launchctl", "start", label],
|
||||
["launchctl", "kickstart", target],
|
||||
["launchctl", "bootstrap", domain, str(plist_path)],
|
||||
["launchctl", "kickstart", target],
|
||||
]
|
||||
|
||||
def test_launchd_status_reports_local_stale_plist_when_unloaded(self, tmp_path, monkeypatch, capsys):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue