Skip to content

Add trigger_exception_handler() for post-mortem debugging of caught exceptions#1996

Open
nshepperd wants to merge 9 commits into
microsoft:mainfrom
nshepperd:easy-postmortem
Open

Add trigger_exception_handler() for post-mortem debugging of caught exceptions#1996
nshepperd wants to merge 9 commits into
microsoft:mainfrom
nshepperd:easy-postmortem

Conversation

@nshepperd

@nshepperd nshepperd commented Feb 1, 2026

Copy link
Copy Markdown

Adds a convenient trigger_exception_handler() function (previously named post_mortem(); renamed per review discussion) that can be called to immediately trigger the graphical postmortem debugger given any exc_info structure. We need to suspend sys.monitoring callbacks while doing this, because the built in reentrancy protection doesn't apply when pydevd suspends directly from normal code instead of a monitoring callback. Functions were added in _pydevd_sys_monitoring to do that.

@nshepperd

Copy link
Copy Markdown
Author

See #1530.

@rchiodo

rchiodo commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for the submission but I think this would need some sort of automated test before we'd accept it. And maybe a section in the readme.md explaining how to use it.

@nshepperd

Copy link
Copy Markdown
Author

Actually I was wondering how to setup tests for this. I figured it out today. Hooray!

@rchiodo

rchiodo commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@rchiodo

rchiodo commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

I'm confused about something. pdb.post_mortem attaches the debugger to the process and starts debugging it. This won't. You have to be already attached, right?

What would be the point of this then? Is it just so you don't have to break on exceptions in the debugger, but instead control when the break happens?

@rchiodo

rchiodo commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

I think ideally this would actually start VS code and open the debugger. Which might be doable, but would probably have to be configurable somehow.

@nshepperd

Copy link
Copy Markdown
Author

Yes, the point is so that you can eg. make a context manager or function decorator to debug exceptions that bubble past it instead of setting bespoke breakpoint, which isn't really directly possible for this. I find something like this indispensable for debugging things like api servers or unit tests (pytest) that have exception handlers that eat exceptions. With this you can just wrap your route handler or set an exception handler with conftest.py and launch the program under the debugger and everything just works.

I think previously debugpy had the 'user uncaught exceptions' breakpoint added for similar use cases, but that's much less specific and in some cases has too many false positives to be useful.

You're right this is a bit different from pdb.post_mortem() in that that actually starts the debugger. I'm not sure about starting vscode though, heh. Maybe it's possible via some sort of vscode:// url? What we could do is at least run debugpy.listen and wait for the debugger to attach? I'm not sure i would actually use that though, usually if i'd want to debug something with vscode I'm already debugging it (eg. I press the 'debug unit test' button) and the issue is just getting the breakpoint.

@nshepperd

Copy link
Copy Markdown
Author

Either way each use cases should work, i think. If you wanted to launch vscode and attach the debugger you can use existing debugpy functions for that and then call post_mortem. Or if we include that in the function you can check if debugpy.is_client_connected() before calling it if you don't want that.

@rchiodo

rchiodo commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

With this you can just wrap your route handler or set an exception handler with conftest.py and launch the program under the debugger and everything just works.

If this is the intended use case, maybe the name is off then. It's not post_mortem, it's more like 'force_exception_handler' or something like that. I feel like 'post_mortem' is intended to be like pdb means it. It starts a debugger even when it's not already attached.

@rchiodo

rchiodo commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Your unit tests also seem to be failing. Not sure if you can see the errors or not

@nshepperd

Copy link
Copy Markdown
Author

trigger_exception_handler(), maybe?

I see the test failures. Cython issue. I forgot to regenerate the cython files. Confusing because it worked locally.

@rchiodo

rchiodo commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

trigger_exception_handler sounds good to me :)

nshepperd and others added 9 commits July 15, 2026 00:48
…ht flag which says whether to apply uncaught exception breakpoint filters, defaulting to True.
…ost_mortem does. Also, add an explanation in the README.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… yet.

