|
|
|
|
|
|
|
|
|
import functools |
|
|
|
from debugpy.common import json, log, messaging, util |
|
|
|
|
|
ACCEPT_CONNECTIONS_TIMEOUT = 60 |
|
|
|
|
|
class ComponentNotAvailable(Exception): |
|
def __init__(self, type): |
|
super().__init__(f"{type.__name__} is not available") |
|
|
|
|
|
class Component(util.Observable): |
|
"""A component managed by a debug adapter: client, launcher, or debug server. |
|
|
|
Every component belongs to a Session, which is used for synchronization and |
|
shared data. |
|
|
|
Every component has its own message channel, and provides message handlers for |
|
that channel. All handlers should be decorated with @Component.message_handler, |
|
which ensures that Session is locked for the duration of the handler. Thus, only |
|
one handler is running at any given time across all components, unless the lock |
|
is released explicitly or via Session.wait_for(). |
|
|
|
Components report changes to their attributes to Session, allowing one component |
|
to wait_for() a change caused by another component. |
|
""" |
|
|
|
def __init__(self, session, stream=None, channel=None): |
|
assert (stream is None) ^ (channel is None) |
|
|
|
try: |
|
lock_held = session.lock.acquire(blocking=False) |
|
assert lock_held, "__init__ of a Component subclass must lock its Session" |
|
finally: |
|
session.lock.release() |
|
|
|
super().__init__() |
|
|
|
self.session = session |
|
|
|
if channel is None: |
|
stream.name = str(self) |
|
channel = messaging.JsonMessageChannel(stream, self) |
|
channel.start() |
|
else: |
|
channel.name = channel.stream.name = str(self) |
|
channel.handlers = self |
|
self.channel = channel |
|
self.is_connected = True |
|
|
|
|
|
self.observers += [lambda *_: self.session.notify_changed()] |
|
|
|
def __str__(self): |
|
return f"{type(self).__name__}[{self.session.id}]" |
|
|
|
@property |
|
def client(self): |
|
return self.session.client |
|
|
|
@property |
|
def launcher(self): |
|
return self.session.launcher |
|
|
|
@property |
|
def server(self): |
|
return self.session.server |
|
|
|
def wait_for(self, *args, **kwargs): |
|
return self.session.wait_for(*args, **kwargs) |
|
|
|
@staticmethod |
|
def message_handler(f): |
|
"""Applied to a message handler to automatically lock and unlock the session |
|
for its duration, and to validate the session state. |
|
|
|
If the handler raises ComponentNotAvailable or JsonIOError, converts it to |
|
Message.cant_handle(). |
|
""" |
|
|
|
@functools.wraps(f) |
|
def lock_and_handle(self, message): |
|
try: |
|
with self.session: |
|
return f(self, message) |
|
except ComponentNotAvailable as exc: |
|
raise message.cant_handle("{0}", exc, silent=True) |
|
except messaging.MessageHandlingError as exc: |
|
if exc.cause is message: |
|
raise |
|
else: |
|
exc.propagate(message) |
|
except messaging.JsonIOError as exc: |
|
raise message.cant_handle( |
|
"{0} disconnected unexpectedly", exc.stream.name, silent=True |
|
) |
|
|
|
return lock_and_handle |
|
|
|
def disconnect(self): |
|
with self.session: |
|
self.is_connected = False |
|
self.session.finalize("{0} has disconnected".format(self)) |
|
|
|
|
|
def missing(session, type): |
|
class Missing(object): |
|
"""A dummy component that raises ComponentNotAvailable whenever some |
|
attribute is accessed on it. |
|
""" |
|
|
|
__getattr__ = __setattr__ = lambda self, *_: report() |
|
__bool__ = __nonzero__ = lambda self: False |
|
|
|
def report(): |
|
try: |
|
raise ComponentNotAvailable(type) |
|
except Exception as exc: |
|
log.reraise_exception("{0} in {1}", exc, session) |
|
|
|
return Missing() |
|
|
|
|
|
class Capabilities(dict): |
|
"""A collection of feature flags for a component. Corresponds to JSON properties |
|
in the DAP "initialize" request or response, other than those that identify the |
|
party. |
|
""" |
|
|
|
PROPERTIES = {} |
|
"""JSON property names and default values for the the capabilities represented |
|
by instances of this class. Keys are names, and values are either default values |
|
or validators. |
|
|
|
If the value is callable, it must be a JSON validator; see debugpy.common.json for |
|
details. If the value is not callable, it is as if json.default(value) validator |
|
was used instead. |
|
""" |
|
|
|
def __init__(self, component, message): |
|
"""Parses an "initialize" request or response and extracts the feature flags. |
|
|
|
For every "X" in self.PROPERTIES, sets self["X"] to the corresponding value |
|
from message.payload if it's present there, or to the default value otherwise. |
|
""" |
|
|
|
assert message.is_request("initialize") or message.is_response("initialize") |
|
|
|
self.component = component |
|
|
|
payload = message.payload |
|
for name, validate in self.PROPERTIES.items(): |
|
value = payload.get(name, ()) |
|
if not callable(validate): |
|
validate = json.default(validate) |
|
|
|
try: |
|
value = validate(value) |
|
except Exception as exc: |
|
raise message.isnt_valid("{0} {1}", json.repr(name), exc) |
|
|
|
assert ( |
|
value != () |
|
), f"{validate} must provide a default value for missing properties." |
|
self[name] = value |
|
|
|
log.debug("{0}", self) |
|
|
|
def __repr__(self): |
|
return f"{type(self).__name__}: {json.repr(dict(self))}" |
|
|
|
def require(self, *keys): |
|
for key in keys: |
|
if not self[key]: |
|
raise messaging.MessageHandlingError( |
|
f"{self.component} does not have capability {json.repr(key)}", |
|
) |
|
|