From b2242fa9ac38b42b95faa5a87228183431f963ae Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sat, 4 Sep 2021 12:05:47 -0700 Subject: [PATCH 1/3] exit with a code --- softioc/imports.py | 2 +- softioc/softioc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/softioc/imports.py b/softioc/imports.py index 5e95e49c..e4d2c594 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -97,7 +97,7 @@ def auto_decode(result, func, args): iocInit.argtypes = () epicsExit = Com.epicsExit -epicsExit.argtypes = () +epicsExit.argtypes = (c_int,) __all__ = [ diff --git a/softioc/softioc.py b/softioc/softioc.py index 843a4a3a..9dec357b 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -36,7 +36,7 @@ def iocInit(dispatcher=None): imports.iocInit() -def safeEpicsExit(): +def safeEpicsExit(code=0): '''Calls epicsExit() after ensuring Python exit handlers called.''' if hasattr(sys, 'exitfunc'): try: @@ -46,7 +46,7 @@ def safeEpicsExit(): finally: # Make sure we don't try the exit handlers more than once! del sys.exitfunc - epicsExit() + epicsExit(code) # The following identifiers will be exported to interactive shell. command_names = [] From 4ff486991b757daa2d76d0d61232d196ecb6d0e0 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 5 Sep 2021 08:34:57 -0700 Subject: [PATCH 2/3] run atexit w/ py3 --- softioc/softioc.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/softioc/softioc.py b/softioc/softioc.py index 9dec357b..10aae8db 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -1,5 +1,6 @@ import os import sys +import atexit from ctypes import * from tempfile import NamedTemporaryFile @@ -38,7 +39,7 @@ def iocInit(dispatcher=None): def safeEpicsExit(code=0): '''Calls epicsExit() after ensuring Python exit handlers called.''' - if hasattr(sys, 'exitfunc'): + if hasattr(sys, 'exitfunc'): # py 2.x try: # Calling epicsExit() will bypass any atexit exit handlers, so call # them explicitly now. @@ -46,6 +47,12 @@ def safeEpicsExit(code=0): finally: # Make sure we don't try the exit handlers more than once! del sys.exitfunc + + elif hasattr(atexit, '_run_exitfuncs'): # py 3.x + atexit._run_exitfuncs() + + # calls epicsExitCallAtExits() + # and then OS exit() epicsExit(code) # The following identifiers will be exported to interactive shell. From 797c3f662b8461ecc5054928cac79149234888f6 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 5 Sep 2021 09:01:28 -0700 Subject: [PATCH 3/3] hook epics exit into interpreter lifecycle allow interactive_ioc(call_exit=False) to cleanup and exit cleanly. --- softioc/asyncio_dispatcher.py | 14 ++++++++++++-- softioc/imports.py | 4 ++++ softioc/softioc.py | 29 +++++++++++++++++++---------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index aa10fe0e..083160a4 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -1,7 +1,7 @@ import asyncio import inspect import threading - +import atexit class AsyncioDispatcher: def __init__(self, loop=None): @@ -14,7 +14,17 @@ def __init__(self, loop=None): if loop is None: # Make one and run it in a background thread self.loop = asyncio.new_event_loop() - threading.Thread(target=self.loop.run_forever).start() + worker = threading.Thread(target=self.loop.run_forever) + # Explicitly manage worker thread as part of interpreter shutdown. + # Otherwise threading module will deadlock trying to join() + # before our atexit hook runs, while the loop is still running. + worker.daemon = True + + @atexit.register + def aioJoin(worker=worker, loop=self.loop): + loop.call_soon_threadsafe(loop.stop) + worker.join() + worker.start() def __call__(self, func, *args): async def async_wrapper(): diff --git a/softioc/imports.py b/softioc/imports.py index e4d2c594..71b59dbd 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -99,6 +99,10 @@ def auto_decode(result, func, args): epicsExit = Com.epicsExit epicsExit.argtypes = (c_int,) +epicsExitCallAtExits = Com.epicsExitCallAtExits +epicsExitCallAtExits.argtypes = () +epicsExitCallAtExits.restype = None + __all__ = [ 'get_field_offsets', diff --git a/softioc/softioc.py b/softioc/softioc.py index 10aae8db..216ebf83 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -11,7 +11,10 @@ __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] -epicsExit = imports.epicsExit +# tie in epicsAtExit() to interpreter lifecycle +@atexit.register +def epicsAtPyExit(): + imports.epicsExitCallAtExits() def iocInit(dispatcher=None): @@ -39,7 +42,7 @@ def iocInit(dispatcher=None): def safeEpicsExit(code=0): '''Calls epicsExit() after ensuring Python exit handlers called.''' - if hasattr(sys, 'exitfunc'): # py 2.x + if hasattr(sys, 'exitfunc'): # py 2.x try: # Calling epicsExit() will bypass any atexit exit handlers, so call # them explicitly now. @@ -48,12 +51,13 @@ def safeEpicsExit(code=0): # Make sure we don't try the exit handlers more than once! del sys.exitfunc - elif hasattr(atexit, '_run_exitfuncs'): # py 3.x + elif hasattr(atexit, '_run_exitfuncs'): # py 3.x atexit._run_exitfuncs() # calls epicsExitCallAtExits() # and then OS exit() - epicsExit(code) + imports.epicsExit(code) +epicsExit = safeEpicsExit # The following identifiers will be exported to interactive shell. command_names = [] @@ -240,10 +244,10 @@ def call_f(*args): # Hacked up exit object so that when soft IOC framework sends us an exit command # we actually exit. class Exiter: - def __repr__(self): - safeEpicsExit() - def __call__(self): - safeEpicsExit() + def __repr__(self): # hack to exit when "called" with no parenthesis + sys.exit(0) + def __call__(self, code=0): + sys.exit(code) exit = Exiter() command_names.append('exit') @@ -312,7 +316,12 @@ def interactive_ioc(context = {}, call_exit = True): # This suppresses irritating exit message introduced by Python3. Alas, # this option is only available in Python 3.6! interact_args = dict(exitmsg = '') - code.interact(local = dict(exports, **context), **interact_args) + try: + code.interact(local = dict(exports, **context), **interact_args) + except SystemExit as e: + if call_exit: + safeEpicsExit(e.code) + raise if call_exit: - safeEpicsExit() + safeEpicsExit(0)