Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion hermes_cli/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,10 +615,41 @@ def _check_gateway_running(profile_dir: Path) -> bool:
"""Check if a gateway is running for a given profile directory."""
try:
from gateway.status import get_running_pid
return get_running_pid(profile_dir / "gateway.pid", cleanup_stale=False) is not None

if get_running_pid(profile_dir / "gateway.pid", cleanup_stale=False) is not None:
return True
except Exception:
pass
return _gateway_runtime_snapshot_running_for_profile(profile_dir)


def _gateway_runtime_snapshot_running_for_profile(profile_dir: Path) -> bool:
"""Return supervisor/process liveness for ``profile_dir``.

``profile list`` historically trusted only ``<profile>/gateway.pid``. On
supervised installs (launchd/systemd/s6), the service can be alive even when
that compatibility PID file is absent; ``hermes status --all`` already uses
the richer runtime snapshot. Reuse that path under a context-local
HERMES_HOME override so every profile is checked against its own service
label/process filter without mutating global environment state.
"""
try:
from hermes_constants import (
reset_hermes_home_override,
set_hermes_home_override,
)
from hermes_cli.gateway import get_gateway_runtime_snapshot
except Exception:
return False

token = set_hermes_home_override(profile_dir)
try:
return bool(get_gateway_runtime_snapshot().running)
except Exception:
return False
finally:
reset_hermes_home_override(token)


def _count_skills(profile_dir: Path) -> int:
"""Count installed skills in a profile."""
Expand Down
37 changes: 33 additions & 4 deletions tests/hermes_cli/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -1414,7 +1414,11 @@ def test_create_profile_returns_correct_path(self, profile_env):
assert result == expected

def test_list_profiles_default_info_fields(self, profile_env):
profiles = list_profiles()
with patch(
"hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile",
return_value=False,
):
profiles = list_profiles()
default = [p for p in profiles if p.name == "default"][0]
assert default.is_default is True
assert default.gateway_running is False
Expand All @@ -1426,25 +1430,50 @@ def test_gateway_running_check_with_pid_file(self, profile_env):
tmp_path = profile_env
default_home = tmp_path / ".hermes"

with patch("gateway.status.get_running_pid", return_value=99999) as mock_get_running_pid:
with patch("gateway.status.get_running_pid", return_value=99999) as mock_get_running_pid, patch(
"hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile",
return_value=False,
) as mock_snapshot:
assert _check_gateway_running(default_home) is True
mock_get_running_pid.assert_called_once_with(
default_home / "gateway.pid",
cleanup_stale=False,
)
mock_snapshot.assert_not_called()

def test_gateway_running_check_falls_back_to_runtime_snapshot(self, profile_env):
"""No PID file can still be running when launchd/systemd supervises it."""
from hermes_cli.profiles import _check_gateway_running
tmp_path = profile_env
default_home = tmp_path / ".hermes"

with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid, patch(
"hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile",
return_value=True,
) as mock_snapshot:
assert _check_gateway_running(default_home) is True
mock_get_running_pid.assert_called_once_with(
default_home / "gateway.pid",
cleanup_stale=False,
)
mock_snapshot.assert_called_once_with(default_home)

def test_gateway_running_check_plain_pid(self, profile_env):
"""Shared PID validator returning None means the profile is not running."""
"""No PID file and no runtime snapshot means the profile is not running."""
from hermes_cli.profiles import _check_gateway_running
tmp_path = profile_env
default_home = tmp_path / ".hermes"

with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid:
with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid, patch(
"hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile",
return_value=False,
) as mock_snapshot:
assert _check_gateway_running(default_home) is False
mock_get_running_pid.assert_called_once_with(
default_home / "gateway.pid",
cleanup_stale=False,
)
mock_snapshot.assert_called_once_with(default_home)

def test_profile_name_boundary_single_char(self):
"""Single alphanumeric character is valid."""
Expand Down