Skip to content

bpo-31901: atexit callbacks should be run at subinterpreter shutdown #4611

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

Merged
merged 4 commits into from Dec 20, 2017
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
3 changes: 3 additions & 0 deletions Doc/library/atexit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ at interpreter termination time they will be run in the order ``C``, ``B``,
program is killed by a signal not handled by Python, when a Python fatal
internal error is detected, or when :func:`os._exit` is called.

.. versionchanged:: 3.7
When used with C-API subinterpreters, registered functions
are local to the interpreter they were registered in.

.. function:: register(func, *args, **kwargs)

Expand Down
1 change: 0 additions & 1 deletion Include/internal/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ typedef struct pyruntimestate {
#define NEXITFUNCS 32
void (*exitfuncs[NEXITFUNCS])(void);
int nexitfuncs;
void (*pyexitfunc)(void);

struct _gc_runtime_state gc;
struct _warnings_runtime_state warnings;
Expand Down
2 changes: 1 addition & 1 deletion Include/pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ PyAPI_FUNC(void) Py_EndInterpreter(PyThreadState *);
* exit functions.
*/
#ifndef Py_LIMITED_API
PyAPI_FUNC(void) _Py_PyAtExit(void (*func)(void));
PyAPI_FUNC(void) _Py_PyAtExit(void (*func)(PyObject *), PyObject *);
#endif
PyAPI_FUNC(int) Py_AtExit(void (*func)(void));

Expand Down
3 changes: 3 additions & 0 deletions Include/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ typedef struct _is {
PyObject *after_forkers_parent;
PyObject *after_forkers_child;
#endif
/* AtExit module */
void (*pyexitfunc)(PyObject *);
PyObject *pyexitmodule;
} PyInterpreterState;
#endif /* !Py_LIMITED_API */

Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_atexit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
import io
import atexit
import os
from test import support
from test.support import script_helper

Expand Down Expand Up @@ -203,6 +204,24 @@ def f():
self.assertEqual(ret, 0)
self.assertEqual(atexit._ncallbacks(), n)

def test_callback_on_subinterpreter_teardown(self):
# This tests if a callback is called on
# subinterpreter teardown.
expected = b"The test has passed!"
r, w = os.pipe()

code = r"""if 1:
import os
import atexit
def callback():
os.write({:d}, b"The test has passed!")
atexit.register(callback)
""".format(w)
ret = support.run_in_subinterp(code)
os.close(w)
self.assertEqual(os.read(r, len(expected)), expected)
os.close(r)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `atexit` module now has its callback stored per interpreter.
72 changes: 40 additions & 32 deletions Modules/atexitmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,13 @@ atexit_cleanup(atexitmodule_state *modstate)
/* Installed into pylifecycle.c's atexit mechanism */

static void
atexit_callfuncs(void)
atexit_callfuncs(PyObject *module)
{
PyObject *exc_type = NULL, *exc_value, *exc_tb, *r;
atexit_callback *cb;
PyObject *module;
atexitmodule_state *modstate;
int i;

module = PyState_FindModule(&atexitmodule);
if (module == NULL)
return;
modstate = GET_ATEXIT_STATE(module);
Expand Down Expand Up @@ -185,7 +183,7 @@ Run all registered exit functions.");
static PyObject *
atexit_run_exitfuncs(PyObject *self, PyObject *unused)
{
atexit_callfuncs();
atexit_callfuncs(self);
if (PyErr_Occurred())
return NULL;
Py_RETURN_NONE;
Expand Down Expand Up @@ -225,13 +223,15 @@ atexit_m_traverse(PyObject *self, visitproc visit, void *arg)
atexitmodule_state *modstate;

modstate = GET_ATEXIT_STATE(self);
for (i = 0; i < modstate->ncallbacks; i++) {
atexit_callback *cb = modstate->atexit_callbacks[i];
if (cb == NULL)
continue;
Py_VISIT(cb->func);
Py_VISIT(cb->args);
Py_VISIT(cb->kwargs);
if (modstate != NULL) {
for (i = 0; i < modstate->ncallbacks; i++) {
atexit_callback *cb = modstate->atexit_callbacks[i];
if (cb == NULL)
continue;
Py_VISIT(cb->func);
Py_VISIT(cb->args);
Py_VISIT(cb->kwargs);
}
}
return 0;
}
Expand All @@ -241,7 +241,9 @@ atexit_m_clear(PyObject *self)
{
atexitmodule_state *modstate;
modstate = GET_ATEXIT_STATE(self);
atexit_cleanup(modstate);
if (modstate != NULL) {
atexit_cleanup(modstate);
}
return 0;
}

Expand All @@ -250,8 +252,10 @@ atexit_free(PyObject *m)
{
atexitmodule_state *modstate;
modstate = GET_ATEXIT_STATE(m);
atexit_cleanup(modstate);
PyMem_Free(modstate->atexit_callbacks);
if (modstate != NULL) {
atexit_cleanup(modstate);
PyMem_Free(modstate->atexit_callbacks);
}
}

PyDoc_STRVAR(atexit_unregister__doc__,
Expand Down Expand Up @@ -310,14 +314,34 @@ upon normal program termination.\n\
Two public functions, register and unregister, are defined.\n\
");

static int
atexit_exec(PyObject *m) {
atexitmodule_state *modstate;

modstate = GET_ATEXIT_STATE(m);
modstate->callback_len = 32;
modstate->ncallbacks = 0;
modstate->atexit_callbacks = PyMem_New(atexit_callback*,
modstate->callback_len);
if (modstate->atexit_callbacks == NULL)
return -1;

_Py_PyAtExit(atexit_callfuncs, m);
return 0;
}

static PyModuleDef_Slot atexit_slots[] = {
{Py_mod_exec, atexit_exec},
{0, NULL}
};

static struct PyModuleDef atexitmodule = {
PyModuleDef_HEAD_INIT,
"atexit",
atexit__doc__,
sizeof(atexitmodule_state),
atexit_methods,
NULL,
atexit_slots,
atexit_m_traverse,
atexit_m_clear,
(freefunc)atexit_free
Expand All @@ -326,21 +350,5 @@ static struct PyModuleDef atexitmodule = {
PyMODINIT_FUNC
PyInit_atexit(void)
{
PyObject *m;
atexitmodule_state *modstate;

m = PyModule_Create(&atexitmodule);
if (m == NULL)
return NULL;

modstate = GET_ATEXIT_STATE(m);
modstate->callback_len = 32;
modstate->ncallbacks = 0;
modstate->atexit_callbacks = PyMem_New(atexit_callback*,
modstate->callback_len);
if (modstate->atexit_callbacks == NULL)
return NULL;

_Py_PyAtExit(atexit_callfuncs);
return m;
return PyModuleDef_Init(&atexitmodule);
}
1 change: 0 additions & 1 deletion Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ static int test_forced_io_encoding(void)
return 0;
}


/*********************************************************
* Test parts of the C-API that work before initialization
*********************************************************/
Expand Down
33 changes: 22 additions & 11 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static _PyInitError initfsencoding(PyInterpreterState *interp);
static _PyInitError initsite(void);
static _PyInitError init_sys_streams(PyInterpreterState *interp);
static _PyInitError initsigs(void);
static void call_py_exitfuncs(void);
static void call_py_exitfuncs(PyInterpreterState *);
static void wait_for_thread_shutdown(void);
static void call_ll_exitfuncs(void);
extern int _PyUnicode_Init(void);
Expand Down Expand Up @@ -1088,6 +1088,10 @@ Py_FinalizeEx(void)

wait_for_thread_shutdown();

/* Get current thread state and interpreter pointer */
tstate = PyThreadState_GET();
interp = tstate->interp;

/* The interpreter is still entirely intact at this point, and the
* exit funcs may be relying on that. In particular, if some thread
* or exit func is still waiting to do an import, the import machinery
Expand All @@ -1097,11 +1101,8 @@ Py_FinalizeEx(void)
* threads created thru it, so this also protects pending imports in
* the threads created via Threading.
*/
call_py_exitfuncs();

/* Get current thread state and interpreter pointer */
tstate = PyThreadState_GET();
interp = tstate->interp;
call_py_exitfuncs(interp);

/* Copy the core config to be able to use it even
after PyInterpreterState_Delete() */
Expand Down Expand Up @@ -1484,6 +1485,8 @@ Py_EndInterpreter(PyThreadState *tstate)

wait_for_thread_shutdown();

call_py_exitfuncs(interp);

if (tstate != interp->tstate_head || tstate->next != NULL)
Py_FatalError("Py_EndInterpreter: not the last thread");

Expand Down Expand Up @@ -2095,20 +2098,28 @@ _Py_FatalInitError(_PyInitError err)
# include "pythread.h"

/* For the atexit module. */
void _Py_PyAtExit(void (*func)(void))
void _Py_PyAtExit(void (*func)(PyObject *), PyObject *module)
{
PyThreadState *ts;
PyInterpreterState *is;

ts = PyThreadState_GET();
is = ts->interp;

/* Guard against API misuse (see bpo-17852) */
assert(_PyRuntime.pyexitfunc == NULL || _PyRuntime.pyexitfunc == func);
_PyRuntime.pyexitfunc = func;
assert(is->pyexitfunc == NULL || is->pyexitfunc == func);

is->pyexitfunc = func;
is->pyexitmodule = module;
}

static void
call_py_exitfuncs(void)
call_py_exitfuncs(PyInterpreterState *istate)
{
if (_PyRuntime.pyexitfunc == NULL)
if (istate->pyexitfunc == NULL)
return;

(*_PyRuntime.pyexitfunc)();
(*istate->pyexitfunc)(istate->pyexitmodule);
PyErr_Clear();
}

Expand Down
2 changes: 2 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ PyInterpreterState_New(void)
interp->after_forkers_parent = NULL;
interp->after_forkers_child = NULL;
#endif
interp->pyexitfunc = NULL;
interp->pyexitmodule = NULL;

HEAD_LOCK();
interp->next = _PyRuntime.interpreters.head;
Expand Down