Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Nov 15, 2023
1 parent 7916cf8 commit f8166dd
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 23 deletions.
126 changes: 103 additions & 23 deletions _pydevd_sys_monitoring/pydevd_sys_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import re
import sys
import threading
from types import CodeType
from typing import Dict, Optional, Set
from types import CodeType, FrameType
from typing import Dict, Optional, Set, Tuple
from os.path import basename, splitext

from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_dont_trace
Expand Down Expand Up @@ -61,6 +62,54 @@ def _notify_skipped_step_in_because_of_filters(py_db, frame):
py_db.notify_skipped_step_in_because_of_filters(frame)


def _get_frame_to_consider_unhandled_exception(depth) -> Tuple[Optional[FrameType], bool]:
try:
return _thread_local_info.f_unhandled, _thread_local_info.is_internal_code_frame
except:
frame = sys._getframe(depth)
f_unhandled = frame
# print('called at', f_unhandled.f_code.co_name, f_unhandled.f_code.co_filename, f_unhandled.f_code.co_firstlineno)
is_internal_code_frame = False
while f_unhandled is not None:
filename = f_unhandled.f_code.co_filename
name = splitext(basename(filename))[0]
if name == 'threading':
if f_unhandled.f_code.co_name in ('__bootstrap', '_bootstrap'):
# We need __bootstrap_inner, not __bootstrap.
return None, False

elif f_unhandled.f_code.co_name in ('__bootstrap_inner', '_bootstrap_inner'):
# Note: be careful not to use threading.current_thread to avoid creating a dummy thread.
is_internal_code_frame = True
break

elif name == 'pydev_monkey':
if f_unhandled.f_code.co_name == '__call__':
is_internal_code_frame = True
break

elif name == 'pydevd':
if f_unhandled.f_code.co_name in ('run', 'main'):
# We need to get to _exec
return None, False

if f_unhandled.f_code.co_name == '_exec':
is_internal_code_frame = True
break

elif f_unhandled.f_back is None:
break

f_unhandled = f_unhandled.f_back

if f_unhandled is not None:
_thread_local_info.is_internal_code_frame = is_internal_code_frame
_thread_local_info.f_unhandled = f_unhandled
return _thread_local_info.f_unhandled, _thread_local_info.is_internal_code_frame

return f_unhandled, is_internal_code_frame


class ThreadInfo:

additional_info: PyDBAdditionalThreadInfo
Expand All @@ -72,27 +121,42 @@ def __init__(self, thread, trace, additional_info):
self.additional_info = additional_info
self.trace = trace

self.frame_to_consider_unhandled_exception = None
self.is_internal_code_frame = False

def _create_thread_info(code_obj):
# func_code_info = get_func_code_info(code_obj)
# if func_code_info is None:
# return None
#
# if func_code_info.always_skip_code:
# return None
def get_frame_to_consider_unhandled_exception(self, depth):
if self.frame_to_consider_unhandled_exception is None:
f_unhandled, is_internal_code_frame = _get_frame_to_consider_unhandled_exception(depth + 1)
self.is_internal_code_frame = is_internal_code_frame
self.frame_to_consider_unhandled_exception = f_unhandled

return self.frame_to_consider_unhandled_exception


def _create_thread_info(depth):
# Don't call threading.currentThread because if we're too early in the process
# we may create a dummy thread.
thread_ident = _get_ident()
t = _thread_active.get(thread_ident)
if t is None:
return None
f_unhandled, is_internal_code_frame = _get_frame_to_consider_unhandled_exception(depth + 1)
if f_unhandled is None:
return None

elif f_unhandled.f_code.co_name in ('__bootstrap_inner', '_bootstrap_inner'):
# Note: be careful not to use threading.current_thread to avoid creating a dummy thread.
t = f_unhandled.f_locals.get('self')
if not isinstance(t, threading.Thread):
return None

# TODO: CHECK HOW TO DEAL WITH DUMMY THREADS BETTER.
# assert self.can_create_dummy_thread
# # Initialize the dummy thread and set the tracing (both are needed to
# # actually stop on breakpoints).
# t = threading.current_thread()
else:
if not is_internal_code_frame:
# This means that the first frame is not in threading nor in pydev.
# In practice this means it's some unmanaged thread, so, creating
# a dummy thread is ok in this use-case.
t = threading.current_thread()
else:
return None

if getattr(t, 'is_pydev_daemon_thread', False):
return ThreadInfo(t, False, None)
Expand Down Expand Up @@ -132,7 +196,7 @@ def __init__(self):
self.filtered_out: Optional[bool] = None


