From 6e7da6bbbd1fb55d74fd5b1426c02537726b308d Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Wed, 15 Nov 2023 10:29:45 -0300 Subject: [PATCH] wip --- _pydevd_bundle/pydevd_cython_wrapper.py | 6 ++ _pydevd_bundle/pydevd_frame.py | 16 ++-- _pydevd_bundle/pydevd_trace_dispatch.py | 4 + .../pydevd_sys_monitoring.py | 86 ++++++++++++++++++- tests_python/test_debugger.py | 13 ++- 5 files changed, 109 insertions(+), 16 deletions(-) diff --git a/_pydevd_bundle/pydevd_cython_wrapper.py b/_pydevd_bundle/pydevd_cython_wrapper.py index 1058519b3..5af95d52a 100644 --- a/_pydevd_bundle/pydevd_cython_wrapper.py +++ b/_pydevd_bundle/pydevd_cython_wrapper.py @@ -49,4 +49,10 @@ fix_top_level_trace_and_get_trace_func = mod.fix_top_level_trace_and_get_trace_func +handle_exception = mod.handle_exception + +should_stop_on_exception = mod.should_stop_on_exception + +is_unhandled_exception = mod.is_unhandled_exception + version = getattr(mod, 'version', 0) diff --git a/_pydevd_bundle/pydevd_frame.py b/_pydevd_bundle/pydevd_frame.py index 5cc851d53..67add2a10 100644 --- a/_pydevd_bundle/pydevd_frame.py +++ b/_pydevd_bundle/pydevd_frame.py @@ -64,7 +64,7 @@ def get_smart_step_into_variant_from_frame_offset(*args, **kwargs): # IFDEF CYTHON -# cdef is_unhandled_exception(container_obj, py_db, frame, int last_raise_line, set raise_lines): +# def is_unhandled_exception(container_obj, py_db, frame, int last_raise_line, set raise_lines): # ELSE def is_unhandled_exception(container_obj, py_db, frame, last_raise_line, raise_lines): # ENDIF @@ -114,8 +114,8 @@ def is_unhandled_exception(container_obj, py_db, frame, last_raise_line, raise_l # ELSE class _TryExceptContainerObj(object): ''' - A dumb container object just to containe the try..except info when needed. Meant to be - persisent among multiple PyDBFrames to the same code object. + A dumb container object just to contain the try..except info when needed. Meant to be + persistent among multiple PyDBFrames to the same code object. ''' try_except_infos = None # ENDIF @@ -1012,11 +1012,11 @@ def trace_dispatch(self, frame, event, arg): # cdef bint was_just_raised; # cdef list check_excs; # ELSE -def should_stop_on_exception(py_db, info, frame, thread, arg, prev_exc_info): +def should_stop_on_exception(py_db, info, frame, thread, arg, prev_user_uncaught_exc_info): # ENDIF should_stop = False - return_exc_info = prev_exc_info + maybe_user_uncaught_exc_info = prev_user_uncaught_exc_info # STATE_SUSPEND = 2 if info.pydev_state != 2: # and breakpoint is not None: @@ -1084,14 +1084,14 @@ def should_stop_on_exception(py_db, info, frame, thread, arg, prev_exc_info): and (frame.f_back is None or py_db.apply_files_filter(frame.f_back, frame.f_back.f_code.co_filename, True)): # User uncaught means that we're currently in user code but the code # up the stack is library code. - exc_info = prev_exc_info + exc_info = prev_user_uncaught_exc_info if not exc_info: exc_info = (arg, frame.f_lineno, set([frame.f_lineno])) else: lines = exc_info[2] lines.add(frame.f_lineno) exc_info = (arg, frame.f_lineno, lines) - return_exc_info = exc_info + maybe_user_uncaught_exc_info = exc_info else: # I.e.: these are only checked if we're not dealing with user uncaught exceptions. if exc_break.notify_on_first_raise_only and py_db.skip_on_exceptions_thrown_in_same_context \ @@ -1123,7 +1123,7 @@ def should_stop_on_exception(py_db, info, frame, thread, arg, prev_exc_info): if exception_breakpoint is not None and exception_breakpoint.expression is not None: py_db.handle_breakpoint_expression(exception_breakpoint, info, frame) - return should_stop, frame, return_exc_info + return should_stop, frame, maybe_user_uncaught_exc_info # Same thing in the main debugger but only considering the file contents, while the one in the main debugger diff --git a/_pydevd_bundle/pydevd_trace_dispatch.py b/_pydevd_bundle/pydevd_trace_dispatch.py index 40a683770..f6291bd23 100644 --- a/_pydevd_bundle/pydevd_trace_dispatch.py +++ b/_pydevd_bundle/pydevd_trace_dispatch.py @@ -34,16 +34,19 @@ def delete_old_compiled_extensions(): if USE_CYTHON_FLAG in ENV_TRUE_LOWER_VALUES: # We must import the cython version if forcing cython from _pydevd_bundle.pydevd_cython_wrapper import trace_dispatch, global_cache_skips, global_cache_frame_skips, fix_top_level_trace_and_get_trace_func + from _pydevd_bundle.pydevd_cython_wrapper import should_stop_on_exception, handle_exception, is_unhandled_exception USING_CYTHON = True elif USE_CYTHON_FLAG in ENV_FALSE_LOWER_VALUES: # Use the regular version if not forcing cython from _pydevd_bundle.pydevd_trace_dispatch_regular import trace_dispatch, global_cache_skips, global_cache_frame_skips, fix_top_level_trace_and_get_trace_func # @UnusedImport + from .pydevd_frame import should_stop_on_exception, handle_exception, is_unhandled_exception else: # Regular: use fallback if not found and give message to user try: from _pydevd_bundle.pydevd_cython_wrapper import trace_dispatch, global_cache_skips, global_cache_frame_skips, fix_top_level_trace_and_get_trace_func + from _pydevd_bundle.pydevd_cython_wrapper import should_stop_on_exception, handle_exception, is_unhandled_exception # This version number is always available from _pydevd_bundle.pydevd_additional_thread_info_regular import version as regular_version @@ -58,5 +61,6 @@ def delete_old_compiled_extensions(): except ImportError: from _pydevd_bundle.pydevd_trace_dispatch_regular import trace_dispatch, global_cache_skips, global_cache_frame_skips, fix_top_level_trace_and_get_trace_func # @UnusedImport + from .pydevd_frame import should_stop_on_exception, handle_exception, is_unhandled_exception pydev_log.show_compile_cython_command_line() diff --git a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py index c48a9a0bd..723471b5a 100644 --- a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py +++ b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py @@ -12,10 +12,13 @@ from _pydevd_bundle import pydevd_dont_trace from _pydevd_bundle.pydevd_additional_thread_info import _set_additional_thread_info_lock, PyDBAdditionalThreadInfo from _pydevd_bundle.pydevd_constants import GlobalDebuggerHolder, ForkSafeLock, \ - PYDEVD_IPYTHON_CONTEXT + PYDEVD_IPYTHON_CONTEXT, EXCEPTION_TYPE_USER_UNHANDLED from pydevd_file_utils import NORM_PATHS_AND_BASE_CONTAINER, \ get_abs_path_real_path_and_base_from_file, \ get_abs_path_real_path_and_base_from_frame +from _pydevd_bundle.pydevd_trace_dispatch import should_stop_on_exception, handle_exception +from _pydevd_bundle.pydevd_constants import EXCEPTION_TYPE_HANDLED +from _pydevd_bundle.pydevd_trace_dispatch import is_unhandled_exception if hasattr(sys, 'monitoring'): DEBUGGER_ID = sys.monitoring.DEBUGGER_ID @@ -194,6 +197,7 @@ def __init__(self): self.bp_line_to_breakpoint: Set[int] = set() self.function_breakpoint = None self.filtered_out: Optional[bool] = None + self.try_except_container_obj: Optional[_TryExceptContainerObj] = None def _get_thread_info(create: bool, depth:int) -> Optional[ThreadInfo]: @@ -443,6 +447,52 @@ def _enable_code_tracing(thread, code, frame, warn_on_filtered_out): # pass +class _TryExceptContainerObj(object): + ''' + A dumb container object just to contain the try..except info when needed. Meant to be + persistent among multiple PyDBFrames to the same code object. + ''' + try_except_infos = None + + +def _unwind_event(code, instruction, exc): + try: + thread_info = _thread_local_info.thread_info + except: + thread_info = _get_thread_info(True, 1) + if thread_info is None: + return + + py_db: object = GlobalDebuggerHolder.global_dbg + if py_db is None or py_db.pydb_disposed: + return + + if not thread_info.trace or thread_info.thread._is_stopped: + # For thread-related stuff we can't disable the code tracing because other + # threads may still want it... + return + + func_code_info: FuncCodeInfo = get_func_code_info(code) + if func_code_info.always_skip_code: + return + + print('_unwind_event', code, exc) + frame = sys._getframe(1) + arg = (type(exc), exc, exc.__traceback__) + + _should_stop, frame, user_uncaught_exc_info = should_stop_on_exception(py_db, thread_info.additional_info, frame, thread_info.thread, arg, None) + if user_uncaught_exc_info: + if func_code_info.try_except_container_obj is None: + container_obj = _TryExceptContainerObj() + container_obj.try_except_infos = py_db.collect_try_except_info(frame.f_code) + func_code_info.try_except_container_obj = container_obj + + if is_unhandled_exception(func_code_info.try_except_container_obj, py_db, frame, user_uncaught_exc_info[1], user_uncaught_exc_info[2]): + print('stop in user uncaught') + handle_exception(py_db, thread_info.thread, frame, user_uncaught_exc_info[0], EXCEPTION_TYPE_USER_UNHANDLED) + return + + def _raise_event(code, instruction, exc): ''' The way this should work is the following: when the user is using @@ -454,8 +504,34 @@ def _raise_event(code, instruction, exc): Note: unlike other events, this one is global and not per-code (so, it cannot be individually enabled/disabled for a given code object). ''' - thread_info = _get_thread_info(True, 1) - if thread_info is None: + try: + thread_info = _thread_local_info.thread_info + except: + thread_info = _get_thread_info(True, 1) + if thread_info is None: + return + + py_db: object = GlobalDebuggerHolder.global_dbg + if py_db is None or py_db.pydb_disposed: + return + + if not thread_info.trace or thread_info.thread._is_stopped: + # For thread-related stuff we can't disable the code tracing because other + # threads may still want it... + return + + func_code_info: FuncCodeInfo = get_func_code_info(code) + if func_code_info.always_skip_code: + return + + print('_raise_event --- ', code, exc) + + frame = sys._getframe(1) + arg = (type(exc), exc, exc.__traceback__) + should_stop, frame, _user_uncaught_exc_info = should_stop_on_exception(py_db, thread_info.additional_info, frame, thread_info.thread, arg, None) + print('!!!! should_stop (in raise)', should_stop) + if should_stop: + handle_exception(py_db, thread_info.thread, frame, arg, EXCEPTION_TYPE_HANDLED) return thread_info.get_frame_to_consider_unhandled_exception(depth=1) @@ -841,11 +917,13 @@ def restart_events(breakpoints_changed: bool=False) -> None: or py_db.has_plugin_exception_breaks) if has_exception_breakpoint_in_pydb: - required_events |= monitor.events.RAISE + required_events |= monitor.events.RAISE | monitor.events.PY_UNWIND print('track RAISE') monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE, _raise_event) + monitor.register_callback(DEBUGGER_ID, monitor.events.PY_UNWIND, _unwind_event) else: monitor.register_callback(DEBUGGER_ID, monitor.events.RAISE, None) + monitor.register_callback(DEBUGGER_ID, monitor.events.PY_UNWIND, None) has_line_breaks = py_db.has_plugin_line_breaks diff --git a/tests_python/test_debugger.py b/tests_python/test_debugger.py index 5656e28e4..53391c37a 100644 --- a/tests_python/test_debugger.py +++ b/tests_python/test_debugger.py @@ -1402,10 +1402,15 @@ def additional_output_checks(writer, stdout, stderr): writer.write_run_thread(hit.thread_id) if not unhandled: - expected_lines = [ - writer.get_line_index_with_content('# exc line'), - writer.get_line_index_with_content('# call exc'), - ] + if sys.version_info[:2] >= (3, 12): + expected_lines = [ + writer.get_line_index_with_content('# call exc'), + ] + else: + expected_lines = [ + writer.get_line_index_with_content('# exc line'), + writer.get_line_index_with_content('# call exc'), + ] for expected_line in expected_lines: hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION)