File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff 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."""
Original file line number Diff line number Diff 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"""
Original file line number Diff line number Diff 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" )
3849def test_StreamInterface_init_cleans_up_when_connect_raises ():
Original file line number Diff line number Diff line change 11"""Meshtastic unit tests for tcp_interface.py"""
22
33import re
4- from unittest .mock import patch
4+ from unittest .mock import MagicMock , patch
55
66import 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
You can’t perform that action at this time.
0 commit comments