Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions pymodbus/client/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,18 @@ def send(self, request: bytes, addr: tuple | None = None) -> int:
if not self.socket:
raise ConnectionException(str(self))
if request:
if waitingbytes := self._in_waiting():
result = self.socket.read(waitingbytes)
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
if (size := self.socket.write(request)) is None: # pragma: no cover
size = 0
return size
try:
if waitingbytes := self._in_waiting():
result = self.socket.read(waitingbytes)
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
if (size := self.socket.write(request)) is None: # pragma: no cover
size = 0
return size
except (BlockingIOError, InterruptedError):
raise
except OSError:
self.close()
raise ConnectionException(str(self)) from None
return 0

def _wait_for_data(self) -> int:
Expand All @@ -296,12 +302,17 @@ def recv(self, size: int | None) -> bytes:
"""Read data from the underlying descriptor."""
if not self.socket:
raise ConnectionException(str(self))
if size is None:
size = self._wait_for_data()
if size > self._in_waiting():
self._wait_for_data()
result = self.socket.read(size)
return result
try:
if size is None:
size = self._wait_for_data()
if size > self._in_waiting():
self._wait_for_data()
return self.socket.read(size)
except (BlockingIOError, InterruptedError):
raise
except OSError:
self.close()
raise ConnectionException(str(self)) from None

def is_socket_open(self) -> bool:
"""Check if socket is open."""
Expand Down
52 changes: 52 additions & 0 deletions test/client/test_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,32 @@ def test_serial_client_cleanup_buffer_before_send(self, mock_serial):
assert not client.send(b"")
assert client.send(b"1234") == 4

def test_serial_client_send_drops_socket_on_os_error(self):
"""Test that a port the OS tore down is not left in place as connected."""
client = ModbusSerialClient("/dev/null")
mock_socket = mock.MagicMock()
mock_socket.in_waiting = 0
mock_socket.write.side_effect = OSError(5, "Input/output error")
client.socket = mock_socket
with pytest.raises(ConnectionException):
client.send(b"1234")
assert not client.connected
assert client.socket is None

def test_serial_client_send_keeps_socket_on_transient_error(self):
"""Test that a transient write error leaves a healthy port in place."""
client = ModbusSerialClient("/dev/null")
mock_socket = mock.MagicMock()
mock_socket.in_waiting = 0
mock_socket.write.side_effect = BlockingIOError(
11, "Resource temporarily unavailable"
)
client.socket = mock_socket
with pytest.raises(BlockingIOError):
client.send(b"1234")
assert client.connected
assert client.socket is mock_socket

def test_serial_client_recv(self):
"""Test the serial client receive method."""
client = ModbusSerialClient("/dev/null")
Expand All @@ -409,6 +435,32 @@ def test_serial_client_recv(self):
assert client.recv(None) == b""
assert client.recv(0) == b""

def test_serial_client_recv_drops_socket_on_os_error(self):
"""Test that a read against a torn-down port drops it rather than escaping."""
client = ModbusSerialClient("/dev/null")
mock_socket = mock.MagicMock()
mock_socket.in_waiting = 10
mock_socket.read.side_effect = OSError(5, "Input/output error")
client.socket = mock_socket
with pytest.raises(ConnectionException):
client.recv(4)
assert not client.connected
assert client.socket is None

def test_serial_client_recv_keeps_socket_on_transient_error(self):
"""Test that a transient read error leaves a healthy port in place."""
client = ModbusSerialClient("/dev/null")
mock_socket = mock.MagicMock()
mock_socket.in_waiting = 10
mock_socket.read.side_effect = BlockingIOError(
11, "Resource temporarily unavailable"
)
client.socket = mock_socket
with pytest.raises(BlockingIOError):
client.recv(4)
assert client.connected
assert client.socket is mock_socket

def test_serial_client_recv_split(self):
"""Test the serial client receive method."""
client = ModbusSerialClient("/dev/null")
Expand Down