From 23bd1ee632e4b9f63e8b240c67045a2d06fd6e09 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:01:00 +0530 Subject: [PATCH] fix(gateway): launchd identity requires darwin, like control_socket --- gateway/restart.py | 11 +++++------ tests/gateway/test_launchd_exit_timeout_drain_cap.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/gateway/restart.py b/gateway/restart.py index cbaf26a5c8..1589e1b01c 100644 --- a/gateway/restart.py +++ b/gateway/restart.py @@ -4,6 +4,7 @@ import math import os import re import subprocess +import sys from collections.abc import Callable, Mapping from hermes_cli.config import DEFAULT_CONFIG @@ -93,6 +94,8 @@ def launchd_service_label(environ: Mapping[str, str] | None = None) -> str | Non ``exit timeout = 1`` for them — treating those as a budget would cap the drain to 0 for a gateway Ctrl+C'd in such a terminal. """ + if sys.platform != "darwin": + return None env = os.environ if environ is None else environ label = str(env.get("XPC_SERVICE_NAME", "") or "").strip() if not label.startswith("ai.hermes"): @@ -110,7 +113,7 @@ def read_launchd_exit_timeout_s( """Live ``ExitTimeOut`` (seconds) launchd enforces for this gateway's job. Returns ``None`` — meaning "no launchd budget applies" — when the process - is not launchd-owned (no ``XPC_SERVICE_NAME``), ``launchctl`` is missing + is not launchd-owned (non-darwin, or no ``ai.hermes`` ``XPC_SERVICE_NAME``), ``launchctl`` is missing or fails, or the print output carries no ``exit timeout`` line. Callers must treat ``None`` as fail-open: the configured drain stands unchanged. """ @@ -118,11 +121,7 @@ def read_launchd_exit_timeout_s( if not label: return None if uid is None: - # launchd is macOS-only; Windows has no os.getuid, so resolve it via getattr. - _getuid = getattr(os, "getuid", None) - if _getuid is None: - return None - uid = _getuid() + uid = os.getuid() # only reachable on darwin: launchd_service_label() is None elsewhere domain = "system" if uid == 0 else f"gui/{uid}" try: proc = run( diff --git a/tests/gateway/test_launchd_exit_timeout_drain_cap.py b/tests/gateway/test_launchd_exit_timeout_drain_cap.py index b7510294a8..cec954d318 100644 --- a/tests/gateway/test_launchd_exit_timeout_drain_cap.py +++ b/tests/gateway/test_launchd_exit_timeout_drain_cap.py @@ -17,6 +17,7 @@ from types import SimpleNamespace import pytest +import gateway.restart as restart_mod from gateway.restart import ( LAUNCHD_STOP_CLEANUP_RESERVE_S, effective_stop_drain_timeout, @@ -28,14 +29,17 @@ from gateway.shutdown_watchdog import resolve_shutdown_watchdog_delay @pytest.mark.parametrize( - "label, expected", + "platform, label, expected", [ - ("ai.hermes.gateway", 60.0 - LAUNCHD_STOP_CLEANUP_RESERVE_S), + ("darwin", "ai.hermes.gateway", 60.0 - LAUNCHD_STOP_CLEANUP_RESERVE_S), # App-coalition label (IDE integrated terminal) is not our job: no budget, drain unchanged. - ("application.com.example.ide.123", 180.0), + ("darwin", "application.com.example.ide.123", 180.0), + # launchd is darwin-only (same predicate as control_socket): a leaked label elsewhere is ignored. + ("linux", "ai.hermes.gateway", 180.0), ], ) -def test_capped_drain_fits_inside_launchd_budget_minus_reserve(label, expected): +def test_capped_drain_fits_inside_launchd_budget_minus_reserve(monkeypatch, platform, label, expected): + monkeypatch.setattr(restart_mod.sys, "platform", platform) # The incident shape: configured 180s, launchd clamps to 60s. assert resolve_launchd_capped_drain(180.0, 60.0) == 60.0 - LAUNCHD_STOP_CLEANUP_RESERVE_S # Never extends a short drain; no launchd budget leaves the configured drain alone.