diff --git a/meshtastic/tcp_interface.py b/meshtastic/tcp_interface.py index 5d27929..efa0950 100644 --- a/meshtastic/tcp_interface.py +++ b/meshtastic/tcp_interface.py @@ -86,18 +86,19 @@ class TCPInterface(StreamInterface): def close(self) -> None: """Close a connection to the device.""" logger.debug("Closing TCP stream") - super().close() # Sometimes the socket read might be blocked in the reader thread. - # Therefore we force the shutdown by closing the socket here + # Therefore force a shutdown first to unblock reader thread reads. self._wantExit = True if self.socket is not None: with contextlib.suppress( Exception ): # Ignore errors in shutdown, because we might have a race with the server self._socket_shutdown() - self.socket.close() + with contextlib.suppress(Exception): + self.socket.close() self.socket = None + super().close() def _writeBytes(self, b: bytes) -> None: """Write an array of bytes to our stream""" @@ -134,7 +135,8 @@ class TCPInterface(StreamInterface): with contextlib.suppress(Exception): self._socket_shutdown() - self.socket.close() + if self.socket is not None: + self.socket.close() self.socket = None time.sleep(1) self.myConnect() diff --git a/meshtastic/tests/test_tcp_interface.py b/meshtastic/tests/test_tcp_interface.py index e123833..60bee57 100644 --- a/meshtastic/tests/test_tcp_interface.py +++ b/meshtastic/tests/test_tcp_interface.py @@ -1,7 +1,7 @@ """Meshtastic unit tests for tcp_interface.py""" import re -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -56,6 +56,28 @@ def test_TCPInterface_without_connecting(): assert iface.socket is None +@pytest.mark.unit +def test_TCPInterface_close_shutdowns_socket_before_super_close(): + """Close should unblock socket reads before waiting on StreamInterface.close().""" + iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False) + sock = MagicMock() + iface.socket = sock + call_order = [] + + with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown: + with patch( + "meshtastic.stream_interface.StreamInterface.close", autospec=True + ) as mock_super_close: + mock_shutdown.side_effect = lambda _self: call_order.append("shutdown") + mock_super_close.side_effect = lambda _self: call_order.append("super_close") + + iface.close() + + assert call_order == ["shutdown", "super_close"] + sock.close.assert_called_once() + assert iface.socket is None + + @pytest.mark.unit def test_TCPInterface_reconnect(): """Test that _reconnect correctly reconnects"""