Skip to content

Commit d407a4f

Browse files
committed
Add postmortem debugging functionality and related tracing controls
1 parent 7ac3d1f commit d407a4f

6 files changed

Lines changed: 226 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ wheelhouse/
3030
*.dist-info/
3131
.installed.cfg
3232
*.egg
33+
build/
3334

3435
# PyInstaller
3536
# Usually these files are written by a python script from a template

src/debugpy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"is_client_connected",
2020
"listen",
2121
"log_to",
22+
"postmortem",
2223
"trace_this_thread",
2324
"wait_for_client",
2425
]

src/debugpy/_vendored/pydevd/_pydevd_sys_monitoring/_pydevd_sys_monitoring.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,81 @@ def stop_monitoring(all_threads=False):
18231823
thread_info.trace = False
18241824

18251825

1826+
# fmt: off
1827+
# IFDEF CYTHON
1828+
# cpdef bint suspend_current_thread_tracing():
1829+
# cdef ThreadInfo thread_info
1830+
# ELSE
1831+
def suspend_current_thread_tracing():
1832+
# ENDIF
1833+
# fmt: on
1834+
"""
1835+
Suspends tracing for the current thread.
1836+
1837+
Returns the previous tracing state (True if tracing was enabled, False otherwise).
1838+
This is useful for temporarily disabling tracing to prevent recursive debugging.
1839+
1840+
Use resume_current_thread_tracing() or set_current_thread_tracing_state() to restore.
1841+
"""
1842+
try:
1843+
thread_info = _thread_local_info.thread_info
1844+
except:
1845+
thread_info = _get_thread_info(False, 1)
1846+
if thread_info is None:
1847+
return False
1848+
previous_state = thread_info.trace
1849+
thread_info.trace = False
1850+
return previous_state
1851+
1852+
1853+
# fmt: off
1854+
# IFDEF CYTHON
1855+
# cpdef resume_current_thread_tracing():
1856+
# cdef ThreadInfo thread_info
1857+
# ELSE
1858+
def resume_current_thread_tracing():
1859+
# ENDIF
1860+
# fmt: on
1861+
"""
1862+
Resumes tracing for the current thread.
1863+
1864+
This unconditionally enables tracing. For conditional restoration,
1865+
use set_current_thread_tracing_state().
1866+
"""
1867+
try:
1868+
thread_info = _thread_local_info.thread_info
1869+
except:
1870+
thread_info = _get_thread_info(False, 1)
1871+
if thread_info is None:
1872+
return
1873+
thread_info.trace = True
1874+
1875+
1876+
# fmt: off
1877+
# IFDEF CYTHON
1878+
# cpdef set_current_thread_tracing_state(bint trace):
1879+
# cdef ThreadInfo thread_info
1880+
# ELSE
1881+
def set_current_thread_tracing_state(trace):
1882+
# ENDIF
1883+
# fmt: on
1884+
"""
1885+
Sets the tracing state for the current thread.
1886+
1887+
:param trace: True to enable tracing, False to disable.
1888+
1889+
This is typically used to restore a previously saved state from
1890+
suspend_current_thread_tracing().
1891+
"""
1892+
try:
1893+
thread_info = _thread_local_info.thread_info
1894+
except:
1895+
thread_info = _get_thread_info(False, 1)
1896+
if thread_info is None:
1897+
return
1898+
thread_info.trace = trace
1899+
1900+
18261901
def update_monitor_events(suspend_requested: Optional[bool]=None) -> None:
18271902
"""
18281903
This should be called when breakpoints change.

src/debugpy/_vendored/pydevd/_pydevd_sys_monitoring/_pydevd_sys_monitoring_cython.pyx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,81 @@ cpdef stop_monitoring(all_threads=False):
18291829
thread_info.trace = False
18301830

18311831

1832+
# fmt: off
1833+
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
1834+
cpdef bint suspend_current_thread_tracing():
1835+
cdef ThreadInfo thread_info
1836+
# ELSE
1837+
# def suspend_current_thread_tracing():
1838+
# ENDIF
1839+
# fmt: on
1840+
"""
1841+
Suspends tracing for the current thread.
1842+
1843+
Returns the previous tracing state (True if tracing was enabled, False otherwise).
1844+
This is useful for temporarily disabling tracing to prevent recursive debugging.
1845+
1846+
Use resume_current_thread_tracing() or set_current_thread_tracing_state() to restore.
1847+
"""
1848+
try:
1849+
thread_info = _thread_local_info.thread_info
1850+
except:
1851+
thread_info = _get_thread_info(False, 1)
1852+
if thread_info is None:
1853+
return False
1854+
previous_state = thread_info.trace
1855+
thread_info.trace = False
1856+
return previous_state
1857+
1858+
1859+
# fmt: off
1860+
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
1861+
cpdef resume_current_thread_tracing():
1862+
cdef ThreadInfo thread_info
1863+
# ELSE
1864+
# def resume_current_thread_tracing():
1865+
# ENDIF
1866+
# fmt: on
1867+
"""
1868+
Resumes tracing for the current thread.
1869+
1870+
This unconditionally enables tracing. For conditional restoration,
1871+
use set_current_thread_tracing_state().
1872+
"""
1873+
try:
1874+
thread_info = _thread_local_info.thread_info
1875+
except:
1876+
thread_info = _get_thread_info(False, 1)
1877+
if thread_info is None:
1878+
return
1879+
thread_info.trace = True
1880+
1881+
1882+
# fmt: off
1883+
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
1884+
cpdef set_current_thread_tracing_state(bint trace):
1885+
cdef ThreadInfo thread_info
1886+
# ELSE
1887+
# def set_current_thread_tracing_state(trace):
1888+
# ENDIF
1889+
# fmt: on
1890+
"""
1891+
Sets the tracing state for the current thread.
1892+
1893+
:param trace: True to enable tracing, False to disable.
1894+
1895+
This is typically used to restore a previously saved state from
1896+
suspend_current_thread_tracing().
1897+
"""
1898+
try:
1899+
thread_info = _thread_local_info.thread_info
1900+
except:
1901+
thread_info = _get_thread_info(False, 1)
1902+
if thread_info is None:
1903+
return
1904+
thread_info.trace = trace
1905+
1906+
18321907
def update_monitor_events(suspend_requested: Optional[bool]=None) -> None:
18331908
"""
18341909
This should be called when breakpoints change.

