Skip to content

Commit 089a503

Browse files
committed
Add system health check to all tests.
1 parent 8de66a2 commit 089a503

File tree

15 files changed

+194
-238
lines changed

15 files changed

+194
-238
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
run: pip install -e . -r requirements.txt
9595

9696
- name: pytest
97-
run: pytest -n0 -v --full-trace --timeout=20
97+
run: pytest -n0 -v --full-trace --timeout=120
9898

9999
analyze:
100100
name: Analyze Python

examples/simulator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ async def run_simulator():
6464
cmdline = [
6565
"--modbus_device",
6666
"device_try",
67+
"--modbus_server",
68+
"server",
6769
]
6870
cmd_args = get_simulator_commandline(cmdline=cmdline)
6971
task = ModbusSimulatorServer(**cmd_args)
@@ -82,6 +84,7 @@ async def run_simulator():
8284
await run_calls(client, 1)
8385

8486
_logger.info("### shutdown client")
87+
client.close()
8588

8689
_logger.info("### shutdown server")
8790
await task.stop()

pymodbus/server/async_io.py

Lines changed: 88 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(self, owner):
5353
host=owner.comm_params.source_address[0],
5454
port=owner.comm_params.source_address[1],
5555
)
56-
super().__init__(params, True)
56+
super().__init__(params, False)
5757
self.server = owner
5858
self.running = False
5959
self.receive_queue = asyncio.Queue()
@@ -170,6 +170,7 @@ async def handle(self):
170170
self.comm_params.comm_name,
171171
)
172172
self.transport_close()
173+
self.callback_disconnected(exc)
173174
else:
174175
Log.error("Unknown error occurred {}", exc)
175176
reset_frame = True # graceful recovery
@@ -252,7 +253,66 @@ def callback_data(self, data: bytes, addr: tuple = None) -> int:
252253
# --------------------------------------------------------------------------- #
253254

254255

