summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorVictor Stinner <vstinner@redhat.com>2019-05-27 22:39:52 (GMT)
committerGitHub <noreply@github.com>2019-05-27 22:39:52 (GMT)
commitcd590a7cede156a4244e7cac61e4504e5344d842 (patch)
tree371aa076f7be6e942e904ebfa6aa6e7dbb2f0678 /Lib
parent23b4b697e5b6cc897696f9c0288c187d2d24bff2 (diff)
downloadcpython-cd590a7cede156a4244e7cac61e4504e5344d842.zip
cpython-cd590a7cede156a4244e7cac61e4504e5344d842.tar.gz
cpython-cd590a7cede156a4244e7cac61e4504e5344d842.tar.bz2
bpo-1230540: Add threading.excepthook() (GH-13515)
Add a new threading.excepthook() function which handles uncaught Thread.run() exception. It can be overridden to control how uncaught exceptions are handled. threading.ExceptHookArgs is not documented on purpose: it should not be used directly. * threading.excepthook() and threading.ExceptHookArgs. * Add _PyErr_Display(): similar to PyErr_Display(), but accept a 'file' parameter. * Add _thread._excepthook(): C implementation of the exception hook calling _PyErr_Display(). * Add _thread._ExceptHookArgs: structseq type. * Add threading._invoke_excepthook_wrapper() which handles the gory details to ensure that everything remains alive during Python shutdown. * Add unit tests.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_threading.py92
-rw-r--r--Lib/threading.py155
2 files changed, 195 insertions, 52 deletions
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 3bfd6fa..8c8cc12 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -1112,6 +1112,98 @@ class ThreadingExceptionTests(BaseTestCase):
# explicitly break the reference cycle to not leak a dangling thread
thread.exc = None
+
+class ThreadRunFail(threading.Thread):
+ def run(self):
+ raise ValueError("run failed")
+
+
+class ExceptHookTests(BaseTestCase):
+ def test_excepthook(self):
+ with support.captured_output("stderr") as stderr:
+ thread = ThreadRunFail(name="excepthook thread")
+ thread.start()
+ thread.join()
+
+ stderr = stderr.getvalue().strip()
+ self.assertIn(f'Exception in thread {thread.name}:\n', stderr)
+ self.assertIn('Traceback (most recent call last):\n', stderr)
+ self.assertIn(' raise ValueError("run failed")', stderr)
+ self.assertIn('ValueError: run failed', stderr)
+
+ @support.cpython_only
+ def test_excepthook_thread_None(self):
+ # threading.excepthook called with thread=None: log the thread
+ # identifier in this case.
+ with support.captured_output("stderr") as stderr:
+ try:
+ raise ValueError("bug")
+ except Exception as exc:
+ args = threading.ExceptHookArgs([*sys.exc_info(), None])
+ threading.excepthook(args)
+
+ stderr = stderr.getvalue().strip()
+ self.assertIn(f'Exception in thread {threading.get_ident()}:\n', stderr)
+ self.assertIn('Traceback (most recent call last):\n', stderr)
+ self.assertIn(' raise ValueError("bug")', stderr)
+ self.assertIn('ValueError: bug', stderr)
+
+ def test_system_exit(self):
+ class ThreadExit(threading.Thread):
+ def run(self):
+ sys.exit(1)
+
+ # threading.excepthook() silently ignores SystemExit
+ with support.captured_output("stderr") as stderr:
+ thread = ThreadExit()
+ thread.start()
+ thread.join()
+
+ self.assertEqual(stderr.getvalue(), '')
+
+ def test_custom_excepthook(self):
+ args = None
+
+ def hook(hook_args):
+ nonlocal args
+ args = hook_args
+
+ try:
+ with support.swap_attr(threading, 'excepthook', hook):
+ thread = ThreadRunFail()
+ thread.start()
+ thread.join()
+
+ self.assertEqual(args.exc_type, ValueError)
+ self.assertEqual(str(args.exc_value), 'run failed')
+ self.assertEqual(args.exc_traceback, args.exc_value.__traceback__)
+ self.assertIs(args.thread, thread)
+ finally:
+ # Break reference cycle
+ args = None
+
+ def test_custom_excepthook_fail(self):
+ def threading_hook(args):
+ raise ValueError("threading_hook failed")
+
+ err_str = None
+
+ def sys_hook(exc_type, exc_value, exc_traceback):
+ nonlocal err_str
+ err_str = str(exc_value)
+
+ with support.swap_attr(threading, 'excepthook', threading_hook), \
+ support.swap_attr(sys, 'excepthook', sys_hook), \
+ support.captured_output('stderr') as stderr:
+ thread = ThreadRunFail()
+ thread.start()
+ thread.join()
+
+ self.assertEqual(stderr.getvalue(),
+ 'Exception in threading.excepthook:\n')
+ self.assertEqual(err_str, 'threading_hook failed')
+
+
class TimerTests(BaseTestCase):
def setUp(self):
diff --git a/Lib/threading.py b/Lib/threading.py
index 77a2bae..3d197ee 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -5,7 +5,6 @@ import sys as _sys
import _thread
from time import monotonic as _time
-from traceback import format_exc as _format_exc
from _weakrefset import WeakSet
from itertools import islice as _islice, count as _count
try:
@@ -27,7 +26,8 @@ __all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
'enumerate', 'main_thread', 'TIMEOUT_MAX',
'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
- 'setprofile', 'settrace', 'local', 'stack_size']
+ 'setprofile', 'settrace', 'local', 'stack_size',
+ 'excepthook', 'ExceptHookArgs']
# Rename some stuff so "from threading import *" is safe
_start_new_thread = _thread.start_new_thread
@@ -752,14 +752,6 @@ class Thread:
"""
_initialized = False
- # Need to store a reference to sys.exc_info for printing
- # out exceptions when a thread tries to use a global var. during interp.
- # shutdown and thus raises an exception about trying to perform some
- # operation on/with a NoneType
- _exc_info = _sys.exc_info
- # Keep sys.exc_clear too to clear the exception just before
- # allowing .join() to return.
- #XXX __exc_clear = _sys.exc_clear
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
@@ -802,9 +794,9 @@ class Thread:
self._started = Event()
self._is_stopped = False
self._initialized = True
- # sys.stderr is not stored in the class like
- # sys.exc_info since it can be changed between instances
+ # Copy of sys.stderr used by self._invoke_excepthook()
self._stderr = _sys.stderr
+ self._invoke_excepthook = _make_invoke_excepthook()
# For debugging and _after_fork()
_dangling.add(self)
@@ -929,47 +921,8 @@ class Thread:
try:
self.run()
- except SystemExit:
- pass
except:
- # If sys.stderr is no more (most likely from interpreter
- # shutdown) use self._stderr. Otherwise still use sys (as in
- # _sys) in case sys.stderr was redefined since the creation of
- # self.
- if _sys and _sys.stderr is not None:
- print("Exception in thread %s:\n%s" %
- (self.name, _format_exc()), file=_sys.stderr)
- elif self._stderr is not None:
- # Do the best job possible w/o a huge amt. of code to
- # approximate a traceback (code ideas from
- # Lib/traceback.py)
- exc_type, exc_value, exc_tb = self._exc_info()
- try:
- print((
- "Exception in thread " + self.name +
- " (most likely raised during interpreter shutdown):"), file=self._stderr)
- print((
- "Traceback (most recent call last):"), file=self._stderr)
- while exc_tb:
- print((
- ' File "%s", line %s, in %s' %
- (exc_tb.tb_frame.f_code.co_filename,
- exc_tb.tb_lineno,
- exc_tb.tb_frame.f_code.co_name)), file=self._stderr)
- exc_tb = exc_tb.tb_next
- print(("%s: %s" % (exc_type, exc_value)), file=self._stderr)
- self._stderr.flush()
- # Make sure that exc_tb gets deleted since it is a memory
- # hog; deleting everything else is just for thoroughness
- finally:
- del exc_type, exc_value, exc_tb
- finally:
- # Prevent a race in
- # test_threading.test_no_refcycle_through_target when
- # the exception keeps the target alive past when we
- # assert that it's dead.
- #XXX self._exc_clear()
- pass
+ self._invoke_excepthook(self)
finally:
with _active_limbo_lock:
try:
@@ -1163,6 +1116,104 @@ class Thread:
def setName(self, name):
self.name = name
+
+try:
+ from _thread import (_excepthook as excepthook,
+ _ExceptHookArgs as ExceptHookArgs)
+except ImportError:
+ # Simple Python implementation if _thread._excepthook() is not available
+ from traceback import print_exception as _print_exception
+ from collections import namedtuple
+
+ _ExceptHookArgs = namedtuple(
+ 'ExceptHookArgs',
+ 'exc_type exc_value exc_traceback thread')
+
+ def ExceptHookArgs(args):
+ return _ExceptHookArgs(*args)
+
+ def excepthook(args, /):
+ """
+ Handle uncaught Thread.run() exception.
+ """
+ if args.exc_type == SystemExit:
+ # silently ignore SystemExit
+ return
+
+ if _sys is not None and _sys.stderr is not None:
+ stderr = _sys.stderr
+ elif args.thread is not None:
+ stderr = args.thread._stderr
+ if stderr is None:
+ # do nothing if sys.stderr is None and sys.stderr was None
+ # when the thread was created
+ return
+ else:
+ # do nothing if sys.stderr is None and args.thread is None
+ return
+
+ if args.thread is not None:
+ name = args.thread.name
+ else:
+ name = get_ident()
+ print(f"Exception in thread {name}:",
+ file=stderr, flush=True)
+ _print_exception(args.exc_type, args.exc_value, args.exc_traceback,
+ file=stderr)
+ stderr.flush()
+
+
+def _make_invoke_excepthook():
+ # Create a local namespace to ensure that variables remain alive
+ # when _invoke_excepthook() is called, even if it is called late during
+ # Python shutdown. It is mostly needed for daemon threads.
+
+ old_excepthook = excepthook
+ old_sys_excepthook = _sys.excepthook
+ if old_excepthook is None:
+ raise RuntimeError("threading.excepthook is None")
+ if old_sys_excepthook is None:
+ raise RuntimeError("sys.excepthook is None")
+
+ sys_exc_info = _sys.exc_info
+ local_print = print
+ local_sys = _sys
+
+ def invoke_excepthook(thread):
+ global excepthook
+ try:
+ hook = excepthook
+ if hook is None:
+ hook = old_excepthook
+
+ args = ExceptHookArgs([*sys_exc_info(), thread])
+
+ hook(args)
+ except Exception as exc:
+ exc.__suppress_context__ = True
+ del exc
+
+ if local_sys is not None and local_sys.stderr is not None:
+ stderr = local_sys.stderr
+ else:
+ stderr = thread._stderr
+
+ local_print("Exception in threading.excepthook:",
+ file=stderr, flush=True)
+
+ if local_sys is not None and local_sys.excepthook is not None:
+ sys_excepthook = local_sys.excepthook
+ else:
+ sys_excepthook = old_sys_excepthook
+
+ sys_excepthook(*sys_exc_info())
+ finally:
+ # Break reference cycle (exception stored in a variable)
+ args = None
+
+ return invoke_excepthook
+
+
# The timer class was contributed by Itamar Shtull-Trauring
class Timer(Thread):