mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
fix(gateway): preflight user D-Bus before systemctl --user start (#14531)
On fresh RHEL/Debian SSH sessions without linger, `systemctl --user start hermes-gateway` fails with 'Failed to connect to bus: No medium found' because /run/user/$UID/bus doesn't exist. Setup previously showed a raw CalledProcessError and continued claiming success, so the gateway never actually started. systemd_start() and systemd_restart() now call _preflight_user_systemd() for the user scope first: - Bus socket already there → no-op (desktop / linger-enabled servers) - Linger off → try loginctl enable-linger (works when polkit permits, needs sudo otherwise), wait for socket - Still unreachable → raise UserSystemdUnavailableError with a clean remediation message pointing to sudo loginctl + hermes gateway run as the foreground fallback Setup's start/restart handlers and gateway_command() catch the new exception and render the multi-line guidance instead of a traceback.
This commit is contained in:
parent
d50be05b1c
commit
d45c738a52
3 changed files with 295 additions and 0 deletions
|
|
@ -5,6 +5,8 @@ import pwd
|
|||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import hermes_cli.gateway as gateway_cli
|
||||
from gateway.restart import (
|
||||
DEFAULT_GATEWAY_RESTART_DRAIN_TIMEOUT,
|
||||
|
|
@ -1083,6 +1085,116 @@ class TestEnsureUserSystemdEnv:
|
|||
assert calls == []
|
||||
|
||||
|
||||
class TestPreflightUserSystemd:
|
||||
"""Tests for _preflight_user_systemd() — D-Bus reachability before systemctl --user.
|
||||
|
||||
Covers issue #5130 / Rick's RHEL 9.6 SSH scenario: setup tries to start the
|
||||
gateway via ``systemctl --user start`` in a shell with no user D-Bus session,
|
||||
which previously failed with a raw ``CalledProcessError`` and no remediation.
|
||||
"""
|
||||
|
||||
def test_noop_when_bus_socket_exists(self, monkeypatch):
|
||||
"""Socket already there (desktop / linger + prior login) → no-op."""
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_user_dbus_socket_path",
|
||||
lambda: type("P", (), {"exists": lambda self: True})(),
|
||||
)
|
||||
# Should not raise, no subprocess calls needed.
|
||||
gateway_cli._preflight_user_systemd()
|
||||
|
||||
def test_raises_when_linger_disabled_and_loginctl_denied(self, monkeypatch):
|
||||
"""Rick's scenario: no D-Bus, no linger, non-root SSH → clear error."""
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_user_dbus_socket_path",
|
||||
lambda: type("P", (), {"exists": lambda self: False})(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "get_systemd_linger_status", lambda: (False, ""),
|
||||
)
|
||||
monkeypatch.setattr(gateway_cli.shutil, "which", lambda _: "/usr/bin/loginctl")
|
||||
|
||||
class _Result:
|
||||
returncode = 1
|
||||
stdout = ""
|
||||
stderr = "Interactive authentication required."
|
||||
|
||||
monkeypatch.setattr(
|
||||
gateway_cli.subprocess, "run", lambda *a, **kw: _Result(),
|
||||
)
|
||||
|
||||
with pytest.raises(gateway_cli.UserSystemdUnavailableError) as exc_info:
|
||||
gateway_cli._preflight_user_systemd()
|
||||
|
||||
msg = str(exc_info.value)
|
||||
assert "sudo loginctl enable-linger" in msg
|
||||
assert "hermes gateway run" in msg # foreground fallback mentioned
|
||||
assert "Interactive authentication required" in msg
|
||||
|
||||
def test_raises_when_loginctl_missing(self, monkeypatch):
|
||||
"""No loginctl binary at all → suggest sudo install + manual fix."""
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_user_dbus_socket_path",
|
||||
lambda: type("P", (), {"exists": lambda self: False})(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "get_systemd_linger_status",
|
||||
lambda: (None, "loginctl not found"),
|
||||
)
|
||||
monkeypatch.setattr(gateway_cli.shutil, "which", lambda _: None)
|
||||
|
||||
with pytest.raises(gateway_cli.UserSystemdUnavailableError) as exc_info:
|
||||
gateway_cli._preflight_user_systemd()
|
||||
|
||||
assert "sudo loginctl enable-linger" in str(exc_info.value)
|
||||
|
||||
def test_linger_enabled_but_socket_still_missing(self, monkeypatch):
|
||||
"""Edge case: linger says yes but the bus socket never came up."""
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_user_dbus_socket_path",
|
||||
lambda: type("P", (), {"exists": lambda self: False})(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "get_systemd_linger_status", lambda: (True, ""),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_wait_for_user_dbus_socket", lambda timeout=3.0: False,
|
||||
)
|
||||
|
||||
with pytest.raises(gateway_cli.UserSystemdUnavailableError) as exc_info:
|
||||
gateway_cli._preflight_user_systemd()
|
||||
|
||||
assert "linger is enabled" in str(exc_info.value)
|
||||
|
||||
def test_enable_linger_succeeds_and_socket_appears(self, monkeypatch, capsys):
|
||||
"""Happy remediation path: polkit allows enable-linger, socket spawns."""
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_user_dbus_socket_path",
|
||||
lambda: type("P", (), {"exists": lambda self: False})(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "get_systemd_linger_status", lambda: (False, ""),
|
||||
)
|
||||
monkeypatch.setattr(gateway_cli.shutil, "which", lambda _: "/usr/bin/loginctl")
|
||||
|
||||
class _OkResult:
|
||||
returncode = 0
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr(
|
||||
gateway_cli.subprocess, "run", lambda *a, **kw: _OkResult(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_cli, "_wait_for_user_dbus_socket",
|
||||
lambda timeout=5.0: True,
|
||||
)
|
||||
|
||||
# Should not raise.
|
||||
gateway_cli._preflight_user_systemd()
|
||||
out = capsys.readouterr().out
|
||||
assert "Enabled linger" in out
|
||||
|
||||
|
||||
class TestProfileArg:
|
||||
"""Tests for _profile_arg — returns '--profile <name>' for named profiles."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue