-
Notifications
You must be signed in to change notification settings - Fork 197
Add trigger_exception_handler() for post-mortem debugging of caught exceptions #1996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
01ea4f0
8827dee
11722c1
ec94771
2100449
24d40bd
7b0cb68
b54991d
92434f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| "listen", | ||
| "log_to", | ||
| "trace_this_thread", | ||
| "trigger_exception_handler", | ||
| "wait_for_client", | ||
| ] | ||
|
|
||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2427,6 +2427,51 @@ def do_stop_on_unhandled_exception(self, thread, frame, frames_byid, arg): | |
| remove_exception_from_frame(frame) | ||
| frame = None | ||
|
|
||
| def trigger_exception_handler(self, excinfo, as_uncaught=True): | ||
| """ | ||
| Triggers post-mortem debugging as if handling an uncaught exception. | ||
|
|
||
| If as_uncaught is True (default), respects exception breakpoint configuration and applies breakpoint filters. | ||
|
|
||
| :param excinfo: A tuple of (exc_type, exc_value, exc_traceback). | ||
| """ | ||
| if not as_uncaught: | ||
| exctype, value, tb = excinfo | ||
|
|
||
| # Walk traceback to build frames list and find user frame | ||
| frames = [] | ||
| user_frame = None | ||
| while tb is not None: | ||
| frame = tb.tb_frame | ||
| # Skip debugger-internal frames, use last user frame | ||
| if self.get_file_type(frame) is None: | ||
| user_frame = frame | ||
| frames.append(frame) | ||
| tb = tb.tb_next | ||
|
|
||
| if user_frame is None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📍 src/debugpy/_vendored/pydevd/pydevd.py:2430 |
||
| pydev_log.debug("trigger_exception_handler: no user frame found in traceback") | ||
| return | ||
|
|
||
| frames_byid = dict([(id(frame), frame) for frame in frames]) | ||
|
|
||
| if PYDEVD_USE_SYS_MONITORING: | ||
| saved_sys_monitoring_trace = pydevd_sys_monitoring.suspend_current_thread_tracing() | ||
| thread = threading.current_thread() | ||
| additional_info = self.set_additional_thread_info(thread) | ||
| additional_info.is_tracing += 1 | ||
|
|
||
| try: | ||
| if as_uncaught: | ||
| self.stop_on_unhandled_exception(self, thread, additional_info, excinfo) | ||
| else: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📍 src/debugpy/_vendored/pydevd/pydevd.py:2466 |
||
| self.do_stop_on_unhandled_exception(thread, user_frame, frames_byid, excinfo) | ||
| finally: | ||
| if PYDEVD_USE_SYS_MONITORING: | ||
| if saved_sys_monitoring_trace: | ||
| pydevd_sys_monitoring.resume_current_thread_tracing() | ||
| additional_info.is_tracing -= 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📍 src/debugpy/_vendored/pydevd/pydevd.py:2455 |
||
|
|
||
| def set_trace_for_frame_and_parents(self, thread_ident: Optional[int], frame, **kwargs): | ||
| disable = kwargs.pop("disable", False) | ||
| assert not kwargs | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,6 +212,33 @@ def trace_this_thread(__should_trace: bool): | |
| """ | ||
|
|
||
|
|
||
| @_api() | ||
| def trigger_exception_handler( | ||
| __excinfo: typing.Tuple[type, BaseException, typing.Any] | BaseException | None = None, | ||
| as_uncaught: bool = True, | ||
| ) -> None: | ||
| """Stops the debugger on an unhandled exception. | ||
|
|
||
| If a debug client is connected, pauses execution as if an | ||
| unhandled exception was caught. This allows inspection of the | ||
| exception and call stack at the point of failure. | ||
|
|
||
| If no exception info is provided, uses sys.exc_info() to get | ||
| the current exception. If there is no current exception and no | ||
| argument is provided, does nothing. | ||
|
|
||
| Safe to call when no debugger is connected (returns immediately). | ||
|
|
||
| Example:: | ||
|
|
||
| try: | ||
| risky_operation() | ||
| except Exception: | ||
| debugpy.trigger_exception_handler() # Uses current exception | ||
| raise | ||
| """ | ||
|
|
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📍 src/debugpy/public_api.py:215 |
||
| def get_cli_options() -> CliOptions | None: | ||
| """Returns the CLI options that were processed by debugpy. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📍 src/debugpy/_vendored/pydevd/_pydevd_sys_monitoring/_pydevd_sys_monitoring.py:1875
When the
exceptbranch freshly creates aThreadInfovia_get_thread_info(True, 1)(tracing enabled), it still reportsprevious_state = True. The caller'sfinallythen seessaved == Trueand re-enables tracing on a thread that was not previously traced, contradicting "restore previous state" and potentially causing unexpected breakpoint stops on background threads. When theThreadInfois created here rather than found, returnFalseso the caller skips the resume. Mirror the same fix in the.pyx(and regenerate the.c).