Skip to content

Commit 357704c

Browse files
authored
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.
1 parent 83d5204 commit 357704c

File tree

8 files changed

+53
-28
lines changed

8 files changed

+53
-28
lines changed

Doc/whatsnew/3.10.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,13 @@ Changes in the Python API
501501
have been renamed to *exc*.
502502
(Contributed by Zackery Spytz and Matthias Bussonnier in :issue:`26389`.)
503503

504+
* :mod:`atexit`: At Python exit, if a callback registered with
505+
:func:`atexit.register` fails, its exception is now logged. Previously, only
506+
some exceptions were logged, and the last exception was always silently
507+
ignored.
508+
(Contributed by Victor Stinner in :issue:`42639`.)
509+
510+
504511
CPython bytecode changes
505512
========================
506513

@@ -519,6 +526,8 @@ Build Changes
519526
* :mod:`sqlite3` requires SQLite 3.7.3 or higher.
520527
(Contributed by Sergey Fedoseev and Erlend E. Aasland :issue:`40744`.)
521528

529+
* The :mod:`atexit` module must now always be built as a built-in module.
530+
(Contributed by Victor Stinner in :issue:`42639`.)
522531

523532

524533
C API Changes

Include/internal/pycore_interp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ struct _is {
233233
PyObject *after_forkers_parent;
234234
PyObject *after_forkers_child;
235235
#endif
236+
236237
/* AtExit module */
237-
void (*atexit_func)(PyObject *);
238238
PyObject *atexit_module;
239239

240240
uint64_t tstate_next_unique_id;

Include/internal/pycore_pylifecycle.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ PyAPI_FUNC(void) _PyErr_Display(PyObject *file, PyObject *exception,
109109

110110
PyAPI_FUNC(void) _PyThreadState_DeleteCurrent(PyThreadState *tstate);
111111

112+
extern void _PyAtExit_Call(PyObject *module);
113+
112114
#ifdef __cplusplus
113115
}
114116
#endif

Lib/test/test_threading.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ def exit_handler():
487487
if not pid:
488488
print("child process ok", file=sys.stderr, flush=True)
489489
# child process
490-
sys.exit()
491490
else:
492491
wait_process(pid, exitcode=0)
493492
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
At Python exit, if a callback registered with :func:`atexit.register` fails,
2+
its exception is now logged. Previously, only some exceptions were logged, and
3+
the last exception was always silently ignored.

Modules/atexitmodule.c

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ atexit_cleanup(struct atexit_state *state)
6565
/* Installed into pylifecycle.c's atexit mechanism */
6666

6767
static void
68-
atexit_callfuncs(PyObject *module)
68+
atexit_callfuncs(PyObject *module, int ignore_exc)
6969
{
7070
assert(!PyErr_Occurred());
7171

@@ -87,18 +87,23 @@ atexit_callfuncs(PyObject *module)
8787

8888
PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs);
8989
if (res == NULL) {
90-
/* Maintain the last exception, but don't leak if there are
91-
multiple exceptions. */
92-
if (exc_type) {
93-
Py_DECREF(exc_type);
94-
Py_XDECREF(exc_value);
95-
Py_XDECREF(exc_tb);
90+
if (ignore_exc) {
91+
_PyErr_WriteUnraisableMsg("in atexit callback", cb->func);
9692
}
97-
PyErr_Fetch(&exc_type, &exc_value, &exc_tb);
98-
if (!PyErr_GivenExceptionMatches(exc_type, PyExc_SystemExit)) {
99-
PySys_WriteStderr("Error in atexit._run_exitfuncs:\n");
100-
PyErr_NormalizeException(&exc_type, &exc_value, &exc_tb);
101-
PyErr_Display(exc_type, exc_value, exc_tb);
93+
else {
94+
/* Maintain the last exception, but don't leak if there are
95+
multiple exceptions. */
96+
if (exc_type) {
97+
Py_DECREF(exc_type);
98+
Py_XDECREF(exc_value);
99+
Py_XDECREF(exc_tb);
100+
}
101+
PyErr_Fetch(&exc_type, &exc_value, &exc_tb);
102+
if (!PyErr_GivenExceptionMatches(exc_type, PyExc_SystemExit)) {
103+
PySys_WriteStderr("Error in atexit._run_exitfuncs:\n");
104+
PyErr_NormalizeException(&exc_type, &exc_value, &exc_tb);
105+
PyErr_Display(exc_type, exc_value, exc_tb);
106+
}
102107
}
103108
}
104109
else {
@@ -108,11 +113,24 @@ atexit_callfuncs(PyObject *module)
108113

109114
atexit_cleanup(state);
110115

111-
if (exc_type) {
112-
PyErr_Restore(exc_type, exc_value, exc_tb);
116+
if (ignore_exc) {
117+
assert(!PyErr_Occurred());
118+
}
119+
else {
120+
if (exc_type) {
121+
PyErr_Restore(exc_type, exc_value, exc_tb);
122+
}
113123
}
114124
}
115125

126+
127+
void
128+
_PyAtExit_Call(PyObject *module)
129+
{
130+
atexit_callfuncs(module, 1);
131+
}
132+
133+
116134
/* ===================================================================== */
117135
/* Module methods. */
118136

@@ -180,7 +198,7 @@ Run all registered exit functions.");
180198
static PyObject *
181199
atexit_run_exitfuncs(PyObject *module, PyObject *unused)
182200
{
183-
atexit_callfuncs(module);
201+
atexit_callfuncs(module, 0);
184202
if (PyErr_Occurred()) {
185203
return NULL;
186204
}
@@ -308,9 +326,8 @@ atexit_exec(PyObject *module)
308326
return -1;
309327
}
310328

311-
PyInterpreterState *is = _PyInterpreterState_GET();
312-
is->atexit_func = atexit_callfuncs;
313-
is->atexit_module = module;
329+
PyInterpreterState *interp = _PyInterpreterState_GET();
330+
interp->atexit_module = module;
314331
return 0;
315332
}
316333

Python/pylifecycle.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,19 +2632,16 @@ Py_ExitStatusException(PyStatus status)
26322632
}
26332633
}
26342634

2635+
26352636
/* Clean up and exit */
26362637

26372638
static void
26382639
call_py_exitfuncs(PyThreadState *tstate)
26392640
{
2640-
PyInterpreterState *interp = tstate->interp;
2641-
if (interp->atexit_func == NULL)
2642-
return;
2643-
2644-
interp->atexit_func(interp->atexit_module);
2645-
_PyErr_Clear(tstate);
2641+
_PyAtExit_Call(tstate->interp->atexit_module);
26462642
}
26472643

2644+
26482645
/* Wait until threading._shutdown completes, provided
26492646
the threading module was imported in the first place.
26502647
The shutdown routine will wait until all non-daemon

setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,8 +854,6 @@ def detect_simple_extensions(self):
854854
# C-optimized pickle replacement
855855
self.add(Extension("_pickle", ["_pickle.c"],
856856
extra_compile_args=['-DPy_BUILD_CORE_MODULE']))
857-
# atexit
858-
self.add(Extension("atexit", ["atexitmodule.c"]))
859857
# _json speedups
860858
self.add(Extension("_json", ["_json.c"],
861859
extra_compile_args=['-DPy_BUILD_CORE_MODULE']))

0 commit comments

Comments
 (0)