diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index 06ee28a3a..1e7d34038 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -1,5 +1,6 @@ """Stream Interface base class """ +import contextlib import io import logging import threading @@ -61,9 +62,20 @@ def __init__( # pylint: disable=R0917 # Start the reader thread after superclass constructor completes init if connectNow: - self.connect() - if not noProto: - self.waitForConfig() + try: + self.connect() + if not noProto: + self.waitForConfig() + except Exception: + # Handshake failed (timeout, config error, bad stream). The caller + # never receives a reference to this half-initialized instance, so + # they cannot call close() themselves. If we don't clean up here, + # the reader thread (already started by connect()) keeps running + # and the underlying stream/socket leaks — the leak compounds on + # every retry from the caller's reconnect loop. + with contextlib.suppress(Exception): + self.close() + raise def connect(self) -> None: """Connect to our radio @@ -136,7 +148,13 @@ def close(self) -> None: # reader thread to close things for us self._wantExit = True if self._rxThread != threading.current_thread(): - self._rxThread.join() # wait for it to exit + try: + self._rxThread.join() # wait for it to exit + except RuntimeError: + # Thread was never started — happens when close() is invoked + # from a failed __init__ before connect() could spawn it. + # Nothing to join; safe to ignore. + pass def _handleLogByte(self, b): """Handle a byte that is part of a log message from the device."""