Skip to content

Commit

Permalink
Handle userUnhandled exception breakpoints. Fixes microsoft#111
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz authored and int19h committed Jun 2, 2020
1 parent 1fb9706 commit 63c0fae
Show file tree
Hide file tree
Showing 15 changed files with 3,961 additions and 3,395 deletions.
7 changes: 7 additions & 0 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ def add_python_exception_breakpoint(
expression,
notify_on_handled_exceptions,
notify_on_unhandled_exceptions,
notify_on_user_unhandled_exceptions,
notify_on_first_raise_only,
ignore_libraries,
):
Expand All @@ -695,6 +696,7 @@ def add_python_exception_breakpoint(
expression=expression,
notify_on_handled_exceptions=notify_on_handled_exceptions,
notify_on_unhandled_exceptions=notify_on_unhandled_exceptions,
notify_on_user_unhandled_exceptions=notify_on_user_unhandled_exceptions,
notify_on_first_raise_only=notify_on_first_raise_only,
ignore_libraries=ignore_libraries,
)
Expand Down Expand Up @@ -723,6 +725,10 @@ def remove_python_exception_breakpoint(self, py_db, exception):
cp = py_db.break_on_caught_exceptions.copy()
cp.pop(exception, None)
py_db.break_on_caught_exceptions = cp

cp = py_db.break_on_user_uncaught_exceptions.copy()
cp.pop(exception, None)
py_db.break_on_user_uncaught_exceptions = cp
except:
pydev_log.exception("Error while removing exception %s", sys.exc_info()[0])

Expand All @@ -747,6 +753,7 @@ def remove_plugins_exception_breakpoint(self, py_db, exception_type, exception):
def remove_all_exception_breakpoints(self, py_db):
py_db.break_on_uncaught_exceptions = {}
py_db.break_on_caught_exceptions = {}
py_db.break_on_user_uncaught_exceptions = {}

