| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- # Copyright (c) Microsoft Corporation. All rights reserved.
- # Licensed under the MIT License. See LICENSE in the project root
- # for license information.
- 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
- # Do this last to avoid triggering useless notifications for assignments above.
- 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)}",
- )
|