fix(termux): disable gateway service flows on android

This commit is contained in:
adybag14-cyber 2026-04-09 10:29:32 +02:00 committed by Teknium
parent 4e40e93b98
commit 3878495972
7 changed files with 153 additions and 48 deletions

View file

@ -39,7 +39,7 @@ def _get_service_pids() -> set:
pids: set = set() pids: set = set()
# --- systemd (Linux): user and system scopes --- # --- systemd (Linux): user and system scopes ---
if is_linux(): if supports_systemd_services():
for scope_args in [["systemctl", "--user"], ["systemctl"]]: for scope_args in [["systemctl", "--user"], ["systemctl"]]:
try: try:
result = subprocess.run( result = subprocess.run(
@ -225,6 +225,16 @@ def stop_profile_gateway() -> bool:
def is_linux() -> bool: def is_linux() -> bool:
return sys.platform.startswith('linux') return sys.platform.startswith('linux')
def is_termux() -> bool:
prefix = os.getenv("PREFIX", "")
return bool(os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix)
def supports_systemd_services() -> bool:
return is_linux() and not is_termux()
def is_macos() -> bool: def is_macos() -> bool:
return sys.platform == 'darwin' return sys.platform == 'darwin'
@ -477,13 +487,15 @@ def install_linux_gateway_from_setup(force: bool = False) -> tuple[str | None, b
def get_systemd_linger_status() -> tuple[bool | None, str]: def get_systemd_linger_status() -> tuple[bool | None, str]:
"""Return whether systemd user lingering is enabled for the current user. """Return systemd linger status for the current user.
Returns: Returns:
(True, "") when linger is enabled. (True, "") when linger is enabled.
(False, "") when linger is disabled. (False, "") when linger is disabled.
(None, detail) when the status could not be determined. (None, detail) when the status could not be determined.
""" """
if is_termux():
return None, "not supported in Termux"
if not is_linux(): if not is_linux():
return None, "not supported on this platform" return None, "not supported on this platform"
@ -766,7 +778,7 @@ def _print_linger_enable_warning(username: str, detail: str | None = None) -> No
def _ensure_linger_enabled() -> None: def _ensure_linger_enabled() -> None:
"""Enable linger when possible so the user gateway survives logout.""" """Enable linger when possible so the user gateway survives logout."""
if not is_linux(): if is_termux() or not is_linux():
return return
import getpass import getpass
@ -1801,7 +1813,7 @@ def _setup_whatsapp():
def _is_service_installed() -> bool: def _is_service_installed() -> bool:
"""Check if the gateway is installed as a system service.""" """Check if the gateway is installed as a system service."""
if is_linux(): if supports_systemd_services():
return get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists() return get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()
elif is_macos(): elif is_macos():
return get_launchd_plist_path().exists() return get_launchd_plist_path().exists()
@ -1810,7 +1822,7 @@ def _is_service_installed() -> bool:
def _is_service_running() -> bool: def _is_service_running() -> bool:
"""Check if the gateway service is currently running.""" """Check if the gateway service is currently running."""
if is_linux(): if supports_systemd_services():
user_unit_exists = get_systemd_unit_path(system=False).exists() user_unit_exists = get_systemd_unit_path(system=False).exists()
system_unit_exists = get_systemd_unit_path(system=True).exists() system_unit_exists = get_systemd_unit_path(system=True).exists()
@ -1983,7 +1995,7 @@ def gateway_setup():
service_installed = _is_service_installed() service_installed = _is_service_installed()
service_running = _is_service_running() service_running = _is_service_running()
if is_linux() and has_conflicting_systemd_units(): if supports_systemd_services() and has_conflicting_systemd_units():
print_systemd_scope_conflict_warning() print_systemd_scope_conflict_warning()
print() print()
@ -1993,7 +2005,7 @@ def gateway_setup():
print_warning("Gateway service is installed but not running.") print_warning("Gateway service is installed but not running.")
if prompt_yes_no(" Start it now?", True): if prompt_yes_no(" Start it now?", True):
try: try:
if is_linux(): if supports_systemd_services():
systemd_start() systemd_start()
elif is_macos(): elif is_macos():
launchd_start() launchd_start()
@ -2044,7 +2056,7 @@ def gateway_setup():
if service_running: if service_running:
if prompt_yes_no(" Restart the gateway to pick up changes?", True): if prompt_yes_no(" Restart the gateway to pick up changes?", True):
try: try:
if is_linux(): if supports_systemd_services():
systemd_restart() systemd_restart()
elif is_macos(): elif is_macos():
launchd_restart() launchd_restart()
@ -2056,7 +2068,7 @@ def gateway_setup():
elif service_installed: elif service_installed:
if prompt_yes_no(" Start the gateway service?", True): if prompt_yes_no(" Start the gateway service?", True):
try: try:
if is_linux(): if supports_systemd_services():
systemd_start() systemd_start()
elif is_macos(): elif is_macos():
launchd_start() launchd_start()
@ -2064,13 +2076,13 @@ def gateway_setup():
print_error(f" Start failed: {e}") print_error(f" Start failed: {e}")
else: else:
print() print()
if is_linux() or is_macos(): if supports_systemd_services() or is_macos():
platform_name = "systemd" if is_linux() else "launchd" platform_name = "systemd" if supports_systemd_services() else "launchd"
if prompt_yes_no(f" Install the gateway as a {platform_name} service? (runs in background, starts on boot)", True): if prompt_yes_no(f" Install the gateway as a {platform_name} service? (runs in background, starts on boot)", True):
try: try:
installed_scope = None installed_scope = None
did_install = False did_install = False
if is_linux(): if supports_systemd_services():
installed_scope, did_install = install_linux_gateway_from_setup(force=False) installed_scope, did_install = install_linux_gateway_from_setup(force=False)
else: else:
launchd_install(force=False) launchd_install(force=False)
@ -2078,7 +2090,7 @@ def gateway_setup():
print() print()
if did_install and prompt_yes_no(" Start the service now?", True): if did_install and prompt_yes_no(" Start the service now?", True):
try: try:
if is_linux(): if supports_systemd_services():
systemd_start(system=installed_scope == "system") systemd_start(system=installed_scope == "system")
else: else:
launchd_start() launchd_start()
@ -2089,12 +2101,18 @@ def gateway_setup():
print_info(" You can try manually: hermes gateway install") print_info(" You can try manually: hermes gateway install")
else: else:
print_info(" You can install later: hermes gateway install") print_info(" You can install later: hermes gateway install")
if is_linux(): if supports_systemd_services():
print_info(" Or as a boot-time service: sudo hermes gateway install --system") print_info(" Or as a boot-time service: sudo hermes gateway install --system")
print_info(" Or run in foreground: hermes gateway") print_info(" Or run in foreground: hermes gateway")
else: else:
print_info(" Service install not supported on this platform.") if is_termux():
print_info(" Run in foreground: hermes gateway") from hermes_constants import display_hermes_home as _dhh
print_info(" Termux does not use systemd/launchd services.")
print_info(" Run in foreground: hermes gateway")
print_info(f" Or start it manually in the background (best effort): nohup hermes gateway >{_dhh()}/logs/gateway.log 2>&1 &")
else:
print_info(" Service install not supported on this platform.")
print_info(" Run in foreground: hermes gateway")
else: else:
print() print()
print_info("No platforms configured. Run 'hermes gateway setup' when ready.") print_info("No platforms configured. Run 'hermes gateway setup' when ready.")
@ -2130,7 +2148,11 @@ def gateway_command(args):
force = getattr(args, 'force', False) force = getattr(args, 'force', False)
system = getattr(args, 'system', False) system = getattr(args, 'system', False)
run_as_user = getattr(args, 'run_as_user', None) run_as_user = getattr(args, 'run_as_user', None)
if is_linux(): if is_termux():
print("Gateway service installation is not supported on Termux.")
print("Run manually: hermes gateway")
sys.exit(1)
if supports_systemd_services():
systemd_install(force=force, system=system, run_as_user=run_as_user) systemd_install(force=force, system=system, run_as_user=run_as_user)
elif is_macos(): elif is_macos():
launchd_install(force) launchd_install(force)
@ -2144,7 +2166,11 @@ def gateway_command(args):
managed_error("uninstall gateway service (managed by NixOS)") managed_error("uninstall gateway service (managed by NixOS)")
return return
system = getattr(args, 'system', False) system = getattr(args, 'system', False)
if is_linux(): if is_termux():
print("Gateway service uninstall is not supported on Termux because there is no managed service to remove.")
print("Stop manual runs with: hermes gateway stop")
sys.exit(1)
if supports_systemd_services():
systemd_uninstall(system=system) systemd_uninstall(system=system)
elif is_macos(): elif is_macos():
launchd_uninstall() launchd_uninstall()
@ -2154,7 +2180,11 @@ def gateway_command(args):
elif subcmd == "start": elif subcmd == "start":
system = getattr(args, 'system', False) system = getattr(args, 'system', False)
if is_linux(): if is_termux():
print("Gateway service start is not supported on Termux because there is no system service manager.")
print("Run manually: hermes gateway")
sys.exit(1)
if supports_systemd_services():
systemd_start(system=system) systemd_start(system=system)
elif is_macos(): elif is_macos():
launchd_start() launchd_start()
@ -2169,7 +2199,7 @@ def gateway_command(args):
if stop_all: if stop_all:
# --all: kill every gateway process on the machine # --all: kill every gateway process on the machine
service_available = False service_available = False
if is_linux() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()):
try: try:
systemd_stop(system=system) systemd_stop(system=system)
service_available = True service_available = True
@ -2190,7 +2220,7 @@ def gateway_command(args):
else: else:
# Default: stop only the current profile's gateway # Default: stop only the current profile's gateway
service_available = False service_available = False
if is_linux() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()):
try: try:
systemd_stop(system=system) systemd_stop(system=system)
service_available = True service_available = True
@ -2218,7 +2248,7 @@ def gateway_command(args):
system = getattr(args, 'system', False) system = getattr(args, 'system', False)
service_configured = False service_configured = False
if is_linux() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()):
service_configured = True service_configured = True
try: try:
systemd_restart(system=system) systemd_restart(system=system)
@ -2235,7 +2265,7 @@ def gateway_command(args):
if not service_available: if not service_available:
# systemd/launchd restart failed — check if linger is the issue # systemd/launchd restart failed — check if linger is the issue
if is_linux(): if supports_systemd_services():
linger_ok, _detail = get_systemd_linger_status() linger_ok, _detail = get_systemd_linger_status()
if linger_ok is not True: if linger_ok is not True:
import getpass import getpass
@ -2272,7 +2302,7 @@ def gateway_command(args):
system = getattr(args, 'system', False) system = getattr(args, 'system', False)
# Check for service first # Check for service first
if is_linux() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()):
systemd_status(deep, system=system) systemd_status(deep, system=system)
elif is_macos() and get_launchd_plist_path().exists(): elif is_macos() and get_launchd_plist_path().exists():
launchd_status(deep) launchd_status(deep)
@ -2289,9 +2319,13 @@ def gateway_command(args):
for line in runtime_lines: for line in runtime_lines:
print(f" {line}") print(f" {line}")
print() print()
print("To install as a service:") if is_termux():
print(" hermes gateway install") print("Termux note:")
print(" sudo hermes gateway install --system") print(" Android may stop background jobs when Termux is suspended")
else:
print("To install as a service:")
print(" hermes gateway install")
print(" sudo hermes gateway install --system")
else: else:
print("✗ Gateway is not running") print("✗ Gateway is not running")
runtime_lines = _runtime_health_lines() runtime_lines = _runtime_health_lines()
@ -2303,5 +2337,8 @@ def gateway_command(args):
print() print()
print("To start:") print("To start:")
print(" hermes gateway # Run in foreground") print(" hermes gateway # Run in foreground")
print(" hermes gateway install # Install as user service") if is_termux():
print(" sudo hermes gateway install --system # Install as boot-time system service") print(" nohup hermes gateway > ~/.hermes/logs/gateway.log 2>&1 & # Best-effort background start")
else:
print(" hermes gateway install # Install as user service")
print(" sudo hermes gateway install --system # Install as boot-time system service")

