From 19d1f435b84224dc9fc5a2c165fe504e0a2edd91 Mon Sep 17 00:00:00 2001 From: jkobject Date: Tue, 23 Jun 2026 17:28:56 +0200 Subject: [PATCH] fix: keep psutil optional for system health --- api/system_health.py | 13 ++++++++--- requirements.txt | 7 +++--- tests/test_issue693_system_health_panel.py | 25 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/api/system_health.py b/api/system_health.py index 576a9faa..a5a02553 100644 --- a/api/system_health.py +++ b/api/system_health.py @@ -22,6 +22,13 @@ _PROC_MEMINFO = Path("/proc/meminfo") _CPU_SAMPLE_SECONDS = 0.05 +def _load_optional_psutil(): + try: + return import_module("psutil") + except ImportError: + raise RuntimeError("psutil_unavailable") from None + + def _checked_at() -> str: return datetime.now(timezone.utc).isoformat() @@ -74,13 +81,13 @@ def _cpu_percent() -> float: try: start = _read_proc_stat_cpu() except OSError: - psutil = import_module("psutil") + psutil = _load_optional_psutil() return _clamp_percent(psutil.cpu_percent(interval=_CPU_SAMPLE_SECONDS)) time.sleep(_CPU_SAMPLE_SECONDS) try: end = _read_proc_stat_cpu() except OSError: - psutil = import_module("psutil") + psutil = _load_optional_psutil() return _clamp_percent(psutil.cpu_percent(interval=0.0)) return _cpu_delta_percent(start, end) @@ -106,7 +113,7 @@ def _memory_usage() -> dict[str, int | float]: try: meminfo = _read_meminfo_kib() except OSError: - vm = import_module("psutil").virtual_memory() + vm = _load_optional_psutil().virtual_memory() total = int(getattr(vm, "total", 0) or 0) if total <= 0: raise RuntimeError("memory_unavailable") from None diff --git a/requirements.txt b/requirements.txt index a97e35a9..6e1b6ee0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ # Hermes Web UI -- minimal Python dependencies # The server uses PyYAML plus cryptography for optional local passkey/WebAuthn support. -# psutil is used only for safe aggregate cross-platform CPU/RAM metrics when -# Linux procfs is unavailable (for example on macOS). # All heavy ML/agent deps live in the Hermes agent venv. # # OPTIONAL: the Edge TTS speech engine (Settings -> Voice -> TTS Engine -> "Edge TTS") # needs `edge-tts`. It is intentionally NOT a hard dependency — the /api/tts # endpoint returns 503 with an install hint when it's absent. Install it only if # you want server-side Microsoft neural voices: pip install edge-tts +# +# OPTIONAL: macOS/non-procfs CPU/RAM system-health metrics need `psutil`. +# Linux uses procfs without this dependency; install only if you want aggregate +# CPU/RAM metrics on platforms without /proc: pip install psutil>=5.9 pyyaml>=6.0 cryptography>=42.0 -psutil>=5.9 diff --git a/tests/test_issue693_system_health_panel.py b/tests/test_issue693_system_health_panel.py index dc447e41..2b8e5036 100644 --- a/tests/test_issue693_system_health_panel.py +++ b/tests/test_issue693_system_health_panel.py @@ -131,6 +131,31 @@ def test_system_health_falls_back_to_psutil_when_procfs_is_unavailable(monkeypat } +def test_system_health_missing_optional_psutil_is_safe_unavailable(monkeypatch): + from api import system_health + + class _MissingProcPath: + def open(self, *args, **kwargs): + raise FileNotFoundError("/private/proc/path") + + def missing_psutil(name): + if name == "psutil": + raise ModuleNotFoundError("No module named 'psutil'") + raise AssertionError(f"unexpected import: {name}") + + monkeypatch.setattr(system_health, "_PROC_STAT", _MissingProcPath()) + monkeypatch.setattr(system_health, "_PROC_MEMINFO", _MissingProcPath()) + monkeypatch.setattr(system_health, "import_module", missing_psutil) + + for collect in (system_health._cpu_percent, system_health._memory_usage): + try: + collect() + except RuntimeError as exc: + assert str(exc) == "psutil_unavailable" + else: # pragma: no cover - defensive regression clarity + raise AssertionError("missing optional psutil should surface a safe unavailable error") + + def test_system_health_procfs_parse_errors_remain_visible(monkeypatch): from api import system_health