If the calling thread had never hit a monitoring event that created its
ThreadInfo, suspend_current_thread_tracing() was a no-op, and the first
monitoring event during the manual do_wait_suspend would create a
ThreadInfo with tracing enabled and re-enter do_wait_suspend, replacing
the tracked exception frames list (making exceptionInfo report 'type
unknown'). Create the ThreadInfo eagerly so tracing is really disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nshepperd nshepperd changed the title Easy post_mortem Add trigger_exception_handler() for post-mortem debugging of caught exceptions Jul 14, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown

@nshepperd please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"
Contributor License Agreement

Contribution License Agreement

This Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
and conveys certain license rights to Microsoft Corporation and its affiliates (“Microsoft”) for Your
contributions to Microsoft open source projects. This Agreement is effective as of the latest signature
date below.

  1. Definitions.
    “Code” means the computer software code, whether in human-readable or machine-executable form,
    that is delivered by You to Microsoft under this Agreement.
    “Project” means any of the projects owned or managed by Microsoft and offered under a license
    approved by the Open Source Initiative (www.opensource.org).
    “Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any
    Project, including but not limited to communication on electronic mailing lists, source code control
    systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
    discussing and improving that Project, but excluding communication that is conspicuously marked or
    otherwise designated in writing by You as “Not a Submission.”
    “Submission” means the Code and any other copyrightable material Submitted by You, including any
    associated comments and documentation.
  2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any
    Project. This Agreement covers any and all Submissions that You, now or in the future (except as
    described in Section 4 below), Submit to any Project.
  3. Originality of Work. You represent that each of Your Submissions is entirely Your original work.
    Should You wish to Submit materials that are not Your original work, You may Submit them separately
    to the Project if You (a) retain all copyright and license information that was in the materials as You
    received them, (b) in the description accompanying Your Submission, include the phrase “Submission
    containing materials of a third party:” followed by the names of the third party and any licenses or other
    restrictions of which You are aware, and (c) follow any other instructions in the Project’s written
    guidelines concerning Submissions.
  4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else
    for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
    Submission is made in the course of Your work for an employer or Your employer has intellectual
    property rights in Your Submission by contract or applicable law, You must secure permission from Your
    employer to make the Submission before signing this Agreement. In that case, the term “You” in this
    Agreement will refer to You and the employer collectively. If You change employers in the future and
    desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
    and secure permission from the new employer before Submitting those Submissions.
  5. Licenses.
  • Copyright License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the
    Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute
    the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third
    parties.
  • Patent License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under
    Your patent claims that are necessarily infringed by the Submission or the combination of the
    Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
    import or otherwise dispose of the Submission alone or with the Project.
  • Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement.
    No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
    granted by implication, exhaustion, estoppel or otherwise.
  1. Representations and Warranties. You represent that You are legally entitled to grant the above
    licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
    have disclosed under Section 3). You represent that You have secured permission from Your employer to
    make the Submission in cases where Your Submission is made in the course of Your work for Your
    employer or Your employer has intellectual property rights in Your Submission by contract or applicable
    law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
    have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
    You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
    REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
    EXPRESSLY STATED IN SECTIONS 3, 4, AND 6, THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
    PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
    NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
  2. Notice to Microsoft. You agree to notify Microsoft in writing of any facts or circumstances of which
    You later become aware that would make Your representations in this Agreement inaccurate in any
    respect.
  3. Information about Submissions. You agree that contributions to Projects and information about
    contributions may be maintained indefinitely and disclosed publicly, including Your name and other
    information that You submit with Your Submission.
  4. Governing Law/Jurisdiction. This Agreement is governed by the laws of the State of Washington, and
    the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
    Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
    exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
    defenses of lack of personal jurisdiction and forum non-conveniens.
  5. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and
    supersedes any and all prior agreements, understandings or communications, written or oral, between
    the parties relating to the subject matter hereof. This Agreement may be assigned by Microsoft.

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@rchiodo rchiodo marked this pull request as ready for review July 14, 2026 20:19
@rchiodo rchiodo requested a review from a team as a code owner July 14, 2026 20:19
@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

LGTM. I'll get somebody else to take a look.

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — @rchiodo is auto-reviewing this PR.

@heejaechang

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — @heejaechang is auto-reviewing this PR.

@heejaechang heejaechang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

if PYDEVD_USE_SYS_MONITORING:
if saved_sys_monitoring_trace:
pydevd_sys_monitoring.resume_current_thread_tracing()
additional_info.is_tracing -= 1