View file

@ -3763,7 +3763,7 @@ def cmd_update(args):
# running gateway needs restarting to pick up the new code. # running gateway needs restarting to pick up the new code.
try: try:
from hermes_cli.gateway import ( from hermes_cli.gateway import (
is_macos, is_linux, _ensure_user_systemd_env, is_macos, supports_systemd_services, _ensure_user_systemd_env,
find_gateway_pids, find_gateway_pids,
_get_service_pids, _get_service_pids,
) )
@ -3774,7 +3774,7 @@ def cmd_update(args):
# --- Systemd services (Linux) --- # --- Systemd services (Linux) ---
# Discover all hermes-gateway* units (default + profiles) # Discover all hermes-gateway* units (default + profiles)
if is_linux(): if supports_systemd_services():
try: try:
_ensure_user_systemd_env() _ensure_user_systemd_env()
except Exception: except Exception:

View file

@ -123,6 +123,10 @@ def uninstall_gateway_service():
if platform.system() != "Linux": if platform.system() != "Linux":
return False return False
prefix = os.getenv("PREFIX", "")
if os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix:
return False
try: try:
from hermes_cli.gateway import get_service_name from hermes_cli.gateway import get_service_name
svc_name = get_service_name() svc_name = get_service_name()

View file

@ -10,6 +10,7 @@ import hermes_cli.gateway as gateway
class TestSystemdLingerStatus: class TestSystemdLingerStatus:
def test_reports_enabled(self, monkeypatch): def test_reports_enabled(self, monkeypatch):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setenv("USER", "alice") monkeypatch.setenv("USER", "alice")
monkeypatch.setattr( monkeypatch.setattr(
gateway.subprocess, gateway.subprocess,
@ -22,6 +23,7 @@ class TestSystemdLingerStatus:
def test_reports_disabled(self, monkeypatch): def test_reports_disabled(self, monkeypatch):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setenv("USER", "alice") monkeypatch.setenv("USER", "alice")
monkeypatch.setattr( monkeypatch.setattr(
gateway.subprocess, gateway.subprocess,
@ -32,6 +34,11 @@ class TestSystemdLingerStatus:
assert gateway.get_systemd_linger_status() == (False, "") assert gateway.get_systemd_linger_status() == (False, "")
def test_reports_termux_as_not_supported(self, monkeypatch):
monkeypatch.setattr(gateway, "is_termux", lambda: True)
assert gateway.get_systemd_linger_status() == (None, "not supported in Termux")
def test_systemd_status_warns_when_linger_disabled(monkeypatch, tmp_path, capsys): def test_systemd_status_warns_when_linger_disabled(monkeypatch, tmp_path, capsys):
unit_path = tmp_path / "hermes-gateway.service" unit_path = tmp_path / "hermes-gateway.service"

View file

@ -8,6 +8,7 @@ import hermes_cli.gateway as gateway
class TestEnsureLingerEnabled: class TestEnsureLingerEnabled:
def test_linger_already_enabled_via_file(self, monkeypatch, capsys): def test_linger_already_enabled_via_file(self, monkeypatch, capsys):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setattr("getpass.getuser", lambda: "testuser") monkeypatch.setattr("getpass.getuser", lambda: "testuser")
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: True)) monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: True))
@ -22,6 +23,7 @@ class TestEnsureLingerEnabled:
def test_status_enabled_skips_enable(self, monkeypatch, capsys): def test_status_enabled_skips_enable(self, monkeypatch, capsys):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setattr("getpass.getuser", lambda: "testuser") monkeypatch.setattr("getpass.getuser", lambda: "testuser")
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False)) monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (True, "")) monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (True, ""))
@ -37,6 +39,7 @@ class TestEnsureLingerEnabled:
def test_loginctl_success_enables_linger(self, monkeypatch, capsys): def test_loginctl_success_enables_linger(self, monkeypatch, capsys):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setattr("getpass.getuser", lambda: "testuser") monkeypatch.setattr("getpass.getuser", lambda: "testuser")
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False)) monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, "")) monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))
@ -59,6 +62,7 @@ class TestEnsureLingerEnabled:
def test_missing_loginctl_shows_manual_guidance(self, monkeypatch, capsys): def test_missing_loginctl_shows_manual_guidance(self, monkeypatch, capsys):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setattr("getpass.getuser", lambda: "testuser") monkeypatch.setattr("getpass.getuser", lambda: "testuser")
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False)) monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (None, "loginctl not found")) monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (None, "loginctl not found"))
@ -76,6 +80,7 @@ class TestEnsureLingerEnabled:
def test_loginctl_failure_shows_manual_guidance(self, monkeypatch, capsys): def test_loginctl_failure_shows_manual_guidance(self, monkeypatch, capsys):
monkeypatch.setattr(gateway, "is_linux", lambda: True) monkeypatch.setattr(gateway, "is_linux", lambda: True)
monkeypatch.setattr(gateway, "is_termux", lambda: False)
monkeypatch.setattr("getpass.getuser", lambda: "testuser") monkeypatch.setattr("getpass.getuser", lambda: "testuser")
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False)) monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, "")) monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))

