diff options
author | Victor Stinner <vstinner@redhat.com> | 2019-05-27 22:39:52 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-27 22:39:52 (GMT) |
commit | cd590a7cede156a4244e7cac61e4504e5344d842 (patch) | |
tree | 371aa076f7be6e942e904ebfa6aa6e7dbb2f0678 /Lib/threading.py | |
parent | 23b4b697e5b6cc897696f9c0288c187d2d24bff2 (diff) | |
download | cpython-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/threading.py')
-rw-r--r-- | Lib/threading.py | 155 |
1 files changed, 103 insertions, 52 deletions
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): |