From f8166dd8050619e9ad18df53708c54d4e21d8c56 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Tue, 14 Nov 2023 22:29:57 -0300 Subject: [PATCH] wip --- .../pydevd_sys_monitoring.py | 126 ++++++++++++++---- tests_python/test_sys_monitoring.py | 44 ++++++ 2 files changed, 147 insertions(+), 23 deletions(-) diff --git a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py index 69a60aad..2790f033 100644 --- a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py +++ b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py @@ -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 @@ -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 @@ -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) @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) # @@ -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 @@ -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) diff --git a/tests_python/test_sys_monitoring.py b/tests_python/test_sys_monitoring.py index 650506ad..c18ef1e2 100644 --- a/tests_python/test_sys_monitoring.py +++ b/tests_python/test_sys_monitoring.py @@ -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) @@ -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)