mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Initial ptvsd commit
This commit is contained in:
parent
9c7077b6fe
commit
3543b25329
12 changed files with 1062 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
5
ptvsd/__init__.py
Normal file
5
ptvsd/__init__.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__version__ = "4.0.0a1"
|
||||
10
ptvsd/__main__.py
Normal file
10
ptvsd/__main__.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__version__ = "4.0.0a1"
|
||||
|
||||
if __name__ == '__main__':
|
||||
import ptvsd.wrapper
|
||||
import pydevd
|
||||
pydevd.main()
|
||||
14
ptvsd/debugger.py
Normal file
14
ptvsd/debugger.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__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()
|
||||
136
ptvsd/futures.py
Normal file
136
ptvsd/futures.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__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
|
||||
294
ptvsd/ipcjson.py
Normal file
294
ptvsd/ipcjson.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__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)
|
||||
14
ptvsd/reraise.py
Normal file
14
ptvsd/reraise.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__version__ = "4.0.0a1"
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
from ptvsd.reraise3 import reraise
|
||||
else:
|
||||
from ptvsd.reraise2 import reraise
|
||||
10
ptvsd/reraise2.py
Normal file
10
ptvsd/reraise2.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__version__ = "4.0.0a1"
|
||||
|
||||
def reraise(exc_info):
|
||||
raise exc_info[0], exc_info[1], exc_info[2]
|
||||
10
ptvsd/reraise3.py
Normal file
10
ptvsd/reraise3.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__version__ = "4.0.0a1"
|
||||
|
||||
def reraise(exc_info):
|
||||
raise exc_info[1].with_traceback(exc_info[2])
|
||||
541
ptvsd/wrapper.py
Normal file
541
ptvsd/wrapper.py
Normal file
|
|
@ -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 <ptvshelp@microsoft.com>"
|
||||
__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
|
||||
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[wheel]
|
||||
universal = 1
|
||||
24
setup.py
Normal file
24
setup.py
Normal file
|
|
@ -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']
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue