From 37d107e03dc8ce226902429a08d4d644141ac7e2 Mon Sep 17 00:00:00 2001 From: Rylen Anil Date: Wed, 29 Apr 2026 09:08:41 +0000 Subject: [PATCH] [verified] fix(gateway): accept user systemd private socket during preflight --- hermes_cli/gateway.py | 40 +++++++++++++++++------- tests/hermes_cli/test_gateway_service.py | 40 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index a3896b5bb..cb47533d8 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -830,6 +830,22 @@ def _user_dbus_socket_path() -> Path: return Path(xdg) / "bus" +def _user_systemd_private_socket_path() -> Path: + """Return the per-user systemd private socket path (regardless of existence).""" + xdg = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}" + return Path(xdg) / "systemd" / "private" + + +def _user_systemd_socket_ready() -> bool: + """Return True when user-scope systemd has a reachable control socket. + + Some distros expose only the per-user systemd private socket even when the + D-Bus session bus socket is absent. ``systemctl --user`` can still work in + that configuration, so preflight checks must treat either socket as valid. + """ + return _user_dbus_socket_path().exists() or _user_systemd_private_socket_path().exists() + + def _ensure_user_systemd_env() -> None: """Ensure DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are set for systemctl --user. @@ -853,28 +869,29 @@ def _ensure_user_systemd_env() -> None: def _wait_for_user_dbus_socket(timeout: float = 3.0) -> bool: - """Poll for the user D-Bus socket to appear, up to ``timeout`` seconds. + """Poll for the user systemd runtime socket(s), up to ``timeout`` seconds. - Linger-enabled user@.service can take a second or two to spawn the socket - after ``loginctl enable-linger`` runs. Returns True once the socket exists. + Linger-enabled user@.service can take a second or two to spawn its control + socket(s) after ``loginctl enable-linger`` runs. Returns True once either + the user D-Bus socket or the per-user systemd private socket exists. """ import time deadline = time.monotonic() + timeout while time.monotonic() < deadline: - if _user_dbus_socket_path().exists(): + if _user_systemd_socket_ready(): _ensure_user_systemd_env() return True time.sleep(0.2) - return _user_dbus_socket_path().exists() + return _user_systemd_socket_ready() def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: - """Ensure ``systemctl --user`` will reach the user D-Bus session bus. + """Ensure ``systemctl --user`` will reach the user-scope systemd instance. - No-op when the bus socket is already there (the common case on desktops - and linger-enabled servers). On fresh SSH sessions where the socket is - missing: + No-op when the user D-Bus socket or per-user systemd private socket is + already there (the common case on desktops and linger-enabled servers). On + fresh SSH sessions where both are missing: * If linger is already enabled, wait briefly for user@.service to spawn the socket. @@ -888,8 +905,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: systemd operations and surface the message to the user. """ _ensure_user_systemd_env() - bus_path = _user_dbus_socket_path() - if bus_path.exists(): + if _user_systemd_socket_ready(): return import getpass @@ -903,7 +919,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: # Linger is on but socket still missing — unusual; fall through to error. _raise_user_systemd_unavailable( username, - reason="User D-Bus socket is missing even though linger is enabled.", + reason="User systemd control sockets are missing even though linger is enabled.", fix_hint=( f" systemctl start user@{os.getuid()}.service\n" " (may require sudo; try again after the command succeeds)" diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index bd429bff2..f37e6837c 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -14,6 +14,26 @@ from gateway.restart import ( ) +class TestUserSystemdPrivateSocketPreflight: + def test_preflight_accepts_private_socket_without_dbus_bus(self, monkeypatch): + monkeypatch.setattr(gateway_cli, "_ensure_user_systemd_env", lambda: None) + monkeypatch.setattr(gateway_cli, "_user_dbus_socket_path", lambda: Path("/tmp/missing-bus")) + monkeypatch.setattr(gateway_cli, "_user_systemd_private_socket_path", lambda: Path("/tmp/private-socket")) + monkeypatch.setattr(Path, "exists", lambda self: str(self) == "/tmp/private-socket") + + gateway_cli._preflight_user_systemd(auto_enable_linger=False) + + def test_wait_for_user_dbus_socket_accepts_private_socket(self, monkeypatch): + calls = [] + monkeypatch.setattr(gateway_cli, "_ensure_user_systemd_env", lambda: calls.append("env")) + monkeypatch.setattr(gateway_cli, "_user_dbus_socket_path", lambda: Path("/tmp/missing-bus")) + monkeypatch.setattr(gateway_cli, "_user_systemd_private_socket_path", lambda: Path("/tmp/private-socket")) + monkeypatch.setattr(Path, "exists", lambda self: str(self) == "/tmp/private-socket") + + assert gateway_cli._wait_for_user_dbus_socket(timeout=0.1) is True + assert calls == ["env"] + + class TestSystemdServiceRefresh: def test_systemd_install_repairs_outdated_unit_without_force(self, tmp_path, monkeypatch): unit_path = tmp_path / "hermes-gateway.service" @@ -1105,6 +1125,10 @@ class TestPreflightUserSystemd: gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: True})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) # Should not raise, no subprocess calls needed. gateway_cli._preflight_user_systemd() @@ -1114,6 +1138,10 @@ class TestPreflightUserSystemd: gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (False, ""), ) @@ -1142,6 +1170,10 @@ class TestPreflightUserSystemd: gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (None, "loginctl not found"), @@ -1159,6 +1191,10 @@ class TestPreflightUserSystemd: gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (True, ""), ) @@ -1177,6 +1213,10 @@ class TestPreflightUserSystemd: gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (False, ""), )