The GSMTAP capture thread died silently whenever a single non-SIM or
truncated GSMTAP datagram arrived (read_packet() raises ValueError,
uncaught in the loop, killing the daemon thread while UDP kept
flowing) — matching the "stops listening after a sniffer restart"
symptom.
Fixes:
- GsmtapListener.iter_messages() now catches ValueError/struct.error
from read_packet(), drops the offending datagram (counted via a new
`dropped` counter) and keeps listening.
- CaptureManager._capture_loop() guards the DB insert so a transient
error can't kill the thread; a top-level guard logs
"ERROR: capture thread stopped on error …" + traceback and clears
the session so /api/status no longer reports a stuck "active".
- Server now logs capture liveness to stderr: "Capture started", a
periodic "Capture alive: session=N messages=M bytes=B dropped=D"
heartbeat (default every 60 s, configurable via log_interval), and
"Capture stopped" (or "empty session discarded").
Tests: +5 (listener survives non-SIM/short packet, capture loop
survives insert error, logs stop-on-error, heartbeat+stop log).
Stays on 1.22.0 (backend-only).