plugin = py_db.plugin
if plugin is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __init__(
expression,
notify_on_handled_exceptions,
notify_on_unhandled_exceptions,
notify_on_user_unhandled_exceptions,
notify_on_first_raise_only,
ignore_libraries
):
Expand All @@ -29,6 +30,7 @@ def __init__(
self.notify_on_unhandled_exceptions = notify_on_unhandled_exceptions
self.notify_on_handled_exceptions = notify_on_handled_exceptions
self.notify_on_first_raise_only = notify_on_first_raise_only
self.notify_on_user_unhandled_exceptions = notify_on_user_unhandled_exceptions
self.ignore_libraries = ignore_libraries

self.type = exctype
Expand Down
6,819 changes: 3,629 additions & 3,190 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.c

Large diffs are not rendered by default.

93 changes: 57 additions & 36 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ cdef class PyDBFrame:
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
def should_stop_on_exception(self, frame, str event, arg):
cdef PyDBAdditionalThreadInfo info;
cdef bint flag;
cdef bint should_stop;
cdef bint was_just_raised;
cdef list check_excs;
# ELSE
# def should_stop_on_exception(self, frame, event, arg):
# ENDIF
Expand Down Expand Up @@ -308,55 +310,71 @@ cdef class PyDBFrame:
pydev_log.exception()

if not should_stop:
was_just_raised = trace.tb_next is None

# It was not handled by any plugin, lets check exception breakpoints.
exception_breakpoint = main_debugger.get_exception_breakpoint(
check_excs = []
exc_break_caught = main_debugger.get_exception_breakpoint(
exception, main_debugger.break_on_caught_exceptions)
if exc_break_caught is not None:
check_excs.append((exc_break_caught, False))

exc_break_user = main_debugger.get_exception_breakpoint(
exception, main_debugger.break_on_user_uncaught_exceptions)
if exc_break_user is not None:
check_excs.append((exc_break_user, True))

for exc_break, is_user_uncaught in check_excs:
# Initially mark that it should stop and then go into exclusions.
should_stop = True

if exception_breakpoint is not None:
if exception is SystemExit and main_debugger.ignore_system_exit_code(value):
return False, frame
should_stop = False

if exception in (GeneratorExit, StopIteration):
elif exception in (GeneratorExit, StopIteration):
# These exceptions are control-flow related (they work as a generator
# pause), so, we shouldn't stop on them.
return False, frame

if exception_breakpoint.condition is not None:
eval_result = main_debugger.handle_breakpoint_condition(info, exception_breakpoint, frame)
if not eval_result:
return False, frame
should_stop = False

if main_debugger.exclude_exception_by_filter(exception_breakpoint, trace):
elif main_debugger.exclude_exception_by_filter(exc_break, trace):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
should_stop = False

if ignore_exception_trace(trace):
return False, frame
elif ignore_exception_trace(trace):
should_stop = False

was_just_raised = just_raised(trace)
if was_just_raised:
elif exc_break.condition is not None and \
not main_debugger.handle_breakpoint_condition(info, exc_break, frame):
should_stop = False

if main_debugger.skip_on_exceptions_thrown_in_same_context:
# Option: Don't break if an exception is caught in the same function from which it is thrown
return False, frame
elif was_just_raised and main_debugger.skip_on_exceptions_thrown_in_same_context:
# Option: Don't break if an exception is caught in the same function from which it is thrown
should_stop = False

if exception_breakpoint.notify_on_first_raise_only:
if main_debugger.skip_on_exceptions_thrown_in_same_context:
# In this case we never stop if it was just raised, so, to know if it was the first we
# need to check if we're in the 2nd method.
if not was_just_raised and not just_raised(trace.tb_next):
return False, frame # I.e.: we stop only when we're at the caller of a method that throws an exception
elif exc_break.notify_on_first_raise_only and main_debugger.skip_on_exceptions_thrown_in_same_context \
and not was_just_raised and not just_raised(trace.tb_next):
# In this case we never stop if it was just raised, so, to know if it was the first we
# need to check if we're in the 2nd method.
should_stop = False # I.e.: we stop only when we're at the caller of a method that throws an exception

else:
if not was_just_raised:
return False, frame # I.e.: we stop only when it was just raised
elif exc_break.notify_on_first_raise_only and not main_debugger.skip_on_exceptions_thrown_in_same_context \
and not was_just_raised:
should_stop = False # I.e.: we stop only when it was just raised

# If it got here we should stop.
should_stop = True
try:
info.pydev_message = exception_breakpoint.qname
except:
info.pydev_message = exception_breakpoint.qname.encode('utf-8')
elif is_user_uncaught and not (
not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, True)
and (frame.f_back is None or main_debugger.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.
should_stop = False

if should_stop:
exception_breakpoint = exc_break
try:
info.pydev_message = exc_break.qname
except:
info.pydev_message = exc_break.qname.encode('utf-8')
break

if should_stop:
# Always add exception to frame (must remove later after we proceed).
Expand Down Expand Up @@ -582,7 +600,10 @@ cdef class PyDBFrame:
return None if event == 'call' else NO_FTRACE

plugin_manager = main_debugger.plugin
has_exception_breakpoints = main_debugger.break_on_caught_exceptions or main_debugger.has_plugin_exception_breaks
has_exception_breakpoints = (
main_debugger.break_on_caught_exceptions
or main_debugger.break_on_user_uncaught_exceptions
or main_debugger.has_plugin_exception_breaks)

stop_frame = info.pydev_step_stop
step_cmd = info.pydev_step_cmd
Expand Down
93 changes: 57 additions & 36 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ def trace_exception(self, frame, event, arg):
# IFDEF CYTHON
# def should_stop_on_exception(self, frame, str event, arg):
# cdef PyDBAdditionalThreadInfo info;
# cdef bint flag;
# cdef bint should_stop;
# cdef bint was_just_raised;
# cdef list check_excs;
# ELSE
def should_stop_on_exception(self, frame, event, arg):
# ENDIF
Expand Down Expand Up @@ -143,55 +145,71 @@ def should_stop_on_exception(self, frame, event, arg):
pydev_log.exception()

if not should_stop:
was_just_raised = trace.tb_next is None

# It was not handled by any plugin, lets check exception breakpoints.
exception_breakpoint = main_debugger.get_exception_breakpoint(
check_excs = []
exc_break_caught = main_debugger.get_exception_breakpoint(
exception, main_debugger.break_on_caught_exceptions)
if exc_break_caught is not None:
check_excs.append((exc_break_caught, False))

exc_break_user = main_debugger.get_exception_breakpoint(
exception, main_debugger.break_on_user_uncaught_exceptions)
if exc_break_user is not None:
check_excs.append((exc_break_user, True))

for exc_break, is_user_uncaught in check_excs:
# Initially mark that it should stop and then go into exclusions.
should_stop = True

if exception_breakpoint is not None:
if exception is SystemExit and main_debugger.ignore_system_exit_code(value):
return False, frame
should_stop = False

if exception in (GeneratorExit, StopIteration):
elif exception in (GeneratorExit, StopIteration):
# These exceptions are control-flow related (they work as a generator
# pause), so, we shouldn't stop on them.
return False, frame

if exception_breakpoint.condition is not None:
eval_result = main_debugger.handle_breakpoint_condition(info, exception_breakpoint, frame)
if not eval_result:
return False, frame
should_stop = False

if main_debugger.exclude_exception_by_filter(exception_breakpoint, trace):
elif main_debugger.exclude_exception_by_filter(exc_break, trace):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
should_stop = False

if ignore_exception_trace(trace):
return False, frame
elif ignore_exception_trace(trace):
should_stop = False

was_just_raised = just_raised(trace)
if was_just_raised:
elif exc_break.condition is not None and \
not main_debugger.handle_breakpoint_condition(info, exc_break, frame):
should_stop = False

if main_debugger.skip_on_exceptions_thrown_in_same_context:
# Option: Don't break if an exception is caught in the same function from which it is thrown
return False, frame
elif was_just_raised and main_debugger.skip_on_exceptions_thrown_in_same_context:
# Option: Don't break if an exception is caught in the same function from which it is thrown
should_stop = False

if exception_breakpoint.notify_on_first_raise_only:
if main_debugger.skip_on_exceptions_thrown_in_same_context:
# In this case we never stop if it was just raised, so, to know if it was the first we
# need to check if we're in the 2nd method.
if not was_just_raised and not just_raised(trace.tb_next):
return False, frame # I.e.: we stop only when we're at the caller of a method that throws an exception
elif exc_break.notify_on_first_raise_only and main_debugger.skip_on_exceptions_thrown_in_same_context \
and not was_just_raised and not just_raised(trace.tb_next):
# In this case we never stop if it was just raised, so, to know if it was the first we
# need to check if we're in the 2nd method.
should_stop = False # I.e.: we stop only when we're at the caller of a method that throws an exception

else:
if not was_just_raised:
return False, frame # I.e.: we stop only when it was just raised
elif exc_break.notify_on_first_raise_only and not main_debugger.skip_on_exceptions_thrown_in_same_context \
and not was_just_raised:
should_stop = False # I.e.: we stop only when it was just raised

# If it got here we should stop.
should_stop = True
try:
info.pydev_message = exception_breakpoint.qname
except:
info.pydev_message = exception_breakpoint.qname.encode('utf-8')
elif is_user_uncaught and not (
not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, True)
and (frame.f_back is None or main_debugger.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.
should_stop = False

if should_stop:
exception_breakpoint = exc_break
try:
info.pydev_message = exc_break.qname
except:
info.pydev_message = exc_break.qname.encode('utf-8')
break

if should_stop:
# Always add exception to frame (must remove later after we proceed).
Expand Down Expand Up @@ -417,7 +435,10 @@ def trace_dispatch(self, frame, event, arg):
return None if event == 'call' else NO_FTRACE

plugin_manager = main_debugger.plugin
has_exception_breakpoints = main_debugger.break_on_caught_exceptions or main_debugger.has_plugin_exception_breaks
has_exception_breakpoints = (
main_debugger.break_on_caught_exceptions
or main_debugger.break_on_user_uncaught_exceptions
or main_debugger.has_plugin_exception_breaks)

stop_frame = info.pydev_step_stop
step_cmd = info.pydev_step_cmd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def make_get_thread_stack_message(self, py_db, seq, thread_id, topmost_frame, fm
# Create a source-reference to be used where we provide the source by decompiling the code.
# Note: When the time comes to retrieve the source reference in this case, we'll
# check the linecache first (see: get_decompiled_source_from_frame_id).
source_reference = pydevd_file_utils.create_source_reference_for_frame_id(frame_id)
source_reference = pydevd_file_utils.create_source_reference_for_frame_id(frame_id, original_filename)
else:
# Check if someone added a source reference to the linecache (Python attrs does this).
if linecache.getline(original_filename, 1):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def cmd_set_py_exception(self, py_db, cmd_id, seq, text):
splitted = text.split(';')
py_db.break_on_uncaught_exceptions = {}
py_db.break_on_caught_exceptions = {}
py_db.break_on_user_uncaught_exceptions = {}
if len(splitted) >= 5:
if splitted[0] == 'true':
break_on_uncaught = True
Expand Down Expand Up @@ -382,6 +383,7 @@ def cmd_set_py_exception(self, py_db, cmd_id, seq, text):
expression=None,
notify_on_handled_exceptions=break_on_caught,
notify_on_unhandled_exceptions=break_on_uncaught,
notify_on_user_unhandled_exceptions=False, # TODO (not currently supported in this API).
notify_on_first_raise_only=True,
ignore_libraries=ignore_libraries,
)
Expand Down Expand Up @@ -480,6 +482,7 @@ def cmd_add_exception_break(self, py_db, cmd_id, seq, text):
py_db, exception, condition, expression,
notify_on_handled_exceptions=int(notify_on_handled_exceptions) > 0,
notify_on_unhandled_exceptions=int(notify_on_unhandled_exceptions) == 1,
notify_on_user_unhandled_exceptions=0, # TODO (not currently supported in this API).
notify_on_first_raise_only=int(notify_on_handled_exceptions) == 2,
ignore_libraries=int(ignore_libraries) > 0,
)
Expand Down
Loading

0 comments on commit 63c0fae

Please sign in to comment.