fix: narrow system health psutil fallback

This commit is contained in:
jkobject authored and nesquena-hermes committed 2026-06-26 20:49:12 +00:00
1 parent 9f5aa98621
commit 0905adcf43
2 files changed
+77 -14

No files matched your search

+21 -14
View File
@@ -1,8 +1,10 @@
"""Safe aggregate host resource metrics for the WebUI VPS panel (#693).
"""Safe aggregate host resource metrics for the WebUI system panel (#693).
The browser only needs coarse CPU/RAM/disk usage. Keep this module intentionally
small and dependency-free: no process lists, command strings, user identities,
environment variables, or filesystem topology leave the server.
The browser only needs coarse CPU/RAM/disk usage. Linux uses procfs first;
platforms without procfs (for example macOS) fall back to psutil for aggregate
CPU/RAM metrics. Keep the payload intentionally small: no process lists,
command strings, user identities, environment variables, or filesystem topology
leave the server.
"""
from __future__ import annotations
@@ -71,12 +73,16 @@ def _cpu_percent() -> float:
"""
try:
start = _read_proc_stat_cpu()
time.sleep(_CPU_SAMPLE_SECONDS)
end = _read_proc_stat_cpu()
return _cpu_delta_percent(start, end)
except Exception:
except OSError:
psutil = import_module("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")
return _clamp_percent(psutil.cpu_percent(interval=0.0))
return _cpu_delta_percent(start, end)
def _read_meminfo_kib() -> dict[str, int]:
@@ -99,6 +105,13 @@ def _read_meminfo_kib() -> dict[str, int]:
def _memory_usage() -> dict[str, int | float]:
try:
meminfo = _read_meminfo_kib()
except OSError:
vm = import_module("psutil").virtual_memory()
total = int(getattr(vm, "total", 0) or 0)
if total <= 0:
raise RuntimeError("memory_unavailable") from None
available = max(0, int(getattr(vm, "available", 0) or 0))
else:
total = int(meminfo.get("MemTotal") or 0) * 1024
if total <= 0:
raise RuntimeError("meminfo_unavailable")
@@ -112,12 +125,6 @@ def _memory_usage() -> dict[str, int | float]:
- meminfo.get("Shmem", 0)
)
available = max(0, int(available_kib) * 1024)
except Exception:
vm = import_module("psutil").virtual_memory()
total = int(getattr(vm, "total", 0) or 0)
if total <= 0:
raise RuntimeError("memory_unavailable") from None
available = max(0, int(getattr(vm, "available", 0) or 0))
used = max(0, min(total, total - available))
return {
"used_bytes": used,
@@ -131,6 +131,62 @@ def test_system_health_falls_back_to_psutil_when_procfs_is_unavailable(monkeypat
}
def test_system_health_procfs_parse_errors_remain_visible(monkeypatch):
from api import system_health
fake_psutil = SimpleNamespace(
cpu_percent=lambda interval=0.0: 42.25,
virtual_memory=lambda: SimpleNamespace(total=1000, available=250),
)
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
monkeypatch.setattr(
system_health,
"_read_proc_stat_cpu",
lambda: (_ for _ in ()).throw(RuntimeError("proc_stat_unavailable")),
)
monkeypatch.setattr(system_health, "_read_meminfo_kib", lambda: {})
try:
system_health._cpu_percent()
except RuntimeError as exc:
assert str(exc) == "proc_stat_unavailable"
else: # pragma: no cover - defensive regression clarity
raise AssertionError("procfs parse RuntimeError should not fall back to psutil")
try:
system_health._memory_usage()
except RuntimeError as exc:
assert str(exc) == "meminfo_unavailable"
else: # pragma: no cover - defensive regression clarity
raise AssertionError("meminfo invariant RuntimeError should not fall back to psutil")
def test_system_health_cpu_second_procfs_read_fallback_does_not_sleep_twice(monkeypatch):
from api import system_health
calls = []
def fake_read_proc_stat_cpu():
calls.append("proc")
if len(calls) == 1:
return (10, 100)
raise FileNotFoundError("/proc/stat disappeared")
def fake_sleep(seconds):
calls.append(("sleep", seconds))
def fake_cpu_percent(interval=0.0):
calls.append(("psutil", interval))
return 12.34
monkeypatch.setattr(system_health, "_read_proc_stat_cpu", fake_read_proc_stat_cpu)
monkeypatch.setattr(system_health.time, "sleep", fake_sleep)
monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(cpu_percent=fake_cpu_percent))
assert system_health._cpu_percent() == 12.3
assert calls == ["proc", ("sleep", system_health._CPU_SAMPLE_SECONDS), "proc", ("psutil", 0.0)]
def test_system_health_route_registered_and_auth_gated(monkeypatch):
assert 'parsed.path == "/api/system/health"' in ROUTES_PY
assert "build_system_health_payload()" in ROUTES_PY