diff --git a/README.md b/README.md index 72f1506a..140bb6fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ +# Python Tools for Visual Studio debug server -# Contributing +## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us diff --git a/ptvsd/__init__.py b/ptvsd/__init__.py new file mode 100644 index 00000000..332469b8 --- /dev/null +++ b/ptvsd/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py new file mode 100644 index 00000000..b1a6e4fb --- /dev/null +++ b/ptvsd/__main__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +if __name__ == '__main__': + import ptvsd.wrapper + import pydevd + pydevd.main() diff --git a/ptvsd/debugger.py b/ptvsd/debugger.py new file mode 100644 index 00000000..9b0f4175 --- /dev/null +++ b/ptvsd/debugger.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +DONT_DEBUG = [] + +def debug(filename, port_num, debug_id, debug_options, run_as): + import sys + import ptvsd.wrapper + import pydevd + sys.argv[1:0] = ['--port', str(port_num), '--client', '127.0.0.1', '--file', filename] + pydevd.main() diff --git a/ptvsd/futures.py b/ptvsd/futures.py new file mode 100644 index 00000000..249ce17b --- /dev/null +++ b/ptvsd/futures.py @@ -0,0 +1,136 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +import sys +import threading +from ptvsd.reraise import reraise + + +class Future(object): + def __init__(self, loop): + self._lock = threading.Lock() + self._loop = loop + self._done = False + self._observed = False + self._done_callbacks = [] + + def __del__(self): + with self._lock: + if self._done and self._exc_info and not self._observed: + print('Unobserved exception in a Future:', file=sys.__stderr__) + traceback.print_exception(self._exc_info[0], self._exc_info[1], self._exc_info[2], file=sys.__stderr__) + + def result(self): + with self._lock: + self._observed = True + if self._exc_info: + reraise(self._exc_info) + return self._result + + def exc_info(self): + with self._lock: + self._observed = True + return self._exc_info + + def set_result(self, result): + with self._lock: + self._result = result + self._exc_info = None + self._done = True + callbacks = list(self._done_callbacks) + def invoke_callbacks(): + for cb in callbacks: + cb(self) + self._loop.call_soon(invoke_callbacks) + + def set_exc_info(self, exc_info): + with self._lock: + self._exc_info = exc_info + self._done = True + callbacks = list(self._done_callbacks) + def invoke_callbacks(): + for cb in callbacks: + cb(self) + self._loop.call_soon(invoke_callbacks) + + def add_done_callback(self, callback): + with self._lock: + done = self._done + self._done_callbacks.append(callback) + if done: + callback(self) + + def remove_done_callback(self, callback): + with self._lock: + self._done_callbacks.remove(callback) + + +class EventLoop(object): + def __init__(self): + self._queue = [] + self._lock = threading.Lock() + self._event = threading.Event() + self._event.set() + + def create_future(self): + return Future(self) + + def run_forever(self): + while True: + self._event.wait() + with self._lock: + queue = self._queue + self._queue = [] + self._event.clear() + for (f, args) in queue: + f(*args) + + def call_soon(self, f, *args): + with self._lock: + self._queue.append((f, args)) + self._event.set() + + def call_soon_threadsafe(self, f, *args): + return self.call_soon(f, *args) + + +class Result(object): + __slots__ = ['value'] + def __init__(self, value): + self.value = value + + +def async(f): + def g(self, loop, *args, **kwargs): + it = f(self, *args, **kwargs) + result = Future(loop) + if it is None: + result.set_result(None) + return result + def callback(fut): + try: + if fut is None: + x = next(it) + else: + exc_info = fut.exc_info() + if exc_info: + x = it.throw(exc_info[0], exc_info[1], exc_info[2]) + else: + x = it.send(fut.result()) + except StopIteration: + result.set_result(None) + except BaseException as ex: + result.set_exc_info(sys.exc_info()) + else: + if isinstance(x, Result): + result.set_result(x.value) + else: + x.add_done_callback(callback) + callback(None) + return result + return g diff --git a/ptvsd/ipcjson.py b/ptvsd/ipcjson.py new file mode 100644 index 00000000..b42b0867 --- /dev/null +++ b/ptvsd/ipcjson.py @@ -0,0 +1,294 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) +# attach scenario, it is loaded on the injected debugger attach thread, and if threading module +# hasn't been loaded already, it will assume that the thread on which it is being loaded is the +# main thread. This will cause issues when the thread goes away after attach completes. + +import json +import os.path +import itertools +import socket +import sys +import traceback + +_TRACE = None + +if sys.version_info[0] >= 3: + from encodings import ascii + def to_bytes(cmd_str): + return ascii.Codec.encode(cmd_str)[0] +else: + def to_bytes(cmd_str): + return cmd_str + +def _str_or_call(m): + try: + callable = m.__call__ + except AttributeError: + return str(m) + else: + return str(callable()) + +def _trace(*msg): + if _TRACE: + _TRACE(''.join(_str_or_call(m) for m in msg) + '\n') + + +SKIP_TB_PREFIXES = [ + os.path.normcase(os.path.dirname(os.path.abspath(__file__))) +] + +class InvalidHeaderError(Exception): pass + +class InvalidContentError(Exception): pass + +class SocketIO(object): + def __init__(self, *args, **kwargs): + super(SocketIO, self).__init__(*args, **kwargs) + self.__buffer = to_bytes('') + self.__port = kwargs.get('port') + self.__socket = kwargs.get('socket') + self.__own_socket = kwargs.get('own_socket', True) + self.__logfile = kwargs.get('logfile') + if self.__socket is None and self.__port is None: + raise ValueError("A 'port' or a 'socket' must be passed to SocketIO initializer as a keyword argument.") + if self.__socket is None: + self.__socket = socket.create_connection(('127.0.0.1', self.__port)) + + def _send(self, **payload): + content = json.dumps(payload).encode('utf-8') + headers = ('Content-Length: %d\r\n\r\n' % (len(content), )).encode('ascii') + if self.__logfile is not None: + self.__logfile.write(content) + self.__logfile.write('\n'.encode('utf-8')) + self.__logfile.flush() + self.__socket.send(headers) + self.__socket.send(content) + + def _buffered_read_line_as_ascii(self): + ''' + Reads bytes until it encounters newline chars, and returns the bytes + ascii decoded, newline chars are excluded from the return value. + Blocks until: newline chars are read OR socket is closed. + ''' + newline = '\r\n'.encode('ascii') + while newline not in self.__buffer: + temp = self.__socket.recv(1024) + if not temp: + break + self.__buffer += temp + + if not self.__buffer: + return None + + try: + index = self.__buffer.index(newline) + except ValueError: + raise InvalidHeaderError('Header line not terminated') + + line = self.__buffer[:index] + self.__buffer = self.__buffer[index+len(newline):] + return line.decode('ascii', 'replace') + + def _buffered_read_as_utf8(self, length): + while len(self.__buffer) < length: + temp = self.__socket.recv(1024) + if not temp: + break + self.__buffer += temp + + if len(self.__buffer) < length: + raise InvalidContentError('Expected to read {0} bytes of content, but only read {1} bytes.'.format(length, len(self.__buffer))) + + content = self.__buffer[:length] + self.__buffer = self.__buffer[length:] + return content.decode('utf-8', 'replace') + + def _wait_for_message(self): + # base protocol defined at https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#base-protocol + # read all headers, ascii encoded separated by '\r\n' + # end of headers is indicated by an empty line + headers = {} + line = self._buffered_read_line_as_ascii() + while line: + parts = line.split(':') + if len(parts) == 2: + headers[parts[0]] = parts[1] + else: + raise InvalidHeaderError("Malformed header, expected 'name: value'\n{0}".format(line)) + line = self._buffered_read_line_as_ascii() + + # end of stream + if not line and not headers: + return + + # validate headers + try: + length_text = headers['Content-Length'] + try: + length = int(length_text) + except ValueError: + raise InvalidHeaderError("Invalid Content-Length: {0}".format(length_text)) + except NameError: + raise InvalidHeaderError('Content-Length not specified in headers') + except KeyError: + raise InvalidHeaderError('Content-Length not specified in headers') + + if length < 0 or length > 2147483647: + raise InvalidHeaderError("Invalid Content-Length: {0}".format(length)) + + # read content, utf-8 encoded + content = self._buffered_read_as_utf8(length) + try: + msg = json.loads(content) + self._receive_message(msg) + except ValueError: + raise InvalidContentError('Error deserializing message content.') + except json.decoder.JSONDecodeError: + raise InvalidContentError('Error deserializing message content.') + + def _close(self): + if self.__own_socket: + self.__socket.close() + +''' +class StandardIO(object): + def __init__(self, stdin, stdout, *args, **kwargs): + super(StandardIO, self).__init__(*args, **kwargs) + try: + self.__stdin = stdin.buffer + self.__stdout = stdout.buffer + except AttributeError: + self.__stdin = stdin + self.__stdout = stdout + + def _send(self, **payload): + data = json.dumps(payload).encode('utf-8') + NEWLINE_BYTES + self.__stdout.write(data) + self.__stdout.flush() + + def _wait_for_message(self): + msg = json.loads(self.__stdin.readline().decode('utf-8', 'replace').rstrip()) + self._receive_message(msg) + + def _close(self): + pass +''' + +class IpcChannel(object): + def __init__(self, *args, **kwargs): + # This class is meant to be last in the list of base classes + # Don't call super because object's __init__ doesn't take arguments + try: + import thread + except: + import _thread as thread + self.__seq = itertools.count() + self.__exit = False + self.__lock = thread.allocate_lock() + self.__message = [] + self.__exit_on_unknown_command = True + + def close(self): + self._close() + + def send_event(self, _name, **kwargs): + with self.__lock: + self._send( + type='event', + seq=next(self.__seq), + event=_name, + body=kwargs, + ) + + def send_response(self, request, success=True, message=None, **kwargs): + with self.__lock: + self._send( + type='response', + seq=next(self.__seq), + request_seq=int(request.get('seq', 0)), + success=success, + command=request.get('command', ''), + message=message or '', + body=kwargs, + ) + + def set_exit(self): + self.__exit = True + + def process_messages(self): + while True: + if self.process_one_message(): + return + + def process_one_message(self): + try: + msg = self.__message.pop(0) + except IndexError: + self._wait_for_message() + try: + msg = self.__message.pop(0) + except IndexError: + return self.__exit + + _trace('Received ', msg) + + try: + if msg['type'] == 'request': + self.on_request(msg) + elif msg['type'] == 'response': + self.on_response(msg) + elif msg['type'] == 'event': + self.on_event(msg) + else: + self.on_invalid_request(msg, {}) + except AssertionError: + raise + except Exception: + _trace('Error ', traceback.format_exc) + traceback.print_exc() + + _trace('self.__exit is ', self.__exit) + return self.__exit + + def on_request(self, request): + assert request.get('type', '') == 'request', "Only handle 'request' messages in on_request" + + cmd = request.get('command', '') + args = request.get('arguments', {}) + target = getattr(self, 'on_' + cmd, self.on_invalid_request) + try: + _trace('Calling ', repr(target)) + target(request, args) + except AssertionError: + raise + except Exception: + self.send_response( + request, + success=False, + message=traceback.format_exc(), + ) + + def on_response(self, msg): + # this class is only used for server side only for now + raise NotImplementedError + + def on_event(self, msg): + # this class is only used for server side only for now + raise NotImplementedError + + def on_invalid_request(self, request, args): + self.send_response(request, success=False, message='Unknown command') + if self.__exit_on_unknown_command: + self.__exit = True + + def _receive_message(self, message): + with self.__lock: + self.__message.append(message) diff --git a/ptvsd/reraise.py b/ptvsd/reraise.py new file mode 100644 index 00000000..acce8865 --- /dev/null +++ b/ptvsd/reraise.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +import sys + +if sys.version_info >= (3,): + from ptvsd.reraise3 import reraise +else: + from ptvsd.reraise2 import reraise diff --git a/ptvsd/reraise2.py b/ptvsd/reraise2.py new file mode 100644 index 00000000..d0dfb7cc --- /dev/null +++ b/ptvsd/reraise2.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +def reraise(exc_info): + raise exc_info[0], exc_info[1], exc_info[2] diff --git a/ptvsd/reraise3.py b/ptvsd/reraise3.py new file mode 100644 index 00000000..58126c24 --- /dev/null +++ b/ptvsd/reraise3.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +def reraise(exc_info): + raise exc_info[1].with_traceback(exc_info[2]) diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py new file mode 100644 index 00000000..f5bc2cd8 --- /dev/null +++ b/ptvsd/wrapper.py @@ -0,0 +1,541 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +from __future__ import print_function, with_statement, absolute_import + +__author__ = "Microsoft Corporation " +__version__ = "4.0.0a1" + +import io +import os +import socket +import sys +import time +import threading +import traceback +import untangle + +try: + from urllib import quote_plus, unquote, unquote_plus +except: + from urllib.parse import quote_plus, unquote, unquote_plus + +import _pydevd_bundle.pydevd_comm as pydevd_comm +from _pydevd_bundle.pydevd_comm import pydevd_log + +import ptvsd.ipcjson as ipcjson +import ptvsd.futures as futures + + +#def ipcjson_trace(s): +# print(s) +#ipcjson._TRACE = ipcjson_trace + + +# Generates VSCode entity IDs, and maps them to corresponding pydevd entity IDs. +# +# For VSCode, IDs are always integers, and uniquely identify the entity among all other +# entities of the same type - e.g. all frames across all threads have unique IDs. +# +# For pydevd, IDs can be integer or strings, and are usually specific to some scope - +# for example, a frame ID is only unique within a given thread. To produce a truly unique +# ID, the IDs of all the outer scopes have to be combined into a tuple. Thus, for example, +# a pydevd frame ID is (thread_id, frame_id). +# +# Variables (evaluation results) technically don't have IDs in pydevd, as it doesn't have +# evaluation persistence. However, for a given frame, any child can be identified by the +# path one needs to walk from the root of the frame to get to that child - and that path, +# represented as a sequence of its consituent components, is used by pydevd commands to +# identify the variable. So we use the tuple representation of the same as its pydevd ID. +# For example, for something like foo[1].bar, its ID is: +# (thread_id, frame_id, 'FRAME', 'foo', 1, 'bar') +# +# For pydevd breakpoints, the ID has to be specified by the caller when creating, so we +# can just reuse the ID that was generated for VSC. However, when referencing the pydevd +# breakpoint later (e.g. to remove it), its ID must be specified together with path to +# file in which that breakpoint is set - i.e. pydevd treats those IDs as scoped to a file. +# So, even though breakpoint IDs are unique across files, use (path, bp_id) as pydevd ID. +class IDMap(object): + def __init__(self): + self._vscode_to_pydevd = {} + self._pydevd_to_vscode = {} + self._next_id = 1 + self._lock = threading.Lock() + + def pairs(self): + with self._lock: + return list(self._pydevd_to_vscode.items()) + + def add(self, pydevd_id): + with self._lock: + vscode_id = self._next_id + if callable(pydevd_id): + pydevd_id = pydevd_id(vscode_id) + self._next_id += 1 + self._vscode_to_pydevd[vscode_id] = pydevd_id + self._pydevd_to_vscode[pydevd_id] = vscode_id + return vscode_id + + def remove(self, pydevd_id=None, vscode_id=None): + with self._lock: + if pydevd_id is None: + pydevd_id = self._vscode_to_pydevd[vscode_id] + elif vscode_id is None: + vscode_id = self._pydevd_to_vscode[pydevd_id] + del self._vscode_to_pydevd[vscode_id] + del self._pydevd_to_vscode[pydevd_id] + + def to_pydevd(self, vscode_id): + return self._vscode_to_pydevd[vscode_id] + + def to_vscode(self, pydevd_id, autogen=True): + try: + return self._pydevd_to_vscode[pydevd_id] + except KeyError: + if autogen: + return self.add(pydevd_id) + else: + raise + + def pydevd_ids(self): + with self._lock: + ids = list(self._pydevd_to_vscode.keys()) + return ids + + def vscode_ids(self): + with self._lock: + ids = list(self._vscode_to_pydevd.keys()) + return ids + +class ExceptionInfo(object): + def __init__(self, name, description): + self.name = name + self.description = description + +# A dummy socket-like object that is given to pydevd in lieu of the real thing. +# It parses pydevd messages and redirects them to the provided handler callback. +# It also provides an interface to send notifications and requests to pydevd; +# for requests, the reply can be asynchronously awaited. +class PydevdSocket(object): + def __init__(self, event_handler): + #self.log = open('pydevd.log', 'w') + self.event_handler = event_handler + self.lock = threading.Lock() + self.seq = 1 + self.pipe_r, self.pipe_w = os.pipe() + self.requests = {} + + def close(self): + pass + + def shutdown(self, mode): + pass + + def recv(self, count): + data = os.read(self.pipe_r, count) + #self.log.write('>>>[' + data.decode('utf8') + ']\n\n') + #self.log.flush() + return data + + def send(self, data): + result = len(data) + data = unquote(data.decode('utf8')) + #self.log.write('<<<[' + data + ']\n\n') + #self.log.flush() + cmd_id, seq, args = data.split('\t', 2) + cmd_id = int(cmd_id) + seq = int(seq) + with self.lock: + loop, fut = self.requests.pop(seq, (None, None)) + if fut is None: + self.event_handler(cmd_id, seq, args) + else: + loop.call_soon_threadsafe(fut.set_result, (cmd_id, seq, args)) + return result + + def make_packet(self, cmd_id, args): + with self.lock: + seq = self.seq + self.seq += 1 + s = "%s\t%s\t%s\n" % (cmd_id, seq, args) + return seq, s + + def pydevd_notify(self, cmd_id, args): + seq, s = self.make_packet(cmd_id, args) + os.write(self.pipe_w, s.encode('utf8')) + + def pydevd_request(self, loop, cmd_id, args): + seq, s = self.make_packet(cmd_id, args) + fut = loop.create_future() + with self.lock: + self.requests[seq] = loop, fut + os.write(self.pipe_w, s.encode('utf8')) + return fut + +# IPC JSON message processor for VSC debugger protocol, mapping it to pydevd protocol. +class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): + def __init__(self, socket, pydevd, logfile=None): + super(VSCodeMessageProcessor, self).__init__(socket=socket, own_socket=False, logfile=logfile) + self.socket = socket + self.pydevd = pydevd + self.stack_traces = {} + self.stack_traces_lock = threading.Lock() + self.active_exceptions = {} + self.active_exceptions_lock = threading.Lock() + self.thread_map = IDMap() + self.frame_map = IDMap() + self.var_map = IDMap() + self.bp_map = IDMap() + self.next_var_ref = 0 + self.loop = futures.EventLoop() + threading.Thread(target = self.loop.run_forever, name = 'ptvsd.EventLoop').start() + + def close(self): + if self.socket: + self.socket.close() + + def pydevd_notify(self, cmd_id, args): + try: + return self.pydevd.pydevd_notify(cmd_id, args) + except: + traceback.print_exc() + raise + + def pydevd_request(self, cmd_id, args): + return self.pydevd.pydevd_request(self.loop, cmd_id, args) + + # Instances of this class provide decorators to mark methods as handlers for various + # pydevd messages - a decorated method is added to the map with the corresponding + # message ID, and is looked up there by pydevd event handler below. + class EventHandlers(dict): + def handler(self, cmd_id): + def decorate(f): + self[cmd_id] = f + return f + return decorate + + pydevd_events = EventHandlers() + + def on_pydevd_event(self, cmd_id, seq, args): + try: + f = self.pydevd_events[cmd_id] + except KeyError: + raise Exception('Unsupported pydevd command ' + str(cmd_id)) + return f(self, seq, args) + + def async_handler(m): + m = futures.async(m) + def f(self, *args, **kwargs): + fut = m(self, self.loop, *args, **kwargs) + def done(fut): + try: + fut.result() + except: + traceback.print_exc(file=sys.__stderr__) + fut.add_done_callback(done) + return f + + @async_handler + def on_initialize(self, request, args): + yield self.pydevd_request(pydevd_comm.CMD_VERSION, '1.1\tWINDOWS\tID') + self.send_response(request, + supportsExceptionInfoRequest=True, + supportsConfigurationDoneRequest=True, + exceptionBreakpointFilters=[ + {'filter': 'raised', 'label': 'Raised Exceptions'}, + {'filter': 'uncaught', 'label': 'Uncaught Exceptions'}, + ] + ) + self.send_event('initialized') + + def on_configurationDone(self, request, args): + self.send_response(request) + + def on_disconnect(self, request, args): + self.send_response(request) + + @async_handler + def on_attach(self, request, args): + self.send_response(request) + yield self.pydevd_request(pydevd_comm.CMD_RUN, '') + self.send_process_event('attach') + + @async_handler + def on_launch(self, request, args): + self.send_response(request) + yield self.pydevd_request(pydevd_comm.CMD_RUN, '') + self.send_process_event('launch') + + def send_process_event(self, start_method): + evt = { + 'name': sys.argv[0], + 'systemProcessId': os.getpid(), + 'isLocalProcess': True, + 'startMethod': start_method, + } + self.send_event('process', **evt) + + @async_handler + def on_threads(self, request, args): + _, _, args = yield self.pydevd_request(pydevd_comm.CMD_LIST_THREADS, '') + xml = untangle.parse(args).xml + try: + xthreads = xml.thread + except AttributeError: + xthreads = [] + + threads = [] + for xthread in xthreads: + tid = self.thread_map.to_vscode(xthread['id']) + try: + name = unquote(xthread['name']) + except KeyError: + name = None + if not (name and name.startswith('pydevd.')): + threads.append({'id': tid, 'name': name}) + + self.send_response(request, threads=threads) + + @async_handler + def on_stackTrace(self, request, args): + tid = int(args['threadId']) + startFrame = int(args['startFrame']) + levels = int(args['levels']) + + tid = self.thread_map.to_pydevd(tid) + with self.stack_traces_lock: + xframes = self.stack_traces[tid] + totalFrames = len(xframes) + + if levels == 0: + levels = totalFrames + + stackFrames = [] + for xframe in xframes: + if startFrame > 0: + startFrame -= 1 + continue + if levels <= 0: + break + levels -= 1 + fid = self.frame_map.to_vscode((tid, int(xframe['id']))) + name = unquote(xframe['name']) + file = unquote(xframe['file']) + line = int(xframe['line']) + stackFrames.append({'id': fid, 'name': name, 'source': {'path': file}, 'line': line, 'column': 0}) + + self.send_response(request, stackFrames=stackFrames, totalFrames=totalFrames) + + @async_handler + def on_scopes(self, request, args): + vsc_fid = int(args['frameId']) + pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid) + pyd_var = (pyd_tid, pyd_fid, 'FRAME') + vsc_var = self.var_map.to_vscode(pyd_var) + scope = {'name': 'Locals', 'expensive': False, 'variablesReference': vsc_var} + self.send_response(request, scopes=[scope]) + + @async_handler + def on_variables(self, request, args): + vsc_var = int(args['variablesReference']) + pyd_var = self.var_map.to_pydevd(vsc_var) + + if len(pyd_var) == 3: + cmd = pydevd_comm.CMD_GET_FRAME + else: + cmd = pydevd_comm.CMD_GET_VARIABLE + + _, _, args = yield self.pydevd_request(cmd, '\t'.join(str(s) for s in pyd_var)) + xml = untangle.parse(args).xml + try: + xvars = xml.var + except AttributeError: + xvars = [] + + vars = [] + for xvar in xvars: + var = { + 'name': unquote(xvar['name']), + 'type': unquote(xvar['type']), + 'value': unquote(xvar['value']), + } + if bool(xvar['isContainer']): + pyd_child = pyd_var + (var['name'],) + var['variablesReference'] = self.var_map.to_vscode(pyd_child) + vars.append(var) + + self.send_response(request, variables=vars) + + @async_handler + def on_pause(self, request, args): + vsc_tid = int(args['threadId']) + if vsc_tid == 0: # VS does this to mean "stop all threads": + for pyd_tid in self.thread_map.pydevd_ids(): + self.pydevd_notify(pydevd_comm.CMD_THREAD_SUSPEND, pyd_tid) + else: + pyd_tid = self.thread_map.to_pydevd(vsc_tid) + self.pydevd_notify(pydevd_comm.CMD_THREAD_SUSPEND, pyd_tid) + self.send_response(request) + + @async_handler + def on_continue(self, request, args): + tid = self.thread_map.to_pydevd(int(args['threadId'])) + self.pydevd_notify(pydevd_comm.CMD_THREAD_RUN, tid) + self.send_response(request) + + @async_handler + def on_next(self, request, args): + tid = self.thread_map.to_pydevd(int(args['threadId'])) + self.pydevd_notify(pydevd_comm.CMD_STEP_OVER, tid) + self.send_response(request) + + @async_handler + def on_stepIn(self, request, args): + tid = self.thread_map.to_pydevd(int(args['threadId'])) + self.pydevd_notify(pydevd_comm.CMD_STEP_INTO, tid) + self.send_response(request) + + @async_handler + def on_stepOut(self, request, args): + tid = self.thread_map.to_pydevd(int(args['threadId'])) + self.pydevd_notify(pydevd_comm.CMD_STEP_RETURN, tid) + self.send_response(request) + + @async_handler + def on_setBreakpoints(self, request, args): + bps = [] + path = args['source']['path'] + src_bps = args.get('breakpoints', []) + + # First, we must delete all existing breakpoints in that source. + for pyd_bpid, vsc_bpid in self.bp_map.pairs(): + self.pydevd_notify(pydevd_comm.CMD_REMOVE_BREAK, 'python-line\t%s\t%s' % (path, vsc_bpid)) + self.bp_map.remove(pyd_bpid, vsc_bpid) + + for src_bp in src_bps: + line = src_bp['line'] + vsc_bpid = self.bp_map.add(lambda vsc_bpid: (path, vsc_bpid)) + self.pydevd_notify(pydevd_comm.CMD_SET_BREAK, '%s\tpython-line\t%s\t%s\tNone\tNone\tNone' % + (vsc_bpid, path, line)) + bps.append({ 'id': vsc_bpid, 'verified': True, 'line': line }) + + self.send_response(request, breakpoints=bps) + + @async_handler + def on_setExceptionBreakpoints(self, request, args): + self.pydevd_notify(pydevd_comm.CMD_REMOVE_EXCEPTION_BREAK, 'python-BaseException') + filters = args['filters'] + break_raised = 'raised' in filters + break_uncaught = 'uncaught' in filters + if break_raised or break_uncaught: + self.pydevd_notify(pydevd_comm.CMD_ADD_EXCEPTION_BREAK, 'python-BaseException\t%s\t%s\t%s' % ( + 2 if break_raised else 0, 1 if break_uncaught else 0, 0)) + self.send_response(request) + + @async_handler + def on_exceptionInfo(self, request, args): + tid = self.thread_map.to_pydevd(args['threadId']) + with self.active_exceptions_lock: + exc = self.active_exceptions[tid] + self.send_response(request, exceptionId=exc.name, description=exc.description, breakMode='unhandled', details={ + 'typeName': exc.name, + 'message': exc.description, + }) + + @pydevd_events.handler(pydevd_comm.CMD_THREAD_CREATE) + def on_pydevd_thread_create(self, seq, args): + xml = untangle.parse(args).xml + tid = self.thread_map.to_vscode(xml.thread['id']) + self.send_event('thread', reason='started', threadId=tid) + + @pydevd_events.handler(pydevd_comm.CMD_THREAD_KILL) + def on_pydevd_thread_kill(self, seq, args): + try: + tid = self.thread_map.to_vscode(args, autogen=False) + except KeyError: + pass + else: + self.send_event('thread', reason='exited', threadId=tid) + + @pydevd_events.handler(pydevd_comm.CMD_THREAD_SUSPEND) + def on_pydevd_thread_suspend(self, seq, args): + xml = untangle.parse(args).xml + tid = xml.thread['id'] + reason = int(xml.thread['stop_reason']) + print(reason, file=open('reason.log', 'a')) + if reason in (pydevd_comm.CMD_STEP_INTO, pydevd_comm.CMD_STEP_OVER, pydevd_comm.CMD_STEP_RETURN): + reason = 'step' + elif reason == pydevd_comm.CMD_STEP_CAUGHT_EXCEPTION: + reason = 'exception' + elif reason == pydevd_comm.CMD_SET_BREAK: + reason = 'breakpoint' + else: + reason = 'pause' + with self.stack_traces_lock: + self.stack_traces[tid] = xml.thread.frame + tid = self.thread_map.to_vscode(tid) + self.send_event('stopped', reason=reason, threadId=tid) + + @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN) + def on_pydevd_thread_run(self, seq, args): + pyd_tid, reason = args.split('\t') + vsc_tid = self.thread_map.to_vscode(pyd_tid) + + # Stack trace, and all frames and variables for this thread are now invalid; clear their IDs. + with self.stack_traces_lock: + del self.stack_traces[pyd_tid] + + for pyd_fid, vsc_fid in self.frame_map.pairs(): + if pyd_fid[0] == pyd_tid: + self.frame_map.remove(pyd_fid, vsc_fid) + + for pyd_var, vsc_var in self.var_map.pairs(): + if pyd_var[0] == pyd_tid: + self.var_map.remove(pyd_var, vsc_var) + + self.send_event('continued', threadId=vsc_tid) + + @pydevd_events.handler(pydevd_comm.CMD_SEND_CURR_EXCEPTION_TRACE) + def on_pydevd_send_curr_exception_trace(self, seq, args): + _, name, description, xml = args.split('\t') + xml = untangle.parse(xml).xml + pyd_tid = xml.thread['id'] + with self.active_exceptions_lock: + self.active_exceptions[pyd_tid] = ExceptionInfo(name, description) + + @pydevd_events.handler(pydevd_comm.CMD_SEND_CURR_EXCEPTION_TRACE_PROCEEDED) + def on_pydevd_send_curr_exception_trace_proceeded(self, seq, args): + pass + + +def start_server(port): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(('127.0.0.1', port)) + server.listen(1) + client, addr = server.accept() + + pydevd = PydevdSocket(lambda *args: proc.on_pydevd_event(*args)) + proc = VSCodeMessageProcessor(client, pydevd) + + server_thread = threading.Thread(target = proc.process_messages, name = 'ptvsd.Server') + server_thread.start() + + return pydevd + +def start_client(host, port): + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + client.connect((host, port)) + + pydevd = PydevdSocket(lambda *args: proc.on_pydevd_event(*args)) + proc = VSCodeMessageProcessor(client, pydevd) + + server_thread = threading.Thread(target = proc.process_messages, name = 'ptvsd.Client') + server_thread.start() + + return pydevd + +# These are the functions pydevd invokes to get a socket to the client. +pydevd_comm.start_server = start_server +pydevd_comm.start_client = start_client diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..5e409001 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..f086441e --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +import sys +from setuptools import setup + +setup(name='ptvsd', + version='4.0.0a1', + description='Visual Studio remote debugging server for Python', + license='MIT', + author='Microsoft Corporation', + author_email='ptvshelp@microsoft.com', + url='https://aka.ms/ptvs', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License'], + packages=['ptvsd'], + install_requires=['untangle', 'pydevd>=1.1.1'] + )