bpo-42639: atexit now logs callbacks exceptions (GH-23771)

At Python exit, if a callback registered with atexit.register()
fails, its exception is now logged. Previously, only some exceptions
were logged, and the last exception was always silently ignored.

Add _PyAtExit_Call() function and remove
PyInterpreterState.atexit_func member. call_py_exitfuncs() now calls
directly _PyAtExit_Call().

The atexit module must now always be built as a built-in module.
This commit is contained in:
Victor Stinner 2020-12-14 23:07:54 +01:00 committed by GitHub
parent 83d52044ae
commit 357704c9f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 53 additions and 28 deletions

View File

@ -501,6 +501,13 @@ Changes in the Python API
have been renamed to *exc*. have been renamed to *exc*.
(Contributed by Zackery Spytz and Matthias Bussonnier in :issue:`26389`.) (Contributed by Zackery Spytz and Matthias Bussonnier in :issue:`26389`.)
* :mod:`atexit`: At Python exit, if a callback registered with
:func:`atexit.register` fails, its exception is now logged. Previously, only
some exceptions were logged, and the last exception was always silently
ignored.
(Contributed by Victor Stinner in :issue:`42639`.)
CPython bytecode changes CPython bytecode changes
======================== ========================
@ -519,6 +526,8 @@ Build Changes
* :mod:`sqlite3` requires SQLite 3.7.3 or higher. * :mod:`sqlite3` requires SQLite 3.7.3 or higher.
(Contributed by Sergey Fedoseev and Erlend E. Aasland :issue:`40744`.) (Contributed by Sergey Fedoseev and Erlend E. Aasland :issue:`40744`.)
* The :mod:`atexit` module must now always be built as a built-in module.
(Contributed by Victor Stinner in :issue:`42639`.)
C API Changes C API Changes

View File

@ -233,8 +233,8 @@ struct _is {
PyObject *after_forkers_parent; PyObject *after_forkers_parent;
PyObject *after_forkers_child; PyObject *after_forkers_child;
#endif #endif
/* AtExit module */ /* AtExit module */
void (*atexit_func)(PyObject *);
PyObject *atexit_module; PyObject *atexit_module;
uint64_t tstate_next_unique_id; uint64_t tstate_next_unique_id;

View File

@ -109,6 +109,8 @@ PyAPI_FUNC(void) _PyErr_Display(PyObject *file, PyObject *exception,
PyAPI_FUNC(void) _PyThreadState_DeleteCurrent(PyThreadState *tstate); PyAPI_FUNC(void) _PyThreadState_DeleteCurrent(PyThreadState *tstate);
extern void _PyAtExit_Call(PyObject *module);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -487,7 +487,6 @@ class ThreadTests(BaseTestCase):
if not pid: if not pid:
print("child process ok", file=sys.stderr, flush=True) print("child process ok", file=sys.stderr, flush=True)
# child process # child process
sys.exit()
else: else:
wait_process(pid, exitcode=0) wait_process(pid, exitcode=0)

View File

@ -0,0 +1,3 @@
At Python exit, if a callback registered with :func:`atexit.register` fails,
its exception is now logged. Previously, only some exceptions were logged, and
the last exception was always silently ignored.

View File

@ -65,7 +65,7 @@ atexit_cleanup(struct atexit_state *state)
/* Installed into pylifecycle.c's atexit mechanism */ /* Installed into pylifecycle.c's atexit mechanism */
static void static void
atexit_callfuncs(PyObject *module) atexit_callfuncs(PyObject *module, int ignore_exc)
{ {
assert(!PyErr_Occurred()); assert(!PyErr_Occurred());
@ -87,6 +87,10 @@ atexit_callfuncs(PyObject *module)
PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs); PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs);
if (res == NULL) { if (res == NULL) {
if (ignore_exc) {
_PyErr_WriteUnraisableMsg("in atexit callback", cb->func);
}
else {
/* Maintain the last exception, but don't leak if there are /* Maintain the last exception, but don't leak if there are
multiple exceptions. */ multiple exceptions. */
if (exc_type) { if (exc_type) {
@ -101,6 +105,7 @@ atexit_callfuncs(PyObject *module)
PyErr_Display(exc_type, exc_value, exc_tb); PyErr_Display(exc_type, exc_value, exc_tb);
} }
} }
}
else { else {
Py_DECREF(res); Py_DECREF(res);
} }
@ -108,11 +113,24 @@ atexit_callfuncs(PyObject *module)
atexit_cleanup(state); atexit_cleanup(state);
if (ignore_exc) {
assert(!PyErr_Occurred());
}
else {
if (exc_type) { if (exc_type) {
PyErr_Restore(exc_type, exc_value, exc_tb); PyErr_Restore(exc_type, exc_value, exc_tb);
} }
}
} }
void
_PyAtExit_Call(PyObject *module)
{
atexit_callfuncs(module, 1);
}
/* ===================================================================== */ /* ===================================================================== */
/* Module methods. */ /* Module methods. */
@ -180,7 +198,7 @@ Run all registered exit functions.");
static PyObject * static PyObject *
atexit_run_exitfuncs(PyObject *module, PyObject *unused) atexit_run_exitfuncs(PyObject *module, PyObject *unused)
{ {
atexit_callfuncs(module); atexit_callfuncs(module, 0);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -308,9 +326,8 @@ atexit_exec(PyObject *module)
return -1; return -1;
} }
PyInterpreterState *is = _PyInterpreterState_GET(); PyInterpreterState *interp = _PyInterpreterState_GET();
is->atexit_func = atexit_callfuncs; interp->atexit_module = module;
is->atexit_module = module;
return 0; return 0;
} }

View File

@ -2632,19 +2632,16 @@ Py_ExitStatusException(PyStatus status)
} }
} }
/* Clean up and exit */ /* Clean up and exit */
static void static void
call_py_exitfuncs(PyThreadState *tstate) call_py_exitfuncs(PyThreadState *tstate)
{ {
PyInterpreterState *interp = tstate->interp; _PyAtExit_Call(tstate->interp->atexit_module);
if (interp->atexit_func == NULL)
return;
interp->atexit_func(interp->atexit_module);
_PyErr_Clear(tstate);
} }
/* Wait until threading._shutdown completes, provided /* Wait until threading._shutdown completes, provided
the threading module was imported in the first place. the threading module was imported in the first place.
The shutdown routine will wait until all non-daemon The shutdown routine will wait until all non-daemon

View File

@ -854,8 +854,6 @@ class PyBuildExt(build_ext):
# C-optimized pickle replacement # C-optimized pickle replacement
self.add(Extension("_pickle", ["_pickle.c"], self.add(Extension("_pickle", ["_pickle.c"],
extra_compile_args=['-DPy_BUILD_CORE_MODULE'])) extra_compile_args=['-DPy_BUILD_CORE_MODULE']))
# atexit
self.add(Extension("atexit", ["atexitmodule.c"]))
# _json speedups # _json speedups
self.add(Extension("_json", ["_json.c"], self.add(Extension("_json", ["_json.c"],
extra_compile_args=['-DPy_BUILD_CORE_MODULE'])) extra_compile_args=['-DPy_BUILD_CORE_MODULE']))