mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Add DebugClient.host_local_debugger().
This commit is contained in:
parent
278640d0ec
commit
6cc5cca1fb
3 changed files with 196 additions and 47 deletions
|
|
@ -1,49 +1,56 @@
|
|||
import threading
|
||||
import warnings
|
||||
|
||||
from . import Closeable
|
||||
from .debugadapter import DebugAdapter
|
||||
from .debugsession import DebugSession
|
||||
|
||||
|
||||
class DebugClient(Closeable):
|
||||
"""A high-level abstraction of a debug client (i.e. editor)."""
|
||||
# TODO: Add a helper function to start a remote debugger for testing
|
||||
# remote debugging?
|
||||
|
||||
def __init__(self, port=8888):
|
||||
super(DebugClient, self).__init__()
|
||||
|
||||
class _LifecycleClient(Closeable):
|
||||
|
||||
def __init__(self, port=8888, breakpoints=None):
|
||||
super(_LifecycleClient, self).__init__()
|
||||
self._port = port
|
||||
self._adapter = None
|
||||
self._session = None
|
||||
|
||||
# TODO: Support starting a remote debugger for testing
|
||||
# remote debugging?
|
||||
self._breakpoints = breakpoints
|
||||
|
||||
def start_debugger(self, argv):
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
self._adapter = DebugAdapter.start(argv, port=self._port)
|
||||
@property
|
||||
def adapter(self):
|
||||
return self._adapter
|
||||
|
||||
def launch_script(self, filename, *argv, **kwargs):
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
def start_debugging(self, launchcfg):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
|
||||
argv = [
|
||||
filename,
|
||||
] + list(argv)
|
||||
self._launch(argv, kwargs)
|
||||
return self._adapter, self._session
|
||||
raise NotImplementedError
|
||||
|
||||
def launch_module(self, module, *argv, **kwargs):
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
def stop_debugging(self):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is None:
|
||||
raise RuntimeError('debugger not running')
|
||||
|
||||
argv = [
|
||||
'-m', module,
|
||||
] + list(argv)
|
||||
self._launch(argv, kwargs)
|
||||
return self._adapter, self._session
|
||||
if self._session is not None:
|
||||
self._detach()
|
||||
self._adapter.close()
|
||||
self._adapter = None
|
||||
|
||||
def attach(self, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is None:
|
||||
raise RuntimeError('debugger not running')
|
||||
if self._session is not None:
|
||||
|
|
@ -53,27 +60,118 @@ class DebugClient(Closeable):
|
|||
return self._session
|
||||
|
||||
def detach(self):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._session is None:
|
||||
raise RuntimeError('not attached')
|
||||
assert self._adapter is not None
|
||||
if not self._session.is_client:
|
||||
raise RuntimeError('detach not supported')
|
||||
|
||||
self._detach()
|
||||
|
||||
# internal methods
|
||||
|
||||
def _close(self):
|
||||
if self._session is not None:
|
||||
self._session.close()
|
||||
if self._adapter is not None:
|
||||
self._adapter.close()
|
||||
|
||||
def _launch(self, argv, kwargs):
|
||||
def _launch(self, argv, script=None, wait_for_attach=None,
|
||||
detachable=True, **kwargs):
|
||||
self._adapter = DebugAdapter.start(
|
||||
argv,
|
||||
host='localhost' if detachable else None,
|
||||
port=self._port,
|
||||
script=script,
|
||||
)
|
||||
self._attach(**kwargs)
|
||||
if wait_for_attach:
|
||||
wait_for_attach()
|
||||
else:
|
||||
self._attach(**kwargs)
|
||||
|
||||
def _attach(self, **kwargs):
|
||||
addr = ('localhost', self._port)
|
||||
self._session = DebugSession.create(addr, **kwargs)
|
||||
self._session = DebugSession.create_client(addr, **kwargs)
|
||||
|
||||
def _detach(self):
|
||||
self._session.close()
|
||||
self._session = None
|
||||
|
||||
|
||||
class DebugClient(_LifecycleClient):
|
||||
"""A high-level abstraction of a debug client (i.e. editor)."""
|
||||
|
||||
# TODO: Manage breakpoints, etc.
|
||||
|
||||
|
||||
class EasyDebugClient(DebugClient):
|
||||
|
||||
def start_detached(self, argv):
|
||||
"""Start an adapter in a background process."""
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
|
||||
# TODO: Launch, handshake and detach?
|
||||
self._adapter = DebugAdapter.start(argv, port=self._port)
|
||||
return self._adapter
|
||||
|
||||
def host_local_debugger(self, argv, script=None, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
addr = ('localhost', self._port)
|
||||
|
||||
def run():
|
||||
self._session = DebugSession.create_server(addr, **kwargs)
|
||||
t = threading.Thread(target=run)
|
||||
t.start()
|
||||
|
||||
def wait():
|
||||
t.join(timeout=self._connecttimeout)
|
||||
if t.is_alive():
|
||||
warnings.warn('timed out waiting for connection')
|
||||
if self._session is None:
|
||||
raise RuntimeError('unable to connect')
|
||||
# Close the adapter when the session closes.
|
||||
self._session.manage_adapter(self._adapter)
|
||||
self._launch(
|
||||
argv,
|
||||
script=script,
|
||||
wait_for_attach=wait,
|
||||
detachable=False,
|
||||
)
|
||||
|
||||
return self._adapter, self._session
|
||||
|
||||
def launch_script(self, filename, *argv, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
|
||||
argv = [
|
||||
filename,
|
||||
] + list(argv)
|
||||
self._launch(argv, **kwargs)
|
||||
return self._adapter, self._session
|
||||
|
||||
def launch_module(self, module, *argv, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('debug client closed')
|
||||
if self._adapter is not None:
|
||||
raise RuntimeError('debugger already running')
|
||||
assert self._session is None
|
||||
|
||||
argv = [
|
||||
'-m', module,
|
||||
] + list(argv)
|
||||
self._launch(argv, **kwargs)
|
||||
return self._adapter, self._session
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ from .message import (
|
|||
raw_read_all as read_messages,
|
||||
raw_write_one as write_message
|
||||
)
|
||||
from .socket import create_client, close, recv_as_read, send_as_write
|
||||
from .socket import (
|
||||
Connection, create_server, create_client, close,
|
||||
recv_as_read, send_as_write,
|
||||
timeout as socket_timeout)
|
||||
from .threading import get_locked_and_waiter
|
||||
from .vsc import parse_message
|
||||
|
||||
|
|
@ -25,22 +28,38 @@ class DebugSessionConnection(Closeable):
|
|||
TIMEOUT = 1.0
|
||||
|
||||
@classmethod
|
||||
def create(cls, addr, timeout=TIMEOUT):
|
||||
def create_client(cls, addr, **kwargs):
|
||||
def connect(addr, timeout):
|
||||
sock = create_client()
|
||||
for _ in range(int(timeout * 10)):
|
||||
try:
|
||||
sock.connect(addr)
|
||||
except OSError:
|
||||
if cls.VERBOSE:
|
||||
print('+', end='')
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('could not connect')
|
||||
return sock
|
||||
return cls._create(connect, addr, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create_server(cls, addr, **kwargs):
|
||||
def connect(addr, timeout):
|
||||
server = create_server(addr)
|
||||
with socket_timeout(server, timeout):
|
||||
client = server.accept()
|
||||
return Connection(client, server)
|
||||
return cls._create(connect, addr, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, connect, addr, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = cls.TIMEOUT
|
||||
sock = create_client()
|
||||
for _ in range(int(timeout * 10)):
|
||||
try:
|
||||
sock.connect(addr)
|
||||
except OSError:
|
||||
if cls.VERBOSE:
|
||||
print('+', end='')
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('could not connect')
|
||||
sock = connect(addr, timeout)
|
||||
if cls.VERBOSE:
|
||||
print('connected')
|
||||
self = cls(sock, ownsock=True)
|
||||
|
|
@ -52,7 +71,14 @@ class DebugSessionConnection(Closeable):
|
|||
self._sock = sock
|
||||
self._ownsock = ownsock
|
||||
|
||||
@property
|
||||
def is_client(self):
|
||||
return self._server is None
|
||||
|
||||
def iter_messages(self):
|
||||
if self.closed:
|
||||
raise RuntimeError('connection closed')
|
||||
|
||||
def stop():
|
||||
return self.closed
|
||||
read = recv_as_read(self._sock)
|
||||
|
|
@ -62,6 +88,9 @@ class DebugSessionConnection(Closeable):
|
|||
yield parse_message(msg)
|
||||
|
||||
def send(self, req):
|
||||
if self.closed:
|
||||
raise RuntimeError('connection closed')
|
||||
|
||||
def stop():
|
||||
return self.closed
|
||||
write = send_as_write(self._sock)
|
||||
|
|
@ -84,10 +113,17 @@ class DebugSession(Closeable):
|
|||
PORT = 8888
|
||||
|
||||
@classmethod
|
||||
def create(cls, addr=None, **kwargs):
|
||||
def create_client(cls, addr=None, **kwargs):
|
||||
if addr is None:
|
||||
addr = (cls.HOST, cls.PORT)
|
||||
conn = DebugSessionConnection.create(addr)
|
||||
conn = DebugSessionConnection.create_client(addr)
|
||||
return cls(conn, owned=True, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create_server(cls, addr=None, **kwargs):
|
||||
if addr is None:
|
||||
addr = (cls.HOST, cls.PORT)
|
||||
conn = DebugSessionConnection.create_server(addr)
|
||||
return cls(conn, owned=True, **kwargs)
|
||||
|
||||
def __init__(self, conn, seq=1000, handlers=(), timeout=None, owned=False):
|
||||
|
|
@ -107,11 +143,18 @@ class DebugSession(Closeable):
|
|||
self._listenerthread = threading.Thread(target=self._listen)
|
||||
self._listenerthread.start()
|
||||
|
||||
@property
|
||||
def is_client(self):
|
||||
return self._conn.is_client
|
||||
|
||||
@property
|
||||
def received(self):
|
||||
return list(self._received)
|
||||
|
||||
def send_request(self, command, **args):
|
||||
if self.closed:
|
||||
raise RuntimeError('session closed')
|
||||
|
||||
wait = args.pop('wait', True)
|
||||
seq = self._seq
|
||||
self._seq += 1
|
||||
|
|
@ -129,10 +172,16 @@ class DebugSession(Closeable):
|
|||
return req
|
||||
|
||||
def add_handler(self, handler, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('session closed')
|
||||
|
||||
self._add_handler(handler, **kwargs)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def wait_for_event(self, event, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('session closed')
|
||||
|
||||
def match(msg):
|
||||
return msg.type == 'event' and msg.event == event
|
||||
handlername = 'event {!r}'.format(event)
|
||||
|
|
@ -141,6 +190,9 @@ class DebugSession(Closeable):
|
|||
|
||||
@contextlib.contextmanager
|
||||
def wait_for_response(self, req, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError('session closed')
|
||||
|
||||
try:
|
||||
command, seq = req.command, req.seq
|
||||
except AttributeError:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import unittest
|
||||
|
||||
from tests.helpers.debugadapter import DebugAdapter
|
||||
from tests.helpers.debugclient import DebugClient
|
||||
from tests.helpers.debugclient import EasyDebugClient as DebugClient
|
||||
from tests.helpers.threading import get_locked_and_waiter
|
||||
from tests.helpers.vsc import parse_message
|
||||
from tests.helpers.workspace import Workspace, PathEntry
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue