diff --git a/at_client/connections/atconnection.py b/at_client/connections/atconnection.py index 1c41125..173fcdd 100644 --- a/at_client/connections/atconnection.py +++ b/at_client/connections/atconnection.py @@ -98,8 +98,16 @@ def disconnect(self): """ Close the socket connection. """ - self._secure_root_socket.close() - self._connected = False + # Always clear _connected, even if close() fails on an already-broken socket + # (e.g. "Bad file descriptor"). Otherwise _connected stays True and the monitor + # restart path (guarded by `if not self._connected`) never rebuilds the socket, + # so the client silently stops receiving notifications until it is recreated. + try: + self._secure_root_socket.close() + except Exception: + pass + finally: + self._connected = False @abstractmethod def parse_raw_response(self, raw_response:str) -> Response: diff --git a/test/disconnect_test.py b/test/disconnect_test.py new file mode 100644 index 0000000..4cebb6e --- /dev/null +++ b/test/disconnect_test.py @@ -0,0 +1,35 @@ +import unittest +from unittest.mock import MagicMock + +from at_client.connections.atconnection import AtConnection + + +class _ConcreteConnection(AtConnection): + """Minimal concrete subclass so we can exercise the base disconnect().""" + + def parse_raw_response(self, raw_response): + return None + + +class DisconnectTest(unittest.TestCase): + """disconnect() must always clear _connected, even if the socket close fails.""" + + def test_disconnect_clears_connected_on_clean_close(self): + conn = _ConcreteConnection.__new__(_ConcreteConnection) # bypass network __init__ + conn._connected = True + conn._secure_root_socket = MagicMock() + conn.disconnect() + conn._secure_root_socket.close.assert_called_once() + self.assertFalse(conn._connected) + + def test_disconnect_clears_connected_even_when_close_raises(self): + conn = _ConcreteConnection.__new__(_ConcreteConnection) + conn._connected = True + conn._secure_root_socket = MagicMock() + conn._secure_root_socket.close.side_effect = OSError(9, "Bad file descriptor") + conn.disconnect() # must not propagate + self.assertFalse(conn._connected) # so the restart path can rebuild the socket + + +if __name__ == "__main__": + unittest.main()