diff --git a/Doc/library/faulthandler.rst b/Doc/library/faulthandler.rst index 59274c1dd7ec35..be0912376bd8ef 100644 --- a/Doc/library/faulthandler.rst +++ b/Doc/library/faulthandler.rst @@ -76,6 +76,10 @@ Fault handler state .. versionchanged:: 3.6 On Windows, a handler for Windows exception is also installed. + .. versionchanged:: 3.10 + The dump now mentions if a garbage collector collection is running + if *all_threads* is true. + .. function:: disable() Disable the fault handler: uninstall the signal handlers installed by diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 528d8ab35123d2..54740628ddeb37 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -1003,6 +1003,13 @@ Add *encoding* and *errors* parameters in :func:`fileinput.input` and when *mode* is "r" and file is compressed, like uncompressed files. (Contributed by Inada Naoki in :issue:`5758`.) +faulthandler +------------ + +The :mod:`faulthandler` module now detects if a fatal error occurs during a +garbage collector collection. +(Contributed by Victor Stinner in :issue:`44466`.) + gc -- diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index 29a70857930c1e..ee3f41a108a14c 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -89,10 +89,12 @@ def get_output(self, code, filename=None, fd=None): output = output.decode('ascii', 'backslashreplace') return output.splitlines(), exitcode - def check_error(self, code, line_number, fatal_error, *, + def check_error(self, code, lineno, fatal_error, *, filename=None, all_threads=True, other_regex=None, fd=None, know_current_thread=True, - py_fatal_error=False): + py_fatal_error=False, + garbage_collecting=False, + function=''): """ Check that the fault handler for fatal errors is enabled and check the traceback from the child process output. @@ -106,20 +108,21 @@ def check_error(self, code, line_number, fatal_error, *, header = 'Thread 0x[0-9a-f]+' else: header = 'Stack' - regex = r""" - (?m)^{fatal_error} - - {header} \(most recent call first\): - File "", line {lineno} in - """ + regex = [f'^{fatal_error}'] if py_fatal_error: - fatal_error += "\nPython runtime state: initialized" - regex = dedent(regex).format( - lineno=line_number, - fatal_error=fatal_error, - header=header).strip() + regex.append("Python runtime state: initialized") + regex.append('') + regex.append(fr'{header} \(most recent call first\):') + if garbage_collecting: + regex.append(' Garbage-collecting') + regex.append(fr' File "", line {lineno} in {function}') + regex = '\n'.join(regex) + if other_regex: - regex += '|' + other_regex + regex = f'(?:{regex}|{other_regex})' + + # Enable MULTILINE flag + regex = f'(?m){regex}' output, exitcode = self.get_output(code, filename=filename, fd=fd) output = '\n'.join(output) self.assertRegex(output, regex) @@ -168,6 +171,42 @@ def test_sigsegv(self): 3, 'Segmentation fault') + @skip_segfault_on_android + def test_gc(self): + # bpo-44466: Detect if the GC is running + self.check_fatal_error(""" + import faulthandler + import gc + import sys + + faulthandler.enable() + + class RefCycle: + def __del__(self): + faulthandler._sigsegv() + + # create a reference cycle which triggers a fatal + # error in a destructor + a = RefCycle() + b = RefCycle() + a.b = b + b.a = a + + # Delete the objects, not the cycle + a = None + b = None + + # Break the reference cycle: call __del__() + gc.collect() + + # Should not reach this line + print("exit", file=sys.stderr) + """, + 9, + 'Segmentation fault', + function='__del__', + garbage_collecting=True) + def test_fatal_error_c_thread(self): self.check_fatal_error(""" import faulthandler diff --git a/Misc/NEWS.d/next/Library/2021-06-21-12-43-04.bpo-44466.NSm6mv.rst b/Misc/NEWS.d/next/Library/2021-06-21-12-43-04.bpo-44466.NSm6mv.rst new file mode 100644 index 00000000000000..69de3edb5a7f95 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-21-12-43-04.bpo-44466.NSm6mv.rst @@ -0,0 +1,2 @@ +The :mod:`faulthandler` module now detects if a fatal error occurs during a +garbage collector collection. Patch by Victor Stinner. diff --git a/Python/traceback.c b/Python/traceback.c index 470324b1afd83f..f7dc5ad6864762 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -4,6 +4,7 @@ #include "Python.h" #include "code.h" +#include "pycore_interp.h" // PyInterpreterState.gc #include "frameobject.h" // PyFrame_GetBack() #include "structmember.h" // PyMemberDef #include "osdefs.h" // SEP @@ -914,6 +915,9 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, break; } write_thread_id(fd, tstate, tstate == current_tstate); + if (tstate == current_tstate && tstate->interp->gc.collecting) { + PUTS(fd, " Garbage-collecting\n"); + } dump_traceback(fd, tstate, 0); tstate = PyThreadState_Next(tstate); nthreads++;