mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-07-03 08:22:30 +02:00
Enhance configuration management and companion node handling
- Added backup functionality for config.yaml during save operations. - Improved YAML saving to support Unicode characters and increased line width. - Introduced a mechanism to sync companion node names to the configuration upon updates. - Updated API endpoints to validate companion node names and persist changes to the configuration. - Enhanced the RepeaterCompanionBridge to load and save preferences, ensuring consistent state management.
This commit is contained in:
@@ -52,6 +52,7 @@ htmlcov/
|
||||
|
||||
# Config
|
||||
config.yaml
|
||||
config.yaml.backup
|
||||
identity.json
|
||||
|
||||
# Data
|
||||
|
||||
@@ -53,9 +53,11 @@ class RepeaterCompanionBridge(CompanionBridge):
|
||||
*,
|
||||
sqlite_handler=None,
|
||||
companion_hash: str = "",
|
||||
on_prefs_saved: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
self._sqlite_handler = sqlite_handler
|
||||
self._companion_hash = companion_hash
|
||||
self._on_prefs_saved = on_prefs_saved
|
||||
super().__init__(
|
||||
identity=identity,
|
||||
packet_injector=packet_injector,
|
||||
@@ -68,6 +70,8 @@ class RepeaterCompanionBridge(CompanionBridge):
|
||||
authenticate_callback=authenticate_callback,
|
||||
initial_contacts=initial_contacts,
|
||||
)
|
||||
# Load persisted prefs (e.g. node_name) from SQLite so matching uses last-saved name
|
||||
self._load_prefs()
|
||||
|
||||
def _save_prefs(self) -> None:
|
||||
"""Persist full NodePrefs as JSON to SQLite."""
|
||||
@@ -79,6 +83,11 @@ class RepeaterCompanionBridge(CompanionBridge):
|
||||
self._sqlite_handler.companion_save_prefs(
|
||||
str(self._companion_hash), prefs_safe
|
||||
)
|
||||
if self._on_prefs_saved:
|
||||
try:
|
||||
self._on_prefs_saved(self.prefs.node_name)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to sync node_name to config: %s", e)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to persist companion prefs: %s", e)
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Shared utilities for Companion (e.g. validation for config sync)."""
|
||||
|
||||
_INVALID_NODE_NAME_CHARS = "\n\r\x00"
|
||||
|
||||
|
||||
def validate_companion_node_name(value: str) -> str:
|
||||
"""Validate node_name for config sync: non-empty, max 31 bytes UTF-8, no control chars."""
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("node_name must be a string")
|
||||
s = value.strip()
|
||||
if not s:
|
||||
raise ValueError("node_name cannot be empty")
|
||||
if len(s.encode("utf-8")) > 31:
|
||||
raise ValueError("node_name too long (max 31 bytes UTF-8)")
|
||||
if any(c in s for c in _INVALID_NODE_NAME_CHARS):
|
||||
raise ValueError("node_name contains invalid characters")
|
||||
return s
|
||||
+10
-3
@@ -111,9 +111,16 @@ def save_config(config_data: Dict[str, Any], config_path: Optional[str] = None)
|
||||
config_file.rename(backup_path)
|
||||
logger.info(f"Created backup at {backup_path}")
|
||||
|
||||
# Save new config
|
||||
with open(config_path, "w") as f:
|
||||
yaml.safe_dump(config_data, f, default_flow_style=False, sort_keys=False)
|
||||
# Save new config (allow_unicode=True so emojis etc. are not escaped as \U0001F47E)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
yaml.safe_dump(
|
||||
config_data,
|
||||
f,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
width=1000000,
|
||||
)
|
||||
|
||||
logger.info(f"Saved configuration to {config_path}")
|
||||
return True
|
||||
|
||||
+22
-1
@@ -4,7 +4,8 @@ import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from repeater.config import get_radio_for_board, load_config
|
||||
from repeater.companion.utils import validate_companion_node_name
|
||||
from repeater.config import get_radio_for_board, load_config, save_config
|
||||
from repeater.config_manager import ConfigManager
|
||||
from repeater.engine import RepeaterHandler
|
||||
from repeater.handler_helpers import (
|
||||
@@ -415,6 +416,25 @@ class RepeaterDaemon:
|
||||
tcp_port = settings.get("tcp_port", 5000)
|
||||
bind_address = settings.get("bind_address", "0.0.0.0")
|
||||
|
||||
def _make_sync_node_name_to_config(companion_name: str):
|
||||
"""Return a callback that syncs node_name to config for this companion (binds name at creation)."""
|
||||
def _sync(new_node_name: str) -> None:
|
||||
try:
|
||||
validated = validate_companion_node_name(new_node_name)
|
||||
except ValueError:
|
||||
return
|
||||
companions = (self.config.get("identities") or {}).get("companions") or []
|
||||
for entry in companions:
|
||||
if entry.get("name") == companion_name:
|
||||
if "settings" not in entry:
|
||||
entry["settings"] = {}
|
||||
entry["settings"]["node_name"] = validated
|
||||
config_path = getattr(self, "config_path", None)
|
||||
if config_path:
|
||||
save_config(self.config, config_path)
|
||||
break
|
||||
return _sync
|
||||
|
||||
bridge = RepeaterCompanionBridge(
|
||||
identity=identity,
|
||||
packet_injector=self.router.inject_packet,
|
||||
@@ -422,6 +442,7 @@ class RepeaterDaemon:
|
||||
radio_config=radio_config,
|
||||
sqlite_handler=sqlite_handler,
|
||||
companion_hash=companion_hash_str,
|
||||
on_prefs_saved=_make_sync_node_name_to_config(name),
|
||||
)
|
||||
|
||||
# Load contacts from SQLite
|
||||
|
||||
@@ -159,7 +159,9 @@ class APIEndpoints:
|
||||
self.auth = AuthAPIEndpoints()
|
||||
|
||||
# Create nested companion object for /api/companion/* routes
|
||||
self.companion = CompanionAPIEndpoints(daemon_instance, event_loop, self.config)
|
||||
self.companion = CompanionAPIEndpoints(
|
||||
daemon_instance, event_loop, self.config, self.config_manager
|
||||
)
|
||||
|
||||
def _is_cors_enabled(self):
|
||||
return self.config.get("web", {}).get("cors_enabled", False)
|
||||
|
||||
@@ -16,6 +16,8 @@ from typing import Optional
|
||||
|
||||
import cherrypy
|
||||
|
||||
from repeater.companion.utils import validate_companion_node_name
|
||||
|
||||
from .auth.middleware import require_auth
|
||||
|
||||
logger = logging.getLogger("CompanionAPI")
|
||||
@@ -29,10 +31,11 @@ class CompanionAPIEndpoints:
|
||||
to the daemon's event loop via ``asyncio.run_coroutine_threadsafe``.
|
||||
"""
|
||||
|
||||
def __init__(self, daemon_instance=None, event_loop=None, config=None):
|
||||
def __init__(self, daemon_instance=None, event_loop=None, config=None, config_manager=None):
|
||||
self.daemon_instance = daemon_instance
|
||||
self.event_loop = event_loop
|
||||
self.config = config or {}
|
||||
self.config_manager = config_manager
|
||||
|
||||
# SSE clients: each gets a thread-safe queue
|
||||
self._sse_clients: list[queue.Queue] = []
|
||||
@@ -515,7 +518,34 @@ class CompanionAPIEndpoints:
|
||||
name = body.get("advert_name", body.get("name", ""))
|
||||
if not name:
|
||||
raise cherrypy.HTTPError(400, "name required")
|
||||
bridge.set_advert_name(name)
|
||||
try:
|
||||
validated_name = validate_companion_node_name(name)
|
||||
except ValueError as e:
|
||||
raise cherrypy.HTTPError(400, str(e)) from e
|
||||
bridge.set_advert_name(validated_name)
|
||||
# Optionally sync node_name to config.yaml so it survives restart
|
||||
companion_name = body.get("companion_name")
|
||||
if companion_name is None and getattr(self.daemon_instance, "identity_manager", None):
|
||||
pubkey = bridge.get_public_key()
|
||||
for reg_name, identity, _ in self.daemon_instance.identity_manager.get_identities_by_type(
|
||||
"companion"
|
||||
):
|
||||
if identity.get_public_key() == pubkey:
|
||||
companion_name = reg_name
|
||||
break
|
||||
if companion_name and self.config_manager:
|
||||
companions = (self.config.get("identities") or {}).get("companions") or []
|
||||
for entry in companions:
|
||||
if entry.get("name") == companion_name:
|
||||
if "settings" not in entry:
|
||||
entry["settings"] = {}
|
||||
entry["settings"]["node_name"] = validated_name
|
||||
try:
|
||||
if not self.config_manager.save_to_file():
|
||||
logger.warning("Failed to save config after set_advert_name")
|
||||
except Exception as e:
|
||||
logger.warning("Error saving config after set_advert_name: %s", e)
|
||||
break
|
||||
return self._success({"name": bridge.prefs.node_name})
|
||||
|
||||
@cherrypy.expose
|
||||
|
||||
Reference in New Issue
Block a user