diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 548f7b45270cc..ecd5bc1c753bd 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -230,7 +230,7 @@ def is_linux() -> bool: def supports_systemd_services() -> bool: - return is_linux() and not is_termux() + return is_linux() and not is_termux() and shutil.which("systemctl") is not None def is_macos() -> bool: @@ -1871,7 +1871,7 @@ def _is_service_running() -> bool: ) if result.stdout.strip() == "active": return True - except subprocess.TimeoutExpired: + except (FileNotFoundError, subprocess.TimeoutExpired): pass if system_unit_exists: @@ -1882,7 +1882,7 @@ def _is_service_running() -> bool: ) if result.stdout.strip() == "active": return True - except subprocess.TimeoutExpired: + except (FileNotFoundError, subprocess.TimeoutExpired): pass return False diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index a4c089b9aa25a..a6410c13f13f3 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2243,6 +2243,7 @@ def setup_gateway(config: dict): from hermes_cli.gateway import ( _is_service_installed, _is_service_running, + supports_systemd_services, has_conflicting_systemd_units, install_linux_gateway_from_setup, print_systemd_scope_conflict_warning, @@ -2255,16 +2256,18 @@ def setup_gateway(config: dict): service_installed = _is_service_installed() service_running = _is_service_running() + supports_systemd = supports_systemd_services() + supports_service_manager = supports_systemd or _is_macos print() - if _is_linux and has_conflicting_systemd_units(): + if supports_systemd and has_conflicting_systemd_units(): print_systemd_scope_conflict_warning() print() if service_running: if prompt_yes_no(" Restart the gateway to pick up changes?", True): try: - if _is_linux: + if supports_systemd: systemd_restart() elif _is_macos: launchd_restart() @@ -2273,14 +2276,14 @@ def setup_gateway(config: dict): elif service_installed: if prompt_yes_no(" Start the gateway service?", True): try: - if _is_linux: + if supports_systemd: systemd_start() elif _is_macos: launchd_start() except Exception as e: print_error(f" Start failed: {e}") - elif _is_linux or _is_macos: - svc_name = "systemd" if _is_linux else "launchd" + elif supports_service_manager: + svc_name = "systemd" if supports_systemd else "launchd" if prompt_yes_no( f" Install the gateway as a {svc_name} service? (runs in background, starts on boot)", True, @@ -2288,7 +2291,7 @@ def setup_gateway(config: dict): try: installed_scope = None did_install = False - if _is_linux: + if supports_systemd: installed_scope, did_install = install_linux_gateway_from_setup(force=False) else: launchd_install(force=False) @@ -2296,7 +2299,7 @@ def setup_gateway(config: dict): print() if did_install and prompt_yes_no(" Start the service now?", True): try: - if _is_linux: + if supports_systemd: systemd_start(system=installed_scope == "system") elif _is_macos: launchd_start() @@ -2307,7 +2310,7 @@ def setup_gateway(config: dict): print_info(" You can try manually: hermes gateway install") else: print_info(" You can install later: hermes gateway install") - if _is_linux: + if supports_systemd: print_info(" Or as a boot-time service: sudo hermes gateway install --system") print_info(" Or run in foreground: hermes gateway") else: diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index b32c7fe787327..5118166813ba1 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -311,6 +311,13 @@ def test_launchd_status_reports_local_stale_plist_when_unloaded(self, tmp_path, class TestGatewayServiceDetection: + def test_supports_systemd_services_requires_systemctl_binary(self, monkeypatch): + monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) + monkeypatch.setattr(gateway_cli, "is_termux", lambda: False) + monkeypatch.setattr(gateway_cli.shutil, "which", lambda name: None) + + assert gateway_cli.supports_systemd_services() is False + def test_is_service_running_checks_system_scope_when_user_scope_is_inactive(self, monkeypatch): user_unit = SimpleNamespace(exists=lambda: True) system_unit = SimpleNamespace(exists=lambda: True) @@ -335,6 +342,23 @@ def fake_run(cmd, capture_output=True, text=True, **kwargs): assert gateway_cli._is_service_running() is True + def test_is_service_running_returns_false_when_systemctl_missing(self, monkeypatch): + unit = SimpleNamespace(exists=lambda: True) + + monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True) + monkeypatch.setattr( + gateway_cli, + "get_systemd_unit_path", + lambda system=False: unit, + ) + + def fake_run(*args, **kwargs): + raise FileNotFoundError("systemctl") + + monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run) + + assert gateway_cli._is_service_running() is False + class TestGatewaySystemServiceRouting: def test_gateway_install_passes_system_flags(self, monkeypatch): diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 0eac69bac20cc..8cb3d3f5b1723 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -1,11 +1,11 @@ -"""Tests for setup_model_provider — verifies the delegation to -select_provider_and_model() and config dict sync.""" +"""Tests for setup.py configuration flows.""" import json import sys import types from hermes_cli.auth import get_active_provider from hermes_cli.config import load_config, save_config +from hermes_cli import setup as setup_mod from hermes_cli.setup import setup_model_provider @@ -142,6 +142,43 @@ def fake_select(): assert reloaded.get("custom_providers") == [{"name": "Local", "base_url": "http://localhost:8080/v1"}] +def test_setup_gateway_skips_service_install_when_systemctl_missing(monkeypatch, capsys): + env = { + "TELEGRAM_BOT_TOKEN": "", + "TELEGRAM_HOME_CHANNEL": "", + "DISCORD_BOT_TOKEN": "", + "DISCORD_HOME_CHANNEL": "", + "SLACK_BOT_TOKEN": "", + "SLACK_HOME_CHANNEL": "", + "MATRIX_HOMESERVER": "https://matrix.example.com", + "MATRIX_USER_ID": "@alice:example.com", + "MATRIX_PASSWORD": "", + "MATRIX_ACCESS_TOKEN": "token", + "BLUEBUBBLES_SERVER_URL": "", + "BLUEBUBBLES_HOME_CHANNEL": "", + "WHATSAPP_ENABLED": "", + "WEBHOOK_ENABLED": "", + } + + monkeypatch.setattr(setup_mod, "get_env_value", lambda key: env.get(key, "")) + monkeypatch.setattr(setup_mod, "prompt_yes_no", lambda *args, **kwargs: False) + monkeypatch.setattr("platform.system", lambda: "Linux") + + import hermes_cli.gateway as gateway_mod + + monkeypatch.setattr(gateway_mod, "supports_systemd_services", lambda: False) + monkeypatch.setattr(gateway_mod, "is_macos", lambda: False) + monkeypatch.setattr(gateway_mod, "_is_service_installed", lambda: False) + monkeypatch.setattr(gateway_mod, "_is_service_running", lambda: False) + + setup_mod.setup_gateway({}) + + out = capsys.readouterr().out + assert "Messaging platforms configured!" in out + assert "Start the gateway to bring your bots online:" in out + assert "hermes gateway" in out + + def test_setup_syncs_custom_provider_removal_from_disk(tmp_path, monkeypatch): """Removing the last custom provider in model setup should persist.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path))