checkin: report host disk free/total bytes (#41)
/recordings is a bind mount of the host checkout, so shutil.disk_usage on it is the disk the recorder writes to and Docker fills. Explicit nulls on error so C2 never keeps a stale reading. Server half: server-26 feat/41-node-disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
04ba5457fa
commit
7f52e0e891
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Optional, Callable, Awaitable, Dict, Any
|
from typing import Optional, Callable, Awaitable, Dict, Any
|
||||||
@@ -8,6 +9,22 @@ from app.config import settings
|
|||||||
from app.internal.logger import logger
|
from app.internal.logger import logger
|
||||||
from app.internal import credentials, key_pickup
|
from app.internal import credentials, key_pickup
|
||||||
|
|
||||||
|
|
||||||
|
def disk_report(path: str) -> Dict[str, Optional[int]]:
|
||||||
|
"""Free/total bytes of the filesystem holding `path` (node-26#41).
|
||||||
|
|
||||||
|
/recordings is a bind mount of the host checkout, so this is the host disk
|
||||||
|
the recorder writes to and Docker's images/build cache live on. A full disk
|
||||||
|
stopped node-002 recording for 17 days while it still showed Online. Nulls
|
||||||
|
on error, so C2 overwrites a stale reading instead of keeping it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
usage = shutil.disk_usage(path)
|
||||||
|
except OSError:
|
||||||
|
return {"disk_free_bytes": None, "disk_total_bytes": None}
|
||||||
|
return {"disk_free_bytes": usage.free, "disk_total_bytes": usage.total}
|
||||||
|
|
||||||
|
|
||||||
CommandCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
CommandCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
||||||
ConfigCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
ConfigCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
||||||
ApiKeyCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
ApiKeyCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
||||||
@@ -372,6 +389,7 @@ class MQTTManager:
|
|||||||
count = devices.get("count") or None
|
count = devices.get("count") or None
|
||||||
if count is not None:
|
if count is not None:
|
||||||
payload["sdr_count"] = count
|
payload["sdr_count"] = count
|
||||||
|
payload.update(disk_report(settings.recordings_path))
|
||||||
self._publish(self._t_checkin, payload, qos=1)
|
self._publish(self._t_checkin, payload, qos=1)
|
||||||
|
|
||||||
def _publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = False):
|
def _publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = False):
|
||||||
|
|||||||
@@ -140,3 +140,16 @@ def test_build_client_does_not_touch_offline_buffer(monkeypatch):
|
|||||||
|
|
||||||
assert list(mqtt_manager._offline_buffer) == [("nodes/test/metadata", {"call_id": "sentinel"})]
|
assert list(mqtt_manager._offline_buffer) == [("nodes/test/metadata", {"call_id": "sentinel"})]
|
||||||
mqtt_manager._offline_buffer.clear()
|
mqtt_manager._offline_buffer.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_disk_report_reads_free_and_total(tmp_path):
|
||||||
|
from app.internal.mqtt_manager import disk_report
|
||||||
|
rep = disk_report(str(tmp_path))
|
||||||
|
assert rep["disk_total_bytes"] > 0
|
||||||
|
assert 0 <= rep["disk_free_bytes"] <= rep["disk_total_bytes"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_disk_report_nulls_when_path_unreadable(tmp_path):
|
||||||
|
# node-26#41: nulls (not omitted keys) so C2 never keeps a stale reading.
|
||||||
|
from app.internal.mqtt_manager import disk_report
|
||||||
|
assert disk_report(str(tmp_path / "missing")) == {"disk_free_bytes": None, "disk_total_bytes": None}
|
||||||
|
|||||||
Reference in New Issue
Block a user