Skip to content

Commit ce5bbcd

Browse files
committed
Fix some leaks/hangs on close: unstarted StreamInterface streams & TCP reader unblock
1 parent c2a01f8 commit ce5bbcd

4 files changed

Lines changed: 43 additions & 6 deletions

File tree

meshtastic/stream_interface.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,11 @@ def close(self) -> None:
149149
except RuntimeError:
150150
# Thread was never started — happens when close() is invoked
151151
# from a failed __init__ before connect() could spawn it.
152-
# Nothing to join; safe to ignore.
153-
pass
152+
# In this case there is no reader thread to close the stream.
153+
if self.stream is not None:
154+
with contextlib.suppress(Exception):
155+
self.stream.close()
156+
self.stream = None
154157

155158
def _handleLogByte(self, b):
156159
"""Handle a byte that is part of a log message from the device."""

meshtastic/tcp_interface.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,17 @@ def myConnect(self) -> None:
7474
def close(self) -> None:
7575
"""Close a connection to the device"""
7676
logger.debug("Closing TCP stream")
77-
super().close()
7877
# Sometimes the socket read might be blocked in the reader thread.
79-
# Therefore we force the shutdown by closing the socket here
78+
# Therefore force a shutdown first to unblock reader thread reads.
8079
self._wantExit = True
8180
if self.socket is not None:
8281
with contextlib.suppress(Exception): # Ignore errors in shutdown, because we might have a race with the server
8382
self._socket_shutdown()
84-
self.socket.close()
83+
with contextlib.suppress(Exception):
84+
self.socket.close()
8585

8686
self.socket = None
87+
super().close()
8788

8889
def _writeBytes(self, b: bytes) -> None:
8990
"""Write an array of bytes to our stream and flush"""

meshtastic/tests/test_stream_interface.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ def test_StreamInterface_close_safe_when_thread_never_started():
3333
iface.close()
3434

3535

36+
@pytest.mark.unit
37+
@pytest.mark.usefixtures("reset_mt_config")
38+
def test_StreamInterface_close_when_thread_never_started_closes_stream():
39+
"""If no reader thread was started, close() should still close the stream."""
40+
iface = StreamInterface(noProto=True, connectNow=False)
41+
stream = MagicMock()
42+
iface.stream = stream
43+
iface.close()
44+
stream.close.assert_called_once()
45+
46+
3647
@pytest.mark.unit
3748
@pytest.mark.usefixtures("reset_mt_config")
3849
def test_StreamInterface_init_cleans_up_when_connect_raises():

meshtastic/tests/test_tcp_interface.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Meshtastic unit tests for tcp_interface.py"""
22

33
import re
4-
from unittest.mock import patch
4+
from unittest.mock import MagicMock, patch
55

66
import pytest
77

@@ -54,3 +54,25 @@ def test_TCPInterface_without_connecting():
5454
with patch("socket.socket"):
5555
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
5656
assert iface.socket is None
57+
58+
59+
@pytest.mark.unit
60+
def test_TCPInterface_close_shutdowns_socket_before_super_close():
61+
"""Close should unblock socket reads before waiting on StreamInterface.close()."""
62+
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
63+
sock = MagicMock()
64+
iface.socket = sock
65+
call_order = []
66+
67+
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
68+
with patch(
69+
"meshtastic.stream_interface.StreamInterface.close", autospec=True
70+
) as mock_super_close:
71+
mock_shutdown.side_effect = lambda _self: call_order.append("shutdown")
72+
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
73+
74+
iface.close()
75+
76+
assert call_order == ["shutdown", "super_close"]
77+
sock.close.assert_called_once()
78+
assert iface.socket is None

0 commit comments

Comments
 (0)