Commit 024e37ad authored by Victor Stinner's avatar Victor Stinner

Issue #11393: Add the new faulthandler module

parent d8545627
......@@ -10,7 +10,8 @@ allowing you to identify bottlenecks in your programs.
.. toctree::
bdb.rst
faulthandler.rst
pdb.rst
profile.rst
timeit.rst
trace.rst
\ No newline at end of file
trace.rst
:mod:`faulthandler` --- Dump the Python traceback
=================================================
.. module:: faulthandler
:synopsis: Dump the Python traceback.
This module contains functions to dump the Python traceback explicitly, on a
fault, after a timeout or on a user signal. Call :func:`faulthandler.enable` to
install fault handlers for :const:`SIGSEGV`, :const:`SIGFPE`, :const:`SIGBUS`
and :const:`SIGILL` signals. You can also enable them at startup by setting the
:envvar:`PYTHONFAULTHANDLER` environment variable or by using :option:`-X`
``faulthandler`` command line option.
The fault handler is compatible with system fault handlers like Apport or
the Windows fault handler. The module uses an alternative stack for signal
handlers, if the :c:func:`sigaltstack` function is available, to be able to
dump the traceback even on a stack overflow.
The fault handler is called on catastrophic cases and so can only use
signal-safe functions (e.g. it cannot allocate memory on the heap). That's why
the traceback is limited: only support ASCII encoding (use the
``backslashreplace`` error handler), limit each string to 100 characters, don't
print the source code (only the filename, the function name and the line
number), limit to 100 frames and 100 threads.
By default, the Python traceback is written to :data:`sys.stderr`. Start your
graphical applications in a terminal and run your server in foreground to see
the traceback, or specify a log file to :func:`faulthandler.enable()`.
The module is implemented in C to be able to dump a traceback on a crash or
when Python is blocked (e.g. deadlock).
.. versionadded:: 3.3
Dump the traceback
------------------
.. function:: dump_traceback(file=sys.stderr, all_threads=False)
Dump the traceback of the current thread, or of all threads if *all_threads*
is ``True``, into *file*.
Fault handler state
-------------------
.. function:: enable(file=sys.stderr, all_threads=False)
Enable the fault handler: install handlers for :const:`SIGSEGV`,
:const:`SIGFPE`, :const:`SIGBUS` and :const:`SIGILL` signals to dump the
Python traceback. It dumps the traceback of the current thread, or all
threads if *all_threads* is ``True``, into *file*.
.. function:: disable()
Disable the fault handler: uninstall the signal handlers installed by
:func:`enable`.
.. function:: is_enabled()
Check if the fault handler is enabled.
Dump the tracebacks after a timeout
-----------------------------------
.. function:: dump_tracebacks_later(timeout, repeat=False, file=sys.stderr, exit=False)
Dump the tracebacks of all threads, after a timeout of *timeout* seconds, or
each *timeout* seconds if *repeat* is ``True``. If *exit* is True, call
:cfunc:`_exit` with status=1 after dumping the tracebacks to terminate
immediatly the process, which is not safe. For example, :cfunc:`_exit`
doesn't flush file buffers. If the function is called twice, the new call
replaces previous parameters (resets the timeout). The timer has a
sub-second resolution.
This function is implemented using a watchdog thread, and therefore is
not available if Python is compiled with threads disabled.
.. function:: cancel_dump_traceback_later()
Cancel the last call to :func:`dump_traceback_later`.
Dump the traceback on a user signal
-----------------------------------
.. function:: register(signum, file=sys.stderr, all_threads=False)
Register a user signal: install a handler for the *signum* signal to dump
the traceback of the current thread, or of all threads if *all_threads* is
``True``, into *file*.
Not available on Windows.
.. function:: unregister(signum)
Unregister a user signal: uninstall the handler of the *signum* signal
installed by :func:`register`.
Not available on Windows.
File descriptor issue
---------------------
:func:`enable`, :func:`dump_traceback_later` and :func:`register` keep the
file descriptor of their *file* argument. If the file is closed and its file
descriptor is reused by a new file, or if :func:`os.dup2` is used to replace
the file descriptor, the traceback will be written into a different file. Call
these functions again each time that the file is replaced.
Example
-------
Example of a segmentation fault on Linux: ::
$ python -q -X faulthandler
>>> import ctypes
>>> ctypes.string_at(0)
Fatal Python error: Segmentation fault
Traceback (most recent call first):
File "/home/python/cpython/Lib/ctypes/__init__.py", line 486 in string_at
File "<stdin>", line 1 in <module>
Segmentation fault
......@@ -498,6 +498,13 @@ These environment variables influence Python's behavior.
separated string, it is equivalent to specifying :option:`-W` multiple
times.
.. envvar:: PYTHONFAULTHANDLER
If this environment variable is set, :func:`faulthandler.enable` is called
at startup: install a handler for :const:`SIGSEGV`, :const:`SIGFPE`,
:const:`SIGBUS` and :const:`SIGILL` signals to dump the Python traceback.
This is equivalent to :option:`-X` ``faulthandler`` option.
Debug-mode variables
~~~~~~~~~~~~~~~~~~~~
......
......@@ -68,6 +68,14 @@ New, Improved, and Deprecated Modules
* Stub
faulthandler
------------
New module: :mod:`faulthandler`.
* :envvar:`PYTHONFAULTHANDLER`
* :option:`-X` ``faulthandler``
os
--
......
......@@ -5,6 +5,8 @@
extern "C" {
#endif
#include "pystate.h"
struct _frame;
/* Traceback interface */
......@@ -28,6 +30,44 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int);
PyAPI_DATA(PyTypeObject) PyTraceBack_Type;
#define PyTraceBack_Check(v) (Py_TYPE(v) == &PyTraceBack_Type)
/* Write the Python traceback into the file 'fd'. For example:
Traceback (most recent call first):
File "xxx", line xxx in <xxx>
File "xxx", line xxx in <xxx>
...
File "xxx", line xxx in <xxx>
Return 0 on success, -1 on error.
This function is written for debug purpose only, to dump the traceback in
the worst case: after a segmentation fault, at fatal error, etc. That's why,
it is very limited. Strings are truncated to 100 characters and encoded to
ASCII with backslashreplace. It doesn't write the source code, only the
function name, filename and line number of each frame. Write only the first
100 frames: if the traceback is truncated, write the line " ...".
This function is signal safe. */
PyAPI_DATA(int) _Py_DumpTraceback(
int fd,
PyThreadState *tstate);
/* Write the traceback of all threads into the file 'fd'. current_thread can be
NULL. Return NULL on success, or an error message on error.
This function is written for debug purpose only. It calls
_Py_DumpTraceback() for each thread, and so has the same limitations. It
only write the traceback of the first 100 threads: write "..." if there are
more threads.
This function is signal safe. */
PyAPI_DATA(const char*) _Py_DumpTracebackThreads(
int fd, PyInterpreterState *interp,
PyThreadState *current_thread);
#ifdef __cplusplus
}
#endif
......
......@@ -157,6 +157,7 @@ option '-uall,-gui'.
"""
import builtins
import faulthandler
import getopt
import json
import os
......@@ -490,6 +491,7 @@ def main(tests=None, testdir=None, verbose=0, quiet=False,
next_single_test = alltests[alltests.index(selected[0])+1]
except IndexError:
next_single_test = None
selected = ['test_faulthandler']
# Remove all the tests that precede start if it's set.
if start:
try:
......@@ -1551,6 +1553,9 @@ def _make_temp_dir_for_build(TEMPDIR):
return TEMPDIR, TESTCWD
if __name__ == '__main__':
# Display the Python traceback on segfault and division by zero
faulthandler.enable()
# Remove regrtest.py's own directory from the module search path. Despite
# the elimination of implicit relative imports, this is still needed to
# ensure that submodules of the test package do not inappropriately appear
......
......@@ -56,11 +56,12 @@ def assert_python_failure(*args, **env_vars):
"""
return _assert_python(False, *args, **env_vars)
def spawn_python(*args):
def spawn_python(*args, **kw):
cmd_line = [sys.executable, '-E']
cmd_line.extend(args)
return subprocess.Popen(cmd_line, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
**kw)
def kill_python(p):
p.stdin.close()
......
This diff is collapsed.
......@@ -87,6 +87,8 @@ Core and Builtins
Library
-------
- Issue #11393: Add the new faulthandler module.
- Issue #11618: Fix the timeout logic in threading.Lock.acquire() under Windows.
- Removed the 'strict' argument to email.parser.Parser, which has been
......
......@@ -127,6 +127,9 @@ _io -I$(srcdir)/Modules/_io _io/_iomodule.c _io/iobase.c _io/fileio.c _io/bytesi
# builtin module avoids some bootstrapping problems and reduces overhead.
zipimport zipimport.c
# faulthandler module
faulthandler faulthandler.c
# The rest of the modules listed in this file are all commented out by
# default. Usually they can be detected and built as dynamically
# loaded modules by the new setup.py script added in Python 2.1. If
......
This diff is collapsed.
......@@ -100,6 +100,7 @@ static char *usage_5 =
" The default module search path uses %s.\n"
"PYTHONCASEOK : ignore case in 'import' statements (Windows).\n"
"PYTHONIOENCODING: Encoding[:errors] used for stdin/stdout/stderr.\n"
"PYTHONFAULTHANDLER: dump the Python traceback on fatal errors.\n"
;
static int
......
......@@ -12,6 +12,7 @@ extern PyObject* PyInit_audioop(void);
extern PyObject* PyInit_binascii(void);
extern PyObject* PyInit_cmath(void);
extern PyObject* PyInit_errno(void);
extern PyObject* PyInit_faulthandler(void);
extern PyObject* PyInit_gc(void);
extern PyObject* PyInit_math(void);
extern PyObject* PyInit__md5(void);
......@@ -82,6 +83,7 @@ struct _inittab _PyImport_Inittab[] = {
{"binascii", PyInit_binascii},
{"cmath", PyInit_cmath},
{"errno", PyInit_errno},
{"faulthandler", PyInit_faulthandler},
{"gc", PyInit_gc},
{"math", PyInit_math},
{"nt", PyInit_nt}, /* Use the NT os functions, not posix */
......
......@@ -1086,6 +1086,10 @@
RelativePath="..\Modules\errnomodule.c"
>
</File>
<File
RelativePath="..\Modules\faulthandler.c"
>
</File>
<File
RelativePath="..\Modules\gcmodule.c"
>
......
......@@ -70,6 +70,8 @@ extern void _PyUnicode_Init(void);
extern void _PyUnicode_Fini(void);
extern int _PyLong_Init(void);
extern void PyLong_Fini(void);
extern int _PyFaulthandler_Init(void);
extern void _PyFaulthandler_Fini(void);
#ifdef WITH_THREAD
extern void _PyGILState_Init(PyInterpreterState *, PyThreadState *);
......@@ -286,6 +288,10 @@ Py_InitializeEx(int install_sigs)
_PyImportHooks_Init();
/* initialize the faulthandler module */
if (_PyFaulthandler_Init())
Py_FatalError("Py_Initialize: can't initialize faulthandler");
/* Initialize _warnings. */
_PyWarnings_Init();
......@@ -454,6 +460,9 @@ Py_Finalize(void)
/* Destroy the database used by _PyImport_{Fixup,Find}Extension */
_PyImport_Fini();
/* unload faulthandler module */
_PyFaulthandler_Fini();
/* Debugging stuff */
#ifdef COUNT_ALLOCS
dump_counts(stdout);
......@@ -2100,11 +2109,23 @@ cleanup:
void
Py_FatalError(const char *msg)
{
const int fd = fileno(stderr);
PyThreadState *tstate;
fprintf(stderr, "Fatal Python error: %s\n", msg);
fflush(stderr); /* it helps in Windows debug build */
if (PyErr_Occurred()) {
PyErr_PrintEx(0);
}
else {
tstate = _Py_atomic_load_relaxed(&_PyThreadState_Current);
if (tstate != NULL) {
fputc('\n', stderr);
fflush(stderr);
_Py_DumpTraceback(fd, tstate);
}
}
#ifdef MS_WINDOWS
{
size_t len = strlen(msg);
......
......@@ -13,6 +13,11 @@
#define OFF(x) offsetof(PyTracebackObject, x)
#define PUTS(fd, str) write(fd, str, strlen(str))
#define MAX_STRING_LENGTH 100
#define MAX_FRAME_DEPTH 100
#define MAX_NTHREADS 100
/* Method from Parser/tokenizer.c */
extern char * PyTokenizer_FindEncoding(int);
......@@ -402,3 +407,233 @@ PyTraceBack_Print(PyObject *v, PyObject *f)
err = tb_printinternal((PyTracebackObject *)v, f, limit);
return err;
}
/* Reverse a string. For example, "abcd" becomes "dcba".
This function is signal safe. */
static void
reverse_string(char *text, const size_t len)
{
char tmp;
size_t i, j;
if (len == 0)
return;
for (i=0, j=len-1; i < j; i++, j--) {
tmp = text[i];
text[i] = text[j];
text[j] = tmp;
}
}
/* Format an integer in range [0; 999999] to decimal,
and write it into the file fd.
This function is signal safe. */
static void
dump_decimal(int fd, int value)
{
char buffer[7];
int len;
if (value < 0 || 999999 < value)
return;
len = 0;
do {
buffer[len] = '0' + (value % 10);
value /= 10;
len++;
} while (value);
reverse_string(buffer, len);
write(fd, buffer, len);
}
/* Format an integer in range [0; 0xffffffff] to hexdecimal of 'width' digits,
and write it into the file fd.
This function is signal safe. */
static void
dump_hexadecimal(int width, unsigned long value, int fd)
{
const char *hexdigits = "0123456789abcdef";
int len;
char buffer[sizeof(unsigned long) * 2 + 1];
len = 0;
do {
buffer[len] = hexdigits[value & 15];
value >>= 4;
len++;
} while (len < width || value);
reverse_string(buffer, len);
write(fd, buffer, len);
}
/* Write an unicode object into the file fd using ascii+backslashreplace.
This function is signal safe. */
static void
dump_ascii(int fd, PyObject *text)
{
Py_ssize_t i, size;
int truncated;
Py_UNICODE *u;
char c;
size = PyUnicode_GET_SIZE(text);
u = PyUnicode_AS_UNICODE(text);
if (MAX_STRING_LENGTH < size) {
size = MAX_STRING_LENGTH;
truncated = 1;
}
else
truncated = 0;
for (i=0; i < size; i++, u++) {
if (*u < 128) {
c = (char)*u;
write(fd, &c, 1);
}
else if (*u < 256) {
PUTS(fd, "\\x");
dump_hexadecimal(2, *u, fd);
}
else
#ifdef Py_UNICODE_WIDE
if (*u < 65536)
#endif
{
PUTS(fd, "\\u");
dump_hexadecimal(4, *u, fd);
#ifdef Py_UNICODE_WIDE
}
else {
PUTS(fd, "\\U");
dump_hexadecimal(8, *u, fd);
#endif
}
}
if (truncated)
PUTS(fd, "...");
}
/* Write a frame into the file fd: "File "xxx", line xxx in xxx".
This function is signal safe. */
static void
dump_frame(int fd, PyFrameObject *frame)
{
PyCodeObject *code;
int lineno;
code = frame->f_code;
PUTS(fd, " File ");
if (code != NULL && code->co_filename != NULL
&& PyUnicode_Check(code->co_filename))
{
write(fd, "\"", 1);
dump_ascii(fd, code->co_filename);
write(fd, "\"", 1);
} else {
PUTS(fd, "???");
}
/* PyFrame_GetLineNumber() was introduced in Python 2.7.0 and 3.2.0 */
lineno = PyCode_Addr2Line(frame->f_code, frame->f_lasti);
PUTS(fd, ", line ");
dump_decimal(fd, lineno);
PUTS(fd, " in ");
if (code != NULL && code->co_name != NULL
&& PyUnicode_Check(code->co_name))
dump_ascii(fd, code->co_name);
else
PUTS(fd, "???");
write(fd, "\n", 1);
}
static int
dump_traceback(int fd, PyThreadState *tstate, int write_header)
{
PyFrameObject *frame;
unsigned int depth;
frame = _PyThreadState_GetFrame(tstate);
if (frame == NULL)
return -1;
if (write_header)
PUTS(fd, "Traceback (most recent call first):\n");
depth = 0;
while (frame != NULL) {
if (MAX_FRAME_DEPTH <= depth) {
PUTS(fd, " ...\n");
break;
}
if (!PyFrame_Check(frame))
break;
dump_frame(fd, frame);
frame = frame->f_back;
depth++;
}
return 0;
}
int
_Py_DumpTraceback(int fd, PyThreadState *tstate)
{
return dump_traceback(fd, tstate, 1);
}
/* Write the thread identifier into the file 'fd': "Current thread 0xHHHH:\" if
is_current is true, "Thread 0xHHHH:\n" otherwise.
This function is signal safe. */
static void
write_thread_id(int fd, PyThreadState *tstate, int is_current)
{
if (is_current)
PUTS(fd, "Current thread 0x");
else
PUTS(fd, "Thread 0x");
dump_hexadecimal(sizeof(long)*2, (unsigned long)tstate->thread_id, fd);
PUTS(fd, ":\n");
}
const char*
_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
PyThreadState *current_thread)
{
PyThreadState *tstate;
unsigned int nthreads;
/* Get the current interpreter from the current thread */
tstate = PyInterpreterState_ThreadHead(interp);
if (tstate == NULL)
return "unable to get the thread head state";
/* Dump the traceback of each thread */
tstate = PyInterpreterState_ThreadHead(interp);
nthreads = 0;
do
{
if (nthreads != 0)
write(fd, "\n", 1);
if (nthreads >= MAX_NTHREADS) {
PUTS(fd, "...\n");
break;
}
write_thread_id(fd, tstate, tstate == current_thread);
dump_traceback(fd, tstate, 0);
tstate = PyThreadState_Next(tstate);
nthreads++;
} while (tstate != NULL);
return NULL;
}
......@@ -9261,7 +9261,7 @@ for ac_func in alarm accept4 setitimer getitimer bind_textdomain_codeset chown \
select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \
setgid sethostname \
setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \
sigaction siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \
sigaction sigaltstack siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \
sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \
truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \
wcscoll wcsftime wcsxfrm writev _getpty
......
......@@ -2507,7 +2507,7 @@ AC_CHECK_FUNCS(alarm accept4 setitimer getitimer bind_textdomain_codeset chown \
select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \
setgid sethostname \
setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \
sigaction siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \
sigaction sigaltstack siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \
sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \
truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \
wcscoll wcsftime wcsxfrm writev _getpty)
......
......@@ -710,6 +710,9 @@
/* Define to 1 if you have the `sigaction' function. */
#undef HAVE_SIGACTION
/* Define to 1 if you have the `sigaltstack' function. */
#undef HAVE_SIGALTSTACK
/* Define to 1 if you have the `siginterrupt' function. */
#undef HAVE_SIGINTERRUPT
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment