Skip to content

Commit 05f5ba1

Browse files
Giu PlataniaGiu Platania
authored andcommitted
Bump project version to 2.0.3 and apply runtime patches for LXMF error handling
1 parent 7c6a3b2 commit 05f5ba1

File tree

7 files changed

+178
-3
lines changed

7 files changed

+178
-3
lines changed

TASK.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# TASKS
2+
- 2026-03-01: DONE. Patch the installed LXMF runtime at startup so integer sync-offer error codes do not crash peer sync and block propagated messages from reaching the hub/UI.
3+
- 2026-03-01: DONE. Bump project version to 2.0.2 across Python, UI, and Electron manifests.
24
- 2026-03-01: DONE. Fix LXMF peer offer-response handling so remote integer error codes do not crash sync processing with "'int' is not iterable".
35
- 2026-03-01: DONE. Make app info prefer the configured hub display name so the UI shows the runtime hub name instead of the package name.
46
- 2026-03-01: DONE. Add a remote launcher script that installs the backend from local source, installs npm when needed, and starts the API plus UI with the WebSocket URL derived from the API URL by default.

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.1",
4+
"version": "2.0.3",
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.1"
3+
version = "2.0.3"
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"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Runtime compatibility patches for the external LXMF package."""
2+
3+
from __future__ import annotations
4+
5+
import importlib
6+
from functools import wraps
7+
from typing import Any
8+
from typing import Callable
9+
10+
_OFFER_RESPONSE_PATCHED = "__rch_offer_response_patched__"
11+
12+
13+
def apply_lxmf_runtime_patches() -> None:
14+
"""Patch external LXMF runtime behavior required by the hub."""
15+
16+
lxmpeer_module = importlib.import_module("LXMF.LXMPeer")
17+
_patch_offer_response_class(lxmpeer_module.LXMPeer)
18+
19+
20+
def _patch_offer_response_class(peer_class: type[Any]) -> None:
21+
"""Wrap ``offer_response`` so integer error codes are handled safely."""
22+
23+
offer_response = getattr(peer_class, "offer_response", None)
24+
if not callable(offer_response):
25+
return
26+
27+
if getattr(offer_response, _OFFER_RESPONSE_PATCHED, False):
28+
return
29+
30+
patched_offer_response = _build_offer_response_wrapper(offer_response)
31+
setattr(patched_offer_response, _OFFER_RESPONSE_PATCHED, True)
32+
setattr(peer_class, "offer_response", patched_offer_response)
33+
34+
35+
def _build_offer_response_wrapper(
36+
original_offer_response: Callable[..., Any],
37+
) -> Callable[..., Any]:
38+
"""Build a wrapper around the installed LXMF ``offer_response`` handler."""
39+
40+
@wraps(original_offer_response)
41+
def patched_offer_response(self: Any, request_receipt: Any) -> Any:
42+
response = getattr(request_receipt, "response", None)
43+
if type(response) is int and _handle_integer_offer_response(self, response):
44+
return None
45+
46+
return original_offer_response(self, request_receipt)
47+
48+
return patched_offer_response
49+
50+
51+
def _handle_integer_offer_response(peer: Any, response: int) -> bool:
52+
"""Return True when an integer response has been handled locally."""
53+
54+
peer_class = peer.__class__
55+
known_error_responses = (
56+
(
57+
getattr(peer_class, "ERROR_INVALID_KEY", None),
58+
"Remote indicated that the peering key was invalid, sync aborted",
59+
),
60+
(
61+
getattr(peer_class, "ERROR_INVALID_DATA", None),
62+
"Remote indicated that the sync offer data was invalid, sync aborted",
63+
),
64+
(
65+
getattr(peer_class, "ERROR_INVALID_STAMP", None),
66+
"Remote indicated that the sync offer stamp was invalid, sync aborted",
67+
),
68+
(
69+
getattr(peer_class, "ERROR_TIMEOUT", None),
70+
"Remote indicated that the sync offer timed out, sync aborted",
71+
),
72+
)
73+
for expected_response, message in known_error_responses:
74+
if expected_response is not None and response == expected_response:
75+
_abort_sync_offer(peer, message)
76+
return True
77+
78+
pass_through_responses = {
79+
expected_response
80+
for expected_response in (
81+
getattr(peer_class, "ERROR_NO_IDENTITY", None),
82+
getattr(peer_class, "ERROR_NO_ACCESS", None),
83+
getattr(peer_class, "ERROR_THROTTLED", None),
84+
)
85+
if expected_response is not None
86+
}
87+
if response in pass_through_responses:
88+
return False
89+
90+
_abort_sync_offer(
91+
peer,
92+
f"Remote returned unknown sync offer response {response}, sync aborted",
93+
)
94+
return True
95+
96+
97+
def _abort_sync_offer(peer: Any, message: str) -> None:
98+
"""Abort the current LXMF sync and tear down the active peer link."""
99+
100+
import RNS
101+
102+
RNS.log(message, RNS.LOG_VERBOSE)
103+
104+
link = getattr(peer, "link", None)
105+
if link is not None:
106+
link.teardown()
107+
108+
peer.link = None
109+
peer.state = getattr(peer.__class__, "IDLE", getattr(peer, "state", None))

reticulum_telemetry_hub/reticulum_server/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from reticulum_telemetry_hub.config.models import HubAppConfig
6464
from reticulum_telemetry_hub.embedded_lxmd import EmbeddedLxmd
6565
from reticulum_telemetry_hub.lxmf_daemon.LXMF import display_name_from_app_data
66+
from reticulum_telemetry_hub.lxmf_runtime import apply_lxmf_runtime_patches
6667
from reticulum_telemetry_hub.reticulum_server.appearance import apply_icon_appearance
6768
from reticulum_telemetry_hub.reticulum_server.announce_capabilities import (
6869
AnnounceCapabilitiesConfig,
@@ -78,6 +79,8 @@
7879
from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
7980
TelemetryController,
8081
)
82+
83+
apply_lxmf_runtime_patches()
8184
from reticulum_telemetry_hub.lxmf_telemetry.sampler import TelemetrySampler
8285
from reticulum_telemetry_hub.lxmf_telemetry.telemeter_manager import TelemeterManager
8386
from reticulum_telemetry_hub.reticulum_server.services import (

tests/test_lxmf_peer.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import importlib
66

7+
import RNS
8+
9+
from reticulum_telemetry_hub.lxmf_runtime import _patch_offer_response_class
10+
711

812
def test_offer_response_handles_integer_error_without_iterating(monkeypatch) -> None:
913
"""Abort cleanly when a remote peer returns an integer error code."""
@@ -33,3 +37,60 @@ def __init__(self, response: int) -> None:
3337
assert link.torn_down is True
3438
assert peer.link is None
3539
assert peer.state == lxmpeer_module.LXMPeer.IDLE
40+
41+
42+
def test_runtime_patch_handles_installed_lxmf_integer_errors(monkeypatch) -> None:
43+
"""Intercept installed LXMF integer errors before the upstream handler iterates them."""
44+
45+
monkeypatch.setattr(RNS, "log", lambda *args, **kwargs: None)
46+
47+
class DummyLink:
48+
def __init__(self) -> None:
49+
self.torn_down = False
50+
51+
def teardown(self) -> None:
52+
self.torn_down = True
53+
54+
class DummyReceipt:
55+
def __init__(self, response: int) -> None:
56+
self.response = response
57+
58+
class DummyPeer:
59+
ERROR_NO_IDENTITY = 1
60+
ERROR_NO_ACCESS = 2
61+
ERROR_THROTTLED = 3
62+
ERROR_INVALID_KEY = 4
63+
ERROR_INVALID_DATA = 5
64+
ERROR_INVALID_STAMP = 6
65+
ERROR_TIMEOUT = 7
66+
IDLE = "idle"
67+
68+
def __init__(self) -> None:
69+
self.link = None
70+
self.state = "busy"
71+
self.original_called = False
72+
self.last_response = None
73+
74+
def offer_response(self, request_receipt) -> None:
75+
self.original_called = True
76+
self.last_response = request_receipt.response
77+
78+
_patch_offer_response_class(DummyPeer)
79+
80+
rejected_peer = DummyPeer()
81+
rejected_peer.link = DummyLink()
82+
83+
rejected_peer.offer_response(DummyReceipt(DummyPeer.ERROR_INVALID_KEY))
84+
85+
assert rejected_peer.original_called is False
86+
assert rejected_peer.link is None
87+
assert rejected_peer.state == DummyPeer.IDLE
88+
89+
passthrough_peer = DummyPeer()
90+
passthrough_peer.link = DummyLink()
91+
92+
passthrough_peer.offer_response(DummyReceipt(DummyPeer.ERROR_NO_ACCESS))
93+
94+
assert passthrough_peer.original_called is True
95+
assert passthrough_peer.link is not None
96+
assert passthrough_peer.last_response == DummyPeer.ERROR_NO_ACCESS

ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rth-core-ui",
3-
"version": "2.0.1",
3+
"version": "2.0.3",
44
"private": true,
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)