diff --git a/src/ptvsd/_util.py b/src/ptvsd/_util.py index 519011ae..42fbc77b 100644 --- a/src/ptvsd/_util.py +++ b/src/ptvsd/_util.py @@ -77,7 +77,8 @@ def log_pydevd_msg(cmdid, seq, args, inbound, else: cmdname = '???' cmd = '{} ({})'.format(cmdid, cmdname) - args = args.replace('\n', '\\n') + if isinstance(args, bytes) or isinstance(args, str): + args = args.replace('\n', '\\n') msg = '{}{:28} [{:>10}]: |{}|'.format(prefix, cmd, seq, args) log(msg) @@ -420,18 +421,28 @@ try: import dis except ImportError: def get_code_lines(code): - return None + raise NotImplementedError else: def get_code_lines(code): - # First, get all line starts for this code object. This does not include - # bodies of nested class and function definitions, as they have their - # own objects. - for _, lineno in dis.findlinestarts(code): - yield lineno + if not isinstance(code, types.CodeType): + path = code + with open(path) as f: + src = f.read() + code = compile(src, path, 'exec', 0, dont_inherit=True) + return get_code_lines(code) - # For nested class and function definitions, their respective code objects - # are constants referenced by this object. - for const in code.co_consts: - if isinstance(const, types.CodeType) and const.co_filename == code.co_filename: - for lineno in get_code_lines(const): - yield lineno \ No newline at end of file + def iterate(): + # First, get all line starts for this code object. This does not include + # bodies of nested class and function definitions, as they have their + # own objects. + for _, lineno in dis.findlinestarts(code): + yield lineno + + # For nested class and function definitions, their respective code objects + # are constants referenced by this object. + for const in code.co_consts: + if isinstance(const, types.CodeType) and const.co_filename == code.co_filename: + for lineno in get_code_lines(const): + yield lineno + + return iterate() \ No newline at end of file diff --git a/src/ptvsd/_vendored/force_pydevd.py b/src/ptvsd/_vendored/force_pydevd.py index e9af9b0c..299deb46 100644 --- a/src/ptvsd/_vendored/force_pydevd.py +++ b/src/ptvsd/_vendored/force_pydevd.py @@ -22,6 +22,8 @@ with vendored('pydevd'): pydevd_constants.CYTHON_SUPPORTED = False # We limit representation size in our representation provider when needed. pydevd_constants.MAXIMUM_VARIABLE_REPRESENTATION_SIZE = 2**32 +# We want CMD_SET_NEXT_STATEMENT to have a response indicating success or failure. +pydevd_constants.GOTO_HAS_RESPONSE = True # Now make sure all the top-level modules and packages in pydevd are diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py index 7613bf74..291f4ae9 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py @@ -115,6 +115,9 @@ USE_LIB_COPY = SUPPORT_GEVENT and \ INTERACTIVE_MODE_AVAILABLE = sys.platform in ('darwin', 'win32') or os.getenv('DISPLAY') is not None IS_PYCHARM = False +# If True, CMD_SET_NEXT_STATEMENT and CMD_RUN_TO_LINE commands have responses indicating success or failure. +GOTO_HAS_RESPONSE = IS_PYCHARM + LOAD_VALUES_ASYNC = os.getenv('PYDEVD_LOAD_VALUES_ASYNC', 'False') == 'True' DEFAULT_VALUE = "__pydevd_value_async" ASYNC_EVAL_TIMEOUT_SEC = 60 diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py index 1a76396f..ec0ad321 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd.py +++ b/src/ptvsd/_vendored/pydevd/pydevd.py @@ -37,7 +37,7 @@ from _pydevd_bundle.pydevd_comm_constants import (CMD_THREAD_SUSPEND, CMD_STEP_I from _pydevd_bundle.pydevd_constants import (IS_JYTH_LESS25, IS_PYCHARM, get_thread_id, get_current_thread_id, dict_keys, dict_iter_items, DebugInfoHolder, PYTHON_SUSPEND, STATE_SUSPEND, STATE_RUN, get_frame, clear_cached_thread_id, INTERACTIVE_MODE_AVAILABLE, SHOW_DEBUG_INFO_ENV, IS_PY34_OR_GREATER, IS_PY2, NULL, - NO_FTRACE) + NO_FTRACE, GOTO_HAS_RESPONSE) from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE from _pydevd_bundle.pydevd_extension_api import DebuggerEventHandler @@ -1181,7 +1181,7 @@ class PyDB(object): if curr_func_name in ('?', ''): curr_func_name = '' - if curr_func_name == func_name: + if func_name == '*' or curr_func_name == func_name: line = next_line frame.f_trace = self.trace_dispatch frame.f_lineno = line @@ -1302,7 +1302,7 @@ class PyDB(object): except ValueError as e: response_msg = "%s" % e finally: - if IS_PYCHARM: + if GOTO_HAS_RESPONSE: seq = info.pydev_message cmd = self.cmd_factory.make_set_next_stmnt_status_message(seq, stop, response_msg) self.writer.add_command(cmd) diff --git a/src/ptvsd/messaging.py b/src/ptvsd/messaging.py index 4d0dd582..d8814187 100644 --- a/src/ptvsd/messaging.py +++ b/src/ptvsd/messaging.py @@ -239,6 +239,9 @@ class RequestFailure(Exception): def __init__(self, message): self.message = message + def __hash__(self): + return hash(self.message) + def __eq__(self, other): if not isinstance(other, RequestFailure): return NotImplemented diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py index 129dc68a..c38c96bc 100644 --- a/src/ptvsd/wrapper.py +++ b/src/ptvsd/wrapper.py @@ -503,7 +503,8 @@ class PydevdSocket(object): raise EOFError seq, s = self.make_packet(cmd_id, args) _util.log_pydevd_msg(cmd_id, seq, args, inbound=False) - os.write(self.pipe_w, s.encode('utf8')) + with self.lock: + os.write(self.pipe_w, s.encode('utf8')) def pydevd_request(self, loop, cmd_id, args, is_json=False): ''' @@ -520,16 +521,16 @@ class PydevdSocket(object): seq, s = self.make_packet(cmd_id, args) _util.log_pydevd_msg(cmd_id, seq, args, inbound=False) fut = loop.create_future() + with self.lock: self.requests[seq] = loop, fut as_bytes = s if not isinstance(as_bytes, bytes): as_bytes = as_bytes.encode('utf-8') - if is_json: os.write(self.pipe_w, ('Content-Length:%s\r\n\r\n' % (len(as_bytes),)).encode('ascii')) - os.write(self.pipe_w, as_bytes) + return fut @@ -1104,6 +1105,7 @@ INITIALIZE_RESPONSE = dict( supportsSetVariable=True, supportsValueFormattingOptions=True, supportTerminateDebuggee=True, + supportsGotoTargetsRequest=False, # https://github.com/Microsoft/ptvsd/issues/1163 exceptionBreakpointFilters=[ { 'filter': 'raised', @@ -1347,6 +1349,8 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): self.frame_map = IDMap() self.var_map = IDMap() self.source_map = IDMap() + self.goto_target_map = IDMap() + self.current_goto_request = None self.enable_source_references = False self.next_var_ref = 0 self._path_mappings = [] @@ -2233,6 +2237,34 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): self.pydevd_notify(pydevd_comm.CMD_STEP_RETURN, tid) self.send_response(request) + @async_handler + def on_gotoTargets(self, request, args): + path = args['source']['path'] + line = args['line'] + target_id = self.goto_target_map.to_vscode((path, line), autogen=True) + self.send_response(request, targets=[{ + 'id': target_id, + 'label': '{}:{}'.format(path, line), + 'line': line, + }]) + + @async_handler + def on_goto(self, request, args): + if self.current_goto_request is not None: + self.send_error_response(request, 'Already processing a "goto" request.') + return + + vsc_tid = args['threadId'] + target_id = args['targetId'] + + pyd_tid = self.thread_map.to_pydevd(vsc_tid) + path, line = self.goto_target_map.to_pydevd(target_id) + + self.current_goto_request = request + self.pydevd_notify( + pydevd_comm.CMD_SET_NEXT_STATEMENT, + '{}\t{}\t*'.format(pyd_tid, line)) + def _get_hit_condition_expression(self, hit_condition): """Following hit condition values are supported @@ -2293,30 +2325,18 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): path = path.encode(sys.getfilesystemencoding()) path = path_to_unicode(pydevd_file_utils.norm_file_to_server(path)) - try: - with open(path) as f: - src = f.read() - except Exception: - pass - else: - try: - code = compile(src, path, 'exec', 0, dont_inherit=True) - except Exception: - pass - else: - try: - lines = sorted(_util.get_code_lines(code)) - except Exception: - pass - else: - if lines: - for bp in args['breakpoints']: - line = bp['line'] - if line not in lines: - # Adjust to the first preceding valid line. - idx = bisect.bisect_left(lines, line) - if idx > 0: - bp['line'] = lines[idx - 1] + try: + lines = sorted(_util.get_code_lines(path)) + except Exception: + pass + else: + for bp in args['breakpoints']: + line = bp['line'] + if line not in lines: + # Adjust to the first preceding valid line. + idx = bisect.bisect_left(lines, line) + if idx > 0: + bp['line'] = lines[idx - 1] _, _, resp_args = yield self.pydevd_request( pydevd_comm.CMD_SET_BREAK, @@ -2573,7 +2593,30 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): @pydevd_events.handler(pydevd_comm.CMD_THREAD_SUSPEND) @async_handler def on_pydevd_thread_suspend(self, seq, args): - pass + xml = self.parse_xml_response(args) + reason = int(xml.thread['stop_reason']) + + # Normally, we rely on CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION instead, + # but we only get this one in response to CMD_SET_NEXT_STATEMENT. + if reason == pydevd_comm.CMD_SET_NEXT_STATEMENT: + pyd_tid = xml.thread['id'] + vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) + self.send_event( + 'stopped', + reason='pause', + threadId=vsc_tid, + allThreadsStopped=True) + + @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN) + def on_pydevd_thread_run(self, seq, args): + pyd_tid, reason = args.split('\t', 2) + reason = int(reason) + vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) + + # Normally, we rely on CMD_THREAD_RESUME_SINGLE_NOTIFICATION instead, + # but we only get this one in response to CMD_SET_NEXT_STATEMENT. + if reason == pydevd_comm.CMD_SET_NEXT_STATEMENT: + self.send_event('continued', threadId=vsc_tid) @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION) @async_handler @@ -2634,10 +2677,6 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): description=exc_desc, **extra) - @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN) - def on_pydevd_thread_run(self, seq, args): - pass # Ignore: only send continued on CMD_THREAD_RESUME_SINGLE_NOTIFICATION - @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_RESUME_SINGLE_NOTIFICATION) def on_pydevd_thread_resume_single_notification(self, seq, args): resumed_info = json.loads(args) @@ -2677,3 +2716,16 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): @pydevd_events.handler(pydevd_comm.CMD_PROCESS_CREATED) def on_pydevd_process_create(self, seq, args): pass + + @pydevd_events.handler(pydevd_comm.CMD_SET_NEXT_STATEMENT) + def on_pydevd_set_next_statement(self, seq, args): + goto_request = self.current_goto_request + assert goto_request is not None + self.current_goto_request = None + + success, message = args.split('\t', 2) + + if success == 'True': + self.send_response(goto_request) + else: + self.send_error_response(goto_request, message) diff --git a/tests/func/test_step.py b/tests/func/test_step.py new file mode 100644 index 00000000..24ba4fe2 --- /dev/null +++ b/tests/func/test_step.py @@ -0,0 +1,88 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, with_statement, unicode_literals + +import pytest + +from tests.helpers import get_marked_line_numbers, print +from tests.helpers.session import DebugSession +from tests.helpers.timeline import Event +from tests.helpers.pattern import ANY + + +@pytest.mark.skip(reason='https://github.com/Microsoft/ptvsd/issues/1163') +def test_set_next_statement(pyfile, run_as, start_method): + @pyfile + def code_to_debug(): + from dbgimporter import import_and_enable_debugger + import_and_enable_debugger() + + def func(): + print(1) #@inner1 + print(2) #@inner2 + print(3) #@outer3 + func() + + line_numbers = get_marked_line_numbers(code_to_debug) + print(line_numbers) + + with DebugSession() as session: + session.initialize( + target=(run_as, code_to_debug), + start_method=start_method, + ignore_unobserved=[Event('continued')], + ) + session.set_breakpoints(code_to_debug, [line_numbers['inner1']]) + session.start_debugging() + + stop = session.wait_for_thread_stopped() + frames = stop.stacktrace.body['stackFrames'] + line = frames[0]['line'] + assert line == line_numbers['inner1'] + + targets = session.send_request('gotoTargets', { + 'source': {'path': code_to_debug}, + 'line': line_numbers['outer3'], + }).wait_for_response().body['targets'] + + assert targets == [{ + 'id': ANY.num, + 'label': ANY.str, + 'line': line_numbers['outer3'] + }] + outer3_target = targets[0]['id'] + + with pytest.raises(Exception): + session.send_request('goto', { + 'threadId': stop.thread_id, + 'targetId': outer3_target, + }).wait_for_response() + + targets = session.send_request('gotoTargets', { + 'source': {'path': code_to_debug}, + 'line': line_numbers['inner2'], + }).wait_for_response().body['targets'] + + assert targets == [{ + 'id': ANY.num, + 'label': ANY.str, + 'line': line_numbers['inner2'], + }] + inner2_target = targets[0]['id'] + + session.send_request('goto', { + 'threadId': stop.thread_id, + 'targetId': inner2_target, + }).wait_for_response() + + session.wait_for_next(Event('continued')) + + stop = session.wait_for_thread_stopped() + frames = stop.stacktrace.body['stackFrames'] + line = frames[0]['line'] + assert line == line_numbers['inner2'] + + session.send_request('continue').wait_for_response() + session.wait_for_exit()