From 957b01002dcd33f0f45232caf03b463d43b46a72 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 25 Mar 2019 10:14:09 -0700 Subject: [PATCH] Add json support for exception info request. Fixes #1222 (#1260) * Add json support for exception info request * Add tests for json exception info request * Address comments and minor tweaks * Rename and fix tests * Pass response to NetCommand --- .../pydevd/_pydevd_bundle/pydevd_api.py | 11 ++- .../pydevd/_pydevd_bundle/pydevd_comm.py | 88 ++++++++++++++++++ .../pydevd_process_net_command_json.py | 9 ++ .../pydevd/tests_python/test_debugger_json.py | 27 ++++++ src/ptvsd/wrapper.py | 91 +++++-------------- 5 files changed, 158 insertions(+), 68 deletions(-) diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 542941f55..909440efc 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -8,7 +8,7 @@ InternalGetVariable, InternalGetArray, InternalLoadFullValue, internal_get_description, internal_get_frame, internal_evaluate_expression, InternalConsoleExec, internal_get_variable_json, internal_change_variable, internal_change_variable_json, - internal_evaluate_expression_json, internal_set_expression_json) + internal_evaluate_expression_json, internal_set_expression_json, internal_get_exception_details_json) from _pydevd_bundle.pydevd_comm_constants import CMD_THREAD_SUSPEND, file_system_encoding from _pydevd_bundle.pydevd_constants import (get_current_thread_id, set_protocol, get_protocol, HTTP_JSON_PROTOCOL, JSON_PROTOCOL, STATE_RUN, IS_PY3K, DebugInfoHolder, dict_keys) @@ -126,6 +126,15 @@ def request_stack(self, py_db, seq, thread_id, fmt=None, timeout=.5): else: py_db.post_internal_command(internal_get_thread_stack, '*') + def request_exception_info_json(self, py_db, request, thread_id): + py_db.post_method_as_internal_command( + thread_id, + internal_get_exception_details_json, + request, + thread_id, + set_additional_thread_info=set_additional_thread_info, + iter_visible_frames_info=py_db.cmd_factory._iter_visible_frames_info) + def request_step(self, py_db, thread_id, step_cmd_id): t = pydevd_find_thread_by_id(thread_id) if t: diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 0507b102d..cf7bd6c80 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -64,6 +64,7 @@ ''' import itertools +import linecache import os from _pydev_bundle.pydev_imports import _queue @@ -1076,6 +1077,93 @@ def internal_get_description(dbg, seq, thread_id, frame_id, expression): dbg.writer.add_command(cmd) +def internal_get_exception_details_json(dbg, request, thread_id, set_additional_thread_info=None, iter_visible_frames_info=None): + ''' Fetch exception details + ''' + try: + thread = pydevd_find_thread_by_id(thread_id) + additional_info = set_additional_thread_info(thread) + topmost_frame = additional_info.get_topmost_frame(thread) + + frames = [] + exc_type = None + exc_desc = None + if topmost_frame is not None: + frame_id_to_lineno = {} + try: + trace_obj = None + frame = topmost_frame + while frame is not None: + if frame.f_code.co_name == 'do_wait_suspend' and frame.f_code.co_filename.endswith('pydevd.py'): + arg = frame.f_locals.get('arg', None) + if arg is not None: + exc_type, exc_desc, trace_obj = arg + break + frame = frame.f_back + + while trace_obj.tb_next is not None: + trace_obj = trace_obj.tb_next + + info = dbg.suspended_frames_manager.get_topmost_frame_and_frame_id_to_line(thread_id) + if info is not None: + topmost_frame, frame_id_to_lineno = info + + if trace_obj is not None: + for frame_id, frame, method_name, filename_in_utf8, lineno in iter_visible_frames_info( + dbg, trace_obj.tb_frame, frame_id_to_lineno): + + line_text = linecache.getline(filename_in_utf8, lineno) + + # Never filter out plugin frames! + if not getattr(frame, 'IS_PLUGIN_FRAME', False): + if not dbg.in_project_scope(filename_in_utf8): + if not dbg.get_use_libraries_filter(): + continue + frames.append((filename_in_utf8, lineno, method_name, line_text)) + finally: + topmost_frame = None + + if exc_desc is not None: + name = exc_desc.__class__.__name__ + description = '%s' % (exc_desc,) + else: + name = 'exception: type unknown' + description = 'exception: no description' + + stack_str = ''.join(traceback.format_list(frames)) + + # This is an extra bit of data used by Visual Studio + source_path = frames[0][0] if frames else '' + + # TODO: breakMode is set to always. This should be retrieved from exception + # breakpoint settings for that exception, or its parent chain. Currently json + # support for setExceptionBreakpoint is not implemented. + response = pydevd_schema.ExceptionInfoResponse( + request_seq=request.seq, + success=True, + command='exceptionInfo', + body=pydevd_schema.ExceptionInfoResponseBody( + exceptionId=name, + description=description, + breakMode=pydevd_schema.ExceptionBreakMode.ALWAYS, + details=pydevd_schema.ExceptionDetails( + message=description, + typeName=name, + stackTrace=stack_str, + source=source_path + ) + ) + ) + except: + exc = get_exception_traceback_str() + response = pydevd_base_schema.build_response(request, kwargs={ + 'success': False, + 'message': exc, + 'body':{} + }) + dbg.writer.add_command(NetCommand(CMD_RETURN, 0, response, is_json=True)) + + class InternalGetBreakpointException(InternalThreadCommand): ''' Send details of exception raised while evaluating conditional breakpoint ''' diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 403e0960c..df902eec6 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -371,6 +371,15 @@ def on_stacktrace_request(self, py_db, request): fmt = fmt.to_dict() self.api.request_stack(py_db, request.seq, thread_id, fmt) + def on_exceptioninfo_request(self, py_db, request): + ''' + :param ExceptionInfoRequest request: + ''' + # : :type exception_into_arguments: ExceptionInfoArguments + exception_into_arguments = request.arguments + thread_id = exception_into_arguments.threadId + self.api.request_exception_info_json(py_db, request, thread_id) + def on_scopes_request(self, py_db, request): ''' Scopes are the top-level items which appear for a frame (so, we receive the frame id diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py index 40eff2914..64702a59c 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py @@ -994,6 +994,33 @@ def test_evaluate(case_setup): writer.finished_ok = True +def test_exception_details(case_setup): + with case_setup.test_file('_debugger_case_exceptions.py') as writer: + json_facade = JsonFacade(writer) + + writer.write_set_protocol('http_json') + writer.write_add_exception_breakpoint_with_policy( + 'IndexError', + notify_on_handled_exceptions=2, # Notify only once + notify_on_unhandled_exceptions=0, + ignore_libraries=1 + ) + + json_facade.write_make_initial_run() + hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION) + + exc_info_request = json_facade.write_request( + pydevd_schema.ExceptionInfoRequest(pydevd_schema.ExceptionInfoArguments(hit.thread_id))) + exc_info_response = json_facade.wait_for_response(exc_info_request) + body = exc_info_response.body + assert body.exceptionId.endswith('IndexError') + assert body.description == 'foo' + assert body.details.kwargs['source'] == writer.TEST_FILE + + writer.write_run_thread(hit.thread_id) + + writer.finished_ok = True + @pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython.') def test_path_translation_and_source_reference(case_setup): diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py index dbf444933..8e3e90ee7 100644 --- a/src/ptvsd/wrapper.py +++ b/src/ptvsd/wrapper.py @@ -12,12 +12,10 @@ import os import platform import pydevd_file_utils -import re import site import socket import sys import threading -import traceback try: import urllib @@ -1882,72 +1880,21 @@ def on_setExceptionBreakpoints(self, request, args): if request is not None: self.send_response(request) - def _parse_exception_details(self, exc_xml, include_stack=True): - exc_name = None - exc_desc = None - exc_source = None - exc_stack = None - try: - xml = self.parse_xml_response(exc_xml) - re_name = r"[\'\"](.*)[\'\"]" - exc_type = xml.thread['exc_type'] - exc_desc = xml.thread['exc_desc'] - try: - exc_name = re.findall(re_name, exc_type)[0] - except IndexError: - exc_name = exc_type - - if include_stack: - xframes = list(xml.thread.frame) - frame_data = [] - for f in xframes: - file_path = unquote_xml_path(f['file']) - if not self.internals_filter.is_internal_path(file_path) \ - and self._should_debug(file_path): - line_no = int(f['line']) - func_name = unquote(f['name']) - if _util.is_py34(): - # NOTE: In 3.4.* format_list requires the text - # to be passed in the tuple list. - line_text = _util.get_line_for_traceback(file_path, - line_no) - frame_data.append((file_path, line_no, - func_name, line_text)) - else: - frame_data.append((file_path, line_no, - func_name, None)) - - exc_stack = ''.join(traceback.format_list(frame_data)) - exc_source = unquote_xml_path(xframes[0]['file']) - if self.internals_filter.is_internal_path(exc_source) or \ - not self._should_debug(exc_source): - exc_source = None - except Exception: - exc_name = 'BaseException' - exc_desc = 'exception: no description' - - return exc_name, exc_desc, exc_source, exc_stack - @async_handler def on_exceptionInfo(self, request, args): - # TODO: docstring pyd_tid = self.thread_map.to_pydevd(args['threadId']) - cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS - _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid) - name, description, source, stack = \ - self._parse_exception_details(resp_args) + pydevd_request = copy.deepcopy(request) + del pydevd_request['seq'] # A new seq should be created for pydevd. + pydevd_request['arguments']['threadId'] = pyd_tid + _, _, resp_args = yield self.pydevd_request( + pydevd_comm.CMD_GET_EXCEPTION_DETAILS, + pydevd_request, + is_json=True) - self.send_response( - request, - exceptionId=name, - description=description, - breakMode=self.exceptions_mgr.get_break_mode(name), - details={'typeName': name, - 'message': description, - 'stackTrace': stack, - 'source': source}, - ) + body = resp_args['body'] + body['breakMode'] = self.exceptions_mgr.get_break_mode(body['exceptionId']) + self.send_response(request, **body) @async_handler def on_completions(self, request, args): @@ -2126,10 +2073,20 @@ def on_pydevd_thread_suspend_single_notification(self, seq, args): reason not in ['step', 'exception', 'breakpoint'] if reason == 'exception': - cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS - _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid) - exc_name, exc_desc, _, _ = \ - self._parse_exception_details(resp_args, include_stack=False) + pydevd_request = { + 'type': 'request', + 'command': 'exceptionInfo', + 'arguments': { + 'threadId': pyd_tid + }, + } + + _, _, resp_args = yield self.pydevd_request( + pydevd_comm.CMD_GET_EXCEPTION_DETAILS, + pydevd_request, + is_json=True) + exc_name = resp_args['body']['exceptionId'] + exc_desc = resp_args['body']['description'] if not self.debug_options.get('BREAK_SYSTEMEXIT_ZERO', False): # SystemExit is qualified on Python 2, and unqualified on Python 3