def _get_thread_info(code: Optional[CodeType], create: bool) -> Optional[ThreadInfo]:
def _get_thread_info(create: bool, depth:int) -> Optional[ThreadInfo]:
'''
Provides thread-related info.
Expand All @@ -145,7 +209,7 @@ def _get_thread_info(code: Optional[CodeType], create: bool) -> Optional[ThreadI
except:
if not create:
return None
thread_info = _create_thread_info(code)
thread_info = _create_thread_info(depth + 1)
if thread_info is None:
return None

Expand Down Expand Up @@ -378,11 +442,26 @@ def _enable_code_tracing(thread, code, frame, warn_on_filtered_out):
# pass


def _raise_event(code, instruction, exc):
'''
The way this should work is the following: when the user is using
pydevd to do the launch and we're on a managed stack, we should consider
unhandled only if it gets into a pydevd. If it's a thread, if it stops
inside the threading and if it's an unmanaged thread (i.e.: QThread)
then stop if it doesn't have a back frame.
'''
thread_info = _get_thread_info(True, 1)
if thread_info is None:
return

thread_info.get_frame_to_consider_unhandled_exception(depth=1)


def _return_event(code, instruction, retval):
try:
thread_info = _thread_local_info.thread_info
except:
thread_info = _get_thread_info(code, True)
thread_info = _get_thread_info(True, 1)
if thread_info is None:
return

Expand Down Expand Up @@ -489,7 +568,7 @@ def _line_event(code, line):
try:
thread_info = _thread_local_info.thread_info
except:
thread_info = _get_thread_info(code, True)
thread_info = _get_thread_info(True, 1)
if thread_info is None:
return

Expand Down Expand Up @@ -681,7 +760,7 @@ def _start_method(code, instruction_offset):
try:
thread_info = _thread_local_info.thread_info
except:
thread_info = _get_thread_info(code, True)
thread_info = _get_thread_info(True, 1)
if thread_info is None:
return

Expand Down Expand Up @@ -710,6 +789,7 @@ def start_monitoring(all_threads=False):
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_RESUME, _start_method)
monitor.register_callback(DEBUGGER_ID, monitor.events.LINE, _line_event)
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_RETURN, _return_event)
monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE, _raise_event)

# monitor.register_callback(DEBUGGER_ID, monitor.events.LINE, self._line_callback)
#
Expand All @@ -725,7 +805,7 @@ def start_monitoring(all_threads=False):
thread_info = _thread_local_info.thread_info
except:
# code=None means we can already get the threading.current_thread.
thread_info = _get_thread_info(code=None, create=True)
thread_info = _get_thread_info(True, 1)
if thread_info is None:
print('start monitoring, thread=', None)
return
Expand All @@ -742,13 +822,13 @@ def stop_monitoring(all_threads=False):
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_RESUME, None)
monitor.register_callback(DEBUGGER_ID, monitor.events.LINE, None)
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_RETURN, None)
monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE, None)
monitor.free_tool_id(monitor.DEBUGGER_ID)
else:
try:
thread_info = _thread_local_info.thread_info
except:
# code=None means we can already get the threading.current_thread.
thread_info = _get_thread_info(code=None, create=False)
thread_info = _get_thread_info(False, 1)
if thread_info is None:
return
print('stop monitoring, thread=', thread_info.thread)
Expand Down
44 changes: 44 additions & 0 deletions tests_python/test_sys_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def _disable_monitoring():
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_START , None)
monitor.register_callback(DEBUGGER_ID, monitor.events.PY_RESUME, None)
monitor.register_callback(DEBUGGER_ID, monitor.events.LINE, None)
monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE, None)
sys.monitoring.free_tool_id(DEBUGGER_ID)


Expand All @@ -25,6 +26,49 @@ def with_monitoring():
_disable_monitoring()


def test_exceptions(with_monitoring):
monitor.set_events(DEBUGGER_ID, monitor.events.RAISE | monitor.events.RERAISE)

found = []

def _on_raise(code, instruction_offset, exc):
if code.co_filename.endswith('sys_monitoring.py'):
found.append(('raise', code.co_name, str(exc), sys._getframe(1).f_lineno))

def _on_reraise(code, instruction_offset, exc):
if code.co_filename.endswith('sys_monitoring.py'):
found.append(('reraise', code.co_name, str(exc), sys._getframe(1).f_lineno))

monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE , _on_raise)
monitor.register_callback(DEBUGGER_ID, monitor.events.RERAISE , _on_reraise)

def method_raise():
raise RuntimeError('err1')

def method_2():
try:
method_raise()
except:
raise

def method():
try:
method_2()
except:
pass

method()
assert found == [
('raise', 'method_raise', 'err1', method_raise.__code__.co_firstlineno + 1),
('raise', 'method_2', 'err1', method_2.__code__.co_firstlineno + 2),
# This will be very tricky to handle.
# See: https://github.com/python/cpython/issues/112086
('reraise', 'method_2', 'err1', method_2.__code__.co_firstlineno + 4),
('reraise', 'method_2', 'err1', method_2.__code__.co_firstlineno + 4),
('raise', 'method', 'err1', method.__code__.co_firstlineno + 2),
]


def test_variables_on_call(with_monitoring):
monitor.set_events(DEBUGGER_ID, monitor.events.PY_START)

Expand Down

0 comments on commit f8166dd

Please sign in to comment.