From b75c4fe41194a8b4497fd1e91d576c005a51fb06 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sat, 30 May 2026 00:42:31 +0200 Subject: [PATCH] server: guard against None snapshots crashing the accept loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If extract_all() returns None (Dashboard init failure), appending it to store["records"] would cause _accept_loop to crash with AttributeError on the next client connection. Since only OSError was caught, the thread died permanently — no further clients could join push_clients, so live broadcasts reached nobody. - collect.py: skip None snapshots instead of appending them - network.py: filter None records in _accept_loop; catch all exceptions during history send (not just OSError) so the thread survives - storage_helpers.py: filter None records when loading from disk Co-Authored-By: Claude Sonnet 4.6 --- server/collect.py | 7 +++++-- server/network.py | 14 ++++++++++---- server/storage_helpers.py | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/server/collect.py b/server/collect.py index bffd385..c21756e 100644 --- a/server/collect.py +++ b/server/collect.py @@ -49,7 +49,7 @@ def run(args: argparse.Namespace) -> None: push_clients, push_lock, push_store_ref = network.start_push_server(args.host, args.port) push_store_ref["preloaded"] = load_last_24h_records(logs_dir, vin) for _r in push_store_ref["preloaded"]: - if "procedural" not in _r: + if _r is not None and "procedural" not in _r: apply_procedural(_r) if push_store_ref["preloaded"]: log.info("Pre-loaded %d record(s) from the last 24 h", len(push_store_ref["preloaded"])) @@ -57,7 +57,7 @@ def run(args: argparse.Namespace) -> None: current_day = datetime.now(timezone.utc).date() store = load_store(out, vin, args.interval) for _r in store["records"]: - if "procedural" not in _r: + if _r is not None and "procedural" not in _r: apply_procedural(_r) push_store_ref["current"] = store last_broadcast: dict = {} @@ -86,6 +86,9 @@ def run(args: argparse.Namespace) -> None: try: we_connect.update(wc) snapshot = extract_all(vehicle) + if snapshot is None: + log.warning("extract_all returned None — skipping this cycle") + continue snapshot = apply_procedural(snapshot) store["records"].append(snapshot) save_store(out, store, args.max_records) diff --git a/server/network.py b/server/network.py index 55e024a..1cbed10 100644 --- a/server/network.py +++ b/server/network.py @@ -24,15 +24,17 @@ def _accept_loop( preloaded = store_ref.get("preloaded", []) if preloaded: - seen_ts = {r.get("ts") for r in preloaded} - extra = [r for r in in_mem if r.get("ts") not in seen_ts] - records = sorted(preloaded + extra, key=lambda r: r.get("ts", "")) + seen_ts = {r.get("ts") for r in preloaded if r is not None} + extra = [r for r in in_mem if r is not None and r.get("ts") not in seen_ts] + records = sorted(preloaded + extra, key=lambda r: r.get("ts", "") if r is not None else "") else: - records = in_mem + records = [r for r in in_mem if r is not None] try: prev: dict = {} for record in records: + if record is None: + continue diff = jay_diff_full(prev, record, combine_upd_add=True) prev = record conn.sendall((json.dumps(diff, default=str) + "\n").encode()) @@ -42,6 +44,10 @@ def _accept_loop( log.debug("Failed to send history to %s", addr) conn.close() continue + except Exception: + log.exception("Error sending history to %s — dropping client", addr) + conn.close() + continue clients.append(conn) except OSError: break diff --git a/server/storage_helpers.py b/server/storage_helpers.py index 9544a08..54851b1 100644 --- a/server/storage_helpers.py +++ b/server/storage_helpers.py @@ -53,11 +53,11 @@ def load_last_24h_records(logs_dir: Path, vin: str) -> list: try: data = json.loads(path.read_text()) if isinstance(data, dict) and "records" in data: - kept = [r for r in data["records"] if r.get("ts", "") >= cutoff_str] + kept = [r for r in data["records"] if r is not None and r.get("ts", "") >= cutoff_str] records.extend(kept) log.debug("Loaded %d/%d records from %s", len(kept), len(data["records"]), path.name) except (json.JSONDecodeError, OSError): log.warning("Could not load %s", path) - records.sort(key=lambda r: r.get("ts", "")) + records.sort(key=lambda r: r.get("ts", "") if r is not None else "") return records \ No newline at end of file