View file

@ -109,7 +109,8 @@ class TestGatewayStopCleanup:
unit_path = tmp_path / "hermes-gateway.service" unit_path = tmp_path / "hermes-gateway.service"
unit_path.write_text("unit\n", encoding="utf-8") unit_path.write_text("unit\n", encoding="utf-8")
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path) monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path)
@ -134,7 +135,8 @@ class TestGatewayStopCleanup:
unit_path = tmp_path / "hermes-gateway.service" unit_path = tmp_path / "hermes-gateway.service"
unit_path.write_text("unit\n", encoding="utf-8") unit_path.write_text("unit\n", encoding="utf-8")
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path) monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path)
@ -256,7 +258,8 @@ class TestGatewayServiceDetection:
user_unit = SimpleNamespace(exists=lambda: True) user_unit = SimpleNamespace(exists=lambda: True)
system_unit = SimpleNamespace(exists=lambda: True) system_unit = SimpleNamespace(exists=lambda: True)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr( monkeypatch.setattr(
gateway_cli, gateway_cli,
@ -278,7 +281,8 @@ class TestGatewayServiceDetection:
class TestGatewaySystemServiceRouting: class TestGatewaySystemServiceRouting:
def test_gateway_install_passes_system_flags(self, monkeypatch): def test_gateway_install_passes_system_flags(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
calls = [] calls = []
@ -294,11 +298,30 @@ class TestGatewaySystemServiceRouting:
assert calls == [(True, True, "alice")] assert calls == [(True, True, "alice")]
def test_gateway_install_reports_termux_manual_mode(self, monkeypatch, capsys):
monkeypatch.setattr(gateway_cli, "is_termux", lambda: True)
monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
try:
gateway_cli.gateway_command(
SimpleNamespace(gateway_command="install", force=False, system=False, run_as_user=None)
)
except SystemExit as exc:
assert exc.code == 1
else:
raise AssertionError("Expected gateway_command to exit on unsupported Termux service install")
out = capsys.readouterr().out
assert "not supported on Termux" in out
assert "Run manually: hermes gateway" in out
def test_gateway_status_prefers_system_service_when_only_system_unit_exists(self, monkeypatch): def test_gateway_status_prefers_system_service_when_only_system_unit_exists(self, monkeypatch):
user_unit = SimpleNamespace(exists=lambda: False) user_unit = SimpleNamespace(exists=lambda: False)
system_unit = SimpleNamespace(exists=lambda: True) system_unit = SimpleNamespace(exists=lambda: True)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr( monkeypatch.setattr(
gateway_cli, gateway_cli,
@ -313,6 +336,20 @@ class TestGatewaySystemServiceRouting:
assert calls == [(False, False)] assert calls == [(False, False)]
def test_gateway_status_on_termux_shows_manual_guidance(self, monkeypatch, capsys):
monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: False)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: True)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "find_gateway_pids", lambda exclude_pids=None: [])
monkeypatch.setattr(gateway_cli, "_runtime_health_lines", lambda: [])
gateway_cli.gateway_command(SimpleNamespace(gateway_command="status", deep=False, system=False))
out = capsys.readouterr().out
assert "Gateway is not running" in out
assert "nohup hermes gateway" in out
assert "install as user service" not in out
def test_gateway_restart_does_not_fallback_to_foreground_when_launchd_restart_fails(self, tmp_path, monkeypatch): def test_gateway_restart_does_not_fallback_to_foreground_when_launchd_restart_fails(self, tmp_path, monkeypatch):
plist_path = tmp_path / "ai.hermes.gateway.plist" plist_path = tmp_path / "ai.hermes.gateway.plist"
plist_path.write_text("plist\n", encoding="utf-8") plist_path.write_text("plist\n", encoding="utf-8")
@ -513,12 +550,22 @@ class TestGeneratedUnitUsesDetectedVenv:
class TestGeneratedUnitIncludesLocalBin: class TestGeneratedUnitIncludesLocalBin:
"""~/.local/bin must be in PATH so uvx/pipx tools are discoverable.""" """~/.local/bin must be in PATH so uvx/pipx tools are discoverable."""
def test_user_unit_includes_local_bin_in_path(self): def test_user_unit_includes_local_bin_in_path(self, monkeypatch):
home = Path.home()
monkeypatch.setattr(
gateway_cli,
"_build_user_local_paths",
lambda home_path, existing: [str(home / ".local" / "bin")],
)
unit = gateway_cli.generate_systemd_unit(system=False) unit = gateway_cli.generate_systemd_unit(system=False)
home = str(Path.home())
assert f"{home}/.local/bin" in unit assert f"{home}/.local/bin" in unit
def test_system_unit_includes_local_bin_in_path(self): def test_system_unit_includes_local_bin_in_path(self, monkeypatch):
monkeypatch.setattr(
gateway_cli,
"_build_user_local_paths",
lambda home_path, existing: [str(home_path / ".local" / "bin")],
)
unit = gateway_cli.generate_systemd_unit(system=True) unit = gateway_cli.generate_systemd_unit(system=True)
# System unit uses the resolved home dir from _system_service_identity # System unit uses the resolved home dir from _system_service_identity
assert "/.local/bin" in unit assert "/.local/bin" in unit

View file

@ -368,9 +368,8 @@ class TestCmdUpdateLaunchdRestart:
monkeypatch.setattr( monkeypatch.setattr(
gateway_cli, "is_macos", lambda: False, gateway_cli, "is_macos", lambda: False,
) )
monkeypatch.setattr( monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
gateway_cli, "is_linux", lambda: True, monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
)
mock_run.side_effect = _make_run_side_effect( mock_run.side_effect = _make_run_side_effect(
commit_count="3", commit_count="3",
@ -429,7 +428,8 @@ class TestCmdUpdateSystemService:
): ):
"""When user systemd is inactive but a system service exists, restart via system scope.""" """When user systemd is inactive but a system service exists, restart via system scope."""
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
mock_run.side_effect = _make_run_side_effect( mock_run.side_effect = _make_run_side_effect(
commit_count="3", commit_count="3",
@ -458,7 +458,8 @@ class TestCmdUpdateSystemService:
): ):
"""When system service restart fails, show the failure message.""" """When system service restart fails, show the failure message."""
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
mock_run.side_effect = _make_run_side_effect( mock_run.side_effect = _make_run_side_effect(
commit_count="3", commit_count="3",
@ -480,7 +481,8 @@ class TestCmdUpdateSystemService:
): ):
"""When both user and system services are active, both are restarted.""" """When both user and system services are active, both are restarted."""
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
mock_run.side_effect = _make_run_side_effect( mock_run.side_effect = _make_run_side_effect(
commit_count="3", commit_count="3",
@ -563,7 +565,8 @@ class TestServicePidExclusion:
): ):
"""After systemd restart, the sweep must exclude the service PID.""" """After systemd restart, the sweep must exclude the service PID."""
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
SERVICE_PID = 55000 SERVICE_PID = 55000
@ -642,7 +645,8 @@ class TestGetServicePids:
"""Unit tests for _get_service_pids().""" """Unit tests for _get_service_pids()."""
def test_returns_systemd_main_pid(self, monkeypatch): def test_returns_systemd_main_pid(self, monkeypatch):
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
def fake_run(cmd, **kwargs): def fake_run(cmd, **kwargs):
@ -691,7 +695,8 @@ class TestGetServicePids:
def test_excludes_zero_pid(self, monkeypatch): def test_excludes_zero_pid(self, monkeypatch):
"""systemd returns MainPID=0 for stopped services; skip those.""" """systemd returns MainPID=0 for stopped services; skip those."""
monkeypatch.setattr(gateway_cli, "is_linux", lambda: True) monkeypatch.setattr(gateway_cli, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway_cli, "is_termux", lambda: False)
monkeypatch.setattr(gateway_cli, "is_macos", lambda: False) monkeypatch.setattr(gateway_cli, "is_macos", lambda: False)
def fake_run(cmd, **kwargs): def fake_run(cmd, **kwargs):