diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index a86b4078c8..0cab694892 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -3218,8 +3218,24 @@ def _systemd_scope_preamble( return system +def _systemd_unit_belongs_to_current_home(system: bool = False) -> bool: + """False (with a warning) when the installed unit pins a HERMES_HOME other than this process's: the + service name then resolved to ANOTHER install's gateway, and stop/disable/unlink would take it down.""" + _sync_hermes_home_from_systemd_unit(system=system) # sudo strips HERMES_HOME; adopt the unit's first + unit_home = _hermes_home_from_systemd_unit_file(system=system) + if unit_home is None or Path(unit_home).expanduser().resolve() == get_hermes_home().resolve(): + return True + print_warning( + f"Refusing to remove {get_systemd_unit_path(system=system)}: it runs HERMES_HOME={unit_home}, " + f"but this process has HERMES_HOME={get_hermes_home()}" + ) + return False + + def systemd_uninstall(system: bool = False): system = _systemd_scope_preamble("uninstall", system, require_installed=False) + if not _systemd_unit_belongs_to_current_home(system): + return _run_systemctl(["stop", get_service_name()], system=system, check=False, timeout=90) _run_systemctl(["disable", get_service_name()], system=system, check=False, timeout=30) diff --git a/hermes_cli/uninstall.py b/hermes_cli/uninstall.py index a17ab7edc2..ba54931224 100644 --- a/hermes_cli/uninstall.py +++ b/hermes_cli/uninstall.py @@ -192,12 +192,14 @@ def uninstall_gateway_service(): def _remove_systemd_gateway() -> bool: """Linux: uninstall systemd services (both user and system scopes).""" - from hermes_cli.gateway import _systemctl_cmd, get_service_name, get_systemd_unit_path + from hermes_cli.gateway import ( + _systemctl_cmd, _systemd_unit_belongs_to_current_home, get_service_name, get_systemd_unit_path, + ) svc_name = get_service_name() removed_any = False for is_system, scope in ((False, "user"), (True, "system")): unit_path = get_systemd_unit_path(system=is_system) - if not unit_path.exists(): + if not unit_path.exists() or not _systemd_unit_belongs_to_current_home(is_system): continue try: if is_system and os.geteuid() != 0: # windows-footgun: ok — Linux-only systemd path diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index da3d2659bd..2b6159699d 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -218,6 +218,25 @@ class TestServiceIdentityForForeignHome: assert gateway_cli.get_service_name() == "hermes-gateway-alpha" +class TestUninstallRefusesForeignUnit: + """systemd_uninstall must not stop/disable/unlink a unit pinned to another HERMES_HOME.""" + + def test_unit_for_other_home_is_left_alone(self, tmp_path, monkeypatch, capsys): + unit_path = tmp_path / "hermes-gateway.service" + unit_path.write_text('[Service]\nEnvironment="HERMES_HOME=/somewhere/else"\n', encoding="utf-8") + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "mine")) + monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path) + monkeypatch.setattr(gateway_cli, "_systemd_scope_preamble", lambda *a, **k: False) + calls = [] + monkeypatch.setattr(gateway_cli, "_run_systemctl", lambda args, **k: calls.append(args)) + + gateway_cli.systemd_uninstall(system=False) + + assert unit_path.exists() + assert calls == [] + assert "/somewhere/else" in capsys.readouterr().out + + class TestGetCronDrainTimeout: def test_missing_config_falls_back_to_default(self, monkeypatch): monkeypatch.delenv("HERMES_CRON_DRAIN_TIMEOUT", raising=False)