fix: persist bridge NIDs across restarts; fix missing Tuple import
bridge.py:
- state_path param: optional JSON file for persisting discovered bridge NIDs
- _load_state: on startup, re-registers each saved NID at its allocated port
so radicle-node reconnects fast without waiting for a re-announce cycle
- _save_state: called whenever a new NID is first discovered
- Added json/Path imports
cli.py:
- cmd_seed: passes state_path={seed_home}/bridge_state.json
- cmd_bridge: passes state_path=~/.radicle-rns/bridge_state.json
- Fixed missing Tuple import (used by _parse_delays return type annotation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4d3fdcf5f9
commit
3ee1fa8c58
|
|
@ -15,6 +15,7 @@ The bridge:
|
||||||
4. Remote bridges forward to their local radicle-node
|
4. Remote bridges forward to their local radicle-node
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import select
|
import select
|
||||||
|
|
@ -23,6 +24,7 @@ import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
from typing import Callable, Dict, List, Optional, Set, Tuple
|
from typing import Callable, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import RNS
|
import RNS
|
||||||
|
|
@ -92,6 +94,7 @@ class RadicleBridge:
|
||||||
auto_seed: bool = True,
|
auto_seed: bool = True,
|
||||||
announce_retry_delays: Tuple[int, ...] = (5, 15, 30),
|
announce_retry_delays: Tuple[int, ...] = (5, 15, 30),
|
||||||
rad_home: Optional[str] = None,
|
rad_home: Optional[str] = None,
|
||||||
|
state_path: Optional[Path] = None,
|
||||||
):
|
):
|
||||||
"""Initialize the bridge.
|
"""Initialize the bridge.
|
||||||
|
|
||||||
|
|
@ -108,6 +111,8 @@ class RadicleBridge:
|
||||||
intervals on LoRa to respect duty cycle limits, e.g. (60, 300, 900).
|
intervals on LoRa to respect duty cycle limits, e.g. (60, 300, 900).
|
||||||
rad_home: RAD_HOME for rad CLI calls. None = use system default.
|
rad_home: RAD_HOME for rad CLI calls. None = use system default.
|
||||||
Set to seed home when bridging a dedicated seed node.
|
Set to seed home when bridging a dedicated seed node.
|
||||||
|
state_path: JSON file to persist discovered bridge NIDs across restarts.
|
||||||
|
None = no persistence.
|
||||||
"""
|
"""
|
||||||
self.listen_port = listen_port
|
self.listen_port = listen_port
|
||||||
self.radicle_host = radicle_host
|
self.radicle_host = radicle_host
|
||||||
|
|
@ -116,6 +121,7 @@ class RadicleBridge:
|
||||||
self.auto_seed = auto_seed
|
self.auto_seed = auto_seed
|
||||||
self.announce_retry_delays = announce_retry_delays
|
self.announce_retry_delays = announce_retry_delays
|
||||||
self.rad_home = rad_home
|
self.rad_home = rad_home
|
||||||
|
self.state_path = state_path
|
||||||
|
|
||||||
# Initialize Reticulum
|
# Initialize Reticulum
|
||||||
self.reticulum = RNS.Reticulum(config_path)
|
self.reticulum = RNS.Reticulum(config_path)
|
||||||
|
|
@ -172,6 +178,9 @@ class RadicleBridge:
|
||||||
RNS.Transport.register_announce_handler(self._handle_announce)
|
RNS.Transport.register_announce_handler(self._handle_announce)
|
||||||
RNS.log("Registered announce handler for bridge discovery", RNS.LOG_INFO)
|
RNS.log("Registered announce handler for bridge discovery", RNS.LOG_INFO)
|
||||||
|
|
||||||
|
# Load persisted NIDs first so radicle-node is ready for reconnects
|
||||||
|
self._load_state()
|
||||||
|
|
||||||
# Announce presence; repeat a few times so peers that come up shortly
|
# Announce presence; repeat a few times so peers that come up shortly
|
||||||
# after us don't miss it due to interface initialisation timing
|
# after us don't miss it due to interface initialisation timing
|
||||||
self.announce()
|
self.announce()
|
||||||
|
|
@ -215,6 +224,48 @@ class RadicleBridge:
|
||||||
)
|
)
|
||||||
RNS.Transport.request_path(bridge_hash)
|
RNS.Transport.request_path(bridge_hash)
|
||||||
|
|
||||||
|
def _load_state(self):
|
||||||
|
"""Load persisted bridge NIDs and re-register them with radicle-node."""
|
||||||
|
if not self.state_path or not self.state_path.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = json.loads(self.state_path.read_text())
|
||||||
|
nids: Dict[str, str] = data.get("bridge_nids", {})
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Could not load bridge state: {e}", RNS.LOG_WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
|
for hex_hash, nid in nids.items():
|
||||||
|
try:
|
||||||
|
bridge_hash = bytes.fromhex(hex_hash)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
with self._remote_bridges_lock:
|
||||||
|
self._remote_bridges[bridge_hash] = 0.0 # unknown age; path maintenance will refresh
|
||||||
|
self._bridge_nids[bridge_hash] = nid
|
||||||
|
# Allocate port and re-register with radicle-node so it can connect
|
||||||
|
threading.Thread(
|
||||||
|
target=self._auto_register_seed,
|
||||||
|
args=(nid, bridge_hash),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
RNS.log(
|
||||||
|
f"Restored bridge state: {hex_hash[:16]} → {nid[:32]}",
|
||||||
|
RNS.LOG_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_state(self):
|
||||||
|
"""Persist current bridge NIDs to disk for next startup."""
|
||||||
|
if not self.state_path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with self._remote_bridges_lock:
|
||||||
|
nids = {h.hex(): n for h, n in self._bridge_nids.items()}
|
||||||
|
self.state_path.write_text(json.dumps({"bridge_nids": nids}, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Could not save bridge state: {e}", RNS.LOG_WARNING)
|
||||||
|
|
||||||
def _reconnect_link(
|
def _reconnect_link(
|
||||||
self, bridge_hash: bytes, timeout: float = 20.0
|
self, bridge_hash: bytes, timeout: float = 20.0
|
||||||
) -> Optional[RNS.Link]:
|
) -> Optional[RNS.Link]:
|
||||||
|
|
@ -660,6 +711,9 @@ class RadicleBridge:
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
if nid_is_new:
|
||||||
|
self._save_state()
|
||||||
|
|
||||||
if self.auto_seed and nid_is_new:
|
if self.auto_seed and nid_is_new:
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._auto_register_seed,
|
target=self._auto_register_seed,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import sys
|
||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity"
|
DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity"
|
||||||
|
|
||||||
|
|
@ -362,6 +362,7 @@ def cmd_seed(args):
|
||||||
auto_seed=True,
|
auto_seed=True,
|
||||||
announce_retry_delays=announce_retry_delays,
|
announce_retry_delays=announce_retry_delays,
|
||||||
rad_home=str(seed_home),
|
rad_home=str(seed_home),
|
||||||
|
state_path=seed_home / "bridge_state.json",
|
||||||
)
|
)
|
||||||
bridge.set_local_radicle_nid(nid)
|
bridge.set_local_radicle_nid(nid)
|
||||||
|
|
||||||
|
|
@ -457,6 +458,7 @@ def cmd_bridge(args):
|
||||||
auto_connect=auto_connect,
|
auto_connect=auto_connect,
|
||||||
auto_seed=auto_seed,
|
auto_seed=auto_seed,
|
||||||
announce_retry_delays=announce_retry_delays,
|
announce_retry_delays=announce_retry_delays,
|
||||||
|
state_path=Path.home() / ".radicle-rns" / "bridge_state.json",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resolve local radicle NID: explicit flag > auto-detect from 'rad self'
|
# Resolve local radicle NID: explicit flag > auto-detect from 'rad self'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue