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 5e95e49c..71b59dbd 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -97,7 +97,11 @@ def auto_decode(result, func, args): iocInit.argtypes = () epicsExit = Com.epicsExit -epicsExit.argtypes = () +epicsExit.argtypes = (c_int,) + +epicsExitCallAtExits = Com.epicsExitCallAtExits +epicsExitCallAtExits.argtypes = () +epicsExitCallAtExits.restype = None __all__ = [ diff --git a/softioc/softioc.py b/softioc/softioc.py index 843a4a3a..216ebf83 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 @@ -10,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): @@ -36,9 +40,9 @@ def iocInit(dispatcher=None): imports.iocInit() -def safeEpicsExit(): +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,7 +50,14 @@ def safeEpicsExit(): finally: # Make sure we don't try the exit handlers more than once! del sys.exitfunc - epicsExit() + + elif hasattr(atexit, '_run_exitfuncs'): # py 3.x + atexit._run_exitfuncs() + + # calls epicsExitCallAtExits() + # and then OS exit() + imports.epicsExit(code) +epicsExit = safeEpicsExit # The following identifiers will be exported to interactive shell. command_names = [] @@ -233,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') @@ -305,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)