Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixup atexit handling #35

Merged
merged 3 commits into from
Sep 10, 2021
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
14 changes: 12 additions & 2 deletions softioc/asyncio_dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import inspect
import threading

import atexit

class AsyncioDispatcher:
def __init__(self, loop=None):
Expand All @@ -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():
Expand Down
6 changes: 5 additions & 1 deletion softioc/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ def auto_decode(result, func, args):
iocInit.argtypes = ()

epicsExit = Com.epicsExit
epicsExit.argtypes = ()
epicsExit.argtypes = (c_int,)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops.

Looks like this was my mistake, back in commit c9548f6


epicsExitCallAtExits = Com.epicsExitCallAtExits
epicsExitCallAtExits.argtypes = ()
epicsExitCallAtExits.restype = None


__all__ = [
Expand Down
36 changes: 26 additions & 10 deletions softioc/softioc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
import atexit
from ctypes import *
from tempfile import NamedTemporaryFile

Expand All @@ -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):
Expand All @@ -36,17 +40,24 @@ 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.
sys.exitfunc()
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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do the same dance as above with sys.exitfunc to make sure we don't get multiple executions of exit handlers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as I read it. _run_exitfuncs() (like epicsExitCallAtExits()) removes callbacks which have run.

https://github.com/python/cpython/blob/06148b1870fceb1a21738761b8e1ac3bf654319b/Modules/atexitmodule.c#L105


# calls epicsExitCallAtExits()
# and then OS exit()
imports.epicsExit(code)
epicsExit = safeEpicsExit

# The following identifiers will be exported to interactive shell.
command_names = []
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will make code.interact raise SystemExit which will call safeEpicsExit, Why do we call sys.exit() here rather than safeEpicsExit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sys.exit() will work the same regardless of how interactive_ioc() is called. This keeps the handling of call_exit=True vs. call_exit=False contained to interactive_ioc() where SystemExit is now caught.

def __call__(self, code=0):
sys.exit(code)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't we calling epicsExit here anymore?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you answered this above in #35 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully :)


exit = Exiter()
command_names.append('exit')
Expand Down Expand Up @@ -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)