Copy link
Copy Markdown
Contributor

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.py:2455
The Skeptic verified (reproduced with a snippet: tracing restored? False) that suspend_current_thread_tracing(), set_additional_thread_info, and is_tracing += 1 all run outside the try. If anything raises in that window — most realistically a KeyboardInterrupt during a live debug session, or a failure in set_additional_thread_info — the finally calling resume_current_thread_tracing() never runs, leaving thread_info.trace = False permanently and silently killing breakpoints on that thread for the rest of the session. Move the suspend + is_tracing += 1 setup inside the try so the finally always restores state.

# explicitly disabled here, the first monitoring event on this thread
# would create it with tracing enabled.
thread_info = _get_thread_info(True, 1)
if thread_info is None:

Copy link
Copy Markdown
Contributor

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 except branch freshly creates a ThreadInfo via _get_thread_info(True, 1) (tracing enabled), it still reports previous_state = True. The caller's finally then sees saved == True and 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 the ThreadInfo is created here rather than found, return False so the caller skips the resume. Mirror the same fix in the .pyx (and regenerate the .c).

}
)

session.request_continue()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 tests/debugpy/test_trigger_exception_handler.py:1
The PR's core justification — suspending sys.monitoring to avoid reentrancy — is untested. None of the 5 tests set a breakpoint condition that re-enters monitoring during the stop, and none assert that breakpoints still fire on the same thread after request_continue(). If suspend_current_thread_tracing() were a no-op (or the resume-leak bugs triggered), every test would still pass. Add a test that hits a real breakpoint on the same thread after trigger_exception_handler returns, proving tracing was correctly restored.

Comment thread src/debugpy/public_api.py
raise
"""


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 src/debugpy/public_api.py:215
The as_uncaught parameter is never documented in the public docstring or README, despite changing a non-obvious, inverted-feeling contract: True respects the uncaught-exception breakpoint filters (may not stop), while False forces post-mortem regardless. Document both as_uncaught semantics and note the as_uncaught=False + library-only-traceback case.

frames.append(frame)
tb = tb.tb_next

if user_frame is None:

Copy link
Copy Markdown
Contributor

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.py:2430
The as_uncaught=False path silently no-ops when the traceback contains only library frames (user_frame stays None → debug log + return). The README/docstring imply as_uncaught=False always enters post-mortem, so a caller wrapping a library-only exception gets a surprising silent non-stop. At minimum document this; consider surfacing it more loudly than a debug-level log.

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for src/debugpy/server/api.py:L396.

📍 src/debugpy/server/api.py:366
Missing negative-path test coverage for the early returns here: no test covers "no client connected → returns gracefully" or "None arg / no current exception → does nothing". These are cheap to add and lock in the documented no-op semantics.

try:
if as_uncaught:
self.stop_on_unhandled_exception(self, thread, additional_info, excinfo)
else:

Copy link
Copy Markdown
Contributor

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.py:2466
self.stop_on_unhandled_exception(self, thread, additional_info, excinfo) is correct — line 765 binds the module-level function as an instance attribute, so the explicit self is the intended py_db first arg. It is NOT a bug, but it reads like a double-self mistake and is a maintenance trap a future reader may "fix" and break. Add a one-line comment (e.g. # instance attr holds a module-level fn; first arg is py_db).



exc_info = session.request("exceptionInfo", {"threadId": occ.body['threadId']})
assert exc_info == some.dict.containing(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 tests/debugpy/test_trigger_exception_handler.py:152
The stray print("About to call trigger_exception_handler with excinfo") in test_trigger_exception_handler_excinfo is leftover debugging. Remove it.

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Overall this is a solid, well-tested addition. The main theme worth addressing before/after merge is robustness of the sys.monitoring tracing state restoration: the suspend + is_tracing setup happens outside the try/finally, the 'previous state' value can be misleading for freshly-created ThreadInfo, and the core reentrancy-suspension behavior is not directly exercised by a test. Also a couple of documentation gaps around as_uncaught. None are blocking.

@rchiodo rchiodo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Okay sorry, I had our automated tools run over it. It found some things to look at if you can. Thanks for the work :)

@rchiodo

rchiodo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

You also need to do this before we can accept it:

@microsoft-github-policy-service agree [company="{your company}"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants