Skip to content

Commit 5f5b427

Browse files
committed
Raise exception when modbus unit unexpectedly closes connection
Raise an error when the modbus unit unexpectedly closes the connection during a receive data operation without returning any data, and log a warning if it does return some but not all data before closing the connection. Detecting and handling this here ensures that ModbusTcpClient._recv doesn't needlessly loop until it times out after the stream is closed by the peer, and also makes it certain that a response of b'' from ModbusTcpClient._recv means there was a timeout and cannot mean that the peer closed the stream, as it could mean before. The previous behavior was to not identify the remote end closing the socket, triggering a timeout, in turn causing the transaction manager to treat it as the modbus unit returning a response with errors in it (raising InvalidMessageReceivedException). This will now raise a ConnectionException, which falls through to the client, bypassing the retry mechanisms. Note that https://docs.python.org/3/library/socket.html does not contain a full description on the socket.recv method; see also pydoc for socket.socket.recv.
1 parent d4f5f9e commit 5f5b427

File tree

1 file changed

+38
-1
lines changed

1 file changed

+38
-1
lines changed

pymodbus/client/sync.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,12 @@ def _recv(self, size):
239239
""" Reads data from the underlying descriptor
240240
241241
:param size: The number of bytes to read
242-
:return: The bytes read
242+
:return: The bytes read if the peer sent a response, or a zero-length
243+
response if no data packets were received from the client at
244+
all.
245+
:raises: ConnectionException if the socket is not initialized, or the
246+
peer either has closed the connection before this method is
247+
invoked or closes it before sending any data before timeout.
243248
"""
244249
if not self.socket:
245250
raise ConnectionException(self.__str__())
@@ -270,6 +275,9 @@ def _recv(self, size):
270275
ready = select.select([self.socket], [], [], end - time_)
271276
if ready[0]:
272277
recv_data = self.socket.recv(recv_size)
278+
if recv_data == b'':
279+
return self._handle_abrupt_socket_close(
280+
size, data, time.time() - time_)
273281
data.append(recv_data)
274282
data_length += len(recv_data)
275283
time_ = time.time()
@@ -286,6 +294,35 @@ def _recv(self, size):
286294

287295
return b"".join(data)
288296

297+
def _handle_abrupt_socket_close(self, size, data, duration):
298+
""" Handle unexpected socket close by remote end
299+
300+
Intended to be invoked after determining that the remote end
301+
has unexpectedly closed the connection, to clean up and handle
302+
the situation appropriately.
303+
304+
:param size: The number of bytes that was attempted to read
305+
:param data: The actual data returned
306+
:param duration: Duration from the read was first attempted
307+
until it was determined that the remote closed the
308+
socket
309+
:return: The more than zero bytes read from the remote end
310+
:raises: ConnectionException If the remote end didn't send any
311+
data at all before closing the connection.
312+
"""
313+
self.close()
314+
readsize = (f"read of {size} bytes" if size
315+
else "unbounded read")
316+
msg = (f'{self.__str__()}: Connection unexpectedly closed '
317+
f'{duration:.6f} seconds into {readsize}')
318+
if data:
319+
result = b"".join(data)
320+
msg += f" after returning {len(result)} bytes"
321+
_logger.warning(msg)
322+
return result
323+
msg += " without response from unit before it closed connection"
324+
raise ConnectionException(msg)
325+
289326
def is_socket_open(self):
290327
return True if self.socket is not None else False
291328

0 commit comments

Comments
 (0)