From 64366fa9b3ba71b8a503a8719eff433f4ea49eb9 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Mon, 2 Nov 2020 15:16:25 +0100 Subject: [PATCH] bpo-41435: Add sys._current_exceptions() function (GH-21689) This adds a new function named sys._current_exceptions() which is equivalent ot sys._current_frames() except that it returns the exceptions currently handled by other threads. It is equivalent to calling sys.exc_info() for each running thread. --- Doc/library/sys.rst | 12 ++++ Include/cpython/pystate.h | 5 ++ Lib/test/test_sys.py | 67 +++++++++++++++++++ .../2020-08-07-13-42-48.bpo-41435.qPWjJA.rst | 1 + Python/clinic/sysmodule.c.h | 22 +++++- Python/pystate.c | 63 +++++++++++++++++ Python/sysmodule.c | 16 +++++ 7 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 2f0840e2a74..f0acfcfe639 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -196,6 +196,18 @@ always available. .. audit-event:: sys._current_frames "" sys._current_frames +.. function:: _current_exceptions() + + Return a dictionary mapping each thread's identifier to the topmost exception + currently active in that thread at the time the function is called. + If a thread is not currently handling an exception, it is not included in + the result dictionary. + + This is most useful for statistical profiling. + + This function should be used for internal and specialized purposes only. + + .. audit-event:: sys._current_exceptions "" sys._current_exceptions .. function:: breakpointhook() diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 5d5e4e33197..25522b4dbec 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -167,6 +167,11 @@ PyAPI_FUNC(PyInterpreterState *) _PyGILState_GetInterpreterStateUnsafe(void); */ PyAPI_FUNC(PyObject *) _PyThread_CurrentFrames(void); +/* The implementation of sys._current_exceptions() Returns a dict mapping + thread id to that thread's current exception. +*/ +PyAPI_FUNC(PyObject *) _PyThread_CurrentExceptions(void); + /* Routines for advanced debuggers, requested by David Beazley. Don't use unless you know what you are doing! */ PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void); diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 30c29a26a99..332ed8f550c 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -432,6 +432,73 @@ class SysModuleTest(unittest.TestCase): leave_g.set() t.join() + @threading_helper.reap_threads + def test_current_exceptions(self): + import threading + import traceback + + # Spawn a thread that blocks at a known place. Then the main + # thread does sys._current_frames(), and verifies that the frames + # returned make sense. + entered_g = threading.Event() + leave_g = threading.Event() + thread_info = [] # the thread's id + + def f123(): + g456() + + def g456(): + thread_info.append(threading.get_ident()) + entered_g.set() + while True: + try: + raise ValueError("oops") + except ValueError: + if leave_g.wait(timeout=support.LONG_TIMEOUT): + break + + t = threading.Thread(target=f123) + t.start() + entered_g.wait() + + # At this point, t has finished its entered_g.set(), although it's + # impossible to guess whether it's still on that line or has moved on + # to its leave_g.wait(). + self.assertEqual(len(thread_info), 1) + thread_id = thread_info[0] + + d = sys._current_exceptions() + for tid in d: + self.assertIsInstance(tid, int) + self.assertGreater(tid, 0) + + main_id = threading.get_ident() + self.assertIn(main_id, d) + self.assertIn(thread_id, d) + self.assertEqual((None, None, None), d.pop(main_id)) + + # Verify that the captured thread frame is blocked in g456, called + # from f123. This is a litte tricky, since various bits of + # threading.py are also in the thread's call stack. + exc_type, exc_value, exc_tb = d.pop(thread_id) + stack = traceback.extract_stack(exc_tb.tb_frame) + for i, (filename, lineno, funcname, sourceline) in enumerate(stack): + if funcname == "f123": + break + else: + self.fail("didn't find f123() on thread's call stack") + + self.assertEqual(sourceline, "g456()") + + # And the next record must be for g456(). + filename, lineno, funcname, sourceline = stack[i+1] + self.assertEqual(funcname, "g456") + self.assertTrue(sourceline.startswith("if leave_g.wait(")) + + # Reap the spawned thread. + leave_g.set() + t.join() + def test_attributes(self): self.assertIsInstance(sys.api_version, int) self.assertIsInstance(sys.argv, list) diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst b/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst new file mode 100644 index 00000000000..d2978f9b4ec --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst @@ -0,0 +1 @@ +Add `sys._current_exceptions()` function to retrieve a dictionary mapping each thread's identifier to the topmost exception currently active in that thread at the time the function is called. \ No newline at end of file diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index c1a9a2d69f0..addd58922e7 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -801,6 +801,26 @@ sys__current_frames(PyObject *module, PyObject *Py_UNUSED(ignored)) return sys__current_frames_impl(module); } +PyDoc_STRVAR(sys__current_exceptions__doc__, +"_current_exceptions($module, /)\n" +"--\n" +"\n" +"Return a dict mapping each thread\'s identifier to its current raised exception.\n" +"\n" +"This function should be used for specialized purposes only."); + +#define SYS__CURRENT_EXCEPTIONS_METHODDEF \ + {"_current_exceptions", (PyCFunction)sys__current_exceptions, METH_NOARGS, sys__current_exceptions__doc__}, + +static PyObject * +sys__current_exceptions_impl(PyObject *module); + +static PyObject * +sys__current_exceptions(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return sys__current_exceptions_impl(module); +} + PyDoc_STRVAR(sys_call_tracing__doc__, "call_tracing($module, func, args, /)\n" "--\n" @@ -945,4 +965,4 @@ sys_getandroidapilevel(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=87baa3357293ea65 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=bbc4963fe86a29d9 input=a9049054013a1b77]*/ diff --git a/Python/pystate.c b/Python/pystate.c index e88898670cd..e37cbd5a657 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1222,6 +1222,69 @@ done: return result; } +PyObject * +_PyThread_CurrentExceptions(void) +{ + PyThreadState *tstate = _PyThreadState_GET(); + + _Py_EnsureTstateNotNULL(tstate); + + if (_PySys_Audit(tstate, "sys._current_exceptions", NULL) < 0) { + return NULL; + } + + PyObject *result = PyDict_New(); + if (result == NULL) { + return NULL; + } + + /* for i in all interpreters: + * for t in all of i's thread states: + * if t's frame isn't NULL, map t's id to its frame + * Because these lists can mutate even when the GIL is held, we + * need to grab head_mutex for the duration. + */ + _PyRuntimeState *runtime = tstate->interp->runtime; + HEAD_LOCK(runtime); + PyInterpreterState *i; + for (i = runtime->interpreters.head; i != NULL; i = i->next) { + PyThreadState *t; + for (t = i->tstate_head; t != NULL; t = t->next) { + _PyErr_StackItem *err_info = _PyErr_GetTopmostException(t); + if (err_info == NULL) { + continue; + } + PyObject *id = PyLong_FromUnsignedLong(t->thread_id); + if (id == NULL) { + goto fail; + } + PyObject *exc_info = PyTuple_Pack( + 3, + err_info->exc_type != NULL ? err_info->exc_type : Py_None, + err_info->exc_value != NULL ? err_info->exc_value : Py_None, + err_info->exc_traceback != NULL ? err_info->exc_traceback : Py_None); + if (exc_info == NULL) { + Py_DECREF(id); + goto fail; + } + int stat = PyDict_SetItem(result, id, exc_info); + Py_DECREF(id); + Py_DECREF(exc_info); + if (stat < 0) { + goto fail; + } + } + } + goto done; + +fail: + Py_CLEAR(result); + +done: + HEAD_UNLOCK(runtime); + return result; +} + /* Python "auto thread state" API. */ /* Keep this as a static, as it is not reliable! It can only diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 749b96455d6..945e639ca57 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1837,6 +1837,21 @@ sys__current_frames_impl(PyObject *module) return _PyThread_CurrentFrames(); } +/*[clinic input] +sys._current_exceptions + +Return a dict mapping each thread's identifier to its current raised exception. + +This function should be used for specialized purposes only. +[clinic start generated code]*/ + +static PyObject * +sys__current_exceptions_impl(PyObject *module) +/*[clinic end generated code: output=2ccfd838c746f0ba input=0e91818fbf2edc1f]*/ +{ + return _PyThread_CurrentExceptions(); +} + /*[clinic input] sys.call_tracing @@ -1953,6 +1968,7 @@ static PyMethodDef sys_methods[] = { METH_FASTCALL | METH_KEYWORDS, breakpointhook_doc}, SYS__CLEAR_TYPE_CACHE_METHODDEF SYS__CURRENT_FRAMES_METHODDEF + SYS__CURRENT_EXCEPTIONS_METHODDEF SYS_DISPLAYHOOK_METHODDEF SYS_EXC_INFO_METHODDEF SYS_EXCEPTHOOK_METHODDEF