diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 908d8992a0..b620571879 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -355,6 +355,8 @@ def _wsl_systemd_operational() -> bool: def supports_systemd_services() -> bool: if not is_linux() or is_termux(): return False + if shutil.which("systemctl") is None: + return False if is_wsl(): return _wsl_systemd_operational() return True @@ -2135,7 +2137,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: @@ -2146,7 +2148,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 e12f7d1a76..96d4f3e569 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2232,6 +2232,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, @@ -2244,16 +2245,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() @@ -2262,14 +2265,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, @@ -2277,7 +2280,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) @@ -2285,7 +2288,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() @@ -2296,7 +2299,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 cba3a8192f..17b8eaac39 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -394,6 +394,13 @@ class TestLaunchdServiceRecovery: 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) @@ -418,6 +425,23 @@ class TestGatewayServiceDetection: 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_systemd_restart_self_requests_graceful_restart_without_reload_or_restart(self, monkeypatch, capsys): diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 4a3f5151f8..ec2b16215d 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -1,5 +1,4 @@ -"""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 @@ -8,6 +7,7 @@ import pytest 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 @@ -144,6 +144,43 @@ def test_setup_custom_providers_synced(tmp_path, monkeypatch): 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))