Set don't trace ptvsd files via json message. Fixes #1220 (#1346)

* Set dont trace ptvsd files via json message.

* Tests for set dont trace

* Cleanup and fixes

* Use custom json to generate DAP schema extensions

* Address comments

* Address more comments

* Try fix the breakpoint() issue

* Always true

* Try this

* Address comments.
This commit is contained in:
Karthik Nadig 2019-04-16 09:11:09 -07:00 committed by GitHub
parent 66deeb108e
commit b26bcd0778
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 417 additions and 48 deletions

View file

@ -87,6 +87,17 @@ def load_schema_data():
return json_schema_data
def load_custom_schema_data():
import os.path
import json
json_file = os.path.join(os.path.dirname(__file__), 'debugProtocolCustom.json')
with open(json_file, 'rb') as json_contents:
json_schema_data = json.loads(json_contents.read())
return json_schema_data
def create_classes_to_generate_structure(json_schema_data):
definitions = json_schema_data['definitions']
@ -490,6 +501,7 @@ def gen_debugger_protocol():
raise AssertionError('Must be run with Python 3.6 onwards (to keep dict order).')
classes_to_generate = create_classes_to_generate_structure(load_schema_data())
classes_to_generate.update(create_classes_to_generate_structure(load_custom_schema_data()))
class_to_generate = fill_properties_and_required_from_base(classes_to_generate)

View file

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Custom Debug Adapter Protocol",
"description": "Extension to the DAP to support additional features.",
"type": "object",
"definitions": {
"SetDebuggerPropertyRequest": {
"allOf": [ { "$ref": "#/definitions/Request" }, {
"type": "object",
"description": "The request can be used to enable or disable debugger features.",
"properties": {
"command": {
"type": "string",
"enum": [ "setDebuggerProperty" ]
},
"arguments": {
"$ref": "#/definitions/SetDebuggerPropertyArguments"
}
},
"required": [ "command", "arguments" ]
}]
},
"SetDebuggerPropertyArguments": {
"type": "object",
"description": "Arguments for 'setDebuggerProperty' request.",
"properties": { }
},
"SetDebuggerPropertyResponse": {
"allOf": [ { "$ref": "#/definitions/Response" }, {
"type": "object",
"description": "Response to 'setDebuggerProperty' request. This is just an acknowledgement, so no body field is required."
}]
}
}
}

View file

