Add DebugClient.host_local_debugger().

This commit is contained in:
Eric Snow 2018-04-10 22:29:44 +00:00
parent 278640d0ec
commit 6cc5cca1fb
3 changed files with 196 additions and 47 deletions

View file

@ -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

View file

@ -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:

View file

@ -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