Suppress signal handlers for the whole init (LXMF.LXMRouter too)
v1.0.2 only neutralised signal.signal around RNS.Reticulum(), but LXMF.LXMRouter.__init__ also calls signal.signal(SIGINT/SIGTERM) (LXMRouter.py 308-309), so init still crashed with "signal only works in main thread of the main interpreter" when the router was constructed in the executor thread. Wrap the entire _start init (both constructors) with signal.signal neutralised instead of just the RNS constructor. Confirmed via upstream source that the only relevant signal registration sites are RNS/Reticulum.py:366-367 and LXMF/LXMRouter.py:308-309, both module-attribute calls, so the module-level patch intercepts both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
11
README.md
11
README.md
@@ -214,11 +214,12 @@ action:
|
||||
apply on reload.
|
||||
- The identity is stored in `config/reticulum/identity` — back it up to keep the
|
||||
same address.
|
||||
- RNS is a standalone-daemon-style library. To embed it cleanly the integration
|
||||
suppresses RNS's own signal handlers (so Home Assistant keeps ownership of
|
||||
SIGINT/SIGTERM — important under Kubernetes) and unregisters its blocking
|
||||
`atexit` persistence, persisting RNS/LXMF state itself, off the event loop,
|
||||
on the `homeassistant_stop` event. RNS still spawns a couple of non-daemon
|
||||
- RNS and LXMF are standalone-daemon-style libraries — both install
|
||||
SIGINT/SIGTERM handlers and `atexit` persistence in their constructors. To
|
||||
embed them cleanly the integration suppresses those signal handlers during
|
||||
init (so Home Assistant keeps ownership of SIGINT/SIGTERM — important under
|
||||
Kubernetes) and unregisters their blocking `atexit` persistence, persisting
|
||||
RNS/LXMF state itself, off the event loop, on the `homeassistant_stop` event. RNS still spawns a couple of non-daemon
|
||||
worker threads, so on shutdown you may see a "non-daemonic threads" notice and
|
||||
a short delay before the process exits (within the pod's termination grace
|
||||
period).
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"loggers": ["RNS", "LXMF"],
|
||||
"requirements": ["rns>=0.9.0", "lxmf>=0.6.0"],
|
||||
"single_config_entry": true,
|
||||
"version": "1.0.2"
|
||||
"version": "1.0.3"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ _LOCAL_DESTINATION: Any = None
|
||||
_IDENTITY: Any = None
|
||||
_ANNOUNCE_HANDLER: Any = None
|
||||
|
||||
|
||||
def _noop_signal(*_args: Any, **_kwargs: Any) -> None:
|
||||
"""Drop-in for ``signal.signal`` used while initialising off the main thread."""
|
||||
return None
|
||||
|
||||
# How long (seconds) to wait for a path/identity to resolve before giving up on
|
||||
# an outbound message.
|
||||
PATH_RESOLVE_TIMEOUT = 15.0
|
||||
@@ -148,7 +153,27 @@ class ReticulumManager:
|
||||
self._push_state()
|
||||
|
||||
def _start(self) -> None:
|
||||
"""Blocking init. Runs in the executor thread."""
|
||||
"""Blocking init wrapper (executor thread).
|
||||
|
||||
Both ``RNS.Reticulum()`` and ``LXMF.LXMRouter()`` call
|
||||
``signal.signal()`` in their constructors, which only works on the main
|
||||
thread. We run init in an executor thread, so neutralise ``signal.signal``
|
||||
for the whole init. As a bonus this stops RNS/LXMF from hijacking Home
|
||||
Assistant's own SIGINT/SIGTERM handling (needed for clean shutdown,
|
||||
especially under Kubernetes).
|
||||
"""
|
||||
patched = threading.current_thread() is not threading.main_thread()
|
||||
saved_signal = signal.signal
|
||||
if patched:
|
||||
signal.signal = _noop_signal # type: ignore[assignment]
|
||||
try:
|
||||
self._start_impl()
|
||||
finally:
|
||||
if patched:
|
||||
signal.signal = saved_signal # type: ignore[assignment]
|
||||
|
||||
def _start_impl(self) -> None:
|
||||
"""Blocking init. Runs in the executor thread (signals suppressed)."""
|
||||
global _RNS_INSTANCE, _LXM_ROUTER, _LOCAL_DESTINATION # noqa: PLW0603
|
||||
global _IDENTITY, _ANNOUNCE_HANDLER # noqa: PLW0603
|
||||
import LXMF # noqa: PLC0415
|
||||
@@ -247,18 +272,7 @@ class ReticulumManager:
|
||||
handle.write(contents)
|
||||
|
||||
def _create_rns_instance(self, rns: Any) -> Any:
|
||||
"""Create the RNS instance from an executor thread.
|
||||
|
||||
``Reticulum.__init__`` calls ``signal.signal()`` with no main-thread
|
||||
guard, but signal handlers can only be installed from the main thread.
|
||||
Since we (correctly) initialise off the event loop, we temporarily
|
||||
neutralise ``signal.signal`` so init completes — and as a bonus this
|
||||
stops RNS from hijacking Home Assistant's own SIGINT/SIGTERM handling,
|
||||
which HA needs for clean shutdown (important under Kubernetes).
|
||||
"""
|
||||
saved_signal = signal.signal
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
signal.signal = lambda *args, **kwargs: None # type: ignore[assignment]
|
||||
"""Create the RNS instance (called with signal handlers suppressed)."""
|
||||
try:
|
||||
return rns.Reticulum(
|
||||
configdir=self.storage_dir, loglevel=self.loglevel
|
||||
@@ -274,8 +288,6 @@ class ReticulumManager:
|
||||
raise
|
||||
_LOGGER.debug("Adopted existing Reticulum instance after %s", err)
|
||||
return adopted
|
||||
finally:
|
||||
signal.signal = saved_signal # type: ignore[assignment]
|
||||
|
||||
@staticmethod
|
||||
def _tame_exit_handler(obj: Any) -> None:
|
||||
|
||||
Reference in New Issue
Block a user