mirror of
https://github.com/python/cpython.git
synced 2025-08-31 14:07:50 +00:00
gh-120144: Make it possible to use sys.monitoring
for bdb and make it default for pdb (#124533)
This commit is contained in:
parent
f48887fb97
commit
a936af924e
7 changed files with 435 additions and 21 deletions
240
Lib/bdb.py
240
Lib/bdb.py
|
@ -2,6 +2,7 @@
|
|||
|
||||
import fnmatch
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
import weakref
|
||||
from contextlib import contextmanager
|
||||
|
@ -16,6 +17,181 @@ class BdbQuit(Exception):
|
|||
"""Exception to give up completely."""
|
||||
|
||||
|
||||
E = sys.monitoring.events
|
||||
|
||||
class _MonitoringTracer:
|
||||
EVENT_CALLBACK_MAP = {
|
||||
E.PY_START: 'call',
|
||||
E.PY_RESUME: 'call',
|
||||
E.PY_THROW: 'call',
|
||||
E.LINE: 'line',
|
||||
E.JUMP: 'jump',
|
||||
E.PY_RETURN: 'return',
|
||||
E.PY_YIELD: 'return',
|
||||
E.PY_UNWIND: 'unwind',
|
||||
E.RAISE: 'exception',
|
||||
E.STOP_ITERATION: 'exception',
|
||||
E.INSTRUCTION: 'opcode',
|
||||
}
|
||||
|
||||
GLOBAL_EVENTS = E.PY_START | E.PY_RESUME | E.PY_THROW | E.PY_UNWIND | E.RAISE
|
||||
LOCAL_EVENTS = E.LINE | E.JUMP | E.PY_RETURN | E.PY_YIELD | E.STOP_ITERATION
|
||||
|
||||
def __init__(self):
|
||||
self._tool_id = sys.monitoring.DEBUGGER_ID
|
||||
self._name = 'bdbtracer'
|
||||
self._tracefunc = None
|
||||
self._disable_current_event = False
|
||||
self._tracing_thread = None
|
||||
self._enabled = False
|
||||
|
||||
def start_trace(self, tracefunc):
|
||||
self._tracefunc = tracefunc
|
||||
self._tracing_thread = threading.current_thread()
|
||||
curr_tool = sys.monitoring.get_tool(self._tool_id)
|
||||
if curr_tool is None:
|
||||
sys.monitoring.use_tool_id(self._tool_id, self._name)
|
||||
elif curr_tool == self._name:
|
||||
sys.monitoring.clear_tool_id(self._tool_id)
|
||||
else:
|
||||
raise ValueError('Another debugger is using the monitoring tool')
|
||||
E = sys.monitoring.events
|
||||
all_events = 0
|
||||
for event, cb_name in self.EVENT_CALLBACK_MAP.items():
|
||||
callback = getattr(self, f'{cb_name}_callback')
|
||||
sys.monitoring.register_callback(self._tool_id, event, callback)
|
||||
if event != E.INSTRUCTION:
|
||||
all_events |= event
|
||||
self.check_trace_func()
|
||||
self.check_trace_opcodes()
|
||||
sys.monitoring.set_events(self._tool_id, self.GLOBAL_EVENTS)
|
||||
self._enabled = True
|
||||
|
||||
def stop_trace(self):
|
||||
self._enabled = False
|
||||
self._tracing_thread = None
|
||||
curr_tool = sys.monitoring.get_tool(self._tool_id)
|
||||
if curr_tool != self._name:
|
||||
return
|
||||
sys.monitoring.clear_tool_id(self._tool_id)
|
||||
self.check_trace_opcodes()
|
||||
sys.monitoring.free_tool_id(self._tool_id)
|
||||
|
||||
def disable_current_event(self):
|
||||
self._disable_current_event = True
|
||||
|
||||
def restart_events(self):
|
||||
if sys.monitoring.get_tool(self._tool_id) == self._name:
|
||||
sys.monitoring.restart_events()
|
||||
|
||||
def callback_wrapper(func):
|
||||
import functools
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args):
|
||||
if self._tracing_thread != threading.current_thread():
|
||||
return
|
||||
try:
|
||||
frame = sys._getframe().f_back
|
||||
ret = func(self, frame, *args)
|
||||
if self._enabled and frame.f_trace:
|
||||
self.check_trace_func()
|
||||
if self._disable_current_event:
|
||||
return sys.monitoring.DISABLE
|
||||
else:
|
||||
return ret
|
||||
except BaseException:
|
||||
self.stop_trace()
|
||||
sys._getframe().f_back.f_trace = None
|
||||
raise
|
||||
finally:
|
||||
self._disable_current_event = False
|
||||
|
||||
return wrapper
|
||||
|
||||
@callback_wrapper
|
||||
def call_callback(self, frame, code, *args):
|
||||
local_tracefunc = self._tracefunc(frame, 'call', None)
|
||||
if local_tracefunc is not None:
|
||||
frame.f_trace = local_tracefunc
|
||||
if self._enabled:
|
||||
sys.monitoring.set_local_events(self._tool_id, code, self.LOCAL_EVENTS)
|
||||
|
||||
@callback_wrapper
|
||||
def return_callback(self, frame, code, offset, retval):
|
||||
if frame.f_trace:
|
||||
frame.f_trace(frame, 'return', retval)
|
||||
|
||||
@callback_wrapper
|
||||
def unwind_callback(self, frame, code, *args):
|
||||
if frame.f_trace:
|
||||
frame.f_trace(frame, 'return', None)
|
||||
|
||||
@callback_wrapper
|
||||
def line_callback(self, frame, code, *args):
|
||||
if frame.f_trace and frame.f_trace_lines:
|
||||
frame.f_trace(frame, 'line', None)
|
||||
|
||||
@callback_wrapper
|
||||
def jump_callback(self, frame, code, inst_offset, dest_offset):
|
||||
if dest_offset > inst_offset:
|
||||
return sys.monitoring.DISABLE
|
||||
inst_lineno = self._get_lineno(code, inst_offset)
|
||||
dest_lineno = self._get_lineno(code, dest_offset)
|
||||
if inst_lineno != dest_lineno:
|
||||
return sys.monitoring.DISABLE
|
||||
if frame.f_trace and frame.f_trace_lines:
|
||||
frame.f_trace(frame, 'line', None)
|
||||
|
||||
@callback_wrapper
|
||||
def exception_callback(self, frame, code, offset, exc):
|
||||
if frame.f_trace:
|
||||
if exc.__traceback__ and hasattr(exc.__traceback__, 'tb_frame'):
|
||||
tb = exc.__traceback__
|
||||
while tb:
|
||||
if tb.tb_frame.f_locals.get('self') is self:
|
||||
return
|
||||
tb = tb.tb_next
|
||||
frame.f_trace(frame, 'exception', (type(exc), exc, exc.__traceback__))
|
||||
|
||||
@callback_wrapper
|
||||
def opcode_callback(self, frame, code, offset):
|
||||
if frame.f_trace and frame.f_trace_opcodes:
|
||||
frame.f_trace(frame, 'opcode', None)
|
||||
|
||||
def check_trace_opcodes(self, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe().f_back
|
||||
while frame is not None:
|
||||
self.set_trace_opcodes(frame, frame.f_trace_opcodes)
|
||||
frame = frame.f_back
|
||||
|
||||
def set_trace_opcodes(self, frame, trace_opcodes):
|
||||
if sys.monitoring.get_tool(self._tool_id) != self._name:
|
||||
return
|
||||
if trace_opcodes:
|
||||
sys.monitoring.set_local_events(self._tool_id, frame.f_code, E.INSTRUCTION)
|
||||
else:
|
||||
sys.monitoring.set_local_events(self._tool_id, frame.f_code, 0)
|
||||
|
||||
def check_trace_func(self, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe().f_back
|
||||
while frame is not None:
|
||||
if frame.f_trace is not None:
|
||||
sys.monitoring.set_local_events(self._tool_id, frame.f_code, self.LOCAL_EVENTS)
|
||||
frame = frame.f_back
|
||||
|
||||
def _get_lineno(self, code, offset):
|
||||
import dis
|
||||
last_lineno = None
|
||||
for start, lineno in dis.findlinestarts(code):
|
||||
if offset < start:
|
||||
return last_lineno
|
||||
last_lineno = lineno
|
||||
return last_lineno
|
||||
|
||||
|
||||
class Bdb:
|
||||
"""Generic Python debugger base class.
|
||||
|
||||
|
@ -30,7 +206,7 @@ class Bdb:
|
|||
is determined by the __name__ in the frame globals.
|
||||
"""
|
||||
|
||||
def __init__(self, skip=None):
|
||||
def __init__(self, skip=None, backend='settrace'):
|
||||
self.skip = set(skip) if skip else None
|
||||
self.breaks = {}
|
||||
self.fncache = {}
|
||||
|
@ -39,6 +215,13 @@ class Bdb:
|
|||
self.trace_opcodes = False
|
||||
self.enterframe = None
|
||||
self.code_linenos = weakref.WeakKeyDictionary()
|
||||
self.backend = backend
|
||||
if backend == 'monitoring':
|
||||
self.monitoring_tracer = _MonitoringTracer()
|
||||
elif backend == 'settrace':
|
||||
self.monitoring_tracer = None
|
||||
else:
|
||||
raise ValueError(f"Invalid backend '{backend}'")
|
||||
|
||||
self._load_breaks()
|
||||
|
||||
|
@ -59,6 +242,18 @@ class Bdb:
|
|||
self.fncache[filename] = canonic
|
||||
return canonic
|
||||
|
||||
def start_trace(self):
|
||||
if self.monitoring_tracer:
|
||||
self.monitoring_tracer.start_trace(self.trace_dispatch)
|
||||
else:
|
||||
sys.settrace(self.trace_dispatch)
|
||||
|
||||
def stop_trace(self):
|
||||
if self.monitoring_tracer:
|
||||
self.monitoring_tracer.stop_trace()
|
||||
else:
|
||||
sys.settrace(None)
|
||||
|
||||
def reset(self):
|
||||
"""Set values of attributes as ready to start debugging."""
|
||||
import linecache
|
||||
|
@ -128,7 +323,10 @@ class Bdb:
|
|||
"""
|
||||
if self.stop_here(frame) or self.break_here(frame):
|
||||
self.user_line(frame)
|
||||
self.restart_events()
|
||||
if self.quitting: raise BdbQuit
|
||||
elif not self.get_break(frame.f_code.co_filename, frame.f_lineno):
|
||||
self.disable_current_event()
|
||||
return self.trace_dispatch
|
||||
|
||||
def dispatch_call(self, frame, arg):
|
||||
|
@ -150,6 +348,7 @@ class Bdb:
|
|||
if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
|
||||
return self.trace_dispatch
|
||||
self.user_call(frame, arg)
|
||||
self.restart_events()
|
||||
if self.quitting: raise BdbQuit
|
||||
return self.trace_dispatch
|
||||
|
||||
|
@ -170,6 +369,7 @@ class Bdb:
|
|||
try:
|
||||
self.frame_returning = frame
|
||||
self.user_return(frame, arg)
|
||||
self.restart_events()
|
||||
finally:
|
||||
self.frame_returning = None
|
||||
if self.quitting: raise BdbQuit
|
||||
|
@ -197,6 +397,7 @@ class Bdb:
|
|||
if not (frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
|
||||
and arg[0] is StopIteration and arg[2] is None):
|
||||
self.user_exception(frame, arg)
|
||||
self.restart_events()
|
||||
if self.quitting: raise BdbQuit
|
||||
# Stop at the StopIteration or GeneratorExit exception when the user
|
||||
# has set stopframe in a generator by issuing a return command, or a
|
||||
|
@ -206,6 +407,7 @@ class Bdb:
|
|||
and self.stopframe.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
|
||||
and arg[0] in (StopIteration, GeneratorExit)):
|
||||
self.user_exception(frame, arg)
|
||||
self.restart_events()
|
||||
if self.quitting: raise BdbQuit
|
||||
|
||||
return self.trace_dispatch
|
||||
|
@ -221,6 +423,7 @@ class Bdb:
|
|||
unconditionally.
|
||||
"""
|
||||
self.user_opcode(frame)
|
||||
self.restart_events()
|
||||
if self.quitting: raise BdbQuit
|
||||
return self.trace_dispatch
|
||||
|
||||
|
@ -336,6 +539,8 @@ class Bdb:
|
|||
frame = self.enterframe
|
||||
while frame is not None:
|
||||
frame.f_trace_opcodes = trace_opcodes
|
||||
if self.monitoring_tracer:
|
||||
self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes)
|
||||
if frame is self.botframe:
|
||||
break
|
||||
frame = frame.f_back
|
||||
|
@ -400,7 +605,7 @@ class Bdb:
|
|||
|
||||
If frame is not specified, debugging starts from caller's frame.
|
||||
"""
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
if frame is None:
|
||||
frame = sys._getframe().f_back
|
||||
self.reset()
|
||||
|
@ -413,7 +618,8 @@ class Bdb:
|
|||
frame.f_trace_lines = True
|
||||
frame = frame.f_back
|
||||
self.set_stepinstr()
|
||||
sys.settrace(self.trace_dispatch)
|
||||
self.enterframe = None
|
||||
self.start_trace()
|
||||
|
||||
def set_continue(self):
|
||||
"""Stop only at breakpoints or when finished.
|
||||
|
@ -424,13 +630,15 @@ class Bdb:
|
|||
self._set_stopinfo(self.botframe, None, -1)
|
||||
if not self.breaks:
|
||||
# no breakpoints; run without debugger overhead
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
frame = sys._getframe().f_back
|
||||
while frame and frame is not self.botframe:
|
||||
del frame.f_trace
|
||||
frame = frame.f_back
|
||||
for frame, (trace_lines, trace_opcodes) in self.frame_trace_lines_opcodes.items():
|
||||
frame.f_trace_lines, frame.f_trace_opcodes = trace_lines, trace_opcodes
|
||||
if self.backend == 'monitoring':
|
||||
self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes)
|
||||
self.frame_trace_lines_opcodes = {}
|
||||
|
||||
def set_quit(self):
|
||||
|
@ -441,7 +649,7 @@ class Bdb:
|
|||
self.stopframe = self.botframe
|
||||
self.returnframe = None
|
||||
self.quitting = True
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
|
||||
# Derived classes and clients can call the following methods
|
||||
# to manipulate breakpoints. These methods return an
|
||||
|
@ -669,6 +877,16 @@ class Bdb:
|
|||
s += f'{lprefix}Warning: lineno is None'
|
||||
return s
|
||||
|
||||
def disable_current_event(self):
|
||||
"""Disable the current event."""
|
||||
if self.backend == 'monitoring':
|
||||
self.monitoring_tracer.disable_current_event()
|
||||
|
||||
def restart_events(self):
|
||||
"""Restart all events."""
|
||||
if self.backend == 'monitoring':
|
||||
self.monitoring_tracer.restart_events()
|
||||
|
||||
# The following methods can be called by clients to use
|
||||
# a debugger to debug a statement or an expression.
|
||||
# Both can be given as a string, or a code object.
|
||||
|
@ -686,14 +904,14 @@ class Bdb:
|
|||
self.reset()
|
||||
if isinstance(cmd, str):
|
||||
cmd = compile(cmd, "<string>", "exec")
|
||||
sys.settrace(self.trace_dispatch)
|
||||
self.start_trace()
|
||||
try:
|
||||
exec(cmd, globals, locals)
|
||||
except BdbQuit:
|
||||
pass
|
||||
finally:
|
||||
self.quitting = True
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
|
||||
def runeval(self, expr, globals=None, locals=None):
|
||||
"""Debug an expression executed via the eval() function.
|
||||
|
@ -706,14 +924,14 @@ class Bdb:
|
|||
if locals is None:
|
||||
locals = globals
|
||||
self.reset()
|
||||
sys.settrace(self.trace_dispatch)
|
||||
self.start_trace()
|
||||
try:
|
||||
return eval(expr, globals, locals)
|
||||
except BdbQuit:
|
||||
pass
|
||||
finally:
|
||||
self.quitting = True
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
|
||||
def runctx(self, cmd, globals, locals):
|
||||
"""For backwards-compatibility. Defers to run()."""
|
||||
|
@ -728,7 +946,7 @@ class Bdb:
|
|||
Return the result of the function call.
|
||||
"""
|
||||
self.reset()
|
||||
sys.settrace(self.trace_dispatch)
|
||||
self.start_trace()
|
||||
res = None
|
||||
try:
|
||||
res = func(*args, **kwds)
|
||||
|
@ -736,7 +954,7 @@ class Bdb:
|
|||
pass
|
||||
finally:
|
||||
self.quitting = True
|
||||
sys.settrace(None)
|
||||
self.stop_trace()
|
||||
return res
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue