Spaces:
Paused
Paused
| # 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}]" | |
| def client(self): | |
| return self.session.client | |
| def launcher(self): | |
| return self.session.launcher | |
| def server(self): | |
| return self.session.server | |
| def wait_for(self, *args, **kwargs): | |
| return self.session.wait_for(*args, **kwargs) | |
| 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(). | |
| """ | |
| 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)}", | |
| ) | |