Skip to content

Commit 211ddf5

Browse files
committed
Send nonfatal runtime errors to event feed
1 parent 5dd1547 commit 211ddf5

18 files changed

+915
-64
lines changed

TASK.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# TASKS
2+
- 2026-03-06: DONE. Surface handled runtime failures in the UI Event Feed by routing non-fatal command, daemon, telemetry, sampler, embedded-LXMD, and LXMF runtime exceptions through the shared event log with regression coverage.
23
- 2026-03-06: DONE. Reap stale northbound WebSocket clients and cancel stray UI reconnect timers to prevent port-8000 file descriptor exhaustion under connection churn.
34
- 2026-03-05: DONE. Enforce Python-backend-only PyPI artifacts by excluding non-backend directories in Poetry packaging config and adding publish-workflow artifact validation for frontend/electron/docs/API paths.
45
- 2026-03-05: DONE. Harden PyPI publish workflow with `PYPI_API_TOKEN` fallback and actionable trusted-publisher mismatch diagnostics after repository identity changes.

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "rch-desktop",
33
"private": true,
4-
"version": "2.0.9",
4+
"version": "2.0.10",
55
"description": "Electron shell for Reticulum Community Hub.",
66
"homepage": "https://github.com/FreeTAKTeam/Reticulum-Community-Hub",
77
"main": "dist/main.js",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "ReticulumTelemetryHub"
3-
version = "2.0.9"
3+
version = "2.0.10"
44
description = "Reticulum-Telemetry-Hub (RTH) manages a complete TCP node across a Reticulum-based network, enabling communication and data sharing between clients like Sideband or Meshchat."
55
authors = ["naman108, corvo"]
66
readme = "README.md"

reticulum_telemetry_hub/embedded_lxmd/embedded.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_enum import (
2121
SID_LXMF_PROPAGATION,
2222
)
23+
from reticulum_telemetry_hub.reticulum_server.event_log import EventLog
24+
from reticulum_telemetry_hub.reticulum_server.runtime_events import (
25+
report_nonfatal_exception,
26+
)
2327

2428
if TYPE_CHECKING:
2529
from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
@@ -111,12 +115,14 @@ def __init__(
111115
destination: RNS.Destination,
112116
config_manager: Optional[HubConfigurationManager] = None,
113117
telemetry_controller: Optional[TelemetryController] = None,
118+
event_log: EventLog | None = None,
114119
) -> None:
115120
self.router = router
116121
self.destination = destination
117122
self.config_manager = config_manager or HubConfigurationManager()
118123
self.config = EmbeddedLxmdConfig.from_manager(self.config_manager)
119124
self.telemetry_controller = telemetry_controller
125+
self.event_log = event_log
120126
self._propagation_observers: list[Callable[[dict[str, Any]], None]] = []
121127
self._propagation_snapshot: bytes | None = None
122128
self._propagation_lock = threading.Lock()
@@ -182,6 +188,25 @@ def add_propagation_observer(
182188

183189
self._propagation_observers.append(observer)
184190

191+
def _report_propagation_exception(
192+
self,
193+
message: str,
194+
exc: Exception,
195+
*,
196+
metadata: dict[str, object] | None = None,
197+
log_level: int | None = None,
198+
) -> dict[str, object] | None:
199+
"""Record a handled embedded-LXMD failure in logs and the event feed."""
200+
201+
return report_nonfatal_exception(
202+
self.event_log,
203+
"propagation_error",
204+
message,
205+
exc,
206+
metadata=metadata,
207+
log_level=log_level if log_level is not None else getattr(RNS, "LOG_ERROR", 1),
208+
)
209+
185210
def propagation_startup_status(self) -> dict[str, Any]:
186211
"""Return propagation startup state for control/status endpoints."""
187212

@@ -269,8 +294,8 @@ def _parse_store_filename(filename: str) -> float | None:
269294

270295
return received
271296

272-
@staticmethod
273297
def _remove_startup_files(
298+
self,
274299
paths: list[str],
275300
*,
276301
reason: str,
@@ -283,9 +308,15 @@ def _remove_startup_files(
283308
except FileNotFoundError:
284309
continue
285310
except Exception as exc: # pragma: no cover - defensive logging
286-
RNS.log(
311+
self._report_propagation_exception(
287312
f"Failed to remove {reason} startup message file '{path}': {exc}",
288-
RNS.LOG_WARNING,
313+
exc,
314+
metadata={
315+
"operation": "startup_prune",
316+
"reason": reason,
317+
"path": path,
318+
},
319+
log_level=RNS.LOG_WARNING,
289320
)
290321
return removed
291322

@@ -387,9 +418,10 @@ def _enable_propagation(self) -> None:
387418
error=str(exc),
388419
mark_finished=True,
389420
)
390-
RNS.log(
421+
self._report_propagation_exception(
391422
f"Failed to enable LXMF propagation node in embedded mode: {exc}",
392-
RNS.LOG_ERROR,
423+
exc,
424+
metadata={"operation": "enable_propagation"},
393425
)
394426
return
395427

@@ -420,37 +452,45 @@ def _announce_delivery(self) -> None:
420452
try:
421453
self.router.announce(self.destination.hash)
422454
except Exception as exc: # pragma: no cover - logging guard
423-
RNS.log(
455+
self._report_propagation_exception(
424456
f"Failed to announce embedded LXMF destination: {exc}",
425-
RNS.LOG_ERROR,
457+
exc,
458+
metadata={"operation": "announce_delivery"},
426459
)
427460

428461
def _announce_propagation(self) -> None:
429462
try:
430463
self.router.announce_propagation_node()
431464
except Exception as exc: # pragma: no cover - logging guard
432-
RNS.log(
465+
self._report_propagation_exception(
433466
f"Failed to announce embedded propagation node: {exc}",
434-
RNS.LOG_ERROR,
467+
exc,
468+
metadata={"operation": "announce_propagation"},
435469
)
436470

437471
def _apply_propagation_runtime_config(self) -> None:
438472
if self.config.auth_required:
439473
try:
440474
self.router.set_authentication(required=True)
441475
except Exception as exc: # pragma: no cover - logging guard
442-
RNS.log(
476+
self._report_propagation_exception(
443477
f"Failed to enable LXMF propagation authentication: {exc}",
444-
RNS.LOG_ERROR,
478+
exc,
479+
metadata={"operation": "set_authentication"},
445480
)
446481

447482
for identity_hash in self.config.control_allowed_identities:
448483
try:
449484
self.router.allow_control(bytes.fromhex(identity_hash))
450485
except Exception as exc: # pragma: no cover - logging guard
451-
RNS.log(
486+
self._report_propagation_exception(
452487
f"Failed to add LXMF propagation control identity '{identity_hash}': {exc}",
453-
RNS.LOG_WARNING,
488+
exc,
489+
metadata={
490+
"operation": "allow_control",
491+
"identity_hash": identity_hash,
492+
},
493+
log_level=RNS.LOG_WARNING,
454494
)
455495

456496
def _baseline_propagation_payload(self) -> dict[str, Any]:
@@ -604,9 +644,10 @@ def _build_propagation_payload(self) -> dict[str, Any] | None:
604644
try:
605645
stats = self.router.compile_stats()
606646
except Exception as exc: # pragma: no cover - defensive logging
607-
RNS.log(
647+
self._report_propagation_exception(
608648
f"Failed to compile LXMF propagation stats: {exc}",
609-
RNS.LOG_ERROR,
649+
exc,
650+
metadata={"operation": "compile_stats"},
610651
)
611652
return None
612653

@@ -641,9 +682,13 @@ def _notify_propagation_observers(self, payload: dict[str, Any]) -> None:
641682
try:
642683
observer(payload)
643684
except Exception as exc: # pragma: no cover - defensive logging
644-
RNS.log(
685+
self._report_propagation_exception(
645686
f"Propagation observer failed: {exc}",
646-
RNS.LOG_ERROR,
687+
exc,
688+
metadata={
689+
"operation": "observer",
690+
"observer": repr(observer),
691+
},
647692
)
648693

649694
def _persist_propagation_snapshot(self, payload: dict[str, Any]) -> None:
@@ -669,9 +714,13 @@ def _persist_propagation_snapshot(self, payload: dict[str, Any]) -> None:
669714
_utcnow(),
670715
)
671716
except Exception as exc: # pragma: no cover - defensive logging
672-
RNS.log(
717+
self._report_propagation_exception(
673718
f"Failed to persist propagation telemetry: {exc}",
674-
RNS.LOG_ERROR,
719+
exc,
720+
metadata={
721+
"operation": "persist_telemetry",
722+
"peer_hash": peer_hash,
723+
},
675724
)
676725

677726
def _deferred_start_jobs(self) -> None:

reticulum_telemetry_hub/lxmf_telemetry/sampler.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import LXMF
1111
import RNS
1212
from reticulum_telemetry_hub.reticulum_server.appearance import apply_icon_appearance
13+
from reticulum_telemetry_hub.reticulum_server.event_log import EventLog
14+
from reticulum_telemetry_hub.reticulum_server.runtime_events import (
15+
report_nonfatal_exception,
16+
)
1317
from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_enum import (
1418
SID_TIME,
1519
)
@@ -61,12 +65,14 @@ def __init__(
6165
) = None,
6266
telemeter_manager: TelemeterManager | None = None,
6367
broadcast_updates: bool = False,
68+
event_log: EventLog | None = None,
6469
) -> None:
6570
self._controller = controller
6671
self._router = router
6772
self._source_destination = source_destination
6873
self._connections = connections if connections is not None else {}
6974
self._broadcast_updates = broadcast_updates
75+
self._event_log = event_log
7076
self._stop_event = threading.Event()
7177
self._thread: threading.Thread | None = None
7278
self._jobs: list[_SamplerJob] = []
@@ -158,7 +164,17 @@ def _invoke_collector(
158164
try:
159165
result = collector()
160166
except Exception as exc: # pragma: no cover - defensive logging
161-
RNS.log(f"Telemetry collector {collector!r} failed: {exc}", RNS.LOG_ERROR)
167+
report_nonfatal_exception(
168+
self._event_log,
169+
"telemetry_error",
170+
f"Telemetry collector {collector!r} failed: {exc}",
171+
exc,
172+
metadata={
173+
"operation": "collect",
174+
"collector": repr(collector),
175+
},
176+
log_level=RNS.LOG_ERROR,
177+
)
162178
return None
163179

164180
if result is None:
@@ -209,9 +225,17 @@ def _process_sample(self, sample: TelemetrySample) -> None:
209225
message.destination_hash = destination.identity.hash
210226
self._router.handle_outbound(message)
211227
except Exception as exc: # pragma: no cover - defensive logging
212-
RNS.log(
228+
report_nonfatal_exception(
229+
self._event_log,
230+
"telemetry_error",
213231
f"Failed to deliver telemetry sample to {destination}: {exc}",
214-
RNS.LOG_ERROR,
232+
exc,
233+
metadata={
234+
"operation": "broadcast",
235+
"destination": str(destination),
236+
"peer_dest": peer_dest,
237+
},
238+
log_level=RNS.LOG_ERROR,
215239
)
216240

217241
# ------------------------------------------------------------------

reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from reticulum_telemetry_hub.reticulum_server.appearance import (
1313
build_telemetry_icon_appearance_value,
1414
)
15+
from reticulum_telemetry_hub.reticulum_server.runtime_events import (
16+
report_nonfatal_exception,
17+
)
1518

1619
import LXMF
1720
import RNS
@@ -762,7 +765,19 @@ def _notify_listener(
762765
try:
763766
self._telemetry_listener(telemetry, peer_hash, timestamp)
764767
except Exception as exc: # pragma: no cover - defensive logging
765-
RNS.log(f"Telemetry listener raised an exception: {exc}", RNS.LOG_WARNING)
768+
report_nonfatal_exception(
769+
self._event_log,
770+
"telemetry_error",
771+
f"Telemetry listener raised an exception: {exc}",
772+
exc,
773+
metadata={
774+
"operation": "listener",
775+
"peer_hash": (
776+
peer_hash.hex() if isinstance(peer_hash, bytes) else peer_hash
777+
),
778+
},
779+
log_level=RNS.LOG_WARNING,
780+
)
766781

767782
def _record_event(
768783
self,

0 commit comments

Comments
 (0)