@ -11478,6 +11478,197 @@ class ExceptionDetails(BaseSchema):
return dct
@register_request('setDebuggerProperty')
@register
class SetDebuggerPropertyRequest(BaseSchema):
"""
The request can be used to enable or disable debugger features.
Note: automatically generated code. Do not edit manually.
"""
__props__ = {
"seq": {
"type": "integer",
"description": "Sequence number."
},
"type": {
"type": "string",
"enum": [
"request"
]
},
"command": {
"type": "string",
"enum": [
"setDebuggerProperty"
]
},
"arguments": {
"type": "SetDebuggerPropertyArguments"
}
}
__refs__ = set(['arguments'])
__slots__ = list(__props__.keys()) + ['kwargs']
def __init__(self, arguments, seq=-1, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused)
"""
:param string type:
:param string command:
:param SetDebuggerPropertyArguments arguments:
:param integer seq: Sequence number.
"""
self.type = 'request'
self.command = 'setDebuggerProperty'
if arguments is None:
self.arguments = SetDebuggerPropertyArguments()
else:
self.arguments = SetDebuggerPropertyArguments(update_ids_from_dap=update_ids_from_dap, **arguments) if arguments.__class__ != SetDebuggerPropertyArguments else arguments
self.seq = seq
self.kwargs = kwargs
def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused)
type = self.type # noqa (assign to builtin)
command = self.command
arguments = self.arguments
seq = self.seq
dct = {
'type': type,
'command': command,
'arguments': arguments.to_dict(update_ids_to_dap=update_ids_to_dap),
'seq': seq,
}
dct.update(self.kwargs)
return dct
@register
class SetDebuggerPropertyArguments(BaseSchema):
"""
Arguments for 'setDebuggerProperty' request.
Note: automatically generated code. Do not edit manually.
"""
__props__ = {}
__refs__ = set()
__slots__ = list(__props__.keys()) + ['kwargs']
def __init__(self, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused)
"""
"""
self.kwargs = kwargs
def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused)
dct = {
}
dct.update(self.kwargs)
return dct
@register_response('setDebuggerProperty')
@register
class SetDebuggerPropertyResponse(BaseSchema):
"""
Response to 'setDebuggerProperty' request. This is just an acknowledgement, so no body field is
required.
Note: automatically generated code. Do not edit manually.
"""
__props__ = {
"seq": {
"type": "integer",
"description": "Sequence number."
},
"type": {
"type": "string",
"enum": [
"response"
]
},
"request_seq": {
"type": "integer",
"description": "Sequence number of the corresponding request."
},
"success": {
"type": "boolean",
"description": "Outcome of the request."
},
"command": {
"type": "string",
"description": "The command requested."
},
"message": {
"type": "string",
"description": "Contains error message if success == false."
},
"body": {
"type": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
],
"description": "Contains request result if success is true and optional error details if success is false."
}
}
__refs__ = set()
__slots__ = list(__props__.keys()) + ['kwargs']
def __init__(self, request_seq, success, command, seq=-1, message=None, body=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused)
"""
:param string type:
:param integer request_seq: Sequence number of the corresponding request.
:param boolean success: Outcome of the request.
:param string command: The command requested.
:param integer seq: Sequence number.
:param string message: Contains error message if success == false.
:param ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string'] body: Contains request result if success is true and optional error details if success is false.
"""
self.type = 'response'
self.request_seq = request_seq
self.success = success
self.command = command
self.seq = seq
self.message = message
self.body = body
self.kwargs = kwargs
def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused)
type = self.type # noqa (assign to builtin)
request_seq = self.request_seq
success = self.success
command = self.command
seq = self.seq
message = self.message
body = self.body
dct = {
'type': type,
'request_seq': request_seq,
'success': success,
'command': command,
'seq': seq,
}
if message is not None:
dct['message'] = message
if body is not None:
dct['body'] = body
dct.update(self.kwargs)
return dct
@register
class ErrorResponseBody(BaseSchema):
"""

View file

@ -9,6 +9,7 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import (SourceBreakpoint, Scope
VariablesResponseBody, SetVariableResponseBody, ModulesResponseBody, SourceResponseBody,
GotoTargetsResponseBody, ExceptionOptions)
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_comm import pydevd_log
from _pydevd_bundle.pydevd_comm_constants import (
CMD_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_OVER, CMD_STEP_INTO_MY_CODE,
CMD_STEP_INTO, CMD_STEP_RETURN_MY_CODE, CMD_STEP_RETURN, CMD_SET_NEXT_STATEMENT)
@ -695,5 +696,49 @@ class _PyDevJsonCommandProcessor(object):
response = pydevd_base_schema.build_response(request, kwargs={'body': {}})
return NetCommand(CMD_RETURN, 0, response, is_json=True)
def _can_set_dont_trace_pattern(self, py_db, start_patterns, end_patterns):
if py_db.is_cache_file_type_empty():
return True
if py_db.dont_trace_external_files.__name__ == 'dont_trace_files_property_request':
return py_db.dont_trace_external_files.start_patterns == start_patterns and \
py_db.dont_trace_external_files.end_patterns == end_patterns
return False
def on_setdebuggerproperty_request(self, py_db, request):
args = request.arguments.kwargs
if 'dontTraceStartPatterns' in args and 'dontTraceEndPatterns' in args:
start_patterns = tuple(args['dontTraceStartPatterns'])
end_patterns = tuple(args['dontTraceEndPatterns'])
if self._can_set_dont_trace_pattern(py_db, start_patterns, end_patterns):
def dont_trace_files_property_request(abs_path):
result = abs_path.startswith(start_patterns) or \
abs_path.endswith(end_patterns)
return result
dont_trace_files_property_request.start_patterns = start_patterns
dont_trace_files_property_request.end_patterns = end_patterns
py_db.dont_trace_external_files = dont_trace_files_property_request
else:
# Don't trace pattern cannot be changed after it is set once. There are caches
# throughout the debugger which rely on always having the same file type.
message = ("Calls to set or change don't trace patterns (via setDebuggerProperty) are not "
"allowed since debugging has already started or don't trace patterns are already set.")
pydevd_log(0, message)
response_args = {'success':False, 'body': {}, 'message': message}
response = pydevd_base_schema.build_response(request, kwargs=response_args)
return NetCommand(CMD_RETURN, 0, response, is_json=True)
# TODO: Support other common settings. Note that not all of these might be relevant to python.
# JustMyCodeStepping: 0 or 1
# AllowOutOfProcessSymbols: 0 or 1
# DisableJITOptimization: 0 or 1
# InterpreterOptions: 0 or 1
# StopOnExceptionCrossingManagedBoundary: 0 or 1
# WarnIfNoUserCodeOnLaunch: 0 or 1
# EnableStepFiltering: true of false
response = pydevd_base_schema.build_response(request, kwargs={'body': {}})
return NetCommand(CMD_RETURN, 0, response, is_json=True)
process_net_command_json = _PyDevJsonCommandProcessor(pydevd_base_schema.from_json).process_net_command_json

View file

@ -137,6 +137,7 @@ forked = False
file_system_encoding = getfilesystemencoding()
_CACHE_FILE_TYPE = {}
#=======================================================================================================================
# PyDBCommandThread
@ -546,7 +547,31 @@ class PyDB(object):
if val is not None:
info.pydev_message = str(val)
def get_file_type(self, abs_real_path_and_basename):
def _internal_get_file_type(self, abs_real_path_and_basename):
basename = abs_real_path_and_basename[-1]
if basename.startswith('<frozen '):
# In Python 3.7 "<frozen ..." appear multiple times during import and should be
# ignored for the user.
return self.PYDEV_FILE
return self._dont_trace_get_file_type(basename)
def dont_trace_external_files(self, abs_path):
'''
:param abs_path:
The result from get_abs_path_real_path_and_base_from_file or
get_abs_path_real_path_and_base_from_frame.
:return
True :
If files should NOT be traced.
False:
If files should be traced.
'''
# By default all external files are traced.
return False
def get_file_type(self, abs_real_path_and_basename, _cache_file_type=_CACHE_FILE_TYPE):
'''
:param abs_real_path_and_basename:
The result from get_abs_path_real_path_and_base_from_file or
@ -563,12 +588,17 @@ class PyDB(object):
None:
If it's a regular user file which should be traced.
'''
basename = abs_real_path_and_basename[-1]
if basename.startswith('<frozen '):
# In Python 3.7 "<frozen ..." appear multiple times during import and should be
# ignored for the user.
return self.PYDEV_FILE
return self._dont_trace_get_file_type(basename)
try:
return _cache_file_type[abs_real_path_and_basename[0]]
except:
file_type = self._internal_get_file_type(abs_real_path_and_basename)
if file_type is None:
file_type = PYDEV_FILE if self.dont_trace_external_files(abs_real_path_and_basename[0]) else None
_cache_file_type[abs_real_path_and_basename[0]] = file_type
return file_type
def is_cache_file_type_empty(self):
return bool(_CACHE_FILE_TYPE)
def get_thread_local_trace_func(self):
try:

View file

@ -0,0 +1,3 @@
def call_me_back(callback):
if callable(callback):
callback()

View file

@ -0,0 +1,7 @@
from _debugger_case_dont_trace import call_me_back
def my_callback():
print('trace me') # Break here
if __name__ == '__main__':
call_me_back(my_callback)
print('TEST SUCEEDED!')

View file

@ -1302,6 +1302,66 @@ def test_goto(case_setup):
writer.finished_ok = True
@pytest.mark.parametrize('dbg_property', ['dont_trace', 'trace', 'change_pattern', 'dont_trace_after_start'])
def test_set_debugger_property(case_setup, dbg_property):
with case_setup.test_file('_debugger_case_dont_trace_test.py') as writer:
json_facade = JsonFacade(writer)
writer.write_set_protocol('http_json')
writer.write_add_breakpoint(writer.get_line_index_with_content('Break here'))
if dbg_property in ('dont_trace', 'change_pattern', 'dont_trace_after_start'):
dbg_request = json_facade.write_request(
pydevd_schema.SetDebuggerPropertyRequest(pydevd_schema.SetDebuggerPropertyArguments(
dontTraceStartPatterns=[],
dontTraceEndPatterns=['dont_trace.py'])))
dbg_response = json_facade.wait_for_response(dbg_request)
assert dbg_response.success
if dbg_property == 'change_pattern':
# Attempting to change pattern after it is set but before start should succeed
dbg_request = json_facade.write_request(
pydevd_schema.SetDebuggerPropertyRequest(pydevd_schema.SetDebuggerPropertyArguments(
dontTraceStartPatterns=[],
dontTraceEndPatterns=['something_else.py'])))
dbg_response = json_facade.wait_for_response(dbg_request)
assert dbg_response.success
json_facade.write_make_initial_run()
hit = writer.wait_for_breakpoint_hit()
stack_trace_request = json_facade.write_request(
pydevd_schema.StackTraceRequest(pydevd_schema.StackTraceArguments(threadId=hit.thread_id)))
stack_trace_response = json_facade.wait_for_response(stack_trace_request)
if dbg_property == 'dont_trace_after_start':
# Attempting to set don't trace after start should fail.
# This has the same effect of not setting the trace.
dbg_request = json_facade.write_request(
pydevd_schema.SetDebuggerPropertyRequest(pydevd_schema.SetDebuggerPropertyArguments(
dontTraceStartPatterns=[],
dontTraceEndPatterns=['something_else.py'])))
dbg_response = json_facade.wait_for_response(dbg_request)
assert not dbg_response.success
stack_trace_request = json_facade.write_request(
pydevd_schema.StackTraceRequest(pydevd_schema.StackTraceArguments(threadId=hit.thread_id)))
stack_trace_response = json_facade.wait_for_response(stack_trace_request)
dont_trace_frames = list(frame for frame in stack_trace_response.body.stackFrames
if frame['source']['path'].endswith('dont_trace.py'))
if dbg_property in ('dont_trace', 'dont_trace_after_start'):
# Since don't trace after start is expected to fail,
# the original pattern still holds.
assert dont_trace_frames == []
else:
assert len(dont_trace_frames) == 1
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):

View file

@ -80,33 +80,13 @@ def path_to_unicode(s):
PTVSD_DIR_PATH = os.path.dirname(os.path.abspath(get_abs_path_real_path_and_base_from_file(__file__)[0])) + os.path.sep
NORM_PTVSD_DIR_PATH = os.path.normcase(PTVSD_DIR_PATH)
def dont_trace_ptvsd_files(file_path):
def dont_trace_ptvsd_files(py_db, file_path):
"""
Returns true if the file should not be traced.
"""
return file_path.startswith(PTVSD_DIR_PATH) or file_path.endswith('ptvsd_launcher.py')
original_get_file_type = pydevd.PyDB.get_file_type
def _get_file_type(py_db, abs_real_path_and_basename, _cache_file_type={}):
abs_path = abs_real_path_and_basename[0]
try:
return _cache_file_type[abs_path]
except KeyError:
file_type = original_get_file_type(py_db, abs_real_path_and_basename)
if file_type is not None:
_cache_file_type[abs_path] = file_type
elif dont_trace_ptvsd_files(abs_path):
_cache_file_type[abs_path] = PYDEV_FILE
else:
_cache_file_type[abs_path] = None
return _cache_file_type[abs_path]
pydevd.PyDB.get_file_type = _get_file_type
pydevd.PyDB.dont_trace_external_files = dont_trace_ptvsd_files
# NOTE: Previously this included sys.prefix, sys.base_prefix and sys.real_prefix
# On some systems those resolve to '/usr'. That means any user code will
@ -1338,6 +1318,14 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
msg = '1.1\t{}\tID'.format(self._client_os_type)
return self.pydevd_request(cmd, msg)
def _get_new_setDebuggerProperty_request(self, **kwargs):
return {
"command": "setDebuggerProperty",
"arguments": kwargs,
"type": "request",
# "seq": seq_id, # A new seq should be created for pydevd.
}
@async_handler
def _handle_launch_or_attach(self, request, args):
self._path_mappings_received = True
@ -1378,6 +1366,14 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
default_success_exitcodes += [3]
self._success_exitcodes = args.get('successExitCodes', default_success_exitcodes)
# Don't trace files under ptvsd, and ptvsd_launcher.py files
# TODO: un-comment this code after fixing https://github.com/Microsoft/ptvsd/issues/1355
#dont_trace_request = self._get_new_setDebuggerProperty_request(
# dontTraceStartPatterns=[PTVSD_DIR_PATH],
# dontTraceEndPatterns=['ptvsd_launcher.py']
#)
#yield self.pydevd_request(-1, dont_trace_request, is_json=True)
def _handle_detach(self):
ptvsd.log.info('Detaching ...')
# TODO: Skip if already detached?
@ -1723,12 +1719,14 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
self.send_response(request, **sys_info)
# VS specific custom message handlers
@async_handler
def on_setDebuggerProperty(self, request, args):
if 'JustMyCodeStepping' in args:
jmc = int(args.get('JustMyCodeStepping', 0)) > 0
self.debug_options['DEBUG_STDLIB'] = not jmc
# TODO: Replace the line below with _forward_request_to_pydevd
# after fixing https://github.com/Microsoft/ptvsd/issues/1355
self.send_response(request)
# PyDevd protocol event handlers

View file

@ -361,12 +361,12 @@ def test_exception_stack(pyfile, run_as, start_method, max_frames):
if max_frames == 'all':
# trace back compresses repeated text
min_expected_lines = 100
max_expected_lines = 220
max_expected_lines = 221
args = {'maxExceptionStackFrames': 0}
elif max_frames == 'default':
# default is all frames
min_expected_lines = 100
max_expected_lines = 220
max_expected_lines = 221
args = {}
else:
min_expected_lines = 10

View file

@ -7,7 +7,6 @@ import pytest
import ptvsd
from ptvsd.wrapper import InternalsFilter
from ptvsd.wrapper import dont_trace_ptvsd_files
INTERNAL_DIR = os.path.dirname(os.path.abspath(ptvsd.__file__))
@pytest.mark.parametrize('path', [
@ -28,18 +27,3 @@ def test_internal_paths(path):
def test_user_file_paths(path):
int_filter = InternalsFilter()
assert not int_filter.is_internal_path(path)
@pytest.mark.parametrize('path, val', [
(os.path.join(INTERNAL_DIR, 'wrapper.py'), True),
(os.path.join(INTERNAL_DIR, 'abcd', 'ptvsd', 'wrapper.py'), True),
(os.path.join(INTERNAL_DIR, 'ptvsd', 'wrapper.py'), True),
(os.path.join(INTERNAL_DIR, 'abcd', 'wrapper.py'), True),
(os.path.join('usr', 'abcd', 'ptvsd', 'wrapper.py'), False),
(os.path.join('C:', 'ptvsd', 'wrapper1.py'), False),
(os.path.join('C:', 'abcd', 'ptvsd', 'ptvsd.py'), False),
(os.path.join('usr', 'ptvsd', 'w.py'), False),
(os.path.join('ptvsd', 'w.py'), False),
(os.path.join('usr', 'abcd', 'ptvsd', 'tangle.py'), False),
])
def test_ptvsd_paths(path, val):
assert val == dont_trace_ptvsd_files(os.path.normcase(path))