src/debugpy/public_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,32 @@ def trace_this_thread(__should_trace: bool):
212212
"""
213213

214214

215+
@_api()
216+
def postmortem(
217+
__excinfo: typing.Tuple[type, BaseException, typing.Any] | None = None
218+
) -> None:
219+
"""Stops the debugger on an unhandled exception.
220+
221+
If a debug client is connected, pauses execution as if an
222+
unhandled exception was caught. This allows inspection of the
223+
exception and call stack at the point of failure.
224+
225+
If no exception info is provided, uses sys.exc_info() to get
226+
the current exception. If there is no current exception and no
227+
argument is provided, does nothing.
228+
229+
Safe to call when no debugger is connected (returns immediately).
230+
231+
Example::
232+
233+
try:
234+
risky_operation()
235+
except Exception:
236+
debugpy.postmortem() # Uses current exception
237+
raise
238+
"""
239+
240+
215241
def get_cli_options() -> CliOptions | None:
216242
"""Returns the CLI options that were processed by debugpy.
217243

src/debugpy/server/api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,51 @@ def trace_this_thread(should_trace):
364364
pydb.enable_tracing()
365365
else:
366366
pydb.disable_tracing()
367+
368+
369+
def postmortem(excinfo=None):
370+
ensure_logging()
371+
372+
if excinfo is None:
373+
excinfo = sys.exc_info()
374+
375+
exctype, value, tb = excinfo
376+
if exctype is None or value is None or tb is None:
377+
log.debug("postmortem() ignored - no exception info")
378+
return
379+
380+
if not is_client_connected():
381+
log.info("postmortem() ignored - debugger not attached")
382+
return
383+
384+
log.debug("postmortem({0!r})", excinfo)
385+
386+
pydb = get_global_debugger()
387+
if pydb is None:
388+
log.warning("postmortem() ignored - no global debugger")
389+
return
390+
391+
thread = threading.current_thread()
392+
additional_info = pydb.set_additional_thread_info(thread)
393+
394+
# Save states for restoration
395+
saved_is_tracing = additional_info.is_tracing
396+
saved_sys_monitoring_trace = None
397+
398+
try:
399+
# Prevent recursive tracing (settrace protection)
400+
additional_info.is_tracing += 1
401+
402+
# For Python 3.12+, suspend sys.monitoring tracing
403+
if hasattr(sys, 'monitoring'):
404+
from _pydevd_sys_monitoring import pydevd_sys_monitoring
405+
saved_sys_monitoring_trace = pydevd_sys_monitoring.suspend_current_thread_tracing()
406+
407+
from _pydevd_bundle.pydevd_breakpoints import stop_on_unhandled_exception
408+
stop_on_unhandled_exception(pydb, thread, additional_info, excinfo)
409+
410+
finally:
411+
additional_info.is_tracing = saved_is_tracing
412+
if saved_sys_monitoring_trace is not None:
413+
from _pydevd_sys_monitoring import pydevd_sys_monitoring
414+
pydevd_sys_monitoring.set_current_thread_tracing_state(saved_sys_monitoring_trace)

0 commit comments

Comments
 (0)