diff -r 5163fb8dc61f Doc/library/faulthandler.rst --- a/Doc/library/faulthandler.rst Tue Mar 10 14:47:41 2015 +0200 +++ b/Doc/library/faulthandler.rst Wed Mar 11 23:03:24 2015 +0800 @@ -47,6 +47,9 @@ Dump the tracebacks of all threads into *file*. If *all_threads* is ``False``, dump only the current thread. + .. versionchanged:: 3.5 + Added support for passing file descriptor to this function. + Fault handler state ------------------- @@ -59,6 +62,12 @@ produce tracebacks for every running thread. Otherwise, dump only the current thread. + The *file* must be kept open until the fault handler is disabled: see + :ref:`issue with file descriptors `. + + .. versionchanged:: 3.5 + Added support for passing file descriptor to this function. + .. function:: disable() Disable the fault handler: uninstall the signal handlers installed by @@ -82,9 +91,16 @@ call replaces previous parameters and resets the timeout. The timer has a sub-second resolution. + The *file* must be kept open until the traceback is dumped or + :func:`cancel_dump_traceback_later` is called: see :ref:`issue with file + descriptors `. + This function is implemented using a watchdog thread and therefore is not available if Python is compiled with threads disabled. + .. versionchanged:: 3.5 + Added support for passing file descriptor to this function. + .. function:: cancel_dump_traceback_later() Cancel the last call to :func:`dump_traceback_later`. @@ -99,7 +115,13 @@ the traceback of all threads, or of the current thread if *all_threads* is ``False``, into *file*. Call the previous handler if chain is ``True``. + The *file* must be kept open until the signal is unregistered by + :func:`unregister`: see :ref:`issue with file descriptors `. + Not available on Windows. + + .. versionchanged:: 3.5 + Added support for passing file descriptor to this function. .. function:: unregister(signum) @@ -110,6 +132,8 @@ Not available on Windows. +.. _faulthandler-fd: + Issue with file descriptors --------------------------- diff -r 5163fb8dc61f Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst Tue Mar 10 14:47:41 2015 +0200 +++ b/Doc/whatsnew/3.5.rst Wed Mar 11 23:03:24 2015 +0800 @@ -417,6 +417,12 @@ * :class:`xmlrpc.client.ServerProxy` is now a :term:`context manager`. (Contributed by Claudiu Popa in :issue:`20627`.) +faulthandler +------------ + +* faulthandler now accepts file descriptors. + (Contributed by Wei Wu in :issue:`23566`.) + Optimizations ============= diff -r 5163fb8dc61f Lib/test/test_faulthandler.py --- a/Lib/test/test_faulthandler.py Tue Mar 10 14:47:41 2015 +0200 +++ b/Lib/test/test_faulthandler.py Wed Mar 11 23:03:24 2015 +0800 @@ -211,6 +211,16 @@ 'Segmentation fault', filename=filename) + def test_enable_fd(self): + self.check_fatal_error(""" + import faulthandler + import sys + faulthandler.enable(sys.stderr.fileno()) + faulthandler._sigsegv() + """, + 4, + 'Segmentation fault') + def test_enable_single_thread(self): self.check_fatal_error(""" import faulthandler @@ -297,18 +307,22 @@ output = subprocess.check_output(args, env=env) self.assertEqual(output.rstrip(), b"True") - def check_dump_traceback(self, filename): + def check_dump_traceback(self, filename=None, use_fd=False): """ Explicitly call dump_traceback() function and check its output. Raise an error if the output doesn't match the expected format. """ code = """ import faulthandler + import sys def funcB(): if {has_filename}: with open({filename}, "wb") as fp: faulthandler.dump_traceback(fp, all_threads=False) + elif {use_fd}: + faulthandler.dump_traceback(sys.stderr.fileno(), + all_threads=False) else: faulthandler.dump_traceback(all_threads=False) @@ -320,28 +334,34 @@ code = code.format( filename=repr(filename), has_filename=bool(filename), + use_fd=use_fd, ) if filename: - lineno = 6 + lineno = 7 + elif use_fd: + lineno = 10 else: - lineno = 8 + lineno = 12 expected = [ 'Stack (most recent call first):', ' File "", line %s in funcB' % lineno, - ' File "", line 11 in funcA', - ' File "", line 13 in ' + ' File "", line 15 in funcA', + ' File "", line 17 in ' ] trace, exitcode = self.get_output(code, filename) self.assertEqual(trace, expected) self.assertEqual(exitcode, 0) def test_dump_traceback(self): - self.check_dump_traceback(None) + self.check_dump_traceback() def test_dump_traceback_file(self): with temporary_filename() as filename: self.check_dump_traceback(filename) + def test_dump_traceback_fd(self): + self.check_dump_traceback(use_fd=True) + def test_truncate(self): maxlen = 500 func_name = 'x' * (maxlen + 50) @@ -433,7 +453,8 @@ with temporary_filename() as filename: self.check_dump_traceback_threads(filename) - def _check_dump_traceback_later(self, repeat, cancel, filename, loops): + def _check_dump_traceback_later(self, repeat, cancel, + filename, loops, use_fd=False): """ 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 @@ -445,6 +466,7 @@ code = """ import faulthandler import time + import sys def func(timeout, repeat, cancel, file, loops): for loop in range(loops): @@ -460,10 +482,12 @@ loops = {loops} if {has_filename}: file = open({filename}, "wb") + elif {use_fd}: + file = sys.stderr.fileno() else: file = None func(timeout, repeat, cancel, file, loops) - if file is not None: + if {has_filename}: file.close() """ code = code.format( @@ -473,6 +497,7 @@ loops=loops, has_filename=bool(filename), filename=repr(filename), + use_fd=use_fd, ) trace, exitcode = self.get_output(code, filename) trace = '\n'.join(trace) @@ -482,7 +507,7 @@ if repeat: count *= 2 header = r'Timeout \(%s\)!\nThread 0x[0-9a-f]+ \(most recent call first\):\n' % timeout_str - regex = expected_traceback(9, 20, header, min_count=count) + regex = expected_traceback(10, 23, header, min_count=count) self.assertRegex(trace, regex) else: self.assertEqual(trace, '') @@ -491,7 +516,7 @@ @unittest.skipIf(not hasattr(faulthandler, 'dump_traceback_later'), 'need faulthandler.dump_traceback_later()') def check_dump_traceback_later(self, repeat=False, cancel=False, - file=False, twice=False): + file=False, twice=False, use_fd=False): if twice: loops = 2 else: @@ -501,7 +526,8 @@ self._check_dump_traceback_later(repeat, cancel, filename, loops) else: - self._check_dump_traceback_later(repeat, cancel, None, loops) + self._check_dump_traceback_later(repeat, cancel, + None, loops, use_fd=use_fd) def test_dump_traceback_later(self): self.check_dump_traceback_later() @@ -515,13 +541,16 @@ def test_dump_traceback_later_file(self): self.check_dump_traceback_later(file=True) + def test_dump_traceback_later_fd(self): + self.check_dump_traceback_later(use_fd=True) + def test_dump_traceback_later_twice(self): self.check_dump_traceback_later(twice=True) @unittest.skipIf(not hasattr(faulthandler, "register"), "need faulthandler.register") def check_register(self, filename=False, all_threads=False, - unregister=False, chain=False): + unregister=False, chain=False, use_fd=False): """ Register a handler displaying the traceback on a user signal. Raise the signal and check the written traceback. @@ -551,6 +580,8 @@ if {has_filename}: file = open({filename}, "wb") + elif {use_fd}: + file = sys.stderr.fileno() else: file = None if chain: @@ -567,7 +598,7 @@ output = sys.stderr print("Error: signal handler not called!", file=output) exitcode = 1 - if file is not None: + if {has_filename}: file.close() sys.exit(exitcode) """ @@ -578,6 +609,7 @@ signum=signum, unregister=unregister, chain=chain, + use_fd=use_fd, ) trace, exitcode = self.get_output(code, filename) trace = '\n'.join(trace) @@ -586,7 +618,7 @@ regex = 'Current thread XXX \(most recent call first\):\n' else: regex = 'Stack \(most recent call first\):\n' - regex = expected_traceback(7, 28, regex) + regex = expected_traceback(7, 30, regex) self.assertRegex(trace, regex) else: self.assertEqual(trace, '') @@ -605,6 +637,9 @@ with temporary_filename() as filename: self.check_register(filename=filename) + def test_register_fd(self): + self.check_register(use_fd=True) + def test_register_threads(self): self.check_register(all_threads=True) diff -r 5163fb8dc61f Misc/NEWS --- a/Misc/NEWS Tue Mar 10 14:47:41 2015 +0200 +++ b/Misc/NEWS Wed Mar 11 23:03:24 2015 +0800 @@ -17,6 +17,8 @@ The usage of os.scandir() reduces the number of calls to os.stat(). Initial patch written by Ben Hoyt. +- Issue #23566: faulthandler now accepts file descriptors. Patch by Wei Wu. + What's New in Python 3.5 alpha 2? ================================= diff -r 5163fb8dc61f Modules/faulthandler.c --- a/Modules/faulthandler.c Tue Mar 10 14:47:41 2015 +0200 +++ b/Modules/faulthandler.c Wed Mar 11 23:03:24 2015 +0800 @@ -133,32 +133,46 @@ call its flush() method. If file is NULL or Py_None, use sys.stderr as the new file. + If file is integer, it will be treated as file descriptor. - On success, return the new file and write the file descriptor into *p_fd. - On error, return NULL. */ + On success, return the file descriptor and write the new file into *file_ptr. + On error, return -1. */ -static PyObject* -faulthandler_get_fileno(PyObject *file, int *p_fd) +static int +faulthandler_get_fileno(PyObject **file_ptr) { PyObject *result; long fd_long; int fd; - + PyObject *file = *file_ptr; + if (file == NULL || file == Py_None) { file = _PySys_GetObjectId(&PyId_stderr); if (file == NULL) { PyErr_SetString(PyExc_RuntimeError, "unable to get sys.stderr"); - return NULL; + return -1; } if (file == Py_None) { PyErr_SetString(PyExc_RuntimeError, "sys.stderr is None"); - return NULL; + return -1; } } + else if (PyLong_Check(file)) { + fd = _PyLong_AsInt(file); + if (fd == -1 && PyErr_Occurred()) + return -1; + if (fd < 0 || !_PyVerify_fd(fd)) { + PyErr_SetString(PyExc_ValueError, + "file is not a valid file descripter"); + return -1; + } + *file_ptr = NULL; + return fd; + } result = _PyObject_CallMethodId(file, &PyId_fileno, ""); if (result == NULL) - return NULL; + return -1; fd = -1; if (PyLong_Check(result)) { @@ -171,7 +185,7 @@ if (fd == -1) { PyErr_SetString(PyExc_RuntimeError, "file.fileno() is not a valid file descriptor"); - return NULL; + return -1; } result = _PyObject_CallMethodId(file, &PyId_flush, ""); @@ -181,8 +195,8 @@ /* ignore flush() error */ PyErr_Clear(); } - *p_fd = fd; - return file; + *file_ptr = file; + return fd; } /* Get the state of the current thread: only call this function if the current @@ -215,8 +229,8 @@ &file, &all_threads)) return NULL; - file = faulthandler_get_fileno(file, &fd); - if (file == NULL) + fd = faulthandler_get_fileno(&file); + if (fd < 0) return NULL; tstate = get_thread_state(); @@ -339,8 +353,8 @@ "|Oi:enable", kwlist, &file, &all_threads)) return NULL; - file = faulthandler_get_fileno(file, &fd); - if (file == NULL) + fd = faulthandler_get_fileno(&file); + if (fd < 0) return NULL; tstate = get_thread_state(); @@ -348,7 +362,7 @@ return NULL; Py_XDECREF(fatal_error.file); - Py_INCREF(file); + Py_XINCREF(file); fatal_error.file = file; fatal_error.fd = fd; fatal_error.all_threads = all_threads; @@ -553,8 +567,8 @@ if (tstate == NULL) return NULL; - file = faulthandler_get_fileno(file, &fd); - if (file == NULL) + fd = faulthandler_get_fileno(&file); + if (fd < 0) return NULL; /* format the timeout */ @@ -567,7 +581,7 @@ cancel_dump_traceback_later(); Py_XDECREF(thread.file); - Py_INCREF(file); + Py_XINCREF(file); thread.file = file; thread.fd = fd; thread.timeout_us = timeout_us; @@ -737,8 +751,8 @@ if (tstate == NULL) return NULL; - file = faulthandler_get_fileno(file, &fd); - if (file == NULL) + fd = faulthandler_get_fileno(&file); + if (fd < 0) return NULL; if (user_signals == NULL) { @@ -760,7 +774,7 @@ } Py_XDECREF(user->file); - Py_INCREF(file); + Py_XINCREF(file); user->file = file; user->fd = fd; user->all_threads = all_threads;