fix: narrow system health psutil fallback
This commit is contained in:
1 parent
9f5aa98621
commit
0905adcf43
2 files changed
+77
-14
No files matched your search
+21
-14
@@ -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
|
||||
|
||||
Reference in new issue
Block a user