fix: keep psutil optional for system health
This commit is contained in:
parent
0905adcf43
commit
19d1f435b8
3 changed files with 39 additions and 6 deletions
|
|
@ -22,6 +22,13 @@ _PROC_MEMINFO = Path("/proc/meminfo")
|
||||||
_CPU_SAMPLE_SECONDS = 0.05
|
_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:
|
def _checked_at() -> str:
|
||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
@ -74,13 +81,13 @@ def _cpu_percent() -> float:
|
||||||
try:
|
try:
|
||||||
start = _read_proc_stat_cpu()
|
start = _read_proc_stat_cpu()
|
||||||
except OSError:
|
except OSError:
|
||||||
psutil = import_module("psutil")
|
psutil = _load_optional_psutil()
|
||||||
return _clamp_percent(psutil.cpu_percent(interval=_CPU_SAMPLE_SECONDS))
|
return _clamp_percent(psutil.cpu_percent(interval=_CPU_SAMPLE_SECONDS))
|
||||||
time.sleep(_CPU_SAMPLE_SECONDS)
|
time.sleep(_CPU_SAMPLE_SECONDS)
|
||||||
try:
|
try:
|
||||||
end = _read_proc_stat_cpu()
|
end = _read_proc_stat_cpu()
|
||||||
except OSError:
|
except OSError:
|
||||||
psutil = import_module("psutil")
|
psutil = _load_optional_psutil()
|
||||||
return _clamp_percent(psutil.cpu_percent(interval=0.0))
|
return _clamp_percent(psutil.cpu_percent(interval=0.0))
|
||||||
return _cpu_delta_percent(start, end)
|
return _cpu_delta_percent(start, end)
|
||||||
|
|
||||||
|
|
@ -106,7 +113,7 @@ def _memory_usage() -> dict[str, int | float]:
|
||||||
try:
|
try:
|
||||||
meminfo = _read_meminfo_kib()
|
meminfo = _read_meminfo_kib()
|
||||||
except OSError:
|
except OSError:
|
||||||
vm = import_module("psutil").virtual_memory()
|
vm = _load_optional_psutil().virtual_memory()
|
||||||
total = int(getattr(vm, "total", 0) or 0)
|
total = int(getattr(vm, "total", 0) or 0)
|
||||||
if total <= 0:
|
if total <= 0:
|
||||||
raise RuntimeError("memory_unavailable") from None
|
raise RuntimeError("memory_unavailable") from None
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
# Hermes Web UI -- minimal Python dependencies
|
# Hermes Web UI -- minimal Python dependencies
|
||||||
# The server uses PyYAML plus cryptography for optional local passkey/WebAuthn support.
|
# 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.
|
# All heavy ML/agent deps live in the Hermes agent venv.
|
||||||
#
|
#
|
||||||
# OPTIONAL: the Edge TTS speech engine (Settings -> Voice -> TTS Engine -> "Edge TTS")
|
# 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
|
# 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
|
# 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
|
# 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
|
pyyaml>=6.0
|
||||||
cryptography>=42.0
|
cryptography>=42.0
|
||||||
psutil>=5.9
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
def test_system_health_procfs_parse_errors_remain_visible(monkeypatch):
|
||||||
from api import system_health
|
from api import system_health
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue