feat(reticulum): implement hot reloading for Reticulum instance and enhance error handling; update configuration access to ensure stability
This commit is contained in:
@@ -406,17 +406,19 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# init RNCP handler
|
# init RNCP handler
|
||||||
self.rncp_handler = RNCPHandler(
|
self.rncp_handler = RNCPHandler(
|
||||||
reticulum_instance=self.reticulum,
|
reticulum_instance=getattr(self, "reticulum", None),
|
||||||
identity=self.identity,
|
identity=self.identity,
|
||||||
storage_dir=self.storage_dir,
|
storage_dir=self.storage_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
# init RNStatus handler
|
# init RNStatus handler
|
||||||
self.rnstatus_handler = RNStatusHandler(reticulum_instance=self.reticulum)
|
self.rnstatus_handler = RNStatusHandler(
|
||||||
|
reticulum_instance=getattr(self, "reticulum", None)
|
||||||
|
)
|
||||||
|
|
||||||
# init RNProbe handler
|
# init RNProbe handler
|
||||||
self.rnprobe_handler = RNProbeHandler(
|
self.rnprobe_handler = RNProbeHandler(
|
||||||
reticulum_instance=self.reticulum,
|
reticulum_instance=getattr(self, "reticulum", None),
|
||||||
identity=self.identity,
|
identity=self.identity,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -797,6 +799,181 @@ class ReticulumMeshChat:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def reload_reticulum(self):
|
||||||
|
print("Hot reloading Reticulum stack...")
|
||||||
|
# Keep reference to old reticulum instance for cleanup
|
||||||
|
old_reticulum = getattr(self, "reticulum", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Signal background loops to exit
|
||||||
|
self._identity_session_id += 1
|
||||||
|
|
||||||
|
# Teardown current identity state and managers
|
||||||
|
# This also calls cleanup_rns_state_for_identity
|
||||||
|
self.teardown_identity()
|
||||||
|
|
||||||
|
# Give loops a moment to finish
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# Aggressively close RNS interfaces to release sockets
|
||||||
|
try:
|
||||||
|
interfaces = []
|
||||||
|
if hasattr(RNS.Transport, "interfaces"):
|
||||||
|
interfaces.extend(RNS.Transport.interfaces)
|
||||||
|
if hasattr(RNS.Transport, "local_client_interfaces"):
|
||||||
|
interfaces.extend(RNS.Transport.local_client_interfaces)
|
||||||
|
|
||||||
|
for interface in interfaces:
|
||||||
|
try:
|
||||||
|
# Generic socketserver shutdown
|
||||||
|
if hasattr(interface, "server") and interface.server:
|
||||||
|
try:
|
||||||
|
interface.server.shutdown()
|
||||||
|
interface.server.server_close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# AutoInterface specific
|
||||||
|
if hasattr(interface, "interface_servers"):
|
||||||
|
for server in interface.interface_servers.values():
|
||||||
|
try:
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TCPClientInterface/etc
|
||||||
|
if hasattr(interface, "socket") and interface.socket:
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# Check if socket is still valid before shutdown
|
||||||
|
if interface.socket.fileno() != -1:
|
||||||
|
try:
|
||||||
|
interface.socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
interface.socket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
interface.detach()
|
||||||
|
interface.detached = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning closing interface during reload: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning during aggressive interface cleanup: {e}")
|
||||||
|
|
||||||
|
# Close RPC listener if it exists on the instance
|
||||||
|
if old_reticulum:
|
||||||
|
if (
|
||||||
|
hasattr(old_reticulum, "rpc_listener")
|
||||||
|
and old_reticulum.rpc_listener
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Listener.close() should close the underlying socket/pipe
|
||||||
|
old_reticulum.rpc_listener.close()
|
||||||
|
# Clear it to be sure
|
||||||
|
old_reticulum.rpc_listener = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning closing RPC listener: {e}")
|
||||||
|
|
||||||
|
# Persist and close RNS instance
|
||||||
|
try:
|
||||||
|
# Use class method to ensure all instances are cleaned up if any
|
||||||
|
RNS.Reticulum.exit_handler()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning during RNS exit: {e}")
|
||||||
|
|
||||||
|
# Clear RNS singleton and internal state to allow re-initialization
|
||||||
|
try:
|
||||||
|
# Reticulum uses private variables for singleton and state control
|
||||||
|
# We need to clear them so we can create a new instance
|
||||||
|
if hasattr(RNS.Reticulum, "_Reticulum__instance"):
|
||||||
|
RNS.Reticulum._Reticulum__instance = None
|
||||||
|
if hasattr(RNS.Reticulum, "_Reticulum__exit_handler_ran"):
|
||||||
|
RNS.Reticulum._Reticulum__exit_handler_ran = False
|
||||||
|
if hasattr(RNS.Reticulum, "_Reticulum__interface_detach_ran"):
|
||||||
|
RNS.Reticulum._Reticulum__interface_detach_ran = False
|
||||||
|
|
||||||
|
# Also clear Transport caches and globals
|
||||||
|
RNS.Transport.interfaces = []
|
||||||
|
RNS.Transport.local_client_interfaces = []
|
||||||
|
RNS.Transport.destinations = []
|
||||||
|
RNS.Transport.active_links = []
|
||||||
|
RNS.Transport.pending_links = []
|
||||||
|
RNS.Transport.announce_handlers = []
|
||||||
|
RNS.Transport.jobs_running = False
|
||||||
|
|
||||||
|
# Clear Identity globals
|
||||||
|
RNS.Identity.known_destinations = {}
|
||||||
|
RNS.Identity.known_ratchets = {}
|
||||||
|
|
||||||
|
# Unregister old exit handlers from atexit if possible
|
||||||
|
try:
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
# Reticulum uses a staticmethod exit_handler
|
||||||
|
atexit.unregister(RNS.Reticulum.exit_handler)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning clearing RNS state: {e}")
|
||||||
|
|
||||||
|
# Remove reticulum instance from self
|
||||||
|
if hasattr(self, "reticulum"):
|
||||||
|
del self.reticulum
|
||||||
|
|
||||||
|
# Wait another moment for sockets to definitely be released by OS
|
||||||
|
# Also give some time for the RPC listener port to settle
|
||||||
|
print("Waiting for ports to settle...")
|
||||||
|
for i in range(10):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
# If we're using AF_INET for RPC, we can check the port
|
||||||
|
# RNS uses 127.0.0.1:37429 by default
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.settimeout(0.5)
|
||||||
|
# Try to bind to the port. If it fails, it's still in use.
|
||||||
|
# RNS uses this for the shared instance listener.
|
||||||
|
# We use a high port number that is unlikely to be used by anything else
|
||||||
|
# but RNS.
|
||||||
|
try:
|
||||||
|
# RNS local_control_port is 37429
|
||||||
|
s.bind(("127.0.0.1", 37429))
|
||||||
|
s.close()
|
||||||
|
print("RPC port 37429 is free.")
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
print(f"RPC port 37429 still in use... (attempt {i + 1}/10)")
|
||||||
|
s.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking RPC port: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Re-setup identity (this starts background loops again)
|
||||||
|
self.running = True
|
||||||
|
self.setup_identity(self.identity)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Hot reload failed: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Try to recover if possible
|
||||||
|
if not hasattr(self, "reticulum"):
|
||||||
|
try:
|
||||||
|
self.setup_identity(self.identity)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def hotswap_identity(self, identity_hash):
|
async def hotswap_identity(self, identity_hash):
|
||||||
try:
|
try:
|
||||||
# load the new identity
|
# load the new identity
|
||||||
@@ -805,7 +982,8 @@ class ReticulumMeshChat:
|
|||||||
if not os.path.exists(identity_file):
|
if not os.path.exists(identity_file):
|
||||||
raise ValueError("Identity file not found")
|
raise ValueError("Identity file not found")
|
||||||
|
|
||||||
new_identity = RNS.Identity.from_file(identity_file)
|
# Validate that the identity file can be loaded
|
||||||
|
RNS.Identity.from_file(identity_file)
|
||||||
|
|
||||||
# 1. teardown old identity
|
# 1. teardown old identity
|
||||||
self.teardown_identity()
|
self.teardown_identity()
|
||||||
@@ -823,7 +1001,9 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# 3. reset state and setup new identity
|
# 3. reset state and setup new identity
|
||||||
self.running = True
|
self.running = True
|
||||||
self.setup_identity(new_identity)
|
# Close old reticulum before setting up new one to avoid port conflicts
|
||||||
|
await self.reload_reticulum()
|
||||||
|
# Note: reload_reticulum already calls setup_identity
|
||||||
|
|
||||||
# 4. broadcast update to clients
|
# 4. broadcast update to clients
|
||||||
await self.websocket_broadcast(
|
await self.websocket_broadcast(
|
||||||
@@ -831,7 +1011,11 @@ class ReticulumMeshChat:
|
|||||||
{
|
{
|
||||||
"type": "identity_switched",
|
"type": "identity_switched",
|
||||||
"identity_hash": identity_hash,
|
"identity_hash": identity_hash,
|
||||||
"display_name": self.config.display_name.get(),
|
"display_name": (
|
||||||
|
self.config.display_name.get()
|
||||||
|
if hasattr(self, "config")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1251,25 +1435,33 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
def _get_reticulum_section(self):
|
def _get_reticulum_section(self):
|
||||||
try:
|
try:
|
||||||
reticulum_config = self.reticulum.config["reticulum"]
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
reticulum_config = self.reticulum.config["reticulum"]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
except Exception:
|
except Exception:
|
||||||
reticulum_config = None
|
reticulum_config = None
|
||||||
|
|
||||||
if not isinstance(reticulum_config, dict):
|
if not isinstance(reticulum_config, dict):
|
||||||
reticulum_config = {}
|
reticulum_config = {}
|
||||||
self.reticulum.config["reticulum"] = reticulum_config
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.config["reticulum"] = reticulum_config
|
||||||
|
|
||||||
return reticulum_config
|
return reticulum_config
|
||||||
|
|
||||||
def _get_interfaces_section(self):
|
def _get_interfaces_section(self):
|
||||||
try:
|
try:
|
||||||
interfaces = self.reticulum.config["interfaces"]
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
interfaces = self.reticulum.config["interfaces"]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
except Exception:
|
except Exception:
|
||||||
interfaces = None
|
interfaces = None
|
||||||
|
|
||||||
if not isinstance(interfaces, dict):
|
if not isinstance(interfaces, dict):
|
||||||
interfaces = {}
|
interfaces = {}
|
||||||
self.reticulum.config["interfaces"] = interfaces
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.config["interfaces"] = interfaces
|
||||||
|
|
||||||
return interfaces
|
return interfaces
|
||||||
|
|
||||||
@@ -1288,8 +1480,10 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
def _write_reticulum_config(self):
|
def _write_reticulum_config(self):
|
||||||
try:
|
try:
|
||||||
self.reticulum.config.write()
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
return True
|
self.reticulum.config.write()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to write Reticulum config: {e}")
|
print(f"Failed to write Reticulum config: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -1310,7 +1504,11 @@ class ReticulumMeshChat:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.reticulum.transport_enabled():
|
if (
|
||||||
|
hasattr(self, "reticulum")
|
||||||
|
and self.reticulum
|
||||||
|
and not self.reticulum.transport_enabled()
|
||||||
|
):
|
||||||
guidance.append(
|
guidance.append(
|
||||||
{
|
{
|
||||||
"id": "transport_disabled",
|
"id": "transport_disabled",
|
||||||
@@ -1576,12 +1774,27 @@ class ReticulumMeshChat:
|
|||||||
async def shutdown(self, app):
|
async def shutdown(self, app):
|
||||||
# force close websocket clients
|
# force close websocket clients
|
||||||
for websocket_client in self.websocket_clients:
|
for websocket_client in self.websocket_clients:
|
||||||
await websocket_client.close(code=WSCloseCode.GOING_AWAY)
|
try:
|
||||||
|
await websocket_client.close(code=WSCloseCode.GOING_AWAY)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# stop reticulum
|
# stop reticulum
|
||||||
RNS.Transport.detach_interfaces()
|
try:
|
||||||
self.reticulum.exit_handler()
|
RNS.Transport.detach_interfaces()
|
||||||
RNS.exit()
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
try:
|
||||||
|
self.reticulum.exit_handler()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
RNS.exit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def run(self, host, port, launch_browser: bool, enable_https: bool = True):
|
def run(self, host, port, launch_browser: bool, enable_https: bool = True):
|
||||||
# create route table
|
# create route table
|
||||||
@@ -2007,7 +2220,7 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"message": "Interface is now disabled",
|
"message": "Interface deleted",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2488,7 +2701,7 @@ class ReticulumMeshChat:
|
|||||||
if allow_overwriting_interface:
|
if allow_overwriting_interface:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"message": "Interface has been saved. Please restart MeshChat for these changes to take effect.",
|
"message": "Interface has been saved",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -2691,8 +2904,13 @@ class ReticulumMeshChat:
|
|||||||
net_io = psutil.net_io_counters()
|
net_io = psutil.net_io_counters()
|
||||||
|
|
||||||
# Get total paths
|
# Get total paths
|
||||||
path_table = self.reticulum.get_path_table()
|
total_paths = 0
|
||||||
total_paths = len(path_table)
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
try:
|
||||||
|
path_table = self.reticulum.get_path_table()
|
||||||
|
total_paths = len(path_table)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Calculate announce rates
|
# Calculate announce rates
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
@@ -2750,9 +2968,23 @@ class ReticulumMeshChat:
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"reticulum_config_path": self.reticulum.configpath,
|
"reticulum_config_path": (
|
||||||
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
getattr(self.reticulum, "configpath", None)
|
||||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"is_connected_to_shared_instance": (
|
||||||
|
getattr(
|
||||||
|
self.reticulum, "is_connected_to_shared_instance", False
|
||||||
|
)
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else False
|
||||||
|
),
|
||||||
|
"is_transport_enabled": (
|
||||||
|
self.reticulum.transport_enabled()
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else False
|
||||||
|
),
|
||||||
"memory_usage": {
|
"memory_usage": {
|
||||||
"rss": memory_info.rss, # Resident Set Size (bytes)
|
"rss": memory_info.rss, # Resident Set Size (bytes)
|
||||||
"vms": memory_info.vms, # Virtual Memory Size (bytes)
|
"vms": memory_info.vms, # Virtual Memory Size (bytes)
|
||||||
@@ -2769,6 +3001,8 @@ class ReticulumMeshChat:
|
|||||||
"announces_per_minute": announces_per_minute,
|
"announces_per_minute": announces_per_minute,
|
||||||
"announces_per_hour": announces_per_hour,
|
"announces_per_hour": announces_per_hour,
|
||||||
},
|
},
|
||||||
|
"is_reticulum_running": hasattr(self, "reticulum")
|
||||||
|
and self.reticulum is not None,
|
||||||
"download_stats": {
|
"download_stats": {
|
||||||
"avg_download_speed_bps": avg_download_speed_bps,
|
"avg_download_speed_bps": avg_download_speed_bps,
|
||||||
},
|
},
|
||||||
@@ -3176,6 +3410,17 @@ class ReticulumMeshChat:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@routes.post("/api/v1/reticulum/reload")
|
||||||
|
async def reticulum_reload(request):
|
||||||
|
success = await self.reload_reticulum()
|
||||||
|
if success:
|
||||||
|
return web.json_response({"message": "Reticulum reloaded successfully"})
|
||||||
|
else:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Failed to reload Reticulum"},
|
||||||
|
status=500,
|
||||||
|
)
|
||||||
|
|
||||||
# serve telephone status
|
# serve telephone status
|
||||||
@routes.get("/api/v1/telephone/status")
|
@routes.get("/api/v1/telephone/status")
|
||||||
async def telephone_status(request):
|
async def telephone_status(request):
|
||||||
@@ -4327,7 +4572,13 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# determine next hop and hop count
|
# determine next hop and hop count
|
||||||
hops = RNS.Transport.hops_to(destination_hash)
|
hops = RNS.Transport.hops_to(destination_hash)
|
||||||
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
|
next_hop_bytes = None
|
||||||
|
next_hop_interface = None
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
|
||||||
|
next_hop_interface = self.reticulum.get_next_hop_if_name(
|
||||||
|
destination_hash
|
||||||
|
)
|
||||||
|
|
||||||
# ensure next hop provided
|
# ensure next hop provided
|
||||||
if next_hop_bytes is None:
|
if next_hop_bytes is None:
|
||||||
@@ -4338,7 +4589,11 @@ class ReticulumMeshChat:
|
|||||||
)
|
)
|
||||||
|
|
||||||
next_hop = next_hop_bytes.hex()
|
next_hop = next_hop_bytes.hex()
|
||||||
next_hop_interface = self.reticulum.get_next_hop_if_name(destination_hash)
|
next_hop_interface = (
|
||||||
|
self.reticulum.get_next_hop_if_name(destination_hash)
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
@@ -4360,7 +4615,8 @@ class ReticulumMeshChat:
|
|||||||
destination_hash = bytes.fromhex(destination_hash)
|
destination_hash = bytes.fromhex(destination_hash)
|
||||||
|
|
||||||
# drop path
|
# drop path
|
||||||
self.reticulum.drop_path(destination_hash)
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.drop_path(destination_hash)
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
@@ -4519,17 +4775,17 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# get rssi
|
# get rssi
|
||||||
rssi = receipt.proof_packet.rssi
|
rssi = receipt.proof_packet.rssi
|
||||||
if rssi is None:
|
if rssi is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
rssi = self.reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
rssi = self.reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
||||||
|
|
||||||
# get snr
|
# get snr
|
||||||
snr = receipt.proof_packet.snr
|
snr = receipt.proof_packet.snr
|
||||||
if snr is None:
|
if snr is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
snr = self.reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
snr = self.reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
||||||
|
|
||||||
# get signal quality
|
# get signal quality
|
||||||
quality = receipt.proof_packet.q
|
quality = receipt.proof_packet.q
|
||||||
if quality is None:
|
if quality is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
quality = self.reticulum.get_packet_q(receipt.proof_packet.packet_hash)
|
quality = self.reticulum.get_packet_q(receipt.proof_packet.packet_hash)
|
||||||
|
|
||||||
# get and format round trip time
|
# get and format round trip time
|
||||||
@@ -4899,39 +5155,48 @@ class ReticulumMeshChat:
|
|||||||
@routes.get("/api/v1/interface-stats")
|
@routes.get("/api/v1/interface-stats")
|
||||||
async def interface_stats(request):
|
async def interface_stats(request):
|
||||||
# get interface stats
|
# get interface stats
|
||||||
interface_stats = self.reticulum.get_interface_stats()
|
interface_stats = {"interfaces": []}
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
try:
|
||||||
|
interface_stats = self.reticulum.get_interface_stats()
|
||||||
|
|
||||||
# ensure transport_id is hex as json_response can't serialize bytes
|
# ensure transport_id is hex as json_response can't serialize bytes
|
||||||
if "transport_id" in interface_stats:
|
if "transport_id" in interface_stats:
|
||||||
interface_stats["transport_id"] = interface_stats["transport_id"].hex()
|
interface_stats["transport_id"] = interface_stats[
|
||||||
|
"transport_id"
|
||||||
|
].hex()
|
||||||
|
|
||||||
# ensure probe_responder is hex as json_response can't serialize bytes
|
# ensure probe_responder is hex as json_response can't serialize bytes
|
||||||
if (
|
if (
|
||||||
"probe_responder" in interface_stats
|
"probe_responder" in interface_stats
|
||||||
and interface_stats["probe_responder"] is not None
|
and interface_stats["probe_responder"] is not None
|
||||||
):
|
):
|
||||||
interface_stats["probe_responder"] = interface_stats[
|
interface_stats["probe_responder"] = interface_stats[
|
||||||
"probe_responder"
|
"probe_responder"
|
||||||
].hex()
|
].hex()
|
||||||
|
|
||||||
# ensure ifac_signature is hex as json_response can't serialize bytes
|
# ensure ifac_signature is hex as json_response can't serialize bytes
|
||||||
for interface in interface_stats["interfaces"]:
|
for interface in interface_stats["interfaces"]:
|
||||||
if "short_name" in interface:
|
if "short_name" in interface:
|
||||||
interface["interface_name"] = interface["short_name"]
|
interface["interface_name"] = interface["short_name"]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"parent_interface_name" in interface
|
"parent_interface_name" in interface
|
||||||
and interface["parent_interface_name"] is not None
|
and interface["parent_interface_name"] is not None
|
||||||
):
|
):
|
||||||
interface["parent_interface_hash"] = interface[
|
interface["parent_interface_hash"] = interface[
|
||||||
"parent_interface_hash"
|
"parent_interface_hash"
|
||||||
].hex()
|
].hex()
|
||||||
|
|
||||||
if interface.get("ifac_signature"):
|
if interface.get("ifac_signature"):
|
||||||
interface["ifac_signature"] = interface["ifac_signature"].hex()
|
interface["ifac_signature"] = interface[
|
||||||
|
"ifac_signature"
|
||||||
|
].hex()
|
||||||
|
|
||||||
if interface.get("hash"):
|
if interface.get("hash"):
|
||||||
interface["hash"] = interface["hash"].hex()
|
interface["hash"] = interface["hash"].hex()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
@@ -4946,7 +5211,13 @@ class ReticulumMeshChat:
|
|||||||
offset = request.query.get("offset", None)
|
offset = request.query.get("offset", None)
|
||||||
|
|
||||||
# get path table, making sure hash and via are in hex as json_response can't serialize bytes
|
# get path table, making sure hash and via are in hex as json_response can't serialize bytes
|
||||||
all_paths = self.reticulum.get_path_table()
|
all_paths = []
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
try:
|
||||||
|
all_paths = self.reticulum.get_path_table()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
total_count = len(all_paths)
|
total_count = len(all_paths)
|
||||||
|
|
||||||
# apply pagination if requested
|
# apply pagination if requested
|
||||||
@@ -5665,7 +5936,8 @@ class ReticulumMeshChat:
|
|||||||
self.database.misc.add_blocked_destination(destination_hash)
|
self.database.misc.add_blocked_destination(destination_hash)
|
||||||
# drop any existing paths to this destination
|
# drop any existing paths to this destination
|
||||||
try:
|
try:
|
||||||
self.reticulum.drop_path(bytes.fromhex(destination_hash))
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.drop_path(bytes.fromhex(destination_hash))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to drop path for blocked destination: {e}")
|
print(f"Failed to drop path for blocked destination: {e}")
|
||||||
return web.json_response({"message": "ok"})
|
return web.json_response({"message": "ok"})
|
||||||
@@ -7121,7 +7393,11 @@ class ReticulumMeshChat:
|
|||||||
"telephone_address_hash": self.telephone_manager.telephone.destination.hexhash
|
"telephone_address_hash": self.telephone_manager.telephone.destination.hexhash
|
||||||
if self.telephone_manager.telephone
|
if self.telephone_manager.telephone
|
||||||
else None,
|
else None,
|
||||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
"is_transport_enabled": (
|
||||||
|
self.reticulum.transport_enabled()
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else False
|
||||||
|
),
|
||||||
"auto_announce_enabled": self.config.auto_announce_enabled.get(),
|
"auto_announce_enabled": self.config.auto_announce_enabled.get(),
|
||||||
"auto_announce_interval_seconds": self.config.auto_announce_interval_seconds.get(),
|
"auto_announce_interval_seconds": self.config.auto_announce_interval_seconds.get(),
|
||||||
"last_announced_at": self.config.last_announced_at.get(),
|
"last_announced_at": self.config.last_announced_at.get(),
|
||||||
@@ -7346,17 +7622,17 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# get rssi
|
# get rssi
|
||||||
rssi = lxmf_message.rssi
|
rssi = lxmf_message.rssi
|
||||||
if rssi is None:
|
if rssi is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
rssi = self.reticulum.get_packet_rssi(lxmf_message.hash)
|
rssi = self.reticulum.get_packet_rssi(lxmf_message.hash)
|
||||||
|
|
||||||
# get snr
|
# get snr
|
||||||
snr = lxmf_message.snr
|
snr = lxmf_message.snr
|
||||||
if snr is None:
|
if snr is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
snr = self.reticulum.get_packet_snr(lxmf_message.hash)
|
snr = self.reticulum.get_packet_snr(lxmf_message.hash)
|
||||||
|
|
||||||
# get quality
|
# get quality
|
||||||
quality = lxmf_message.q
|
quality = lxmf_message.q
|
||||||
if quality is None:
|
if quality is None and hasattr(self, "reticulum") and self.reticulum:
|
||||||
quality = self.reticulum.get_packet_q(lxmf_message.hash)
|
quality = self.reticulum.get_packet_q(lxmf_message.hash)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -7792,9 +8068,15 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# physical link info
|
# physical link info
|
||||||
physical_link = {
|
physical_link = {
|
||||||
"rssi": self.reticulum.get_packet_rssi(lxmf_message.hash),
|
"rssi": self.reticulum.get_packet_rssi(lxmf_message.hash)
|
||||||
"snr": self.reticulum.get_packet_snr(lxmf_message.hash),
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
"q": self.reticulum.get_packet_q(lxmf_message.hash),
|
else None,
|
||||||
|
"snr": self.reticulum.get_packet_snr(lxmf_message.hash)
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else None,
|
||||||
|
"q": self.reticulum.get_packet_q(lxmf_message.hash)
|
||||||
|
if hasattr(self, "reticulum") and self.reticulum
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.database.telemetry.upsert_telemetry(
|
self.database.telemetry.upsert_telemetry(
|
||||||
@@ -8315,7 +8597,8 @@ class ReticulumMeshChat:
|
|||||||
identity_hash = announced_identity.hash.hex()
|
identity_hash = announced_identity.hash.hex()
|
||||||
if self.is_destination_blocked(identity_hash):
|
if self.is_destination_blocked(identity_hash):
|
||||||
print(f"Dropping telephone announce from blocked source: {identity_hash}")
|
print(f"Dropping telephone announce from blocked source: {identity_hash}")
|
||||||
self.reticulum.drop_path(destination_hash)
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.drop_path(destination_hash)
|
||||||
return
|
return
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
@@ -8369,7 +8652,8 @@ class ReticulumMeshChat:
|
|||||||
identity_hash = announced_identity.hash.hex()
|
identity_hash = announced_identity.hash.hex()
|
||||||
if self.is_destination_blocked(identity_hash):
|
if self.is_destination_blocked(identity_hash):
|
||||||
print(f"Dropping announce from blocked source: {identity_hash}")
|
print(f"Dropping announce from blocked source: {identity_hash}")
|
||||||
self.reticulum.drop_path(destination_hash)
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.drop_path(destination_hash)
|
||||||
return
|
return
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
@@ -8556,7 +8840,8 @@ class ReticulumMeshChat:
|
|||||||
identity_hash = announced_identity.hash.hex()
|
identity_hash = announced_identity.hash.hex()
|
||||||
if self.is_destination_blocked(identity_hash):
|
if self.is_destination_blocked(identity_hash):
|
||||||
print(f"Dropping announce from blocked source: {identity_hash}")
|
print(f"Dropping announce from blocked source: {identity_hash}")
|
||||||
self.reticulum.drop_path(destination_hash)
|
if hasattr(self, "reticulum") and self.reticulum:
|
||||||
|
self.reticulum.drop_path(destination_hash)
|
||||||
return
|
return
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
@@ -8994,40 +9279,50 @@ class NomadnetFileDownloader(NomadnetDownloader):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
# parse command line args
|
# parse command line args
|
||||||
|
def env_bool(env_name, default=False):
|
||||||
|
val = os.environ.get(env_name)
|
||||||
|
if val is None:
|
||||||
|
return default
|
||||||
|
return val.lower() in ("true", "1", "yes", "on")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="ReticulumMeshChat")
|
parser = argparse.ArgumentParser(description="ReticulumMeshChat")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host",
|
"--host",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
default="127.0.0.1",
|
default=os.environ.get("MESHCHAT_HOST", "127.0.0.1"),
|
||||||
type=str,
|
type=str,
|
||||||
help="The address the web server should listen on.",
|
help="The address the web server should listen on. Can also be set via MESHCHAT_HOST environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port",
|
"--port",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
default="8000",
|
default=int(os.environ.get("MESHCHAT_PORT", "8000")),
|
||||||
type=int,
|
type=int,
|
||||||
help="The port the web server should listen on.",
|
help="The port the web server should listen on. Can also be set via MESHCHAT_PORT environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--headless",
|
"--headless",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Web browser will not automatically launch when this flag is passed.",
|
default=env_bool("MESHCHAT_HEADLESS", False),
|
||||||
|
help="Web browser will not automatically launch when this flag is passed. Can also be set via MESHCHAT_HEADLESS environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--identity-file",
|
"--identity-file",
|
||||||
type=str,
|
type=str,
|
||||||
help="Path to a Reticulum Identity file to use as your LXMF address.",
|
default=os.environ.get("MESHCHAT_IDENTITY_FILE"),
|
||||||
|
help="Path to a Reticulum Identity file to use as your LXMF address. Can also be set via MESHCHAT_IDENTITY_FILE environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--identity-base64",
|
"--identity-base64",
|
||||||
type=str,
|
type=str,
|
||||||
help="A base64 encoded Reticulum Identity to use as your LXMF address.",
|
default=os.environ.get("MESHCHAT_IDENTITY_BASE64"),
|
||||||
|
help="A base64 encoded Reticulum Identity to use as your LXMF address. Can also be set via MESHCHAT_IDENTITY_BASE64 environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--identity-base32",
|
"--identity-base32",
|
||||||
type=str,
|
type=str,
|
||||||
help="A base32 encoded Reticulum Identity to use as your LXMF address.",
|
default=os.environ.get("MESHCHAT_IDENTITY_BASE32"),
|
||||||
|
help="A base32 encoded Reticulum Identity to use as your LXMF address. Can also be set via MESHCHAT_IDENTITY_BASE32 environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--generate-identity-file",
|
"--generate-identity-file",
|
||||||
@@ -9042,17 +9337,20 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--auto-recover",
|
"--auto-recover",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Attempt to automatically recover the SQLite database on startup before serving the app.",
|
default=env_bool("MESHCHAT_AUTO_RECOVER", False),
|
||||||
|
help="Attempt to automatically recover the SQLite database on startup before serving the app. Can also be set via MESHCHAT_AUTO_RECOVER environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--auth",
|
"--auth",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enable basic authentication for the web interface.",
|
default=env_bool("MESHCHAT_AUTH", False),
|
||||||
|
help="Enable basic authentication for the web interface. Can also be set via MESHCHAT_AUTH environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-https",
|
"--no-https",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Disable HTTPS and use HTTP instead.",
|
default=env_bool("MESHCHAT_NO_HTTPS", False),
|
||||||
|
help="Disable HTTPS and use HTTP instead. Can also be set via MESHCHAT_NO_HTTPS environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--backup-db",
|
"--backup-db",
|
||||||
@@ -9067,12 +9365,14 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--reticulum-config-dir",
|
"--reticulum-config-dir",
|
||||||
type=str,
|
type=str,
|
||||||
help="Path to a Reticulum config directory for the RNS stack to use (e.g: ~/.reticulum)",
|
default=os.environ.get("MESHCHAT_RETICULUM_CONFIG_DIR"),
|
||||||
|
help="Path to a Reticulum config directory for the RNS stack to use (e.g: ~/.reticulum). Can also be set via MESHCHAT_RETICULUM_CONFIG_DIR environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--storage-dir",
|
"--storage-dir",
|
||||||
type=str,
|
type=str,
|
||||||
help="Path to a directory for storing databases and config files (default: ./storage)",
|
default=os.environ.get("MESHCHAT_STORAGE_DIR"),
|
||||||
|
help="Path to a directory for storing databases and config files (default: ./storage). Can also be set via MESHCHAT_STORAGE_DIR environment variable.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--test-exception-message",
|
"--test-exception-message",
|
||||||
|
|||||||
Reference in New Issue
Block a user