refactor: simplify _get_service_pids — dedupe systemd scopes, fix self-import, harden launchd parsing

- Loop over user/system scope args instead of duplicating the systemd block
- Call get_launchd_label() directly instead of self-importing from hermes_cli.gateway
- Validate launchd output by checking parts[2] matches expected label (skip header)
- Add race-condition assumption docstring
This commit is contained in:
kshitijk4poor
2026-04-06 10:09:04 +05:30
committed by Teknium
parent a2a9ad7431
commit d3d5b895f6
2 changed files with 34 additions and 56 deletions

View File

@@ -32,76 +32,53 @@ def _get_service_pids() -> set:
"""Return PIDs currently managed by systemd or launchd gateway services. """Return PIDs currently managed by systemd or launchd gateway services.
Used to avoid killing freshly-restarted service processes when sweeping Used to avoid killing freshly-restarted service processes when sweeping
for stale manual gateway processes after a service restart. for stale manual gateway processes after a service restart. Relies on the
service manager having committed the new PID before the restart command
returns (true for both systemd and launchd in practice).
""" """
pids: set = set() pids: set = set()
# --- systemd (Linux) --- # --- systemd (Linux): user and system scopes ---
if is_linux(): if is_linux():
try: for scope_args in [["systemctl", "--user"], ["systemctl"]]:
result = subprocess.run( try:
["systemctl", "--user", "list-units", "hermes-gateway*", result = subprocess.run(
"--plain", "--no-legend", "--no-pager"], scope_args + ["list-units", "hermes-gateway*",
capture_output=True, text=True, timeout=5, "--plain", "--no-legend", "--no-pager"],
) capture_output=True, text=True, timeout=5,
for line in result.stdout.strip().splitlines(): )
parts = line.split() for line in result.stdout.strip().splitlines():
if not parts or not parts[0].endswith(".service"): parts = line.split()
continue if not parts or not parts[0].endswith(".service"):
svc = parts[0] continue
try: svc = parts[0]
show = subprocess.run( try:
["systemctl", "--user", "show", svc, show = subprocess.run(
"--property=MainPID", "--value"], scope_args + ["show", svc,
capture_output=True, text=True, timeout=5, "--property=MainPID", "--value"],
) capture_output=True, text=True, timeout=5,
pid = int(show.stdout.strip()) )
if pid > 0: pid = int(show.stdout.strip())
pids.add(pid) if pid > 0:
except (ValueError, subprocess.TimeoutExpired): pids.add(pid)
pass except (ValueError, subprocess.TimeoutExpired):
except (FileNotFoundError, subprocess.TimeoutExpired): pass
pass except (FileNotFoundError, subprocess.TimeoutExpired):
pass
# Also check system scope
try:
result = subprocess.run(
["systemctl", "list-units", "hermes-gateway*",
"--plain", "--no-legend", "--no-pager"],
capture_output=True, text=True, timeout=5,
)
for line in result.stdout.strip().splitlines():
parts = line.split()
if not parts or not parts[0].endswith(".service"):
continue
svc = parts[0]
try:
show = subprocess.run(
["systemctl", "show", svc,
"--property=MainPID", "--value"],
capture_output=True, text=True, timeout=5,
)
pid = int(show.stdout.strip())
if pid > 0:
pids.add(pid)
except (ValueError, subprocess.TimeoutExpired):
pass
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
# --- launchd (macOS) --- # --- launchd (macOS) ---
if is_macos(): if is_macos():
try: try:
from hermes_cli.gateway import get_launchd_label label = get_launchd_label()
result = subprocess.run( result = subprocess.run(
["launchctl", "list", get_launchd_label()], ["launchctl", "list", label],
capture_output=True, text=True, timeout=5, capture_output=True, text=True, timeout=5,
) )
if result.returncode == 0: if result.returncode == 0:
# Output format: "PID\tStatus\tLabel" header then data line # Output: "PID\tStatus\tLabel" header, then one data line
for line in result.stdout.strip().splitlines(): for line in result.stdout.strip().splitlines():
parts = line.split() parts = line.split()
if parts: if len(parts) >= 3 and parts[2] == label:
try: try:
pid = int(parts[0]) pid = int(parts[0])
if pid > 0: if pid > 0:

View File

@@ -662,6 +662,7 @@ class TestGetServicePids:
def test_returns_launchd_pid(self, monkeypatch): def test_returns_launchd_pid(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_linux", lambda: False) monkeypatch.setattr(gateway_cli, "is_linux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: True) monkeypatch.setattr(gateway_cli, "is_macos", lambda: True)
monkeypatch.setattr(gateway_cli, "get_launchd_label", lambda: "ai.hermes.gateway")
def fake_run(cmd, **kwargs): def fake_run(cmd, **kwargs):
joined = " ".join(str(c) for c in cmd) joined = " ".join(str(c) for c in cmd)