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.
|
apply on reload.
|
||||||
- The identity is stored in `config/reticulum/identity` — back it up to keep the
|
- The identity is stored in `config/reticulum/identity` — back it up to keep the
|
||||||
same address.
|
same address.
|
||||||
- RNS is a standalone-daemon-style library. To embed it cleanly the integration
|
- RNS and LXMF are standalone-daemon-style libraries — both install
|
||||||
suppresses RNS's own signal handlers (so Home Assistant keeps ownership of
|
SIGINT/SIGTERM handlers and `atexit` persistence in their constructors. To
|
||||||
SIGINT/SIGTERM — important under Kubernetes) and unregisters its blocking
|
embed them cleanly the integration suppresses those signal handlers during
|
||||||
`atexit` persistence, persisting RNS/LXMF state itself, off the event loop,
|
init (so Home Assistant keeps ownership of SIGINT/SIGTERM — important under
|
||||||
on the `homeassistant_stop` event. RNS still spawns a couple of non-daemon
|
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
|
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
|
a short delay before the process exits (within the pod's termination grace
|
||||||
period).
|
period).
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
"loggers": ["RNS", "LXMF"],
|
"loggers": ["RNS", "LXMF"],
|
||||||
"requirements": ["rns>=0.9.0", "lxmf>=0.6.0"],
|
"requirements": ["rns>=0.9.0", "lxmf>=0.6.0"],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.0.2"
|
"version": "1.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ _LOCAL_DESTINATION: Any = None
|
|||||||
_IDENTITY: Any = None
|
_IDENTITY: Any = None
|
||||||
_ANNOUNCE_HANDLER: 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
|
# How long (seconds) to wait for a path/identity to resolve before giving up on
|
||||||
# an outbound message.
|
# an outbound message.
|
||||||
PATH_RESOLVE_TIMEOUT = 15.0
|
PATH_RESOLVE_TIMEOUT = 15.0
|
||||||
@@ -148,7 +153,27 @@ class ReticulumManager:
|
|||||||
self._push_state()
|
self._push_state()
|
||||||
|
|
||||||
def _start(self) -> None:
|
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 _RNS_INSTANCE, _LXM_ROUTER, _LOCAL_DESTINATION # noqa: PLW0603
|
||||||
global _IDENTITY, _ANNOUNCE_HANDLER # noqa: PLW0603
|
global _IDENTITY, _ANNOUNCE_HANDLER # noqa: PLW0603
|
||||||
import LXMF # noqa: PLC0415
|
import LXMF # noqa: PLC0415
|
||||||
@@ -247,18 +272,7 @@ class ReticulumManager:
|
|||||||
handle.write(contents)
|
handle.write(contents)
|
||||||
|
|
||||||
def _create_rns_instance(self, rns: Any) -> Any:
|
def _create_rns_instance(self, rns: Any) -> Any:
|
||||||
"""Create the RNS instance from an executor thread.
|
"""Create the RNS instance (called with signal handlers suppressed)."""
|
||||||
|
|
||||||
``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]
|
|
||||||
try:
|
try:
|
||||||
return rns.Reticulum(
|
return rns.Reticulum(
|
||||||
configdir=self.storage_dir, loglevel=self.loglevel
|
configdir=self.storage_dir, loglevel=self.loglevel
|
||||||
@@ -274,8 +288,6 @@ class ReticulumManager:
|
|||||||
raise
|
raise
|
||||||
_LOGGER.debug("Adopted existing Reticulum instance after %s", err)
|
_LOGGER.debug("Adopted existing Reticulum instance after %s", err)
|
||||||
return adopted
|
return adopted
|
||||||
finally:
|
|
||||||
signal.signal = saved_signal # type: ignore[assignment]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _tame_exit_handler(obj: Any) -> None:
|
def _tame_exit_handler(obj: Any) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user