Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
Add json support for exception info request. Fixes #1222 (#1260)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
karthiknadig authored Mar 25, 2019
1 parent 3538b2c commit 957b010
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 68 deletions.
11 changes: 10 additions & 1 deletion src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
88 changes: 88 additions & 0 deletions src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
'''

import itertools
import linecache
import os

from _pydev_bundle.pydev_imports import _queue
Expand Down Expand Up @@ -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 '''

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
91 changes: 24 additions & 67 deletions src/ptvsd/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 957b010

Please sign in to comment.