255-
class ModbusTcpServer(ModbusProtocol):
256+
class ModbusBaseServer(ModbusProtocol):
257+
"""Common functionality for all server classes"""
258+
259+
def __init__(
260+
self,
261+
params: CommParams,
262+
context,
263+
ignore_missing_slaves,
264+
broadcast_enable,
265+
response_manipulator,
266+
request_tracer,
267+
identity,
268+
framer,
269+
) -> None:
270+
"""Initialize base server."""
271+
super().__init__(
272+
params,
273+
True,
274+
)
275+
self.loop = asyncio.get_running_loop()
276+
self.decoder = ServerDecoder()
277+
self.context = context or ModbusServerContext()
278+
self.control = ModbusControlBlock()
279+
self.ignore_missing_slaves = ignore_missing_slaves
280+
self.broadcast_enable = broadcast_enable
281+
self.response_manipulator = response_manipulator
282+
self.request_tracer = request_tracer
283+
self.handle_local_echo = False
284+
if isinstance(identity, ModbusDeviceIdentification):
285+
self.control.Identity.update(identity)
286+
self.framer = framer
287+
self.serving: asyncio.Future = asyncio.Future()
288+
289+
def callback_new_connection(self):
290+
"""Handle incoming connect."""
291+
return ModbusServerRequestHandler(self)
292+
293+
async def shutdown(self):
294+
"""Shutdown server."""
295+
await self.server_close()
296+
297+
async def server_close(self):
298+
"""Close server."""
299+
if not self.serving.done():
300+
self.serving.set_result(True)
301+
self.transport_close()
302+
303+
async def serve_forever(self):
304+
"""Start endless loop."""
305+
if self.transport:
306+
raise RuntimeError(
307+
"Can't call serve_forever on an already running server object"
308+
)
309+
await self.transport_listen()
310+
Log.info("Server listening.")
311+
await self.serving
312+
Log.info("Server graceful shutdown.")
313+
314+
315+
class ModbusTcpServer(ModbusBaseServer):
256316
"""A modbus threaded tcp socket server.
257317
258318
We inherit and overload the socket server so that we
@@ -263,7 +323,7 @@ class ModbusTcpServer(ModbusProtocol):
263323
def __init__(
264324
self,
265325
context,
266-
framer=None,
326+
framer=ModbusSocketFramer,
267327
identity=None,
268328
address=("", 502),
269329
ignore_missing_slaves=False,
@@ -300,60 +360,16 @@ def __init__(
300360
),
301361
)
302362
params.source_address = address
303-
304363
super().__init__(
305364
params,
306-
True,
365+
context,
366+
ignore_missing_slaves,
367+
broadcast_enable,
368+
response_manipulator,
369+
request_tracer,
370+
identity,
371+
framer,
307372
)
308-
self.decoder = ServerDecoder()
309-
self.framer = framer or ModbusSocketFramer
310-
self.context = context or ModbusServerContext()
311-
self.control = ModbusControlBlock()
312-
self.ignore_missing_slaves = ignore_missing_slaves
313-
self.broadcast_enable = broadcast_enable
314-
self.response_manipulator = response_manipulator
315-
self.request_tracer = request_tracer
316-
if isinstance(identity, ModbusDeviceIdentification):
317-
self.control.Identity.update(identity)
318-
319-
# asyncio future that will be done once server has started
320-
self.serving = asyncio.Future()
321-
self.serving_done = asyncio.Future()
322-
# constructors cannot be declared async, so we have to
323-
# defer the initialization of the server
324-
self.handle_local_echo = False
325-
326-
def handle_new_connection(self):
327-
"""Handle incoming connect."""
328-
return ModbusServerRequestHandler(self)
329-
330-
async def serve_forever(self):
331-
"""Start endless loop."""
332-
if self.transport:
333-
raise RuntimeError(
334-
"Can't call serve_forever on an already running server object"
335-
)
336-
337-
await self.transport_listen()
338-
self.serving.set_result(True)
339-
Log.info("Server(TCP) listening.")
340-
try:
341-
await self.transport.serve_forever()
342-
except asyncio.exceptions.CancelledError:
343-
self.serving_done.set_result(False)
344-
raise
345-
except Exception as exc: # pylint: disable=broad-except
346-
Log.error("Server unexpected exception {}", exc)
347-
self.serving_done.set_result(True)
348-
Log.info("Server graceful shutdown.")
349-
350-
async def shutdown(self):
351-
"""Shutdown server."""
352-
await self.server_close()
353-
354-
async def server_close(self):
355-
"""Close server."""
356-
self.transport_close()
357373

358374

359375
class ModbusTlsServer(ModbusTcpServer):
@@ -367,7 +383,7 @@ class ModbusTlsServer(ModbusTcpServer):
367383
def __init__( # pylint: disable=too-many-arguments
368384
self,
369385
context,
370-
framer=None,
386+
framer=ModbusTlsFramer,
371387
identity=None,
372388
address=("", 502),
373389
sslctx=None,
@@ -422,7 +438,7 @@ def __init__( # pylint: disable=too-many-arguments
422438
)
423439

424440

425-
class ModbusUdpServer(ModbusProtocol):
441+
class ModbusUdpServer(ModbusBaseServer):
426442
"""A modbus threaded udp socket server.
427443
428444
We inherit and overload the socket server so that we
@@ -433,7 +449,7 @@ class ModbusUdpServer(ModbusProtocol):
433449
def __init__(
434450
self,
435451
context,
436-
framer=None,
452+
framer=ModbusSocketFramer,
437453
identity=None,
438454
address=("", 502),
439455
ignore_missing_slaves=False,
@@ -468,62 +484,17 @@ def __init__(
468484
reconnect_delay_max=0.0,
469485
timeout_connect=0.0,
470486
),
471-
True,
487+
context,
488+
ignore_missing_slaves,
489+
broadcast_enable,
490+
response_manipulator,
491+
request_tracer,
492+
identity,
493+
framer,
472494
)
473495

474-
self.loop = asyncio.get_running_loop()
475-
self.decoder = ServerDecoder()
476-
self.framer = framer or ModbusSocketFramer
477-
self.context = context or ModbusServerContext()
478-
self.control = ModbusControlBlock()
479-
self.ignore_missing_slaves = ignore_missing_slaves
480-
self.broadcast_enable = broadcast_enable
481-
self.response_manipulator = response_manipulator
482-
self.request_tracer = request_tracer
483-
if isinstance(identity, ModbusDeviceIdentification):
484-
self.control.Identity.update(identity)
485-
self.stop_serving = self.loop.create_future()
486-
# asyncio future that will be done once server has started
487-
self.serving = asyncio.Future()
488-
self.serving_done = asyncio.Future()
489-
self.handle_local_echo = False
490-
491-
def handle_new_connection(self):
492-
"""Handle incoming connect."""
493-
return ModbusServerRequestHandler(self)
494-
495-
async def serve_forever(self):
496-
"""Start endless loop."""
497-
if self.transport:
498-
raise RuntimeError(
499-
"Can't call serve_forever on an already running server object"
500-
)
501-
try:
502-
await self.transport_listen()
503-
except asyncio.exceptions.CancelledError:
504-
self.serving_done.set_result(False)
505-
raise
506-
except Exception as exc:
507-
Log.error("Server unexpected exception {}", exc)
508-
self.serving_done.set_result(False)
509-
raise RuntimeError(exc) from exc
510-
Log.info("Server(UDP) listening.")
511-
self.serving.set_result(True)
512-
await self.stop_serving
513-
self.serving_done.set_result(True)
514-
515-
async def shutdown(self):
516-
"""Shutdown server."""
517-
await self.server_close()
518-
519-
async def server_close(self):
520-
"""Close server."""
521-
self.transport_close()
522-
if not self.stop_serving.done():
523-
self.stop_serving.set_result(True)
524-
525496

