diff --git a/Doc/library/debug.rst b/Doc/library/debug.rst index b2ee4fa0981..c69fb1c8465 100644 --- a/Doc/library/debug.rst +++ b/Doc/library/debug.rst @@ -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 diff --git a/Doc/library/faulthandler.rst b/Doc/library/faulthandler.rst new file mode 100644 index 00000000000..b203d37fc2f --- /dev/null +++ b/Doc/library/faulthandler.rst @@ -0,0 +1,129 @@ +: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 "", line 1 in + Segmentation fault + diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index b5a9b32c74d..8a5a66278db 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~ diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index 2199f0ac72a..602b2b70e3b 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -68,6 +68,14 @@ New, Improved, and Deprecated Modules * Stub +faulthandler +------------ + +New module: :mod:`faulthandler`. + + * :envvar:`PYTHONFAULTHANDLER` + * :option:`-X` ``faulthandler`` + os -- diff --git a/Include/traceback.h b/Include/traceback.h index 69e3d05453b..9a8f2a63adb 100644 --- a/Include/traceback.h +++ b/Include/traceback.h @@ -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 + File "xxx", line xxx in + ... + File "xxx", line xxx in + + 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 diff --git a/Lib/test/regrtest.py b/Lib/test/regrtest.py index f6cf0a7c65d..a5a50c2d9d5 100755 --- a/Lib/test/regrtest.py +++ b/Lib/test/regrtest.py @@ -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 diff --git a/Lib/test/script_helper.py b/Lib/test/script_helper.py index 371c33d1384..e556eca52ac 100644 --- a/Lib/test/script_helper.py +++ b/Lib/test/script_helper.py @@ -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() diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py new file mode 100644 index 00000000000..82a76ed4c50 --- /dev/null +++ b/Lib/test/test_faulthandler.py @@ -0,0 +1,469 @@ +from contextlib import contextmanager +import faulthandler +import re +import signal +import subprocess +import sys +from test import support, script_helper +import tempfile +import unittest + +try: + from resource import setrlimit, RLIMIT_CORE, error as resource_error +except ImportError: + prepare_subprocess = None +else: + def prepare_subprocess(): + # don't create core file + try: + setrlimit(RLIMIT_CORE, (0, 0)) + except (ValueError, resource_error): + pass + +def expected_traceback(lineno1, lineno2, header, count=1): + regex = header + regex += r' File "\", line %s in func\n' % lineno1 + regex += r' File "\", line %s in \' % lineno2 + if count != 1: + regex = (regex + '\n') * (count - 1) + regex + return '^' + regex + '$' + +@contextmanager +def temporary_filename(): + filename = tempfile.mktemp() + try: + yield filename + finally: + support.unlink(filename) + +class FaultHandlerTests(unittest.TestCase): + def get_output(self, code, expect_success, filename=None): + """ + Run the specified code in Python (in a new child process) and read the + output from the standard error or from a file (if filename is set). + Return the output lines as a list. + + Strip the reference count from the standard error for Python debug + build, and replace "Current thread 0x00007f8d8fbd9700" by "Current + thread XXX". + """ + options = {} + if prepare_subprocess: + options['preexec_fn'] = prepare_subprocess + process = script_helper.spawn_python('-c', code, **options) + stdout, stderr = process.communicate() + exitcode = process.wait() + if expect_success: + self.assertEqual(exitcode, 0) + else: + self.assertNotEqual(exitcode, 0) + if filename: + with open(filename, "rb") as fp: + output = fp.read() + else: + output = support.strip_python_stderr(stdout) + output = output.decode('ascii', 'backslashreplace') + output = re.sub('Current thread 0x[0-9a-f]+', + 'Current thread XXX', + output) + return output.splitlines() + + def check_fatal_error(self, code, line_number, name_regex, + filename=None, all_threads=False): + """ + Check that the fault handler for fatal errors is enabled and check the + traceback from the child process output. + + Raise an error if the output doesn't match the expected format. + """ + if all_threads: + header = 'Current thread XXX' + else: + header = 'Traceback (most recent call first)' + regex = """ +^Fatal Python error: {name} + +{header}: + File "", line {lineno} in $ +""".strip() + regex = regex.format( + lineno=line_number, + name=name_regex, + header=re.escape(header)) + output = self.get_output(code, False, filename) + output = '\n'.join(output) + self.assertRegex(output, regex) + + def test_read_null(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._read_null() +""".strip(), + 3, + '(?:Segmentation fault|Bus error)') + + def test_sigsegv(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._sigsegv() +""".strip(), + 3, + 'Segmentation fault') + + @unittest.skipIf(sys.platform == 'win32', + "SIGFPE cannot be caught on Windows") + def test_sigfpe(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._sigfpe() +""".strip(), + 3, + 'Floating point exception') + + @unittest.skipIf(not hasattr(faulthandler, '_sigbus'), + "need faulthandler._sigbus()") + def test_sigbus(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._sigbus() +""".strip(), + 3, + 'Bus error') + + @unittest.skipIf(not hasattr(faulthandler, '_sigill'), + "need faulthandler._sigill()") + def test_sigill(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._sigill() +""".strip(), + 3, + 'Illegal instruction') + + def test_fatal_error(self): + self.check_fatal_error(""" +import faulthandler +faulthandler._fatal_error(b'xyz') +""".strip(), + 2, + 'xyz') + + @unittest.skipIf(not hasattr(faulthandler, '_stack_overflow'), + 'need faulthandler._stack_overflow()') + def test_stack_overflow(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._stack_overflow() +""".strip(), + 3, + '(?:Segmentation fault|Bus error)') + + def test_gil_released(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable() +faulthandler._read_null(True) +""".strip(), + 3, + '(?:Segmentation fault|Bus error)') + + def test_enable_file(self): + with temporary_filename() as filename: + self.check_fatal_error(""" +import faulthandler +output = open({filename}, 'wb') +faulthandler.enable(output) +faulthandler._read_null(True) +""".strip().format(filename=repr(filename)), + 4, + '(?:Segmentation fault|Bus error)', + filename=filename) + + def test_enable_threads(self): + self.check_fatal_error(""" +import faulthandler +faulthandler.enable(all_threads=True) +faulthandler._read_null(True) +""".strip(), + 3, + '(?:Segmentation fault|Bus error)', + all_threads=True) + + def test_disable(self): + code = """ +import faulthandler +faulthandler.enable() +faulthandler.disable() +faulthandler._read_null() +""".strip() + not_expected = 'Fatal Python error' + stderr = self.get_output(code, False) + stder = '\n'.join(stderr) + self.assertTrue(not_expected not in stderr, + "%r is present in %r" % (not_expected, stderr)) + + def test_is_enabled(self): + was_enabled = faulthandler.is_enabled() + try: + faulthandler.enable() + self.assertTrue(faulthandler.is_enabled()) + faulthandler.disable() + self.assertFalse(faulthandler.is_enabled()) + finally: + if was_enabled: + faulthandler.enable() + else: + faulthandler.disable() + + def check_dump_traceback(self, filename): + """ + Explicitly call dump_traceback() function and check its output. + Raise an error if the output doesn't match the expected format. + """ + code = """ +import faulthandler + +def funcB(): + if {has_filename}: + with open({filename}, "wb") as fp: + faulthandler.dump_traceback(fp) + else: + faulthandler.dump_traceback() + +def funcA(): + funcB() + +funcA() +""".strip() + code = code.format( + filename=repr(filename), + has_filename=bool(filename), + ) + if filename: + lineno = 6 + else: + lineno = 8 + expected = [ + 'Traceback (most recent call first):', + ' File "", line %s in funcB' % lineno, + ' File "", line 11 in funcA', + ' File "", line 13 in ' + ] + trace = self.get_output(code, True, filename) + self.assertEqual(trace, expected) + + def test_dump_traceback(self): + self.check_dump_traceback(None) + with temporary_filename() as filename: + self.check_dump_traceback(filename) + + def check_dump_traceback_threads(self, filename): + """ + Call explicitly dump_traceback(all_threads=True) and check the output. + Raise an error if the output doesn't match the expected format. + """ + code = """ +import faulthandler +from threading import Thread, Event +import time + +def dump(): + if {filename}: + with open({filename}, "wb") as fp: + faulthandler.dump_traceback(fp, all_threads=True) + else: + faulthandler.dump_traceback(all_threads=True) + +class Waiter(Thread): + # avoid blocking if the main thread raises an exception. + daemon = True + + def __init__(self): + Thread.__init__(self) + self.running = Event() + self.stop = Event() + + def run(self): + self.running.set() + self.stop.wait() + +waiter = Waiter() +waiter.start() +waiter.running.wait() +dump() +waiter.stop.set() +waiter.join() +""".strip() + code = code.format(filename=repr(filename)) + output = self.get_output(code, True, filename) + output = '\n'.join(output) + if filename: + lineno = 8 + else: + lineno = 10 + regex = """ +^Thread 0x[0-9a-f]+: +(?: File ".*threading.py", line [0-9]+ in wait +)? File ".*threading.py", line [0-9]+ in wait + File "", line 23 in run + File ".*threading.py", line [0-9]+ in _bootstrap_inner + File ".*threading.py", line [0-9]+ in _bootstrap + +Current thread XXX: + File "", line {lineno} in dump + File "", line 28 in $ +""".strip() + regex = regex.format(lineno=lineno) + self.assertRegex(output, regex) + + def test_dump_traceback_threads(self): + self.check_dump_traceback_threads(None) + with temporary_filename() as filename: + self.check_dump_traceback_threads(filename) + + def _check_dump_tracebacks_later(self, repeat, cancel, filename): + """ + Check how many times the traceback is written in timeout x 2.5 seconds, + or timeout x 3.5 seconds if cancel is True: 1, 2 or 3 times depending + on repeat and cancel options. + + Raise an error if the output doesn't match the expect format. + """ + code = """ +import faulthandler +import time + +def func(repeat, cancel, timeout): + pause = timeout * 2.5 + a = time.time() + time.sleep(pause) + faulthandler.cancel_dump_tracebacks_later() + b = time.time() + # Check that sleep() was not interrupted + assert (b -a) >= pause + + if cancel: + pause = timeout * 1.5 + a = time.time() + time.sleep(pause) + b = time.time() + # Check that sleep() was not interrupted + assert (b -a) >= pause + +timeout = 0.5 +repeat = {repeat} +cancel = {cancel} +if {has_filename}: + file = open({filename}, "wb") +else: + file = None +faulthandler.dump_tracebacks_later(timeout, + repeat=repeat, file=file) +func(repeat, cancel, timeout) +if file is not None: + file.close() +""".strip() + code = code.format( + filename=repr(filename), + has_filename=bool(filename), + repeat=repeat, + cancel=cancel, + ) + trace = self.get_output(code, True, filename) + trace = '\n'.join(trace) + + if repeat: + count = 2 + else: + count = 1 + header = 'Thread 0x[0-9a-f]+:\n' + regex = expected_traceback(7, 30, header, count=count) + self.assertRegex(trace, '^%s$' % regex) + + @unittest.skipIf(not hasattr(faulthandler, 'dump_tracebacks_later'), + 'need faulthandler.dump_tracebacks_later()') + def check_dump_tracebacks_later(self, repeat=False, cancel=False, + file=False): + if file: + with temporary_filename() as filename: + self._check_dump_tracebacks_later(repeat, cancel, filename) + else: + self._check_dump_tracebacks_later(repeat, cancel, None) + + def test_dump_tracebacks_later(self): + self.check_dump_tracebacks_later() + + def test_dump_tracebacks_later_repeat(self): + self.check_dump_tracebacks_later(repeat=True) + + def test_dump_tracebacks_later_repeat_cancel(self): + self.check_dump_tracebacks_later(repeat=True, cancel=True) + + def test_dump_tracebacks_later_file(self): + self.check_dump_tracebacks_later(file=True) + + @unittest.skipIf(not hasattr(faulthandler, "register"), + "need faulthandler.register") + def check_register(self, filename=False, all_threads=False): + """ + Register a handler displaying the traceback on a user signal. Raise the + signal and check the written traceback. + + Raise an error if the output doesn't match the expected format. + """ + code = """ +import faulthandler +import os +import signal + +def func(signum): + os.kill(os.getpid(), signum) + +signum = signal.SIGUSR1 +if {has_filename}: + file = open({filename}, "wb") +else: + file = None +faulthandler.register(signum, file=file, all_threads={all_threads}) +func(signum) +if file is not None: + file.close() +""".strip() + code = code.format( + filename=repr(filename), + has_filename=bool(filename), + all_threads=all_threads, + ) + trace = self.get_output(code, True, filename) + trace = '\n'.join(trace) + if all_threads: + regex = 'Current thread XXX:\n' + else: + regex = 'Traceback \(most recent call first\):\n' + regex = expected_traceback(6, 14, regex) + self.assertTrue(re.match(regex, trace), + "[%s] doesn't match [%s]: use_filename=%s, all_threads=%s" + % (regex, trace, bool(filename), all_threads)) + + def test_register(self): + self.check_register() + + def test_register_file(self): + with temporary_filename() as filename: + self.check_register(filename=filename) + + def test_register_threads(self): + self.check_register(all_threads=True) + + +def test_main(): + support.run_unittest(FaultHandlerTests) + +if __name__ == "__main__": + test_main() diff --git a/Misc/NEWS b/Misc/NEWS index 33d9997f75f..fff54b8f7a0 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -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 diff --git a/Modules/Setup.dist b/Modules/Setup.dist index 56f331434cb..2859fa58bce 100644 --- a/Modules/Setup.dist +++ b/Modules/Setup.dist @@ -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 diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c new file mode 100644 index 00000000000..751e8dbb9db --- /dev/null +++ b/Modules/faulthandler.c @@ -0,0 +1,971 @@ +#include "Python.h" +#include "pythread.h" +#include +#include +#include +#include + +#ifdef WITH_THREAD +# define FAULTHANDLER_LATER +#endif + +#ifndef MS_WINDOWS + /* register() is useless on Windows, because only SIGSEGV and SIGILL can be + handled by the process, and these signals can only be used with enable(), + not using register() */ +# define FAULTHANDLER_USER +#endif + +#define PUTS(fd, str) write(fd, str, strlen(str)) + +#ifdef HAVE_SIGACTION +typedef struct sigaction _Py_sighandler_t; +#else +typedef PyOS_sighandler_t _Py_sighandler_t; +#endif + +typedef struct { + int signum; + int enabled; + const char* name; + _Py_sighandler_t previous; + int all_threads; +} fault_handler_t; + +static struct { + int enabled; + PyObject *file; + int fd; + int all_threads; +} fatal_error = {0, NULL, -1, 0}; + +#ifdef FAULTHANDLER_LATER +static struct { + PyObject *file; + int fd; + PY_TIMEOUT_T timeout_ms; /* timeout in microseconds */ + int repeat; + volatile int running; + PyInterpreterState *interp; + int exit; + /* released by parent thread when cancel request */ + PyThread_type_lock cancel_event; + /* released by child thread when joined */ + PyThread_type_lock join_event; +} thread; +#endif + +#ifdef FAULTHANDLER_USER +typedef struct { + int enabled; + PyObject *file; + int fd; + int all_threads; + _Py_sighandler_t previous; +} user_signal_t; + +static user_signal_t *user_signals; + +/* the following macros come from Python: Modules/signalmodule.c */ +#if defined(PYOS_OS2) && !defined(PYCC_GCC) +#define NSIG 12 +#endif +#ifndef NSIG +# if defined(_NSIG) +# define NSIG _NSIG /* For BSD/SysV */ +# elif defined(_SIGMAX) +# define NSIG (_SIGMAX + 1) /* For QNX */ +# elif defined(SIGMAX) +# define NSIG (SIGMAX + 1) /* For djgpp */ +# else +# define NSIG 64 /* Use a reasonable default value */ +# endif +#endif + +#endif /* FAULTHANDLER_USER */ + + +static fault_handler_t faulthandler_handlers[] = { +#ifdef SIGBUS + {SIGBUS, 0, "Bus error", }, +#endif +#ifdef SIGILL + {SIGILL, 0, "Illegal instruction", }, +#endif + {SIGFPE, 0, "Floating point exception", }, + /* define SIGSEGV at the end to make it the default choice if searching the + handler fails in faulthandler_fatal_error() */ + {SIGSEGV, 0, "Segmentation fault", } +}; +static const unsigned char faulthandler_nsignals = \ + sizeof(faulthandler_handlers) / sizeof(faulthandler_handlers[0]); + +#ifdef HAVE_SIGALTSTACK +static stack_t stack; +#endif + + +/* Get the file descriptor of a file by calling its fileno() method and then + call its flush() method. + + If file is NULL or Py_None, use sys.stderr as the new file. + + On success, return the new file and write the file descriptor into *p_fd. + On error, return NULL. */ + +static PyObject* +faulthandler_get_fileno(PyObject *file, int *p_fd) +{ + PyObject *result; + long fd_long; + int fd; + + if (file == NULL || file == Py_None) { + file = PySys_GetObject("stderr"); + if (file == NULL) { + PyErr_SetString(PyExc_RuntimeError, "unable to get sys.stderr"); + return NULL; + } + } + + result = PyObject_CallMethod(file, "fileno", ""); + if (result == NULL) + return NULL; + + fd = -1; + if (PyLong_Check(result)) { + fd_long = PyLong_AsLong(result); + if (0 <= fd_long && fd_long < INT_MAX) + fd = (int)fd_long; + } + Py_DECREF(result); + + if (fd == -1) { + PyErr_SetString(PyExc_RuntimeError, + "file.fileno() is not a valid file descriptor"); + return NULL; + } + + result = PyObject_CallMethod(file, "flush", ""); + if (result != NULL) + Py_DECREF(result); + else { + /* ignore flush() error */ + PyErr_Clear(); + } + *p_fd = fd; + return file; +} + +static PyObject* +faulthandler_dump_traceback_py(PyObject *self, + PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"file", "all_threads", NULL}; + PyObject *file = NULL; + int all_threads = 0; + PyThreadState *tstate; + const char *errmsg; + int fd; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "|Oi:dump_traceback", kwlist, + &file, &all_threads)) + return NULL; + + file = faulthandler_get_fileno(file, &fd); + if (file == NULL) + return NULL; + + /* The caller holds the GIL and so PyThreadState_Get() can be used */ + tstate = PyThreadState_Get(); + if (tstate == NULL) { + PyErr_SetString(PyExc_RuntimeError, + "unable to get the current thread state"); + return NULL; + } + + if (all_threads) { + errmsg = _Py_DumpTracebackThreads(fd, tstate->interp, tstate); + if (errmsg != NULL) { + PyErr_SetString(PyExc_RuntimeError, errmsg); + return NULL; + } + } + else { + _Py_DumpTraceback(fd, tstate); + } + Py_RETURN_NONE; +} + + +/* Handler of SIGSEGV, SIGFPE, SIGBUS and SIGILL signals. + + Display the current Python traceback, restore the previous handler and call + the previous handler. + + On Windows, don't call explictly the previous handler, because Windows + signal handler would not be called (for an unknown reason). The execution of + the program continues at faulthandler_fatal_error() exit, but the same + instruction will raise the same fault (signal), and so the previous handler + will be called. + + This function is signal safe and should only call signal safe functions. */ + +static void +faulthandler_fatal_error( + int signum +#ifdef HAVE_SIGACTION + , siginfo_t *siginfo, void *ucontext +#endif +) +{ + const int fd = fatal_error.fd; + unsigned int i; + fault_handler_t *handler = NULL; + PyThreadState *tstate; + + if (!fatal_error.enabled) + return; + + for (i=0; i < faulthandler_nsignals; i++) { + handler = &faulthandler_handlers[i]; + if (handler->signum == signum) + break; + } + if (handler == NULL) { + /* faulthandler_nsignals == 0 (unlikely) */ + return; + } + + /* restore the previous handler */ +#ifdef HAVE_SIGACTION + (void)sigaction(handler->signum, &handler->previous, NULL); +#else + (void)signal(handler->signum, handler->previous); +#endif + handler->enabled = 0; + + PUTS(fd, "Fatal Python error: "); + PUTS(fd, handler->name); + PUTS(fd, "\n\n"); + + /* SIGSEGV, SIGFPE, SIGBUS and SIGILL are synchronous signals and so are + delivered to the thread that caused the fault. Get the Python thread + state of the current thread. + + PyThreadState_Get() doesn't give the state of the thread that caused the + fault if the thread released the GIL, and so this function cannot be + used. Read the thread local storage (TLS) instead: call + PyGILState_GetThisThreadState(). */ + tstate = PyGILState_GetThisThreadState(); + if (tstate == NULL) + return; + + if (fatal_error.all_threads) + _Py_DumpTracebackThreads(fd, tstate->interp, tstate); + else + _Py_DumpTraceback(fd, tstate); + +#ifndef MS_WINDOWS + /* call the previous signal handler: it is called if we use sigaction() + thanks to SA_NODEFER flag, otherwise it is deferred */ + raise(signum); +#else + /* on Windows, don't call explictly the previous handler, because Windows + signal handler would not be called */ +#endif +} + +/* Install handler for fatal signals (SIGSEGV, SIGFPE, ...). */ + +static PyObject* +faulthandler_enable(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"file", "all_threads", NULL}; + PyObject *file = NULL; + int all_threads = 0; + unsigned int i; + fault_handler_t *handler; +#ifdef HAVE_SIGACTION + struct sigaction action; +#endif + int err; + int fd; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "|Oi:enable", kwlist, &file, &all_threads)) + return NULL; + + file = faulthandler_get_fileno(file, &fd); + if (file == NULL) + return NULL; + + Py_XDECREF(fatal_error.file); + Py_INCREF(file); + fatal_error.file = file; + fatal_error.fd = fd; + fatal_error.all_threads = all_threads; + + if (!fatal_error.enabled) { + fatal_error.enabled = 1; + + for (i=0; i < faulthandler_nsignals; i++) { + handler = &faulthandler_handlers[i]; +#ifdef HAVE_SIGACTION + action.sa_sigaction = faulthandler_fatal_error; + sigemptyset(&action.sa_mask); + /* Do not prevent the signal from being received from within + its own signal handler */ + action.sa_flags = SA_NODEFER; +#ifdef HAVE_SIGALTSTACK + if (stack.ss_sp != NULL) { + /* Call the signal handler on an alternate signal stack + provided by sigaltstack() */ + action.sa_flags |= SA_ONSTACK; + } +#endif + err = sigaction(handler->signum, &action, &handler->previous); +#else + handler->previous = signal(handler->signum, + faulthandler_fatal_error); + err = (handler->previous == SIG_ERR); +#endif + if (err) { + PyErr_SetFromErrno(PyExc_RuntimeError); + return NULL; + } + handler->enabled = 1; + } + } + Py_RETURN_NONE; +} + +static void +faulthandler_disable(void) +{ + unsigned int i; + fault_handler_t *handler; + + if (fatal_error.enabled) { + fatal_error.enabled = 0; + for (i=0; i < faulthandler_nsignals; i++) { + handler = &faulthandler_handlers[i]; + if (!handler->enabled) + continue; +#ifdef HAVE_SIGACTION + (void)sigaction(handler->signum, &handler->previous, NULL); +#else + (void)signal(handler->signum, handler->previous); +#endif + handler->enabled = 0; + } + } + + Py_CLEAR(fatal_error.file); +} + +static PyObject* +faulthandler_disable_py(PyObject *self) +{ + if (!fatal_error.enabled) { + Py_INCREF(Py_False); + return Py_False; + } + faulthandler_disable(); + Py_INCREF(Py_True); + return Py_True; +} + +static PyObject* +faulthandler_is_enabled(PyObject *self) +{ + return PyBool_FromLong(fatal_error.enabled); +} + +#ifdef FAULTHANDLER_LATER + +static void +faulthandler_thread(void *unused) +{ + PyLockStatus st; + const char* errmsg; + PyThreadState *current; + int ok; + + do { + st = PyThread_acquire_lock_timed(thread.cancel_event, + thread.timeout_ms, 0); + if (st == PY_LOCK_ACQUIRED) { + /* Cancelled by user */ + break; + } + /* Timeout => dump traceback */ + assert(st == PY_LOCK_FAILURE); + + /* get the thread holding the GIL, NULL if no thread hold the GIL */ + current = _Py_atomic_load_relaxed(&_PyThreadState_Current); + + errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, current); + ok = (errmsg == NULL); + + if (thread.exit) + _exit(1); + } while (ok && thread.repeat); + + /* The only way out */ + thread.running = 0; + PyThread_release_lock(thread.join_event); + PyThread_release_lock(thread.cancel_event); +} + +static void +faulthandler_cancel_dump_traceback_later(void) +{ + if (thread.running) { + /* Notify cancellation */ + PyThread_release_lock(thread.cancel_event); + /* Wait for thread to join */ + PyThread_acquire_lock(thread.join_event, 1); + assert(thread.running == 0); + PyThread_release_lock(thread.join_event); + } + Py_CLEAR(thread.file); +} + +static PyObject* +faulthandler_dump_traceback_later(PyObject *self, + PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"timeout", "repeat", "file", "exit", NULL}; + double timeout; + PY_TIMEOUT_T timeout_ms; + int repeat = 0; + PyObject *file = NULL; + int fd; + int exit = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "d|iOi:dump_tracebacks_later", kwlist, + &timeout, &repeat, &file, &exit)) + return NULL; + timeout *= 1e6; + if (timeout >= (double) PY_TIMEOUT_MAX) { + PyErr_SetString(PyExc_OverflowError, "timeout value is too large"); + return NULL; + } + timeout_ms = (PY_TIMEOUT_T)timeout; + if (timeout_ms <= 0) { + PyErr_SetString(PyExc_ValueError, "timeout must be greater than 0"); + return NULL; + } + + file = faulthandler_get_fileno(file, &fd); + if (file == NULL) + return NULL; + + /* Cancel previous thread, if running */ + faulthandler_cancel_dump_traceback_later(); + + Py_XDECREF(thread.file); + Py_INCREF(file); + thread.file = file; + thread.fd = fd; + thread.timeout_ms = timeout_ms; + thread.repeat = repeat; + thread.interp = PyThreadState_Get()->interp; + thread.exit = exit; + + /* Arm these locks to serve as events when released */ + PyThread_acquire_lock(thread.join_event, 1); + PyThread_acquire_lock(thread.cancel_event, 1); + + thread.running = 1; + if (PyThread_start_new_thread(faulthandler_thread, NULL) == -1) { + thread.running = 0; + Py_CLEAR(thread.file); + PyErr_SetString(PyExc_RuntimeError, + "unable to start watchdog thread"); + return NULL; + } + + Py_RETURN_NONE; +} + +static PyObject* +faulthandler_cancel_dump_traceback_later_py(PyObject *self) +{ + faulthandler_cancel_dump_traceback_later(); + Py_RETURN_NONE; +} +#endif /* FAULTHANDLER_LATER */ + +#ifdef FAULTHANDLER_USER +/* Handler of user signals (e.g. SIGUSR1). + + Dump the traceback of the current thread, or of all threads if + thread.all_threads is true. + + This function is signal safe and should only call signal safe functions. */ + +static void +faulthandler_user(int signum) +{ + user_signal_t *user; + PyThreadState *tstate; + + user = &user_signals[signum]; + if (!user->enabled) + return; + + /* PyThreadState_Get() doesn't give the state of the current thread if + the thread doesn't hold the GIL. Read the thread local storage (TLS) + instead: call PyGILState_GetThisThreadState(). */ + tstate = PyGILState_GetThisThreadState(); + if (tstate == NULL) { + /* unable to get the current thread, do nothing */ + return; + } + + if (user->all_threads) + _Py_DumpTracebackThreads(user->fd, tstate->interp, tstate); + else + _Py_DumpTraceback(user->fd, tstate); +} + +static PyObject* +faulthandler_register(PyObject *self, + PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"signum", "file", "all_threads", NULL}; + int signum; + PyObject *file = NULL; + int all_threads = 0; + int fd; + unsigned int i; + user_signal_t *user; + _Py_sighandler_t previous; +#ifdef HAVE_SIGACTION + struct sigaction action; +#endif + int err; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "i|Oi:register", kwlist, + &signum, &file, &all_threads)) + return NULL; + + if (signum < 1 || NSIG <= signum) { + PyErr_SetString(PyExc_ValueError, "signal number out of range"); + return NULL; + } + + for (i=0; i < faulthandler_nsignals; i++) { + if (faulthandler_handlers[i].signum == signum) { + PyErr_Format(PyExc_RuntimeError, + "signal %i cannot be registered by register(), " + "use enable() instead", + signum); + return NULL; + } + } + + file = faulthandler_get_fileno(file, &fd); + if (file == NULL) + return NULL; + + if (user_signals == NULL) { + user_signals = calloc(NSIG, sizeof(user_signal_t)); + if (user_signals == NULL) + return PyErr_NoMemory(); + } + user = &user_signals[signum]; + + if (!user->enabled) { +#ifdef HAVE_SIGACTION + action.sa_handler = faulthandler_user; + sigemptyset(&action.sa_mask); + /* if the signal is received while the kernel is executing a system + call, try to restart the system call instead of interrupting it and + return EINTR */ + action.sa_flags = SA_RESTART; +#ifdef HAVE_SIGALTSTACK + if (stack.ss_sp != NULL) { + /* Call the signal handler on an alternate signal stack + provided by sigaltstack() */ + action.sa_flags |= SA_ONSTACK; + } +#endif + err = sigaction(signum, &action, &previous); +#else + previous = signal(signum, faulthandler_user); + err = (previous == SIG_ERR); +#endif + if (err) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + } + + Py_XDECREF(user->file); + Py_INCREF(file); + user->file = file; + user->fd = fd; + user->all_threads = all_threads; + user->previous = previous; + user->enabled = 1; + + Py_RETURN_NONE; +} + +static int +faulthandler_unregister(user_signal_t *user, int signum) +{ + if (user->enabled) + return 0; + user->enabled = 0; +#ifdef HAVE_SIGACTION + (void)sigaction(signum, &user->previous, NULL); +#else + (void)signal(signum, user->previous); +#endif + Py_CLEAR(user->file); + user->fd = -1; + return 1; +} + +static PyObject* +faulthandler_unregister_py(PyObject *self, PyObject *args) +{ + int signum; + user_signal_t *user; + int change; + + if (!PyArg_ParseTuple(args, "i:unregister", &signum)) + return NULL; + + if (signum < 1 || NSIG <= signum) { + PyErr_SetString(PyExc_ValueError, "signal number out of range"); + return NULL; + } + + user = &user_signals[signum]; + change = faulthandler_unregister(user, signum); + return PyBool_FromLong(change); +} +#endif /* FAULTHANDLER_USER */ + + +static PyObject * +faulthandler_read_null(PyObject *self, PyObject *args) +{ + int *x = NULL, y; + int release_gil = 0; + if (!PyArg_ParseTuple(args, "|i:_read_null", &release_gil)) + return NULL; + if (release_gil) { + Py_BEGIN_ALLOW_THREADS + y = *x; + Py_END_ALLOW_THREADS + } else + y = *x; + return PyLong_FromLong(y); + +} + +static PyObject * +faulthandler_sigsegv(PyObject *self, PyObject *args) +{ +#if defined(MS_WINDOWS) + /* faulthandler_fatal_error() restores the previous signal handler and then + gives back the execution flow to the program. In a normal case, the + SIGSEGV was raised by the kernel because of a fault, and so if the + program retries to execute the same instruction, the fault will be + raised again. + + Here the fault is simulated by a fake SIGSEGV signal raised by the + application. We have to raise SIGSEGV at lease twice: once for + faulthandler_fatal_error(), and one more time for the previous signal + handler. */ + while(1) + raise(SIGSEGV); +#else + raise(SIGSEGV); +#endif + Py_RETURN_NONE; +} + +static PyObject * +faulthandler_sigfpe(PyObject *self, PyObject *args) +{ + /* Do an integer division by zero: raise a SIGFPE on Intel CPU, but not on + PowerPC. Use volatile to disable compile-time optimizations. */ + volatile int x = 1, y = 0, z; + z = x / y; + /* if the division by zero didn't raise a SIGFPE, raise it manually */ + raise(SIGFPE); + Py_RETURN_NONE; +} + +#ifdef SIGBUS +static PyObject * +faulthandler_sigbus(PyObject *self, PyObject *args) +{ + raise(SIGBUS); + Py_RETURN_NONE; +} +#endif + +#ifdef SIGILL +static PyObject * +faulthandler_sigill(PyObject *self, PyObject *args) +{ +#if defined(MS_WINDOWS) + /* see faulthandler_sigsegv() for the explanation about while(1) */ + while(1) + raise(SIGILL); +#else + raise(SIGILL); +#endif + Py_RETURN_NONE; +} +#endif + +static PyObject * +faulthandler_fatal_error_py(PyObject *self, PyObject *args) +{ + char *message; + if (!PyArg_ParseTuple(args, "y:fatal_error", &message)) + return NULL; + Py_FatalError(message); + Py_RETURN_NONE; +} + +#if defined(HAVE_SIGALTSTACK) && defined(HAVE_SIGACTION) +static PyObject * +faulthandler_stack_overflow(PyObject *self) +{ + /* allocate 4096 bytes on the stack at each call */ + unsigned char buffer[4096]; + buffer[0] = 1; + buffer[4095] = 2; + faulthandler_stack_overflow(self); + return PyLong_FromLong(buffer[0] + buffer[4095]); +} +#endif + + +static int +faulthandler_traverse(PyObject *module, visitproc visit, void *arg) +{ +#ifdef FAULTHANDLER_USER + unsigned int index; +#endif + +#ifdef FAULTHANDLER_LATER + Py_VISIT(thread.file); +#endif +#ifdef FAULTHANDLER_USER + if (user_signals != NULL) { + for (index=0; index < NSIG; index++) + Py_VISIT(user_signals[index].file); + } +#endif + Py_VISIT(fatal_error.file); + return 0; +} + +PyDoc_STRVAR(module_doc, +"faulthandler module."); + +static PyMethodDef module_methods[] = { + {"enable", + (PyCFunction)faulthandler_enable, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("enable(file=sys.stderr, all_threads=False): " + "enable the fault handler")}, + {"disable", (PyCFunction)faulthandler_disable_py, METH_NOARGS, + PyDoc_STR("disable(): disable the fault handler")}, + {"is_enabled", (PyCFunction)faulthandler_is_enabled, METH_NOARGS, + PyDoc_STR("is_enabled()->bool: check if the handler is enabled")}, + {"dump_traceback", + (PyCFunction)faulthandler_dump_traceback_py, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("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")}, +#ifdef FAULTHANDLER_LATER + {"dump_tracebacks_later", + (PyCFunction)faulthandler_dump_traceback_later, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("dump_tracebacks_later(timeout, repeat=False, file=sys.stderr):\n" + "dump the traceback of all threads in timeout seconds,\n" + "or each timeout seconds if repeat is True.")}, + {"cancel_dump_tracebacks_later", + (PyCFunction)faulthandler_cancel_dump_traceback_later_py, METH_NOARGS, + PyDoc_STR("cancel_dump_tracebacks_later():\ncancel the previous call " + "to dump_tracebacks_later().")}, +#endif + +#ifdef FAULTHANDLER_USER + {"register", + (PyCFunction)faulthandler_register, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("register(signum, file=sys.stderr, all_threads=False): " + "register an handler for the signal 'signum': dump the " + "traceback of the current thread, or of all threads if " + "all_threads is True, into file")}, + {"unregister", + faulthandler_unregister_py, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("unregister(signum): unregister the handler of the signal " + "'signum' registered by register()")}, +#endif + + {"_read_null", faulthandler_read_null, METH_VARARGS, + PyDoc_STR("_read_null(release_gil=False): read from NULL, raise " + "a SIGSEGV or SIGBUS signal depending on the platform")}, + {"_sigsegv", faulthandler_sigsegv, METH_VARARGS, + PyDoc_STR("_sigsegv(): raise a SIGSEGV signal")}, + {"_sigfpe", (PyCFunction)faulthandler_sigfpe, METH_NOARGS, + PyDoc_STR("_sigfpe(): raise a SIGFPE signal")}, +#ifdef SIGBUS + {"_sigbus", (PyCFunction)faulthandler_sigbus, METH_NOARGS, + PyDoc_STR("_sigbus(): raise a SIGBUS signal")}, +#endif +#ifdef SIGILL + {"_sigill", (PyCFunction)faulthandler_sigill, METH_NOARGS, + PyDoc_STR("_sigill(): raise a SIGILL signal")}, +#endif + {"_fatal_error", faulthandler_fatal_error_py, METH_VARARGS, + PyDoc_STR("_fatal_error(message): call Py_FatalError(message)")}, +#if defined(HAVE_SIGALTSTACK) && defined(HAVE_SIGACTION) + {"_stack_overflow", (PyCFunction)faulthandler_stack_overflow, METH_NOARGS, + PyDoc_STR("_stack_overflow(): recursive call to raise a stack overflow")}, +#endif + {NULL, NULL} /* terminator */ +}; + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "faulthandler", + module_doc, + 0, /* non negative size to be able to unload the module */ + module_methods, + NULL, + faulthandler_traverse, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit_faulthandler(void) +{ + return PyModule_Create(&module_def); +} + +/* Call faulthandler.enable() if PYTHONFAULTHANDLER environment variable is + defined, or if sys._xoptions has a 'faulthandler' key. */ + +static int +faulthandler_env_options(void) +{ + PyObject *xoptions, *key, *module, *res; + int enable; + + if (!Py_GETENV("PYTHONFAULTHANDLER")) { + xoptions = PySys_GetXOptions(); + if (xoptions == NULL) + return -1; + + key = PyUnicode_FromString("faulthandler"); + if (key == NULL) + return -1; + + enable = PyDict_Contains(xoptions, key); + Py_DECREF(key); + if (!enable) + return 0; + } + else + enable = 1; + + module = PyImport_ImportModule("faulthandler"); + if (module == NULL) { + return -1; + } + res = PyObject_CallMethod(module, "enable", ""); + Py_DECREF(module); + if (res == NULL) + return -1; + Py_DECREF(res); + return 0; +} + +int _PyFaulthandler_Init(void) +{ +#ifdef HAVE_SIGALTSTACK + int err; + + /* Try to allocate an alternate stack for faulthandler() signal handler to + * be able to allocate memory on the stack, even on a stack overflow. If it + * fails, ignore the error. */ + stack.ss_flags = 0; + stack.ss_size = SIGSTKSZ; + stack.ss_sp = PyMem_Malloc(stack.ss_size); + if (stack.ss_sp != NULL) { + err = sigaltstack(&stack, NULL); + if (err) { + PyMem_Free(stack.ss_sp); + stack.ss_sp = NULL; + } + } +#endif +#ifdef FAULTHANDLER_LATER + thread.running = 0; + thread.file = NULL; + thread.cancel_event = PyThread_allocate_lock(); + thread.join_event = PyThread_allocate_lock(); + if (!thread.cancel_event || !thread.join_event) { + PyErr_SetString(PyExc_RuntimeError, + "could not allocate locks for faulthandler"); + return -1; + } +#endif + + return faulthandler_env_options(); +} + +void _PyFaulthandler_Fini(void) +{ +#ifdef FAULTHANDLER_USER + unsigned int i; +#endif + +#ifdef FAULTHANDLER_LATER + /* later */ + faulthandler_cancel_dump_traceback_later(); + if (thread.cancel_event) { + PyThread_free_lock(thread.cancel_event); + thread.cancel_event = NULL; + } + if (thread.join_event) { + PyThread_free_lock(thread.join_event); + thread.join_event = NULL; + } +#endif + +#ifdef FAULTHANDLER_USER + /* user */ + if (user_signals != NULL) { + for (i=0; i < NSIG; i++) + faulthandler_unregister(&user_signals[i], i+1); + free(user_signals); + user_signals = NULL; + } +#endif + + /* fatal */ + faulthandler_disable(); +#ifdef HAVE_SIGALTSTACK + if (stack.ss_sp != NULL) { + PyMem_Free(stack.ss_sp); + stack.ss_sp = NULL; + } +#endif +} diff --git a/Modules/main.c b/Modules/main.c index 7ff4908738a..9137c85e227 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -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 diff --git a/PC/config.c b/PC/config.c index 1fc2b40a192..be51b55108a 100644 --- a/PC/config.c +++ b/PC/config.c @@ -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 */ diff --git a/PCbuild/pythoncore.vcproj b/PCbuild/pythoncore.vcproj index b8187618bba..ed6e1587ba5 100644 --- a/PCbuild/pythoncore.vcproj +++ b/PCbuild/pythoncore.vcproj @@ -1086,6 +1086,10 @@ RelativePath="..\Modules\errnomodule.c" > + + diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 38b2ab84fbd..f787a4f8b37 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.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); diff --git a/Python/traceback.c b/Python/traceback.c index 59bb3f0d151..37673d93e04 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -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; +} + diff --git a/configure b/configure index 4b6929275f4..1d44f0d8bb8 100755 --- a/configure +++ b/configure @@ -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 diff --git a/configure.in b/configure.in index b41a4b6cba0..f1a31399563 100644 --- a/configure.in +++ b/configure.in @@ -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) diff --git a/pyconfig.h.in b/pyconfig.h.in index 7320260f8b2..8b5c5922fe6 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -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