gh-149085: Add max_threads keyword to faulthandler.dump_traceback() (GH-149106)

Add a keyword-only `max_threads` argument to `dump_traceback()` and
`dump_traceback_later()`, defaulting to 100 to preserve existing
behavior. Allows server processes with many worker threads to dump
beyond the historical 100-thread cap (previously a hardcoded
`MAX_NTHREADS = 100` in `Python/traceback.c`).

The cap matters in practice: tstates are prepended to the
PyInterpreterState linked list, so the dump walks newest-first. With
more than 100 threads alive, the main thread (oldest, at the tail) is
silently elided from watchdog dumps -- exactly the thread that's
usually wanted.

The hardcoded value is moved to a new internal macro
`_Py_TRACEBACK_MAX_NTHREADS` in `pycore_traceback.h` so the in-tree
fatal-signal callers all reference one source of truth.
This commit is contained in:
Eric Froemling
2026-04-30 06:27:57 -07:00
committed by GitHub
parent 4599335a83
commit 7686abe063
14 changed files with 349 additions and 75 deletions
+24 -7
View File
@@ -31,7 +31,8 @@ tracebacks:
* Each string is limited to 500 characters.
* Only the filename, the function name and the line number are
displayed. (no source code)
* It is limited to 100 frames and 100 threads.
* It is limited to 100 frames per thread, and 100 threads
(configurable via *max_threads*).
* The order is reversed: the most recent call is shown first.
By default, the Python traceback is written to :data:`sys.stderr`. To see
@@ -55,16 +56,20 @@ at Python startup.
Dumping the traceback
---------------------
.. function:: dump_traceback(file=sys.stderr, all_threads=True)
.. function:: dump_traceback(file=sys.stderr, all_threads=True, *, max_threads=100)
Dump the tracebacks of all threads into *file*. If *all_threads* is
``False``, dump only the current thread.
``False``, dump only the current thread. *max_threads* caps the number
of threads dumped.
.. seealso:: :func:`traceback.print_tb`, which can be used to print a traceback object.
.. versionchanged:: 3.5
Added support for passing file descriptor to this function.
.. versionchanged:: next
Added the *max_threads* keyword argument.
Dumping the C stack
-------------------
@@ -100,7 +105,7 @@ instead of the stack, even if the operating system supports dumping stacks.
Fault handler state
-------------------
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True)
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True, *, max_threads=100)
Enable the fault handler: install handlers for the :const:`~signal.SIGSEGV`,
:const:`~signal.SIGFPE`, :const:`~signal.SIGABRT`, :const:`~signal.SIGBUS`
@@ -116,6 +121,8 @@ Fault handler state
traceback, unless the system does not support it. See :func:`dump_c_stack` for
more information on compatibility.
*max_threads* caps the number of threads dumped when a fatal signal fires.
.. versionchanged:: 3.5
Added support for passing file descriptor to this function.
@@ -133,6 +140,9 @@ Fault handler state
.. versionchanged:: 3.14
The dump now displays the C stack trace if *c_stack* is true.
.. versionchanged:: next
Added the *max_threads* keyword argument.
.. function:: disable()
Disable the fault handler: uninstall the signal handlers installed by
@@ -146,7 +156,7 @@ Fault handler state
Dumping the tracebacks after a timeout
--------------------------------------
.. function:: dump_traceback_later(timeout, repeat=False, file=sys.stderr, exit=False)
.. function:: dump_traceback_later(timeout, repeat=False, file=sys.stderr, exit=False, *, max_threads=100)
Dump the tracebacks of all threads, after a timeout of *timeout* seconds, or
every *timeout* seconds if *repeat* is ``True``. If *exit* is ``True``, call
@@ -154,7 +164,7 @@ Dumping the tracebacks after a timeout
:c:func:`!_exit` exits the process immediately, which means it doesn't do any
cleanup like flushing file buffers.) If the function is called twice, the new
call replaces previous parameters and resets the timeout. The timer has a
sub-second resolution.
sub-second resolution. *max_threads* caps the number of threads dumped.
The *file* must be kept open until the traceback is dumped or
:func:`cancel_dump_traceback_later` is called: see :ref:`issue with file
@@ -168,6 +178,9 @@ Dumping the tracebacks after a timeout
.. versionchanged:: 3.7
This function is now always available.
.. versionchanged:: next
Added the *max_threads* keyword argument.
.. function:: cancel_dump_traceback_later()
Cancel the last call to :func:`dump_traceback_later`.
@@ -176,11 +189,12 @@ Dumping the tracebacks after a timeout
Dumping the traceback on a user signal
--------------------------------------
.. function:: register(signum, file=sys.stderr, all_threads=True, chain=False)
.. function:: register(signum, file=sys.stderr, all_threads=True, chain=False, *, max_threads=100)
Register a user signal: install a handler for the *signum* signal to dump
the traceback of all threads, or of the current thread if *all_threads* is
``False``, into *file*. Call the previous handler if chain is ``True``.
*max_threads* caps the number of threads dumped.
The *file* must be kept open until the signal is unregistered by
:func:`unregister`: see :ref:`issue with file descriptors <faulthandler-fd>`.
@@ -190,6 +204,9 @@ Dumping the traceback on a user signal
.. versionchanged:: 3.5
Added support for passing file descriptor to this function.
.. versionchanged:: next
Added the *max_threads* keyword argument.
.. function:: unregister(signum)
Unregister a user signal: uninstall the handler of the *signum* signal
+9
View File
@@ -905,6 +905,15 @@ difflib
(Contributed by Jiahao Li in :gh:`134580`.)
faulthandler
------------
* Added the *max_threads* parameter in :func:`faulthandler.enable`,
:func:`faulthandler.dump_traceback`, :func:`faulthandler.dump_traceback_later`,
and :func:`faulthandler.register`.
(Contributed by Eric Froemling in :gh:`149085`.)
functools
---------
+3
View File
@@ -42,6 +42,7 @@ struct faulthandler_user_signal {
int chain;
_Py_sighandler_t previous;
PyInterpreterState *interp;
Py_ssize_t max_threads;
};
#endif /* FAULTHANDLER_USER */
@@ -57,6 +58,7 @@ struct _faulthandler_runtime_state {
void *exc_handler;
#endif
int c_stack;
Py_ssize_t max_threads;
} fatal_error;
struct {
@@ -68,6 +70,7 @@ struct _faulthandler_runtime_state {
int exit;
char *header;
size_t header_len;
Py_ssize_t max_threads;
/* The main thread always holds this lock. It is only released when
faulthandler_thread() is interrupted before this thread exits, or at
Python exit. */
+1
View File
@@ -1897,6 +1897,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mask));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(match));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_length));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_threads));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxdigits));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxevents));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxlen));
+1
View File
@@ -620,6 +620,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(mask)
STRUCT_FOR_ID(match)
STRUCT_FOR_ID(max_length)
STRUCT_FOR_ID(max_threads)
STRUCT_FOR_ID(maxdigits)
STRUCT_FOR_ID(maxevents)
STRUCT_FOR_ID(maxlen)
+1
View File
@@ -1895,6 +1895,7 @@ extern "C" {
INIT_ID(mask), \
INIT_ID(match), \
INIT_ID(max_length), \
INIT_ID(max_threads), \
INIT_ID(maxdigits), \
INIT_ID(maxevents), \
INIT_ID(maxlen), \
+2 -1
View File
@@ -61,7 +61,8 @@ extern void _Py_DumpTraceback(
extern const char* _Py_DumpTracebackThreads(
int fd,
PyInterpreterState *interp,
PyThreadState *current_tstate);
PyThreadState *current_tstate,
Py_ssize_t max_threads);
/* Write a Unicode object into the file descriptor fd. Encode the string to
ASCII using the backslashreplace error handler.
+4
View File
@@ -2260,6 +2260,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(max_threads);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(maxdigits);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
+110
View File
@@ -719,6 +719,76 @@ class FaultHandlerTests(unittest.TestCase):
def test_dump_traceback_later_twice(self):
self.check_dump_traceback_later(loops=2)
def test_dump_traceback_max_threads(self):
# max_threads caps the dump and writes "...\n" when truncated.
# Spawn N worker threads, dump with cap < N, and verify the
# marker is present and exactly CAP thread headers are written.
code = dedent("""
import faulthandler
import sys
import threading
NTHREADS = 6
CAP = 3
ready = threading.Barrier(NTHREADS + 1)
stop = threading.Event()
def worker():
ready.wait()
stop.wait()
threads = [threading.Thread(target=worker) for _ in range(NTHREADS)]
for t in threads:
t.start()
ready.wait()
try:
faulthandler.dump_traceback(file=sys.stderr, max_threads=CAP)
finally:
stop.set()
for t in threads:
t.join()
""").strip()
proc = script_helper.assert_python_ok('-c', code)
output = proc.err
# Truncation marker is written on its own line when the cap is hit.
self.assertIn(b"\n...\n", output)
# Cap of 3 means exactly 3 thread headers in the dump.
self.assertEqual(output.count(b"Thread 0x"), 3)
@skip_segfault_on_android
@unittest.skipIf(support.Py_GIL_DISABLED,
"fatal-signal handler only dumps the current thread "
"when the GIL is disabled")
def test_enable_max_threads(self):
# enable(max_threads=N) caps the thread dump produced when a
# fatal signal fires.
code = dedent("""
import faulthandler
import threading
NTHREADS = 6
CAP = 3
ready = threading.Barrier(NTHREADS + 1)
stop = threading.Event()
def worker():
ready.wait()
stop.wait()
for _ in range(NTHREADS):
threading.Thread(target=worker, daemon=True).start()
ready.wait()
faulthandler.enable(max_threads=CAP)
faulthandler._sigsegv()
""").strip()
output, exitcode = self.get_output(code)
output = '\n'.join(output)
# Cap of 3 means the dump is truncated with "..." on its own line.
self.assertIn("\n...\n", output)
self.assertNotEqual(exitcode, 0)
@unittest.skipIf(not hasattr(faulthandler, "register"),
"need faulthandler.register")
def check_register(self, filename=False, all_threads=False,
@@ -825,6 +895,46 @@ class FaultHandlerTests(unittest.TestCase):
def test_register_chain(self):
self.check_register(chain=True)
@unittest.skipIf(not hasattr(faulthandler, "register"),
"need faulthandler.register")
def test_register_max_threads(self):
# register(max_threads=N) caps the thread dump produced when
# the registered signal fires.
code = dedent("""
import faulthandler
import signal
import threading
NTHREADS = 6
CAP = 3
ready = threading.Barrier(NTHREADS + 1)
stop = threading.Event()
def worker():
ready.wait()
stop.wait()
threads = [threading.Thread(target=worker) for _ in range(NTHREADS)]
for t in threads:
t.start()
ready.wait()
try:
faulthandler.register(signal.SIGUSR1, all_threads=True,
max_threads=CAP)
signal.raise_signal(signal.SIGUSR1)
finally:
stop.set()
for t in threads:
t.join()
""").strip()
proc = script_helper.assert_python_ok('-c', code)
output = proc.err
# Cap of 3 means the dump is truncated with "..." on its own line.
self.assertIn(b"\n...\n", output)
# Cap of 3 means exactly 3 thread headers in the dump.
self.assertEqual(output.count(b"Thread 0x"), 3)
@contextmanager
def check_stderr_none(self):
stderr = sys.stderr
@@ -0,0 +1,3 @@
Add a *max_threads* keyword argument to :func:`faulthandler.dump_traceback`,
:func:`faulthandler.dump_traceback_later`, :func:`faulthandler.enable`, and
:func:`faulthandler.register`.
+141 -44
View File
@@ -6,23 +6,26 @@ preserve
# include "pycore_gc.h" // PyGC_Head
# include "pycore_runtime.h" // _Py_ID()
#endif
#include "pycore_abstract.h" // _PyNumber_Index()
#include "pycore_long.h" // _PyLong_UnsignedInt_Converter()
#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
PyDoc_STRVAR(faulthandler_dump_traceback_py__doc__,
"dump_traceback($module, /, file=sys.stderr, all_threads=True)\n"
"dump_traceback($module, /, file=sys.stderr, all_threads=True, *,\n"
" max_threads=100)\n"
"--\n"
"\n"
"Dump the traceback of the current thread into file.\n"
"\n"
"Dump the traceback of all threads if all_threads is true.");
"Dump the traceback of all threads if all_threads is true. max_threads\n"
"caps the number of threads dumped.");
#define FAULTHANDLER_DUMP_TRACEBACK_PY_METHODDEF \
{"dump_traceback", _PyCFunction_CAST(faulthandler_dump_traceback_py), METH_FASTCALL|METH_KEYWORDS, faulthandler_dump_traceback_py__doc__},
static PyObject *
faulthandler_dump_traceback_py_impl(PyObject *module, PyObject *file,
int all_threads);
int all_threads, Py_ssize_t max_threads);
static PyObject *
faulthandler_dump_traceback_py(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -30,7 +33,7 @@ faulthandler_dump_traceback_py(PyObject *module, PyObject *const *args, Py_ssize
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 2
#define NUM_KEYWORDS 3
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@@ -39,7 +42,7 @@ faulthandler_dump_traceback_py(PyObject *module, PyObject *const *args, Py_ssize
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(file), &_Py_ID(all_threads), },
.ob_item = { &_Py_ID(file), &_Py_ID(all_threads), &_Py_ID(max_threads), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -48,17 +51,18 @@ faulthandler_dump_traceback_py(PyObject *module, PyObject *const *args, Py_ssize
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"file", "all_threads", NULL};
static const char * const _keywords[] = {"file", "all_threads", "max_threads", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "dump_traceback",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
PyObject *argsbuf[3];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
PyObject *file = NULL;
int all_threads = 1;
Py_ssize_t max_threads = 100;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 0, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -74,12 +78,33 @@ faulthandler_dump_traceback_py(PyObject *module, PyObject *const *args, Py_ssize
goto skip_optional_pos;
}
}
all_threads = PyObject_IsTrue(args[1]);
if (all_threads < 0) {
goto exit;
if (args[1]) {
all_threads = PyObject_IsTrue(args[1]);
if (all_threads < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_pos;
}
}
skip_optional_pos:
return_value = faulthandler_dump_traceback_py_impl(module, file, all_threads);
if (!noptargs) {
goto skip_optional_kwonly;
}
{
Py_ssize_t ival = -1;
PyObject *iobj = _PyNumber_Index(args[2]);
if (iobj != NULL) {
ival = PyLong_AsSsize_t(iobj);
Py_DECREF(iobj);
}
if (ival == -1 && PyErr_Occurred()) {
goto exit;
}
max_threads = ival;
}
skip_optional_kwonly:
return_value = faulthandler_dump_traceback_py_impl(module, file, all_threads, max_threads);
exit:
return return_value;
@@ -149,7 +174,8 @@ exit:
}
PyDoc_STRVAR(faulthandler_py_enable__doc__,
"enable($module, /, file=sys.stderr, all_threads=True, c_stack=True)\n"
"enable($module, /, file=sys.stderr, all_threads=True, c_stack=True, *,\n"
" max_threads=100)\n"
"--\n"
"\n"
"Enable the fault handler.");
@@ -159,7 +185,8 @@ PyDoc_STRVAR(faulthandler_py_enable__doc__,
static PyObject *
faulthandler_py_enable_impl(PyObject *module, PyObject *file,
int all_threads, int c_stack);
int all_threads, int c_stack,
Py_ssize_t max_threads);
static PyObject *
faulthandler_py_enable(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -167,7 +194,7 @@ faulthandler_py_enable(PyObject *module, PyObject *const *args, Py_ssize_t nargs
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 3
#define NUM_KEYWORDS 4
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@@ -176,7 +203,7 @@ faulthandler_py_enable(PyObject *module, PyObject *const *args, Py_ssize_t nargs
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(file), &_Py_ID(all_threads), &_Py_ID(c_stack), },
.ob_item = { &_Py_ID(file), &_Py_ID(all_threads), &_Py_ID(c_stack), &_Py_ID(max_threads), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -185,18 +212,19 @@ faulthandler_py_enable(PyObject *module, PyObject *const *args, Py_ssize_t nargs
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"file", "all_threads", "c_stack", NULL};
static const char * const _keywords[] = {"file", "all_threads", "c_stack", "max_threads", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "enable",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[3];
PyObject *argsbuf[4];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
PyObject *file = NULL;
int all_threads = 1;
int c_stack = 1;
Py_ssize_t max_threads = 100;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 0, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -221,12 +249,33 @@ faulthandler_py_enable(PyObject *module, PyObject *const *args, Py_ssize_t nargs
goto skip_optional_pos;
}
}
c_stack = PyObject_IsTrue(args[2]);
if (c_stack < 0) {
goto exit;
if (args[2]) {
c_stack = PyObject_IsTrue(args[2]);
if (c_stack < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_pos;
}
}
skip_optional_pos:
return_value = faulthandler_py_enable_impl(module, file, all_threads, c_stack);
if (!noptargs) {
goto skip_optional_kwonly;
}
{
Py_ssize_t ival = -1;
PyObject *iobj = _PyNumber_Index(args[3]);
if (iobj != NULL) {
ival = PyLong_AsSsize_t(iobj);
Py_DECREF(iobj);
}
if (ival == -1 && PyErr_Occurred()) {
goto exit;
}
max_threads = ival;
}
skip_optional_kwonly:
return_value = faulthandler_py_enable_impl(module, file, all_threads, c_stack, max_threads);
exit:
return return_value;
@@ -280,13 +329,14 @@ exit:
PyDoc_STRVAR(faulthandler_dump_traceback_later__doc__,
"dump_traceback_later($module, /, timeout, repeat=False,\n"
" file=sys.stderr, exit=False)\n"
" file=sys.stderr, exit=False, *, max_threads=100)\n"
"--\n"
"\n"
"Dump the traceback of all threads in timeout seconds.\n"
"\n"
"If repeat is true, the tracebacks of all threads are dumped every timeout\n"
"seconds. If exit is true, call _exit(1) which is not safe.");
"seconds. If exit is true, call _exit(1) which is not safe. max_threads\n"
"caps the number of threads dumped.");
#define FAULTHANDLER_DUMP_TRACEBACK_LATER_METHODDEF \
{"dump_traceback_later", _PyCFunction_CAST(faulthandler_dump_traceback_later), METH_FASTCALL|METH_KEYWORDS, faulthandler_dump_traceback_later__doc__},
@@ -294,7 +344,8 @@ PyDoc_STRVAR(faulthandler_dump_traceback_later__doc__,
static PyObject *
faulthandler_dump_traceback_later_impl(PyObject *module,
PyObject *timeout_obj, int repeat,
PyObject *file, int exit);
PyObject *file, int exit,
Py_ssize_t max_threads);
static PyObject *
faulthandler_dump_traceback_later(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -302,7 +353,7 @@ faulthandler_dump_traceback_later(PyObject *module, PyObject *const *args, Py_ss
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 4
#define NUM_KEYWORDS 5
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@@ -311,7 +362,7 @@ faulthandler_dump_traceback_later(PyObject *module, PyObject *const *args, Py_ss
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(timeout), &_Py_ID(repeat), &_Py_ID(file), &_Py_ID(exit), },
.ob_item = { &_Py_ID(timeout), &_Py_ID(repeat), &_Py_ID(file), &_Py_ID(exit), &_Py_ID(max_threads), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -320,19 +371,20 @@ faulthandler_dump_traceback_later(PyObject *module, PyObject *const *args, Py_ss
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"timeout", "repeat", "file", "exit", NULL};
static const char * const _keywords[] = {"timeout", "repeat", "file", "exit", "max_threads", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "dump_traceback_later",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[4];
PyObject *argsbuf[5];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
PyObject *timeout_obj;
int repeat = 0;
PyObject *file = NULL;
int exit = 0;
Py_ssize_t max_threads = 100;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 1, /*maxpos*/ 4, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -358,12 +410,33 @@ faulthandler_dump_traceback_later(PyObject *module, PyObject *const *args, Py_ss
goto skip_optional_pos;
}
}
exit = PyObject_IsTrue(args[3]);
if (exit < 0) {
goto exit;
if (args[3]) {
exit = PyObject_IsTrue(args[3]);
if (exit < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_pos;
}
}
skip_optional_pos:
return_value = faulthandler_dump_traceback_later_impl(module, timeout_obj, repeat, file, exit);
if (!noptargs) {
goto skip_optional_kwonly;
}
{
Py_ssize_t ival = -1;
PyObject *iobj = _PyNumber_Index(args[4]);
if (iobj != NULL) {
ival = PyLong_AsSsize_t(iobj);
Py_DECREF(iobj);
}
if (ival == -1 && PyErr_Occurred()) {
goto exit;
}
max_threads = ival;
}
skip_optional_kwonly:
return_value = faulthandler_dump_traceback_later_impl(module, timeout_obj, repeat, file, exit, max_threads);
exit:
return return_value;
@@ -391,20 +464,22 @@ faulthandler_cancel_dump_traceback_later_py(PyObject *module, PyObject *Py_UNUSE
PyDoc_STRVAR(faulthandler_register_py__doc__,
"register($module, /, signum, file=sys.stderr, all_threads=True,\n"
" chain=False)\n"
" chain=False, *, max_threads=100)\n"
"--\n"
"\n"
"Register a handler for the signal \'signum\'.\n"
"\n"
"Dump the traceback of the current thread, or of all threads if\n"
"all_threads is True, into file.");
"all_threads is True, into file. max_threads caps the number of threads\n"
"dumped.");
#define FAULTHANDLER_REGISTER_PY_METHODDEF \
{"register", _PyCFunction_CAST(faulthandler_register_py), METH_FASTCALL|METH_KEYWORDS, faulthandler_register_py__doc__},
static PyObject *
faulthandler_register_py_impl(PyObject *module, int signum, PyObject *file,
int all_threads, int chain);
int all_threads, int chain,
Py_ssize_t max_threads);
static PyObject *
faulthandler_register_py(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -412,7 +487,7 @@ faulthandler_register_py(PyObject *module, PyObject *const *args, Py_ssize_t nar
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 4
#define NUM_KEYWORDS 5
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@@ -421,7 +496,7 @@ faulthandler_register_py(PyObject *module, PyObject *const *args, Py_ssize_t nar
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(signum), &_Py_ID(file), &_Py_ID(all_threads), &_Py_ID(chain), },
.ob_item = { &_Py_ID(signum), &_Py_ID(file), &_Py_ID(all_threads), &_Py_ID(chain), &_Py_ID(max_threads), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -430,19 +505,20 @@ faulthandler_register_py(PyObject *module, PyObject *const *args, Py_ssize_t nar
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"signum", "file", "all_threads", "chain", NULL};
static const char * const _keywords[] = {"signum", "file", "all_threads", "chain", "max_threads", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "register",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[4];
PyObject *argsbuf[5];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
int signum;
PyObject *file = NULL;
int all_threads = 1;
int chain = 0;
Py_ssize_t max_threads = 100;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 1, /*maxpos*/ 4, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -471,12 +547,33 @@ faulthandler_register_py(PyObject *module, PyObject *const *args, Py_ssize_t nar
goto skip_optional_pos;
}
}
chain = PyObject_IsTrue(args[3]);
if (chain < 0) {
goto exit;
if (args[3]) {
chain = PyObject_IsTrue(args[3]);
if (chain < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_pos;
}
}
skip_optional_pos:
return_value = faulthandler_register_py_impl(module, signum, file, all_threads, chain);
if (!noptargs) {
goto skip_optional_kwonly;
}
{
Py_ssize_t ival = -1;
PyObject *iobj = _PyNumber_Index(args[4]);
if (iobj != NULL) {
ival = PyLong_AsSsize_t(iobj);
Py_DECREF(iobj);
}
if (ival == -1 && PyErr_Occurred()) {
goto exit;
}
max_threads = ival;
}
skip_optional_kwonly:
return_value = faulthandler_register_py_impl(module, signum, file, all_threads, chain, max_threads);
exit:
return return_value;
@@ -685,4 +782,4 @@ exit:
#ifndef FAULTHANDLER__RAISE_EXCEPTION_METHODDEF
#define FAULTHANDLER__RAISE_EXCEPTION_METHODDEF
#endif /* !defined(FAULTHANDLER__RAISE_EXCEPTION_METHODDEF) */
/*[clinic end generated code: output=31bf0149d0d02ccf input=a9049054013a1b77]*/
/*[clinic end generated code: output=2452d767c85130a6 input=a9049054013a1b77]*/
+40 -18
View File
@@ -185,7 +185,8 @@ get_thread_state(void)
static void
faulthandler_dump_traceback(int fd, int all_threads,
PyInterpreterState *interp)
PyInterpreterState *interp,
Py_ssize_t max_threads)
{
static volatile int reentrant = 0;
@@ -205,7 +206,7 @@ faulthandler_dump_traceback(int fd, int all_threads,
PyThreadState *tstate = PyGILState_GetThisThreadState();
if (all_threads == 1) {
(void)_Py_DumpTracebackThreads(fd, NULL, tstate);
(void)_Py_DumpTracebackThreads(fd, NULL, tstate, max_threads);
}
else {
if (all_threads == FT_IGNORE_ALL_THREADS) {
@@ -243,16 +244,19 @@ faulthandler.dump_traceback as faulthandler_dump_traceback_py
file: object(py_default="sys.stderr") = NULL
all_threads: bool = True
*
max_threads: Py_ssize_t = 100
Dump the traceback of the current thread into file.
Dump the traceback of all threads if all_threads is true.
Dump the traceback of all threads if all_threads is true. max_threads
caps the number of threads dumped.
[clinic start generated code]*/
static PyObject *
faulthandler_dump_traceback_py_impl(PyObject *module, PyObject *file,
int all_threads)
/*[clinic end generated code: output=34efece0ca18314f input=b832ec55e27a7898]*/
int all_threads, Py_ssize_t max_threads)
/*[clinic end generated code: output=ee1bbc2668e56e77 input=38630eb40e641de6]*/
{
PyThreadState *tstate;
const char *errmsg;
@@ -273,7 +277,7 @@ faulthandler_dump_traceback_py_impl(PyObject *module, PyObject *file,
/* gh-128400: Accessing other thread states while they're running
* isn't safe if those threads are running. */
_PyEval_StopTheWorld(interp);
errmsg = _Py_DumpTracebackThreads(fd, NULL, tstate);
errmsg = _Py_DumpTracebackThreads(fd, NULL, tstate, max_threads);
_PyEval_StartTheWorld(interp);
if (errmsg != NULL) {
PyErr_SetString(PyExc_RuntimeError, errmsg);
@@ -409,7 +413,8 @@ faulthandler_fatal_error(int signum)
}
faulthandler_dump_traceback(fd, deduce_all_threads(),
fatal_error.interp);
fatal_error.interp,
fatal_error.max_threads);
faulthandler_dump_c_stack(fd);
_Py_DumpExtensionModules(fd, fatal_error.interp);
@@ -485,7 +490,8 @@ faulthandler_exc_handler(struct _EXCEPTION_POINTERS *exc_info)
}
faulthandler_dump_traceback(fd, deduce_all_threads(),
fatal_error.interp);
fatal_error.interp,
fatal_error.max_threads);
faulthandler_dump_c_stack(fd);
/* call the next exception handler */
@@ -590,14 +596,17 @@ faulthandler.enable as faulthandler_py_enable
file: object(py_default="sys.stderr") = NULL
all_threads: bool = True
c_stack: bool = True
*
max_threads: Py_ssize_t = 100
Enable the fault handler.
[clinic start generated code]*/
static PyObject *
faulthandler_py_enable_impl(PyObject *module, PyObject *file,
int all_threads, int c_stack)
/*[clinic end generated code: output=580d89b5eb62f1cb input=77277746a88b25ca]*/
int all_threads, int c_stack,
Py_ssize_t max_threads)
/*[clinic end generated code: output=7ee655332317c47a input=e64759714f27b466]*/
{
int fd;
PyThreadState *tstate;
@@ -617,6 +626,7 @@ faulthandler_py_enable_impl(PyObject *module, PyObject *file,
fatal_error.all_threads = all_threads;
fatal_error.interp = PyThreadState_GetInterpreter(tstate);
fatal_error.c_stack = c_stack;
fatal_error.max_threads = max_threads;
if (faulthandler_enable() < 0) {
return NULL;
@@ -703,7 +713,8 @@ faulthandler_thread(void *unused)
(void)_Py_write_noraise(thread.fd, thread.header, (int)thread.header_len);
errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL);
errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL,
thread.max_threads);
ok = (errmsg == NULL);
if (thread.exit)
@@ -777,18 +788,22 @@ faulthandler.dump_traceback_later
repeat: bool = False
file: object(py_default="sys.stderr") = NULL
exit: bool = False
*
max_threads: Py_ssize_t = 100
Dump the traceback of all threads in timeout seconds.
If repeat is true, the tracebacks of all threads are dumped every timeout
seconds. If exit is true, call _exit(1) which is not safe.
seconds. If exit is true, call _exit(1) which is not safe. max_threads
caps the number of threads dumped.
[clinic start generated code]*/
static PyObject *
faulthandler_dump_traceback_later_impl(PyObject *module,
PyObject *timeout_obj, int repeat,
PyObject *file, int exit)
/*[clinic end generated code: output=a24d80d694d25ba2 input=fd005625ecc2ba9a]*/
PyObject *file, int exit,
Py_ssize_t max_threads)
/*[clinic end generated code: output=543a0f3807113394 input=6836555ee157ddb4]*/
{
PyTime_t timeout, timeout_us;
int fd;
@@ -861,6 +876,7 @@ faulthandler_dump_traceback_later_impl(PyObject *module,
thread.exit = exit;
thread.header = header;
thread.header_len = header_len;
thread.max_threads = max_threads;
/* Arm these locks to serve as events when released */
PyThread_acquire_lock(thread.running, 1);
@@ -945,7 +961,8 @@ faulthandler_user(int signum)
if (!user->enabled)
return;
faulthandler_dump_traceback(user->fd, user->all_threads, user->interp);
faulthandler_dump_traceback(user->fd, user->all_threads, user->interp,
user->max_threads);
#ifdef HAVE_SIGACTION
if (user->chain) {
@@ -995,17 +1012,21 @@ faulthandler.register as faulthandler_register_py
file: object(py_default="sys.stderr") = NULL
all_threads: bool = True
chain: bool = False
*
max_threads: Py_ssize_t = 100
Register a handler for the signal 'signum'.
Dump the traceback of the current thread, or of all threads if
all_threads is True, into file.
all_threads is True, into file. max_threads caps the number of threads
dumped.
[clinic start generated code]*/
static PyObject *
faulthandler_register_py_impl(PyObject *module, int signum, PyObject *file,
int all_threads, int chain)
/*[clinic end generated code: output=1f770cee150a56cd input=ae9de829e850907b]*/
int all_threads, int chain,
Py_ssize_t max_threads)
/*[clinic end generated code: output=d63a5b4f388dee5f input=c75096a20de502fe]*/
{
int fd;
user_signal_t *user;
@@ -1056,6 +1077,7 @@ faulthandler_register_py_impl(PyObject *module, int signum, PyObject *file,
user->all_threads = all_threads;
user->chain = chain;
user->interp = PyThreadState_GetInterpreter(tstate);
user->max_threads = max_threads;
user->enabled = 1;
Py_RETURN_NONE;
+1 -1
View File
@@ -3342,7 +3342,7 @@ _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState *interp,
/* display the current Python stack */
#ifndef Py_GIL_DISABLED
_Py_DumpTracebackThreads(fd, interp, tstate);
_Py_DumpTracebackThreads(fd, interp, tstate, 0);
#else
_Py_DumpTraceback(fd, tstate);
#endif
+9 -4
View File
@@ -55,7 +55,7 @@
#define MAX_STRING_LENGTH 500
#define MAX_FRAME_DEPTH 100
#define MAX_NTHREADS 100
#define DEFAULT_MAX_NTHREADS 100
/* Function from Parser/tokenizer/file_tokenizer.c */
extern char* _PyTokenizer_FindEncodingFilename(int, PyObject *);
@@ -1265,8 +1265,13 @@ write_thread_id(int fd, PyThreadState *tstate, int is_current)
handlers if signals were received. */
const char* _Py_NO_SANITIZE_THREAD
_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
PyThreadState *current_tstate)
PyThreadState *current_tstate,
Py_ssize_t max_threads)
{
if (max_threads == 0) {
max_threads = DEFAULT_MAX_NTHREADS;
}
if (current_tstate == NULL) {
/* _Py_DumpTracebackThreads() is called from signal handlers by
faulthandler.
@@ -1310,13 +1315,13 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
return "unable to get the thread head state";
/* Dump the traceback of each thread */
unsigned int nthreads = 0;
Py_ssize_t nthreads = 0;
_Py_BEGIN_SUPPRESS_IPH
do
{
if (nthreads != 0)
PUTS(fd, "\n");
if (nthreads >= MAX_NTHREADS) {
if (nthreads >= max_threads) {
PUTS(fd, "...\n");
break;
}