526-
class ModbusSerialServer(ModbusProtocol):
497+
class ModbusSerialServer(ModbusBaseServer):
527498
"""A modbus threaded serial socket server.
528499
529500
We inherit and overload the socket server so that we
@@ -570,56 +541,15 @@ def __init__(
570541
baudrate=kwargs.get("baudrate", 19200),
571542
stopbits=kwargs.get("stopbits", 1),
572543
),
573-
True,
544+
context,
545+
kwargs.get("ignore_missing_slaves", False),
546+
kwargs.get("broadcast_enable", False),
547+
kwargs.get("request_tracer", None),
548+
kwargs.get("response_manipulator", None),
549+
kwargs.get("identity", None),
550+
framer,
574551
)
575-
576-
self.loop = asyncio.get_running_loop()
577552
self.handle_local_echo = kwargs.get("handle_local_echo", False)
578-
self.ignore_missing_slaves = kwargs.get("ignore_missing_slaves", False)
579-
self.broadcast_enable = kwargs.get("broadcast_enable", False)
580-
self.framer = framer or ModbusRtuFramer
581-
self.decoder = ServerDecoder()
582-
self.context = context or ModbusServerContext()
583-
self.response_manipulator = kwargs.get("response_manipulator", None)
584-
self.control = ModbusControlBlock()
585-
if isinstance(identity, ModbusDeviceIdentification):
586-
self.control.Identity.update(identity)
587-
self.request_tracer = kwargs.get("request_tracer", None)
588-
self.server = None
589-
self.control = ModbusControlBlock()
590-
identity = kwargs.get("identity")
591-
if isinstance(identity, ModbusDeviceIdentification):
592-
self.control.Identity.update(identity)
593-
594-
async def start(self):
595-
"""Start connecting."""
596-
597-
def handle_new_connection(self):
598-
"""Handle incoming connect."""
599-
return ModbusServerRequestHandler(self)
600-
601-
async def shutdown(self):
602-
"""Terminate server."""
603-
self.transport_close()
604-
if self.server:
605-
self.server.close()
606-
await asyncio.wait_for(self.server.wait_closed(), 10)
607-
self.server = None
608-
609-
async def serve_forever(self):
610-
"""Start endless loop."""
611-
if self.server:
612-
raise RuntimeError(
613-
"Can't call serve_forever on an already running server object"
614-
)
615-
Log.info("Server(Serial) listening.")
616-
await self.transport_listen()
617-
try:
618-
await self.transport.serve_forever()
619-
except asyncio.exceptions.CancelledError:
620-
raise
621-
except Exception as exc: # pylint: disable=broad-except
622-
Log.error("Server unexpected exception {}", exc)
623553

624554

625555
# --------------------------------------------------------------------------- #
@@ -779,7 +709,6 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-defa
779709
server = ModbusSerialServer(
780710
context, kwargs.pop("framer", ModbusAsciiFramer), identity=identity, **kwargs
781711
)
782-
await server.start()
783712
await _serverList.run(server, custom_functions)
784713

785714

pymodbus/server/simulator/http_server.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def __init__(
167167
info_name=server["identity"]
168168
)
169169
self.modbus_server = comm(framer=framer, context=datastore, **server)
170-
170+
self.serving: asyncio.Future = asyncio.Future()
171171
self.log_file = log_file
172172
self.site = None
173173
self.http_host = http_host
@@ -234,6 +234,7 @@ async def start_modbus_server(self, app):
234234
async def stop_modbus_server(self, app):
235235
"""Stop modbus server."""
236236
Log.info("Stopping modbus server")
237+
await self.modbus_server.shutdown()
237238
app["modbus_server"].cancel()
238239
with contextlib.suppress(asyncio.exceptions.CancelledError):
239240
await app["modbus_server"]
@@ -254,13 +255,15 @@ async def run_forever(self, only_start=False):
254255
if only_start:
255256
return
256257
while True:
257-
await asyncio.sleep(1)
258+
await self.serving()
258259

259260
async def stop(self):
260261
"""Stop modbus and http servers."""
261262
await self.site.stop()
262263
self.site = None
263-
await asyncio.sleep(1)
264+
if not self.serving.done():
265+
self.serving.set_result(True)
266+
await asyncio.sleep(0.1)
264267

265268
async def handle_html_static(self, request):
266269
"""Handle static html."""

0 commit comments

Comments
 (0)