From 7f52e0e8918539b787df37c3f2df86db06ba450c Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 26 Oct 2026 02:24:07 -0400 Subject: [PATCH] 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 --- drb-edge-node/app/internal/mqtt_manager.py | 18 ++++++++++++++++++ drb-edge-node/tests/test_mqtt_manager.py | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/drb-edge-node/app/internal/mqtt_manager.py b/drb-edge-node/app/internal/mqtt_manager.py index 834c059..09eb12a 100644 --- a/drb-edge-node/app/internal/mqtt_manager.py +++ b/drb-edge-node/app/internal/mqtt_manager.py @@ -1,5 +1,6 @@ import asyncio import json +import shutil from datetime import datetime, timezone from collections import deque 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 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]] ConfigCallback = 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 if count is not None: payload["sdr_count"] = count + payload.update(disk_report(settings.recordings_path)) self._publish(self._t_checkin, payload, qos=1) def _publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = False): diff --git a/drb-edge-node/tests/test_mqtt_manager.py b/drb-edge-node/tests/test_mqtt_manager.py index cfcb60f..96686d6 100644 --- a/drb-edge-node/tests/test_mqtt_manager.py +++ b/drb-edge-node/tests/test_mqtt_manager.py @@ -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"})] 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}