summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVictor Stinner <victor.stinner@haypocalc.com>2011-03-30 23:31:06 (GMT)
committerVictor Stinner <victor.stinner@haypocalc.com>2011-03-30 23:31:06 (GMT)
commit024e37adccd9f0d879b014da28b02d04f0866f8c (patch)
tree9ca8e6548244610dd235876a87f639a1942647d4
parentd85456279f129e19a3f4c8ba0b3d05f5bdbfca1d (diff)
downloadcpython-024e37adccd9f0d879b014da28b02d04f0866f8c.zip
cpython-024e37adccd9f0d879b014da28b02d04f0866f8c.tar.gz
cpython-024e37adccd9f0d879b014da28b02d04f0866f8c.tar.bz2
Issue #11393: Add the new faulthandler module
-rw-r--r--Doc/library/debug.rst3
-rw-r--r--Doc/library/faulthandler.rst129
-rw-r--r--Doc/using/cmdline.rst7
-rw-r--r--Doc/whatsnew/3.3.rst8
-rw-r--r--Include/traceback.h40
-rwxr-xr-xLib/test/regrtest.py5
-rw-r--r--Lib/test/script_helper.py5
-rw-r--r--Lib/test/test_faulthandler.py469
-rw-r--r--Misc/NEWS2
-rw-r--r--Modules/Setup.dist3
-rw-r--r--Modules/faulthandler.c971
-rw-r--r--Modules/main.c1
-rw-r--r--PC/config.c2
-rw-r--r--PCbuild/pythoncore.vcproj4
-rw-r--r--Python/pythonrun.c21
-rw-r--r--Python/traceback.c235
-rwxr-xr-xconfigure2
-rw-r--r--configure.in2
-rw-r--r--pyconfig.h.in3
19 files changed, 1907 insertions, 5 deletions
diff --git a/Doc/library/debug.rst b/Doc/library/debug.rst
index b2ee4fa..c69fb1c 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 0000000..b203d37
--- /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 "<stdin>", line 1 in <module>
+ Segmentation fault
+
diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst
index b5a9b32..8a5a662 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 2199f0a..602b2b7 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 69e3d05..9a8f2a6 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 <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
diff --git a/Lib/test/regrtest.py b/Lib/test/regrtest.py
index f6cf0a7..a5a50c2 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 371c33d..e556eca 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 0000000..82a76ed
--- /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 "\<string\>", line %s in func\n' % lineno1
+ regex += r' File "\<string\>", line %s in \<module\>' % 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 "<string>", line {lineno} in <module>$
+""".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 "<string>", line %s in funcB' % lineno,
+ ' File "<string>", line 11 in funcA',
+ ' File "<string>", line 13 in <module>'
+ ]
+ 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 "<string>", 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 "<string>", line {lineno} in dump
+ File "<string>", line 28 in <module>$
+""".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 33d9997..fff54b8 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 56f3314..2859fa5 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 0000000..751e8db
--- /dev/null
+++ b/Modules/faulthandler.c
@@ -0,0 +1,971 @@
+#include "Python.h"
+#include "pythread.h"
+#include <signal.h>
+#include <object.h>
+#include <frameobject.h>
+#include <signal.h>
+
+#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 7ff4908..9137c85 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 1fc2b40..be51b55 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 b818761..ed6e158 100644
--- a/PCbuild/pythoncore.vcproj
+++ b/PCbuild/pythoncore.vcproj
@@ -1087,6 +1087,10 @@
>
</File>
<File
+ RelativePath="..\Modules\faulthandler.c"
+ >
+ </File>
+ <File
RelativePath="..\Modules\gcmodule.c"
>
</File>
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 38b2ab8..f787a4f 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 59bb3f0..37673d9 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 4b69292..1d44f0d 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 b41a4b6..f1a3139 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 7320260..8b5c592 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