| // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:dap/dap.dart'; |
| import 'package:dds_service_extensions/dds_service_extensions.dart'; |
| import 'package:json_rpc_2/error_code.dart' as json_rpc_errors; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:vm_service/vm_service.dart' as vm; |
| |
| import '../../../dds.dart'; |
| import '../../rpc_error_codes.dart'; |
| import '../base_debug_adapter.dart'; |
| import '../isolate_manager.dart'; |
| import '../logging.dart'; |
| import '../progress_reporter.dart'; |
| import '../protocol_converter.dart'; |
| import '../protocol_stream.dart'; |
| import '../utils.dart'; |
| import '../variables.dart'; |
| import 'mixins.dart'; |
| |
| /// The mime type to send with source responses to the client. |
| /// |
| /// This is used so if the source name does not end with ".dart" the client can |
| /// still tell which language to use (for syntax highlighting, etc.). |
| /// |
| /// https://github.com/microsoft/vscode/issues/8182#issuecomment-231151640 |
| const dartMimeType = 'text/x-dart'; |
| |
| /// Maximum number of toString()s to be called when responding to variables |
| /// requests from the client. |
| /// |
| /// Setting this too high can have a performance impact, for example if the |
| /// client requests 500 items in a variablesRequest for a list. |
| const maxToStringsPerEvaluation = 100; |
| |
| /// An expression that evaluates to the exception for the current thread. |
| /// |
| /// In order to support some functionality like "Copy Value" in VS Code's |
| /// Scopes/Variables window, each variable must have a valid "evaluateName" (an |
| /// expression that evaluates to it). Since we show exceptions in there we use |
| /// this magic value as an expression that maps to it. |
| /// |
| /// This is not intended to be used by the user directly, although if they |
| /// evaluate it as an expression and the current thread has an exception, it |
| /// will work. |
| const threadExceptionExpression = r'$_threadException'; |
| |
| /// Typedef for handlers of VM Service stream events. |
| typedef _StreamEventHandler<T> = FutureOr<void> Function(T data); |
| |
| /// A null result passed to `sendResponse` functions when there is no result. |
| /// |
| /// Because the signature of `sendResponse` is generic, an argument must be |
| /// provided even when the generic type is `void`. This value is used to make |
| /// it clearer in calling code that the result is unused. |
| const _noResult = null; |
| |
| /// Pattern for extracting useful error messages from an evaluation exception. |
| final _evalErrorMessagePattern = RegExp('Error: (.*)'); |
| |
| /// Pattern for extracting useful error messages from an unhandled exception. |
| final _exceptionMessagePattern = RegExp('Unhandled exception:\n(.*)'); |
| |
| /// Pattern for a trailing semicolon. |
| final _trailingSemicolonPattern = RegExp(r';$'); |
| |
| /// An implementation of [AttachRequestArguments] that includes all fields used |
| /// by the Dart CLI and test debug adapters. |
| /// |
| /// This class represents the data passed from the client editor to the debug |
| /// adapter in attachRequest, which is a request to start debugging an |
| /// application. |
| /// |
| /// Specialized adapters (such as Flutter) have their own versions of this |
| /// class. |
| class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments |
| implements AttachRequestArguments { |
| /// The VM Service URI to attach to. |
| /// |
| /// Either this or [vmServiceInfoFile] must be supplied. |
| final String? vmServiceUri; |
| |
| /// The VM Service info file to extract the VM Service URI from to attach to. |
| /// |
| /// Either this or [vmServiceUri] must be supplied. |
| final String? vmServiceInfoFile; |
| |
| /// A reader for protocol arguments that throws detailed exceptions if |
| /// arguments aren't of the correct type. |
| static final arg = DebugAdapterArgumentReader('attach'); |
| |
| DartAttachRequestArguments({ |
| this.vmServiceUri, |
| this.vmServiceInfoFile, |
| super.restart, |
| super.name, |
| super.cwd, |
| super.additionalProjectPaths, |
| super.debugSdkLibraries, |
| super.debugExternalPackageLibraries, |
| super.showGettersInDebugViews, |
| super.evaluateGettersInDebugViews, |
| super.evaluateToStringInDebugViews, |
| super.sendLogsToClient, |
| super.sendCustomProgressEvents = null, |
| super.allowAnsiColorOutput, |
| }) : super( |
| // env is not supported for Dart attach because we don't spawn a process. |
| env: null, |
| ); |
| |
| DartAttachRequestArguments.fromMap(super.obj) |
| : vmServiceUri = arg.read<String?>(obj, 'vmServiceUri'), |
| vmServiceInfoFile = arg.read<String?>(obj, 'vmServiceInfoFile'), |
| super.fromMap(); |
| |
| @override |
| Map<String, Object?> toJson() => { |
| ...super.toJson(), |
| if (vmServiceUri != null) 'vmServiceUri': vmServiceUri, |
| if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile, |
| }; |
| |
| static DartAttachRequestArguments fromJson(Map<String, Object?> obj) => |
| DartAttachRequestArguments.fromMap(obj); |
| } |
| |
| /// A common base for [DartLaunchRequestArguments] and |
| /// [DartAttachRequestArguments] for fields that are common to both. |
| class DartCommonLaunchAttachRequestArguments extends RequestArguments { |
| /// A reader for protocol arguments that throws detailed exceptions if |
| /// arguments aren't of the correct type. |
| static final arg = DebugAdapterArgumentReader('launch/attach'); |
| |
| /// Optional data from the previous, restarted session. |
| /// The data is sent as the 'restart' attribute of the 'terminated' event. |
| /// The client should leave the data intact. |
| final Object? restart; |
| |
| final String? name; |
| final String? cwd; |
| |
| /// Environment variables to pass to the launched process. |
| final Map<String, String>? env; |
| |
| /// Paths that should be considered the users local code. |
| /// |
| /// These paths will generally be all of the open folders in the users editor |
| /// and are used to determine whether a library is "external" or not to |
| /// support debugging "just my code" where SDK/Pub package code will be marked |
| /// as not-debuggable. |
| final List<String>? additionalProjectPaths; |
| |
| /// Whether SDK libraries should be marked as debuggable. |
| /// |
| /// Treated as `true` if null. If `false`, "step in" will not step into SDK |
| /// libraries. |
| final bool? debugSdkLibraries; |
| |
| /// Whether to send custom progress events for long-running operations. |
| /// |
| /// If `false` or `null`, will send standard DAP progress notifications. |
| final bool? sendCustomProgressEvents; |
| |
| /// Whether external package libraries should be marked as debuggable. |
| /// |
| /// Treated as `true` if null. If `false`, "step in" will not step into |
| /// libraries in packages that are not either the local package or a path |
| /// dependency. This allows users to debug "just their code" and treat Pub |
| /// packages as block boxes. |
| final bool? debugExternalPackageLibraries; |
| |
| /// Whether to show getters in debug views like hovers and the variables |
| /// list. |
| final bool? showGettersInDebugViews; |
| |
| /// Whether to eagerly evaluate getters in debug views like hovers and the |
| /// variables list. |
| /// |
| /// If `true`, getters will be invoked automatically and included inline with |
| /// other fields (implies [showGettersInDebugViews]). |
| /// |
| /// If `false`, getters will not be included unless [showGettersInDebugViews] |
| /// is `true`, in which case they will be wrapped and only evaluated when the |
| /// user expands them. |
| final bool? evaluateGettersInDebugViews; |
| |
| /// Whether to call toString() on objects in debug views like hovers and the |
| /// variables list. |
| /// |
| /// Invoking toString() has a performance cost and may introduce side-effects, |
| /// although users may expected this functionality. null is treated like false |
| /// although clients may have their own defaults (for example Dart-Code sends |
| /// true by default at the time of writing). |
| final bool? evaluateToStringInDebugViews; |
| |
| /// Whether to send debug logging to clients in a custom `dart.log` event. This |
| /// is used both by the out-of-process tests to ensure the logs contain enough |
| /// information to track down issues, but also by Dart-Code to capture VM |
| /// service traffic in a unified log file. |
| final bool? sendLogsToClient; |
| |
| /// Whether to allow ansi color codes in OutputEvents. These may be used to |
| /// highlight user code in stack traces. |
| /// |
| /// Generally, we should only output codes that work equally with both dark |
| /// and light themes because we don't know what the clients colour scheme |
| /// looks like. |
| final bool? allowAnsiColorOutput; |
| |
| DartCommonLaunchAttachRequestArguments({ |
| required this.restart, |
| required this.name, |
| required this.cwd, |
| required this.env, |
| required this.additionalProjectPaths, |
| required this.debugSdkLibraries, |
| required this.debugExternalPackageLibraries, |
| // TODO(dantup): Make this 'required' after Flutter subclasses have been |
| // updated. |
| this.showGettersInDebugViews, |
| // TODO(dantup): Make this 'required' after Flutter subclasses have been |
| // updated. |
| this.allowAnsiColorOutput, |
| required this.evaluateGettersInDebugViews, |
| required this.evaluateToStringInDebugViews, |
| required this.sendLogsToClient, |
| this.sendCustomProgressEvents = false, |
| }); |
| |
| DartCommonLaunchAttachRequestArguments.fromMap(Map<String, Object?> obj) |
| : restart = arg.read<Object?>(obj, 'restart'), |
| name = arg.read<String?>(obj, 'name'), |
| cwd = arg.read<String?>(obj, 'cwd'), |
| env = arg.readOptionalMap<String, String>(obj, 'env'), |
| additionalProjectPaths = |
| arg.readOptionalList<String>(obj, 'additionalProjectPaths'), |
| debugSdkLibraries = arg.read<bool?>(obj, 'debugSdkLibraries'), |
| debugExternalPackageLibraries = |
| arg.read<bool?>(obj, 'debugExternalPackageLibraries'), |
| showGettersInDebugViews = |
| arg.read<bool?>(obj, 'showGettersInDebugViews'), |
| evaluateGettersInDebugViews = |
| arg.read<bool?>(obj, 'evaluateGettersInDebugViews'), |
| evaluateToStringInDebugViews = |
| arg.read<bool?>(obj, 'evaluateToStringInDebugViews'), |
| sendLogsToClient = arg.read<bool?>(obj, 'sendLogsToClient'), |
| sendCustomProgressEvents = |
| arg.read<bool?>(obj, 'sendCustomProgressEvents'), |
| allowAnsiColorOutput = arg.read<bool?>(obj, 'allowAnsiColorOutput'); |
| |
| Map<String, Object?> toJson() => { |
| if (restart != null) 'restart': restart, |
| if (name != null) 'name': name, |
| if (cwd != null) 'cwd': cwd, |
| if (env != null) 'env': env, |
| if (additionalProjectPaths != null) |
| 'additionalProjectPaths': additionalProjectPaths, |
| if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries, |
| if (debugExternalPackageLibraries != null) |
| 'debugExternalPackageLibraries': debugExternalPackageLibraries, |
| if (showGettersInDebugViews != null) |
| 'showGettersInDebugViews': showGettersInDebugViews, |
| if (evaluateGettersInDebugViews != null) |
| 'evaluateGettersInDebugViews': evaluateGettersInDebugViews, |
| if (evaluateToStringInDebugViews != null) |
| 'evaluateToStringInDebugViews': evaluateToStringInDebugViews, |
| if (sendLogsToClient != null) 'sendLogsToClient': sendLogsToClient, |
| if (sendCustomProgressEvents != null) |
| 'sendCustomProgressEvents': sendCustomProgressEvents, |
| if (allowAnsiColorOutput != null) |
| 'allowAnsiColorOutput': allowAnsiColorOutput, |
| }; |
| } |
| |
| /// A base DAP Debug Adapter implementation for running and debugging Dart-based |
| /// applications (including Flutter and Tests). |
| /// |
| /// This class implements all functionality common to Dart, Flutter and Test |
| /// debug sessions, including things like breakpoints and expression eval. |
| /// |
| /// Sub-classes should handle the launching/attaching of apps and any custom |
| /// behaviour (such as Flutter's Hot Reload). This is generally done by overriding |
| /// `fooImpl` methods that are called during the handling of a `fooRequest` from |
| /// the client. |
| /// |
| /// A DebugAdapter instance will be created per application being debugged (in |
| /// multi-session mode, one DebugAdapter corresponds to one incoming TCP |
| /// connection, though a client may make multiple of these connections if it |
| /// wants to debug multiple scripts concurrently, such as with a compound launch |
| /// configuration in VS Code). |
| /// |
| /// The lifecycle is described in the DAP spec here: |
| /// https://microsoft.github.io/debug-adapter-protocol/overview#initialization |
| /// |
| /// In summary: |
| /// |
| /// The client will create a connection to the server (which will create an |
| /// instance of the debug adapter) and send an `initializeRequest` message, |
| /// wait for the server to return a response and then an initializedEvent |
| /// The client will then send breakpoints and exception config |
| /// (`setBreakpointsRequest`, `setExceptionBreakpoints`) and then a |
| /// `configurationDoneRequest`. |
| /// Finally, the client will send a `launchRequest` or `attachRequest` to start |
| /// running/attaching to the script. |
| /// |
| /// The client will continue to send requests during the debug session that may |
| /// be in response to user actions (for example changing breakpoints or typing |
| /// an expression into an evaluation console) or to events sent by the server |
| /// (for example when the server sends a `StoppedEvent` it may cause the client |
| /// to then send a `stackTraceRequest` or `scopesRequest` to get variables). |
| abstract class DartDebugAdapter<TL extends LaunchRequestArguments, |
| TA extends AttachRequestArguments> extends BaseDebugAdapter<TL, TA> |
| with FileUtils { |
| late final DartCommonLaunchAttachRequestArguments args; |
| final _debuggerInitializedCompleter = Completer<void>(); |
| final _configurationDoneCompleter = Completer<void>(); |
| |
| /// Manages VM Isolates and their events, including fanning out any requests |
| /// to set breakpoints etc. from the client to all Isolates. |
| late final IsolateManager isolateManager; |
| |
| /// A helper that handlers converting to/from DAP and VM Service types. |
| late ProtocolConverter _converter; |
| |
| /// All active VM Service subscriptions. |
| /// |
| /// TODO(dantup): This may be changed to use StreamManager as part of using |
| /// DDS in this process. |
| final _subscriptions = <StreamSubscription<vm.Event>>[]; |
| |
| /// The VM service of the app being debugged. |
| /// |
| /// `null` if the session is running in noDebug mode of the connection has not |
| /// yet been made. |
| vm.VmService? vmService; |
| |
| /// The root of the Dart SDK containing the VM running the debug adapter. |
| late final String dartSdkRoot; |
| |
| /// Mappings of file paths to 'org-dartlang-sdk:///' URIs used for translating |
| /// URIs/paths between the DAP client and the VM. |
| /// |
| /// Keys are the base file paths and the values are the base URIs. Neither |
| /// value should contain trailing slashes. |
| final orgDartlangSdkMappings = <String, Uri>{}; |
| |
| /// The [DartInitializeRequestArguments] provided by the client in the |
| /// `initialize` request. |
| /// |
| /// `null` if the `initialize` request has not yet been made. |
| DartInitializeRequestArguments? _initializeArgs; |
| |
| /// Whether to use IPv6 for DAP/Debugger services. |
| final bool ipv6; |
| |
| /// A logger for printing diagnostic information. |
| final Logger? logger; |
| |
| /// Whether the current debug session is an attach request (as opposed to a |
| /// launch request). Only set during [attachRequest] so will always be `false` |
| /// prior to that. |
| bool isAttach = false; |
| |
| /// A list of evaluateNames for InstanceRef IDs. |
| /// |
| /// When providing variables for fields/getters or items in maps/arrays, we |
| /// need to provide an expression to the client that evaluates to that |
| /// variable so that functionality like "Add to Watch" or "Copy Value" can |
| /// work. For example, if a user expands a list named `myList` then the 1st |
| /// [Variable] returned should have an evaluateName of `myList[0]`. The `foo` |
| /// getter of that object would then have an evaluateName of `myList[0].foo`. |
| /// |
| /// Since those expressions aren't round-tripped as child variables are |
| /// requested we build them up as we send variables out, so we can append to |
| /// them when returning elements/map entries/fields/getters. |
| final _evaluateNamesForInstanceRefIds = <String, String>{}; |
| |
| /// A list of all possible project paths that should be considered the users |
| /// own code. |
| /// |
| /// This is made up of the folder containing the 'program' being executed, the |
| /// 'cwd' and any 'additionalProjectPaths' from the launch arguments. |
| late final List<String> projectPaths = [ |
| args.cwd, |
| if (args is DartLaunchRequestArguments) |
| path.dirname((args as DartLaunchRequestArguments).program), |
| ...?args.additionalProjectPaths, |
| ].nonNulls.toList(); |
| |
| /// Whether we have already sent the [TerminatedEvent] to the client. |
| /// |
| /// This is tracked so that we don't send multiple if there are multiple |
| /// events that suggest the session ended (such as a process exiting and the |
| /// VM Service closing). |
| bool _hasSentTerminatedEvent = false; |
| |
| /// Whether verbose internal logs (such as VM Service traffic) should be sent |
| /// to the client in `dart.log` events. |
| bool get sendLogsToClient => _sendLogsToClient; |
| var _sendLogsToClient = false; |
| |
| /// Whether or not the DAP is terminating. |
| /// |
| /// When set to `true`, some requests that return "Service Disappeared" errors |
| /// will be caught and dropped as these are expected if the process is |
| /// terminating. |
| /// |
| /// This flag may be set by incoming requests from the client |
| /// (terminateRequest/disconnectRequest) or when a process terminates, or the |
| /// VM Service disconnects. |
| bool isTerminating = false; |
| |
| /// Whether or not the current termination is happening because the user |
| /// chose to detach from an attached process. |
| /// |
| /// This affects the message a user sees when the adapter shuts down ('exited' |
| /// vs 'detached'). |
| bool isDetaching = false; |
| |
| /// Whether this adapter set the --pause-isolates-on-start flag, specifying |
| /// that isolates should pause on starting. |
| /// |
| /// Normally this will be true, but it may be set to false if the user |
| /// also manually passed the --pause-isolates-on-start flag. |
| bool pauseIsolatesOnStartSetByDap = true; |
| |
| /// Whether this adapter set the --pause-isolates-on-exit flag, specifying |
| /// that isolates should pause on exiting. |
| /// |
| /// Normally this will be true, but it may be set to false if the user |
| /// also manually passed the --pause-isolates-on-exit flag. |
| bool pauseIsolatesOnExitSetByDap = true; |
| |
| /// A [Future] that completes when the last queued OutputEvent has been sent. |
| /// |
| /// Calls to [SendOutput] will reserve their place in this queue and |
| /// subsequent calls will chain their own sends onto this (and replace it) to |
| /// preserve order. |
| Future? _lastOutputEvent; |
| |
| /// Capabilities of the DDS instance available in the connected VM Service. |
| /// |
| /// If the VM Service is not yet connected, does not have a DDS instance, or |
| /// the version has not been fetched, all capabilities will be false. |
| _DdsCapabilities _ddsCapabilities = _DdsCapabilities.empty; |
| |
| /// The ID of the custom VM Service stream that emits events intended for |
| /// tools/IDEs. |
| static final toolEventStreamId = 'ToolEvent'; |
| |
| /// Removes any breakpoints or pause behaviour and resumes any paused |
| /// isolates. |
| /// |
| /// This is useful when detaching from a process that was attached to, where |
| /// the user would not expect the script to continue to pause on breakpoints |
| /// the had set while attached. |
| Future<void> preventBreakingAndResume() async { |
| await _withErrorHandling(() async { |
| // Remove anything that may cause us to pause again. |
| await Future.wait([ |
| isolateManager.clearAllBreakpoints(), |
| isolateManager.setExceptionPauseMode('None'), |
| ]); |
| // Once those have completed, it's safe to resume anything paused. |
| await isolateManager.resumeAll(); |
| }); |
| } |
| |
| DartDebugAdapter( |
| ByteStreamServerChannel channel, { |
| this.ipv6 = false, |
| @Deprecated('DAP never spawns DDS now, this `enableDds` does nothing') |
| bool enableDds = true, |
| @Deprecated('DAP never spawns DDS now, this `enableAuthCodes` does nothing') |
| bool enableAuthCodes = true, |
| this.logger, |
| Function? onError, |
| }) : super(channel, onError: onError) { |
| channel.closed.then((_) => shutdown()); |
| |
| final vmPath = Platform.resolvedExecutable; |
| dartSdkRoot = path.dirname(path.dirname(vmPath)); |
| orgDartlangSdkMappings[dartSdkRoot] = Uri.parse('org-dartlang-sdk:///sdk'); |
| |
| isolateManager = IsolateManager(this); |
| _converter = ProtocolConverter(this); |
| } |
| |
| /// Completes when the debugger initialization has completed. Used to delay |
| /// processing isolate events while initialization is still running to avoid |
| /// race conditions (for example if an isolate unpauses before we have |
| /// processed its initial paused state). |
| Future<void> get debuggerInitialized => _debuggerInitializedCompleter.future; |
| |
| bool get evaluateToStringInDebugViews => |
| args.evaluateToStringInDebugViews ?? false; |
| |
| /// The [InitializeRequestArguments] provided by the client in the |
| /// `initialize` request. |
| /// |
| /// `null` if the `initialize` request has not yet been made. |
| DartInitializeRequestArguments? get initializeArgs => _initializeArgs; |
| |
| /// Whether or not this adapter can handle the restartRequest. |
| /// |
| /// If false, the editor will just terminate the debug session and start a new |
| /// one when the user asks to restart. If true, the adapter must implement |
| /// the [restartRequest] method and handle its own restart (for example the |
| /// Flutter adapter will perform a Hot Restart). |
| bool get supportsRestartRequest => false; |
| |
| /// Whether the VM Service closing should be used as a signal to terminate the |
| /// debug session. |
| /// |
| /// It is generally better to handle termination when the debuggee terminates |
| /// instead, since this ensures the stdout/stderr streams have been drained. |
| /// However, that's not possible in some cases (for example 'runInTerminal' |
| /// or attaching), so this is the only signal we have. |
| /// |
| /// It is up to the subclass DA to provide this value correctly based on |
| /// whether it will call [handleSessionTerminate] itself upon process |
| /// termination. |
| bool get terminateOnVmServiceClose; |
| |
| /// Whether to subscribe to stdout/stderr through the VM Service. |
| /// |
| /// This is set by [attachRequest] so that any output will still be captured and |
| /// sent to the client without needing to access the process. |
| /// |
| /// [launchRequest] reads the stdout/stderr streams directly and does not need |
| /// to have them sent via the VM Service. |
| var _subscribeToOutputStreams = false; |
| |
| /// Overridden by sub-classes to handle when the client sends an |
| /// `attachRequest` (a request to attach to a running app). |
| /// |
| /// Sub-classes can use the [args] field to access the arguments provided |
| /// to this request. |
| Future<void> attachImpl(); |
| |
| /// [attachRequest] is called by the client when it wants us to attach to |
| /// an existing app. This will only be called once (and only one of this or |
| /// launchRequest will be called). |
| @override |
| Future<void> attachRequest( |
| Request request, |
| TA args, |
| void Function() sendResponse, |
| ) async { |
| try { |
| this.args = args as DartCommonLaunchAttachRequestArguments; |
| isAttach = true; |
| _subscribeToOutputStreams = true; |
| |
| // Common setup. |
| await _prepareForLaunchOrAttach(null); |
| |
| // Delegate to the sub-class to attach to the process. |
| await attachImpl(); |
| |
| sendResponse(); |
| } on DebugAdapterException catch (e) { |
| // Any errors that are thrown as part of an AttachRequest should be shown |
| // to the user. |
| throw DebugAdapterException(e.message, showToUser: true); |
| } |
| } |
| |
| /// Builds an evaluateName given a parent VM InstanceRef ID and a suffix. |
| /// |
| /// If [parentInstanceRefId] is `null`, or we have no evaluateName for it, |
| /// will return null. |
| String? buildEvaluateName( |
| String suffix, { |
| required String? parentInstanceRefId, |
| }) { |
| final parentEvaluateName = |
| _evaluateNamesForInstanceRefIds[parentInstanceRefId]; |
| return combineEvaluateName(parentEvaluateName, suffix); |
| } |
| |
| /// Builds an evaluateName given a prefix and a suffix. |
| /// |
| /// If [prefix] is null, will return be null. |
| String? combineEvaluateName(String? prefix, String suffix) { |
| return prefix != null ? '$prefix$suffix' : null; |
| } |
| |
| /// configurationDone is called by the client when it has finished sending |
| /// any initial configuration (such as breakpoints and exception pause |
| /// settings). |
| /// |
| /// We delay processing `launchRequest`/`attachRequest` until this request has |
| /// been sent to ensure we're not still getting breakpoints (which are sent |
| /// per-file) while we're launching and initializing over the VM Service. |
| @override |
| Future<void> configurationDoneRequest( |
| Request request, |
| ConfigurationDoneArguments? args, |
| void Function() sendResponse, |
| ) async { |
| _configurationDoneCompleter.complete(); |
| sendResponse(); |
| } |
| |
| /// Connects to the VM Service at [uri] and initializes debugging. |
| /// |
| /// This method will be called by sub-classes when they are ready to start |
| /// a debug session and may provide a URI given by the user (in the case |
| /// of attach) or from something like a vm-service-info file or Flutter |
| /// app.debugPort message. |
| /// |
| /// The URI protocol will be changed to ws/wss but otherwise not normalized. |
| /// The caller should handle any other normalisation (such as adding /ws to |
| /// the end if required). |
| /// |
| /// The implementation for this method is run in try/catch and any |
| /// exceptions during initialization will result in the debug adapter |
| /// reporting an error to the user and shutting down. |
| Future<void> connectDebugger(Uri uri) async { |
| try { |
| await _connectDebuggerImpl(uri); |
| } catch (error, stack) { |
| final message = 'Failed to connect/initialize debugger for $uri:\n' |
| '$error\n$stack'; |
| logger?.call(message); |
| sendConsoleOutput(message); |
| shutdown(); |
| } |
| } |
| |
| /// Connects to the VM Service at [uri] and initializes debugging. |
| /// |
| /// This is the implementation for [connectDebugger] which is executed in a |
| /// try/catch. |
| Future<void> _connectDebuggerImpl(Uri uri) async { |
| uri = vmServiceUriToWebSocket(uri); |
| |
| logger?.call('Connecting to debugger at $uri'); |
| sendConsoleOutput('Connecting to VM Service at $uri'); |
| final vmService = await _vmServiceConnectUri(uri.toString()); |
| logger?.call('Connected to debugger at $uri!'); |
| sendConsoleOutput('Connected to the VM Service.'); |
| |
| // Fetch DDS capabilities. |
| final supportedProtocols = await vmService.getSupportedProtocols(); |
| final ddsProtocol = supportedProtocols.protocols |
| ?.firstWhereOrNull((protocol) => protocol.protocolName == 'DDS'); |
| if (ddsProtocol != null) { |
| _ddsCapabilities = _DdsCapabilities( |
| major: ddsProtocol.major ?? 0, |
| minor: ddsProtocol.minor ?? 0, |
| ); |
| } |
| final supportsCustomStreams = _ddsCapabilities.supportsCustomStreams; |
| |
| // Send debugger URI to the client. |
| sendDebuggerUris(uri); |
| |
| this.vmService = vmService; |
| |
| unawaited(vmService.onDone.then((_) => _handleVmServiceClosed())); |
| |
| // Handlers must be wrapped to handle Service Disappeared errors if async |
| // code tries to call the VM Service after termination begins. |
| final wrap = _wrapHandlerWithErrorHandling; |
| _subscriptions.addAll([ |
| vmService.onIsolateEvent.listen(wrap(handleIsolateEvent)), |
| vmService.onDebugEvent.listen(wrap(handleDebugEvent)), |
| vmService.onLoggingEvent.listen(wrap(handleLoggingEvent)), |
| vmService.onExtensionEvent.listen(wrap(handleExtensionEvent)), |
| vmService.onServiceEvent.listen(wrap(handleServiceEvent)), |
| if (supportsCustomStreams) |
| vmService.onEvent(toolEventStreamId).listen(wrap(handleToolEvent)), |
| if (_subscribeToOutputStreams) ...[ |
| vmService.onStdoutEvent.listen(wrap(_handleStdoutEvent)), |
| vmService.onStderrEvent.listen(wrap(_handleStderrEvent)), |
| ], |
| ]); |
| await Future.wait([ |
| vmService.streamListen(vm.EventStreams.kIsolate), |
| vmService.streamListen(vm.EventStreams.kDebug), |
| vmService.streamListen(vm.EventStreams.kLogging), |
| vmService.streamListen(vm.EventStreams.kExtension), |
| vmService.streamListen(vm.EventStreams.kService), |
| if (supportsCustomStreams) vmService.streamListen(toolEventStreamId), |
| if (_subscribeToOutputStreams) ...[ |
| vmService.streamListen(vm.EventStreams.kStdout), |
| vmService.streamListen(vm.EventStreams.kStderr), |
| ], |
| ]); |
| |
| final vmInfo = await vmService.getVM(); |
| logger?.call('Connected to ${vmInfo.name} on ${vmInfo.operatingSystem}'); |
| |
| // Let the subclass do any existing setup once we have a connection. |
| await debuggerConnected(vmInfo); |
| |
| await _configureIsolateSettings(vmService); |
| await _withErrorHandling( |
| () => _configureExistingIsolates(vmService, vmInfo), |
| ); |
| |
| _debuggerInitializedCompleter.complete(); |
| } |
| |
| // This is intended for subclasses to override to provide a URI converter to |
| // resolve package URIs to local paths. |
| UriConverter? uriConverter() { |
| return null; |
| } |
| |
| void sendDebuggerUris(Uri uri) { |
| // Send a custom event with the VM Service URI as the editor might want to |
| // know about this (for example so it can connect an embedded DevTools to |
| // this app). |
| sendEvent( |
| RawEventBody({ |
| 'vmServiceUri': uri.toString(), |
| }), |
| eventType: 'dart.debuggerUris', |
| ); |
| } |
| |
| /// Starts reporting progress to the client for a single operation. |
| /// |
| /// The returned [DapProgressReporter] can be used to send updated messages |
| /// and to complete progress (hiding the progress notification). |
| /// |
| /// Clients will use [title] as a prefix for all updates, appending [message] |
| /// in the form: |
| /// |
| /// title: message |
| /// |
| /// When `update` is called, the new message will replace the previous |
| /// message but the title prefix will remain. |
| DapProgressReporter startProgressNotification( |
| String id, |
| String title, { |
| String? message, |
| }) { |
| return DapProgressReporter.start(this, id, title, message: message); |
| } |
| |
| Future<void> _configureIsolateSettings( |
| vm.VmService vmService, |
| ) async { |
| // If this is an attach workflow, check whether pause_isolates_on_start or |
| // pause_isolates_on_exit were already set, and if not set them (note: this |
| // is already done as part of the launch workflow): |
| if (isAttach) { |
| const pauseIsolatesOnStart = 'pause_isolates_on_start'; |
| const pauseIsolatesOnExit = 'pause_isolates_on_exit'; |
| final flags = (await vmService.getFlagList()).flags ?? <vm.Flag>[]; |
| for (final flag in flags) { |
| final flagName = flag.name; |
| final isPauseIsolatesFlag = |
| flagName == pauseIsolatesOnStart || flagName == pauseIsolatesOnExit; |
| if (flagName == null || !isPauseIsolatesFlag) continue; |
| |
| if (flag.valueAsString == 'true') { |
| if (flagName == pauseIsolatesOnStart) { |
| pauseIsolatesOnStartSetByDap = false; |
| } |
| if (flagName == pauseIsolatesOnExit) { |
| pauseIsolatesOnExitSetByDap = false; |
| } |
| } else { |
| _setVmFlagTo(vmService, flagName: flagName, valueAsString: 'true'); |
| } |
| } |
| } |
| |
| try { |
| // Make sure DDS waits for DAP to be ready to resume before forwarding |
| // resume requests to the VM Service: |
| await vmService.requirePermissionToResume( |
| onPauseStart: true, |
| onPauseExit: true, |
| ); |
| |
| // Specify whether DDS should wait for a user-initiated resume as well as a |
| // DAP-initiated resume: |
| await vmService.requireUserPermissionToResume( |
| onPauseStart: !pauseIsolatesOnStartSetByDap, |
| onPauseExit: !pauseIsolatesOnExitSetByDap, |
| ); |
| } catch (e) { |
| // If DDS is not enabled, calling these DDS service extensions will fail. |
| // Therefore catch and log any errors. |
| logger?.call('Failure configuring isolate settings: $e'); |
| } |
| } |
| |
| Future<void> _setVmFlagTo( |
| vm.VmService vmService, { |
| required String flagName, |
| required String valueAsString, |
| }) async { |
| try { |
| await vmService.setFlag(flagName, valueAsString); |
| } catch (e) { |
| logger?.call('Failed to to set VM flag $flagName to $valueAsString: $e'); |
| } |
| } |
| |
| /// Process any existing isolates that may have been created before the |
| /// streams above were set up. |
| Future<void> _configureExistingIsolates( |
| vm.VmService vmService, |
| vm.VM vmInfo, |
| ) async { |
| final existingIsolateRefs = vmInfo.isolates; |
| final existingIsolates = existingIsolateRefs != null |
| ? await Future.wait(existingIsolateRefs |
| .map((isolateRef) => isolateRef.id) |
| .nonNulls |
| .map(vmService.getIsolate)) |
| : <vm.Isolate>[]; |
| await Future.wait(existingIsolates.map((isolate) async { |
| // Isolates may have the "None" pauseEvent kind at startup, so infer it |
| // from the runnable field. |
| final pauseEventKind = isolate.runnable ?? false |
| ? vm.EventKind.kIsolateRunnable |
| : vm.EventKind.kIsolateStart; |
| await isolateManager.registerIsolate(isolate, pauseEventKind); |
| |
| // If the Isolate already has a Pause event we can give it to the |
| // IsolateManager to handle (if it's PausePostStart it will re-configure |
| // the isolate before resuming), otherwise we can just resume it (if it's |
| // runnable - otherwise we'll handle this when it becomes runnable in an |
| // event later). |
| if (isolate.pauseEvent?.kind?.startsWith('Pause') ?? false) { |
| await isolateManager.handleEvent( |
| isolate.pauseEvent!, |
| ); |
| } else if (isolate.runnable == true) { |
| await isolateManager.readyToResumeIsolate(isolate); |
| } |
| })); |
| } |
| |
| /// Handles the clients "continue" ("resume") request for the thread in |
| /// [args.threadId]. |
| @override |
| Future<void> continueRequest( |
| Request request, |
| ContinueArguments args, |
| void Function(ContinueResponseBody) sendResponse, |
| ) async { |
| // When we resume, it's always possible that the VM will shut down (because |
| // it was paused-on-exit and we just allowed it to complete and exit), so |
| // we should handle shutdown errors and just accept them as successful |
| // resumes. |
| await _withErrorHandling(() => isolateManager.resumeThread(args.threadId)); |
| sendResponse(ContinueResponseBody(allThreadsContinued: false)); |
| } |
| |
| /// [customRequest] handles any messages that do not match standard messages |
| /// in the spec. |
| /// |
| /// This is used to allow a client/DA to have custom methods outside of the |
| /// spec. It is up to the client/DA to negotiate which custom messages are |
| /// allowed. |
| /// |
| /// Implementations of this method must call super for any requests they are |
| /// not handling. The base implementation will reject the request as unknown. |
| /// |
| /// Custom message starting with _ are considered internal and are liable to |
| /// change without warning. |
| @override |
| Future<void> customRequest( |
| Request request, |
| RawRequestArguments? args, |
| void Function(Object?) sendResponse, |
| ) async { |
| switch (request.command) { |
| // Used by tests to validate available protocols (e.g. DDS). There may be |
| // value in making this available to clients in future, but for now it's |
| // internal. |
| case '_getSupportedProtocols': |
| final protocols = await vmService?.getSupportedProtocols(); |
| sendResponse(protocols?.toJson()); |
| break; |
| |
| // Used to toggle debug settings such as whether SDK/Packages are |
| // debuggable while the session is in progress. |
| case 'updateDebugOptions': |
| if (args != null) { |
| await _updateDebugOptions(args.args); |
| } |
| sendResponse(_noResult); |
| break; |
| |
| // Used to enable/disable sending logs to the client. This can also be |
| // enabled in launch args, but this allows selective logging to produce |
| // more targeted log files (used by Dart-Code's "Capture Debugging Logs" |
| // command). |
| case 'updateSendLogsToClient': |
| if (args != null) { |
| await _updateSendLogsToClient(args.args); |
| } |
| sendResponse(_noResult); |
| break; |
| |
| // Allows an editor to call a service/service extension that it was told |
| // about via a custom 'dart.serviceRegistered' or |
| // 'dart.serviceExtensionAdded' event. |
| case 'callService': |
| final method = args?.args['method'] as String?; |
| if (method == null) { |
| throw DebugAdapterException( |
| 'Method is required to call services/service extensions', |
| ); |
| } |
| final params = args?.args['params'] as Map<String, Object?>?; |
| final response = await vmService?.callServiceExtension( |
| method, |
| args: params, |
| ); |
| sendResponse(response?.json); |
| break; |
| |
| // Used to reload sources for all isolates. This supports Hot Reload for |
| // Dart apps. Flutter's DAP handles this command itself (and sends it |
| // through the run daemon) as it needs to perform additional work to |
| // rebuild widgets afterwards. |
| case 'hotReload': |
| await isolateManager.reloadSources(); |
| sendResponse(_noResult); |
| break; |
| |
| // Called by VS Code extension to have us force a re-evaluation of |
| // variables if settings are modified that globally change the format |
| // of numbers (in the case where format specifiers are not explicitly |
| // provided, such as the Variables pane). |
| case '_invalidateAreas': |
| // We just send the invalidate request back to the client. DAP only |
| // allows these to originate in the DAP server, but we have case where |
| // the client knows that these have become stale (because the user |
| // changed some config) so we have to bounce it through the server. |
| final areas = args?.args['areas'] as List<Object?>?; |
| final stringArears = areas?.whereType<String>().toList(); |
| // Trigger the invalidation. |
| sendEvent(InvalidatedEventBody(areas: stringArears)); |
| // Respond to the incoming request. |
| sendResponse(_noResult); |
| break; |
| |
| // Used by tests to force a GC for a given DAP threadId. |
| case '_collectAllGarbage': |
| final threadId = args?.args['threadId'] as int; |
| final isolateId = isolateManager.getThread(threadId)?.isolate.id; |
| // Trigger the GC. |
| if (isolateId != null) { |
| await vmService?.callMethod( |
| '_collectAllGarbage', |
| isolateId: isolateId, |
| ); |
| } |
| |
| // Respond to the incoming request. |
| sendResponse(_noResult); |
| break; |
| |
| default: |
| await super.customRequest(request, args, sendResponse); |
| } |
| } |
| |
| /// Overridden by sub-classes to perform any additional setup after the VM |
| /// Service is connected. |
| Future<void> debuggerConnected(vm.VM vmInfo); |
| |
| /// Overridden by sub-classes to handle when the client sends a |
| /// `disconnectRequest` (a forceful request to shut down). |
| Future<void> disconnectImpl(); |
| |
| /// [disconnectRequest] is called by the client when it wants to forcefully shut |
| /// us down quickly. This comes after the `terminateRequest` which is intended |
| /// to allow a graceful shutdown. |
| /// |
| /// It's not very obvious from the names, but `terminateRequest` is sent first |
| /// (a request for a graceful shutdown) and `disconnectRequest` second (a |
| /// request for a forced shutdown). |
| /// |
| /// https://microsoft.github.io/debug-adapter-protocol/overview#debug-session-end |
| @override |
| Future<void> disconnectRequest( |
| Request request, |
| DisconnectArguments? args, |
| void Function() sendResponse, |
| ) async { |
| isTerminating = true; |
| |
| await disconnectImpl(); |
| sendResponse(); |
| |
| await shutdown(); |
| } |
| |
| /// evaluateRequest is called by the client to evaluate a string expression. |
| /// |
| /// This could come from the user typing into an input (for example VS Code's |
| /// Debug Console), automatic refresh of a Watch window, or called as part of |
| /// an operation like "Copy Value" for an item in the watch/variables window. |
| /// |
| /// If execution is not paused, the `frameId` will not be provided. |
| @override |
| Future<void> evaluateRequest( |
| Request request, |
| EvaluateArguments args, |
| void Function(EvaluateResponseBody) sendResponse, |
| ) async { |
| final frameId = args.frameId; |
| |
| // If the frameId was supplied, it maps to an ID we provided from stored |
| // data so we need to look up the isolate + frame index for it. |
| ThreadInfo? thread; |
| int? frameIndex; |
| if (frameId != null) { |
| final data = isolateManager.getStoredData(frameId); |
| if (data != null) { |
| thread = data.thread; |
| frameIndex = (data.data as vm.Frame).index; |
| } |
| } |
| |
| // To support global evaluation, we allow passing a file:/// URI in the |
| // context argument. This is always from the repl. |
| final context = args.context; |
| final targetScriptFileUri = context != null && |
| context.startsWith('file://') && |
| context.endsWith('.dart') |
| ? Uri.tryParse(context) |
| : null; |
| |
| /// Clipboard context means the user has chosen to copy the value to the |
| /// clipboard, so we should strip any quotes and expand to the full string. |
| final isClipboard = args.context == 'clipboard'; |
| |
| /// In the repl, we should also expand the full string, but keep the quotes |
| /// because that's our indicator it is a string (eg. "1" vs 1). Since |
| /// we override context with script IDs for global evaluation, we must |
| /// also treat presence of targetScriptFileUri as repl. |
| final isRepl = args.context == 'repl' || targetScriptFileUri != null; |
| |
| final shouldSuppressQuotes = isClipboard; |
| final shouldExpandTruncatedValues = isClipboard || isRepl; |
| |
| if ((thread == null || frameIndex == null) && targetScriptFileUri == null) { |
| throw DebugAdapterException( |
| 'Evaluation is only supported when the debugger is paused ' |
| 'unless you have a Dart file active in the editor'); |
| } |
| |
| // Parse the expression for trailing format specifiers. |
| final expressionData = EvaluationExpression.parse( |
| args.expression |
| .trim() |
| // Remove any trailing semicolon as the VM only evaluates expressions |
| // but a user may have highlighted a whole line/statement to send for |
| // evaluation. |
| .replaceFirst(_trailingSemicolonPattern, ''), |
| ); |
| final expression = expressionData.expression; |
| var format = expressionData.format ?? |
| // If we didn't parse a format specifier, fall back to the format in |
| // the arguments. |
| VariableFormat.fromDapValueFormat(args.format); |
| |
| if (shouldSuppressQuotes) { |
| format = format != null |
| ? VariableFormat.from(format, noQuotes: true) |
| : VariableFormat.noQuotes(); |
| } |
| |
| final exceptionReference = thread?.exceptionReference; |
| // The value in the constant `frameExceptionExpression` is used as a special |
| // expression that evaluates to the exception on the current thread. This |
| // allows us to construct evaluateNames that evaluate to the fields down the |
| // tree to support some of the debugger functionality (for example |
| // "Copy Value", which re-evaluates). |
| final isExceptionExpression = expression == threadExceptionExpression || |
| expression.startsWith('$threadExceptionExpression.'); |
| |
| vm.Response? result; |
| try { |
| if (thread != null && |
| exceptionReference != null && |
| isExceptionExpression) { |
| result = await _evaluateExceptionExpression( |
| exceptionReference, |
| expression, |
| thread, |
| ); |
| } else if (thread != null && frameIndex != null) { |
| result = await vmEvaluateInFrame( |
| thread, |
| frameIndex, |
| expression, |
| ); |
| } else if (targetScriptFileUri != null && |
| // Since we can't currently get a thread, we assume the first thread is |
| // a reasonable target for global evaluation. |
| (thread = isolateManager.threads.firstOrNull) != null && |
| thread != null) { |
| final library = await thread.getLibraryForFileUri(targetScriptFileUri); |
| if (library == null) { |
| // Wrapped in DebugAdapterException in the catch below. |
| throw 'Unable to find the library for $targetScriptFileUri'; |
| } |
| |
| result = await vmEvaluate(thread, library.id!, expression); |
| } |
| } catch (e) { |
| final rawMessage = '$e'; |
| |
| // Error messages can be quite verbose and don't fit well into a |
| // single-line watch window. For example: |
| // |
| // evaluateInFrame: (113) Expression compilation error |
| // org-dartlang-debug:synthetic_debug_expression:1:5: Error: A value of type 'String' can't be assigned to a variable of type 'num'. |
| // 1 + "a" |
| // ^ |
| // |
| // So in the case of a Watch context, try to extract the useful message. |
| if (args.context == 'watch') { |
| throw DebugAdapterException(extractEvaluationErrorMessage(rawMessage)); |
| } |
| |
| throw DebugAdapterException(rawMessage); |
| } |
| |
| if (result is vm.ErrorRef) { |
| throw DebugAdapterException(result.message ?? '<error ref>'); |
| } else if (result is vm.Sentinel) { |
| throw DebugAdapterException(result.valueAsString ?? '<collected>'); |
| } else if (result is vm.InstanceRef && thread != null) { |
| final resultString = await _converter.convertVmInstanceRefToDisplayString( |
| thread, |
| result, |
| allowCallingToString: |
| evaluateToStringInDebugViews || shouldExpandTruncatedValues, |
| format: format, |
| allowTruncatedValue: !shouldExpandTruncatedValues, |
| ); |
| |
| final variablesReference = _converter.isSimpleKind(result.kind) |
| ? 0 |
| : thread.storeData(VariableData(result, format)); |
| |
| // Store the expression that gets this object as we may need it to |
| // compute evaluateNames for child objects later. |
| storeEvaluateName(result, expression); |
| |
| sendResponse(EvaluateResponseBody( |
| result: resultString, |
| variablesReference: variablesReference, |
| )); |
| } else { |
| throw DebugAdapterException( |
| 'Unknown evaluation response type: ${result?.runtimeType}', |
| ); |
| } |
| } |
| |
| /// Tries to extract the useful part from an evaluation exception message. |
| /// |
| /// If no message could be extracted, returns the whole original error. |
| String extractEvaluationErrorMessage(String rawError) { |
| final match = _evalErrorMessagePattern.firstMatch(rawError); |
| final shortError = match?.group(1); |
| return shortError ?? rawError; |
| } |
| |
| /// Tries to extract the useful part from an unhandled exception message. |
| /// |
| /// If no message could be extracted, returns the whole original error. |
| String extractUnhandledExceptionMessage(String rawError) { |
| final match = _exceptionMessagePattern.firstMatch(rawError); |
| final shortError = match?.group(1); |
| return shortError ?? rawError; |
| } |
| |
| /// Handles a detach request, removing breakpoints and unpausing paused |
| /// isolates. |
| Future<void> handleDetach() async { |
| isDetaching = true; |
| await preventBreakingAndResume(); |
| } |
| |
| /// Sends a [TerminatedEvent] if one has not already been sent. |
| /// |
| /// Waits for any in-progress output events to complete first. |
| void handleSessionTerminate([String exitSuffix = '']) async { |
| await _waitForPendingOutputEvents(); |
| |
| if (_hasSentTerminatedEvent) { |
| return; |
| } |
| |
| isTerminating = true; |
| _hasSentTerminatedEvent = true; |
| |
| // Always add a leading newline since the last written text might not have |
| // had one. |
| final reason = isDetaching ? 'Detached' : 'Exited'; |
| sendConsoleOutput('\n$reason$exitSuffix.'); |
| sendEvent(TerminatedEventBody()); |
| } |
| |
| /// [initializeRequest] is the first call from the client during |
| /// initialization and allows exchanging capabilities and configuration |
| /// between client and server. |
| /// |
| /// The lifecycle is described in the DAP spec here: |
| /// https://microsoft.github.io/debug-adapter-protocol/overview#initialization |
| /// with a summary in this classes description. |
| @override |
| Future<void> initializeRequest( |
| Request request, |
| DartInitializeRequestArguments args, |
| void Function(Capabilities) sendResponse, |
| ) async { |
| // Capture args so we can read capabilities later. |
| _initializeArgs = args; |
| |
| // TODO(dantup): Capture/honor editor-specific settings like linesStartAt1 |
| sendResponse(Capabilities( |
| exceptionBreakpointFilters: [ |
| ExceptionBreakpointsFilter( |
| filter: 'All', |
| label: 'All Exceptions', |
| defaultValue: false, |
| ), |
| ExceptionBreakpointsFilter( |
| filter: 'Unhandled', |
| label: 'Uncaught Exceptions', |
| defaultValue: true, |
| ), |
| ], |
| supportsClipboardContext: true, |
| supportsConditionalBreakpoints: true, |
| supportsConfigurationDoneRequest: true, |
| supportsDelayedStackTraceLoading: true, |
| supportsEvaluateForHovers: true, |
| supportsValueFormattingOptions: true, |
| supportsLogPoints: true, |
| supportsRestartRequest: supportsRestartRequest, |
| supportsRestartFrame: true, |
| supportsTerminateRequest: true, |
| )); |
| |
| // This must only be sent AFTER the response! |
| sendEvent(InitializedEventBody()); |
| } |
| |
| /// Checks whether this library is from an external package. |
| /// |
| /// This is used to support debugging "Just My Code" so Pub packages can be |
| /// marked as not-debuggable. |
| /// |
| /// A library is considered local if the path is within the 'cwd' or |
| /// 'additionalProjectPaths' in the launch arguments. An editor should include |
| /// the paths of all open workspace folders in 'additionalProjectPaths' to |
| /// support this feature correctly. |
| Future<bool> isExternalPackageLibrary(ThreadInfo thread, Uri uri) async { |
| if (!uri.isScheme('package')) { |
| return false; |
| } |
| |
| final packageFileLikeUri = await thread.resolveUriToPackageLibPath(uri); |
| if (packageFileLikeUri == null) { |
| return false; |
| } |
| |
| return !isInUserProject(packageFileLikeUri); |
| } |
| |
| /// Checks whether [uri] is inside the users project. This is used to support |
| /// debugging "Just My Code" (via [isExternalPackageLibrary]) and also for |
| /// stack trace highlighting, where non-user code will be faded. |
| bool isInUserProject(Uri targetUri) { |
| if (!isSupportedFileScheme(targetUri)) { |
| return false; |
| } |
| |
| // We could already be 'file', or we could be another supported file scheme |
| // like dart-macro+file, but we can only call toFilePath() on a file URI |
| // and we use the equivalent path to decide if this is within the workspace. |
| var targetPath = targetUri.replace(scheme: 'file').toFilePath(); |
| |
| // Always compare paths case-insensitively to avoid any issues where APIs |
| // may have returned different casing (e.g. Windows drive letters). It's |
| // almost certain a user wouldn't have a "local" package and an "external" |
| // package with paths differing only be case. |
| targetPath = targetPath.toLowerCase(); |
| |
| return projectPaths |
| .map((projectPath) => projectPath.toLowerCase()) |
| .any((projectPath) => path.isWithin(projectPath, targetPath)); |
| } |
| |
| /// Checks whether this library is from the SDK. |
| bool isSdkLibrary(Uri uri) => uri.isScheme('dart'); |
| |
| /// Overridden by sub-classes to handle when the client sends a |
| /// `launchRequest` (a request to start running/debugging an app). |
| /// |
| /// Sub-classes can use the [args] field to access the arguments provided |
| /// to this request. |
| Future<void> launchImpl(); |
| |
| /// [launchRequest] is called by the client when it wants us to start the app |
| /// to be run/debug. This will only be called once (and only one of this or |
| /// [attachRequest] will be called). |
| @override |
| Future<void> launchRequest( |
| Request request, |
| TL args, |
| void Function() sendResponse, |
| ) async { |
| try { |
| this.args = args as DartCommonLaunchAttachRequestArguments; |
| isAttach = false; |
| |
| // Common setup. |
| await _prepareForLaunchOrAttach(args.noDebug); |
| |
| // Delegate to the sub-class to launch the process. |
| await launchAndRespond(sendResponse); |
| } on DebugAdapterException catch (e) { |
| // Any errors that are thrown as part of an AttachRequest should be shown |
| // to the user. |
| throw DebugAdapterException(e.message, showToUser: true); |
| } |
| } |
| |
| /// Overridden by sub-classes that need to control when the response is sent |
| /// during the launch process. |
| Future<void> launchAndRespond(void Function() sendResponse) async { |
| await launchImpl(); |
| sendResponse(); |
| } |
| |
| /// Checks whether a library URI should be considered debuggable. |
| /// |
| /// Initial values are provided in the launch arguments, but may be updated |
| /// by the `updateDebugOptions` custom request. |
| Future<bool> libraryIsDebuggable(ThreadInfo thread, Uri uri) async { |
| if (isSdkLibrary(uri)) { |
| return isolateManager.debugSdkLibraries; |
| } else if (!isolateManager.debugExternalPackageLibraries && |
| await isExternalPackageLibrary(thread, uri)) { |
| return false; |
| } else { |
| return true; |
| } |
| } |
| |
| /// Handles the clients "next" ("step over") request for the thread in |
| /// [args.threadId]. |
| @override |
| Future<void> nextRequest( |
| Request request, |
| NextArguments args, |
| void Function() sendResponse, |
| ) async { |
| await isolateManager.resumeThread(args.threadId, vm.StepOption.kOver); |
| sendResponse(); |
| } |
| |
| /// Handles the clients "pause" request for the thread in [args.threadId]. |
| @override |
| Future<void> pauseRequest( |
| Request request, |
| PauseArguments args, |
| void Function() sendResponse, |
| ) async { |
| await isolateManager.pauseThread(args.threadId); |
| sendResponse(); |
| } |
| |
| /// Handles the clients "restartFrame" request for the frame in |
| /// [args.frameId]. |
| @override |
| Future<void> restartFrameRequest( |
| Request request, |
| RestartFrameArguments args, |
| void Function() sendResponse, |
| ) async { |
| final data = isolateManager.getStoredData(args.frameId); |
| if (data == null) { |
| // Thread/frame is no longer valid. |
| return; |
| } |
| |
| final thread = data.thread; |
| final frame = data.data; |
| final frameIndex = frame is vm.Frame ? frame.index : null; |
| if (frameIndex == null) { |
| return; |
| } |
| |
| await isolateManager.rewindThread(thread.threadId, frameIndex: frameIndex); |
| sendResponse(); |
| } |
| |
| /// restart is called by the client when the user invokes a restart (for |
| /// example with the button on the debug toolbar). |
| /// |
| /// The base implementation of this method throws. It is up to a debug adapter |
| /// that advertises `supportsRestartRequest` to override this method. |
| @override |
| Future<void> restartRequest( |
| Request request, |
| RestartArguments? args, |
| void Function() sendResponse, |
| ) async { |
| throw DebugAdapterException( |
| 'restartRequest was called on an adapter that ' |
| 'does not provide an implementation', |
| ); |
| } |
| |
| /// [scopesRequest] is called by the client to request all of the variables |
| /// scopes available for a given stack frame. |
| @override |
| Future<void> scopesRequest( |
| Request request, |
| ScopesArguments args, |
| void Function(ScopesResponseBody) sendResponse, |
| ) async { |
| final storedData = isolateManager.getStoredData(args.frameId); |
| final thread = storedData?.thread; |
| final data = storedData?.data; |
| final frameData = data is vm.Frame ? data : null; |
| final scopes = <Scope>[]; |
| |
| if (frameData != null && thread != null) { |
| scopes.add(Scope( |
| name: 'Locals', |
| presentationHint: 'locals', |
| variablesReference: thread.storeData( |
| FrameScopeData(frameData, FrameScopeDataKind.locals), |
| ), |
| expensive: false, |
| )); |
| |
| scopes.add(Scope( |
| name: 'Globals', |
| presentationHint: 'globals', |
| variablesReference: thread.storeData( |
| FrameScopeData(frameData, FrameScopeDataKind.globals), |
| ), |
| expensive: false, |
| )); |
| |
| // If the top frame has an exception, add an additional section to allow |
| // that to be inspected. |
| final exceptionReference = thread.exceptionReference; |
| if (exceptionReference != null) { |
| scopes.add(Scope( |
| name: 'Exceptions', |
| variablesReference: exceptionReference, |
| expensive: false, |
| )); |
| } |
| } |
| |
| sendResponse(ScopesResponseBody(scopes: scopes)); |
| } |
| |
| /// Sends an OutputEvent with a trailing newline to the console. |
| /// |
| /// This method sends output directly and does not go through [sendOutput] |
| /// because that method is async and queues output. Console output is for |
| /// adapter-level output that does not require this and we want to ensure |
| /// it's sent immediately (for example during shutdown/exit). |
| void sendConsoleOutput(String? message) { |
| sendEvent(OutputEventBody(output: '$message\n')); |
| } |
| |
| /// Sends an OutputEvent (without a newline, since calls to this method |
| /// may be using buffered data that is not split cleanly on newlines). |
| /// |
| /// To ensure output is sent to the client in the correct order even if |
| /// processing stack frames requires async calls, this function will insert |
| /// output events into a queue and only send them when previous calls have |
| /// been completed. |
| void sendOutput( |
| String category, |
| String message, { |
| int? variablesReference, |
| @Deprecated( |
| 'parseStackFrames has no effect, stack frames are always parsed') |
| bool? parseStackFrames, |
| }) async { |
| // Reserve our place in the queue be inserting a future that we can complete |
| // after we have sent the output event. |
| final completer = Completer<void>(); |
| final previousEvent = _lastOutputEvent ?? Future.value(); |
| _lastOutputEvent = completer.future; |
| |
| try { |
| final outputEvents = await _buildOutputEvents( |
| category, |
| message, |
| variablesReference: variablesReference, |
| ); |
| |
| // Chain our sends onto the end of the previous one, and complete our Future |
| // once done so that the next one can go. |
| await previousEvent; |
| outputEvents.forEach(sendEvent); |
| } finally { |
| completer.complete(); |
| } |
| } |
| |
| /// Sends an OutputEvent for [message], prefixed with [prefix] and with [message] |
| /// indented to after the prefix. |
| /// |
| /// Assumes the output is in full lines and will always include a terminating |
| /// newline. |
| void sendPrefixedOutput(String category, String prefix, String message) { |
| final indentString = ' ' * prefix.length; |
| final indentedMessage = |
| message.trimRight().split('\n').join('\n$indentString'); |
| sendOutput(category, '$prefix$indentedMessage\n'); |
| } |
| |
| /// Handles a request from the client to set breakpoints. |
| /// |
| /// This method can be called at any time (before the app is launched or while |
| /// the app is running) and will include the new full set of breakpoints for |
| /// the file URI in [args.source.path]. |
| /// |
| /// The VM requires breakpoints to be set per-isolate so these will be passed |
| /// to [isolateManager] that will fan them out to each isolate. |
| /// |
| /// When new isolates are registered, it is [isolateManager]'s responsibility |
| /// to ensure all breakpoints are given to them (and like at startup, this |
| /// must happen before they are resumed). |
| @override |
| Future<void> setBreakpointsRequest( |
| Request request, |
| SetBreakpointsArguments args, |
| void Function(SetBreakpointsResponseBody) sendResponse, |
| ) async { |
| final breakpoints = args.breakpoints ?? []; |
| |
| final path = args.source.path; |
| final name = args.source.name; |
| final uri = path != null |
| ? normalizeUri(fromClientPathOrUri(path)).toString() |
| : name!; |
| |
| // Use a completer to track when the response is sent, so any events related |
| // to these breakpoints are not sent before the client has the IDs. |
| final completer = Completer<void>(); |
| |
| final clientBreakpoints = breakpoints |
| .map((bp) => ClientBreakpoint(bp, completer.future)) |
| .toList(); |
| await isolateManager.setBreakpoints(uri, clientBreakpoints); |
| |
| sendResponse(SetBreakpointsResponseBody( |
| breakpoints: clientBreakpoints |
| // Send breakpoints back as unverified and with our generated IDs so we |
| // can update them with a 'breakpoint' event when we get the |
| // 'BreakpointAdded'/'BreakpointResolved' events from the VM. |
| .map((bp) => Breakpoint(id: bp.id, verified: false)) |
| .toList(), |
| )); |
| completer.complete(); |
| } |
| |
| /// Handles a request from the client to set exception pause modes. |
| /// |
| /// This method can be called at any time (before the app is launched or while |
| /// the app is running). |
| /// |
| /// The VM requires exception modes to be set per-isolate so these will be |
| /// passed to [isolateManager] that will fan them out to each isolate. |
| /// |
| /// When new isolates are registered, it is [isolateManager]'s responsibility |
| /// to ensure the pause mode is given to them (and like at startup, this |
| /// must happen before they are resumed). |
| @override |
| Future<void> setExceptionBreakpointsRequest( |
| Request request, |
| SetExceptionBreakpointsArguments args, |
| void Function(SetExceptionBreakpointsResponseBody) sendResponse, |
| ) async { |
| final mode = args.filters.contains('All') |
| ? 'All' |
| : args.filters.contains('Unhandled') |
| ? 'Unhandled' |
| : 'None'; |
| |
| await isolateManager.setExceptionPauseMode(mode); |
| |
| sendResponse(SetExceptionBreakpointsResponseBody()); |
| } |
| |
| /// Shuts down the debug adapter, including terminating/detaching from the |
| /// debugee if required. |
| @override |
| @nonVirtual |
| Future<void> shutdown() async { |
| await _waitForPendingOutputEvents(); |
| handleSessionTerminate(); |
| |
| // Delay the shutdown slightly to allow any pending responses (such as the |
| // terminate response) to be sent. |
| // |
| // If we don't wait long enough here, the client may miss events like the |
| // TerminatedEvent. Waiting too long is generally not an issue, as the |
| // client can terminate the process itself once it processes the |
| // TerminatedEvent. |
| |
| Future.delayed( |
| Duration(milliseconds: 500), |
| () => super.shutdown(), |
| ); |
| } |
| |
| /// Converts a URI in the form org-dartlang-sdk:///sdk/lib/collection/hash_set.dart |
| /// to a local file-like URI based on the current SDK. |
| Uri? convertOrgDartlangSdkToPath(Uri uri) { |
| // org-dartlang-sdk URIs can be in multiple forms: |
| // |
| // - org-dartlang-sdk:///sdk/lib/collection/hash_set.dart |
| // - org-dartlang-sdk:///runtime/lib/convert_patch.dart |
| // |
| // We currently only handle the sdk folder, as we don't know which runtime |
| // is being used (this code is shared) and do not want to map to the wrong |
| // sources. |
| for (final mapping in orgDartlangSdkMappings.entries) { |
| final mapPath = mapping.key; |
| final mapUri = mapping.value; |
| if (uri.isScheme(mapUri.scheme) && uri.path.startsWith(mapUri.path)) { |
| return Uri.file( |
| path.joinAll([ |
| mapPath, |
| ...uri.pathSegments.skip(mapUri.pathSegments.length), |
| ]), |
| ); |
| } |
| } |
| |
| return null; |
| } |
| |
| /// Converts a file path inside the current SDK root into a URI in the |
| /// form org-dartlang-sdk:///sdk/lib/collection/hash_set.dart. |
| String? convertPathToOrgDartlangSdk(String input) { |
| // TODO(dantup): Remove this once Flutter code has been updated to |
| // use convertUriToOrgDartlangSdk. |
| return convertUriToOrgDartlangSdk(Uri.file(input))?.toFilePath(); |
| } |
| |
| /// Converts a file URI inside the current SDK root into a URI in the |
| /// form org-dartlang-sdk:///sdk/lib/collection/hash_set.dart. |
| Uri? convertUriToOrgDartlangSdk(Uri input) { |
| // TODO(dantup): We may need to expand this if we start using |
| // macro-generated files in the SDK. |
| if (!input.isScheme('file')) { |
| return null; |
| } |
| final inputPath = input.toFilePath(); |
| |
| for (final mapping in orgDartlangSdkMappings.entries) { |
| final mapPath = mapping.key; |
| final mapUri = mapping.value; |
| if (path.isWithin(mapPath, inputPath)) { |
| final relative = path.relative(inputPath, from: mapPath); |
| return Uri( |
| scheme: mapUri.scheme, |
| host: '', |
| pathSegments: [...mapUri.pathSegments, ...path.split(relative)], |
| ); |
| } |
| } |
| |
| return null; |
| } |
| |
| /// [sourceRequest] is called by the client to request source code for a given |
| /// source. |
| /// |
| /// The client may provide a whole source or just an int sourceReference (the |
| /// spec originally had only sourceReference but now supports whole sources). |
| /// |
| /// The supplied sourceReference should correspond to a ScriptRef instance |
| /// that was stored to generate the sourceReference when sent to the client. |
| @override |
| Future<void> sourceRequest( |
| Request request, |
| SourceArguments args, |
| void Function(SourceResponseBody) sendResponse, |
| ) async { |
| final storedData = isolateManager.getStoredData( |
| args.source?.sourceReference ?? args.sourceReference, |
| ); |
| if (storedData == null) { |
| throw StateError('source reference is no longer valid'); |
| } |
| final thread = storedData.thread; |
| final data = storedData.data; |
| final scriptRef = data is vm.ScriptRef ? data : null; |
| if (scriptRef == null) { |
| throw StateError('source reference was not a valid script'); |
| } |
| |
| final script = await thread.getScript(scriptRef); |
| final scriptSource = script.source; |
| if (scriptSource == null) { |
| throw DebugAdapterException('<source not available>'); |
| } |
| |
| sendResponse( |
| SourceResponseBody(content: scriptSource, mimeType: dartMimeType), |
| ); |
| } |
| |
| /// Handles a request from the client for the call stack for [args.threadId]. |
| /// |
| /// This is usually called after we sent a [StoppedEvent] to the client |
| /// notifying it that execution of an isolate has paused and it wants to |
| /// populate the call stack view. |
| /// |
| /// Clients may fetch the frames in batches and VS Code in particular will |
| /// send two requests initially - one for the top frame only, and then one for |
| /// the next 19 frames. For better performance, the first request is satisfied |
| /// entirely from the threads pauseEvent.topFrame so we do not need to |
| /// round-trip to the VM Service. |
| @override |
| Future<void> stackTraceRequest( |
| Request request, |
| StackTraceArguments args, |
| void Function(StackTraceResponseBody) sendResponse, |
| ) async { |
| // We prefer to provide frames in small batches. Rather than tell the client |
| // how many frames there really are (which can be expensive to compute - |
| // especially for web) we just add 20 on to the last frame we actually send, |
| // as described in the spec: |
| // |
| // "Returning monotonically increasing totalFrames values for subsequent |
| // requests can be used to enforce paging in the client." |
| const stackFrameBatchSize = 20; |
| |
| final threadId = args.threadId; |
| final thread = isolateManager.getThread(threadId); |
| final topFrame = thread?.pauseEvent?.topFrame; |
| final startFrame = args.startFrame ?? 0; |
| final numFrames = args.levels ?? 0; |
| var totalFrames = 1; |
| |
| if (thread == null) { |
| if (isolateManager.isInvalidThreadId(threadId)) { |
| throw DebugAdapterException('Thread $threadId was not found'); |
| } else { |
| // This condition means the thread ID was valid but the isolate has |
| // since exited so rather than displaying an error, just return an empty |
| // response because the client will be no longer interested in the |
| // response. |
| sendResponse(StackTraceResponseBody( |
| stackFrames: [], |
| totalFrames: 0, |
| )); |
| return; |
| } |
| } |
| |
| if (!thread.paused) { |
| throw DebugAdapterException('Thread $threadId is not paused'); |
| } |
| |
| final stackFrames = <StackFrame>[]; |
| // If the request is only for the top frame, we may be able to satisfy it |
| // from the threads `pauseEvent.topFrame`. |
| if (startFrame == 0 && numFrames == 1 && topFrame != null) { |
| totalFrames = 1 + stackFrameBatchSize; |
| final dapTopFrame = await _converter.convertVmToDapStackFrame( |
| thread, |
| topFrame, |
| isTopFrame: true, |
| ); |
| stackFrames.add(dapTopFrame); |
| } else { |
| // Otherwise, send the request on to the VM. |
| // The VM doesn't support fetching an arbitrary slice of frames, only a |
| // maximum limit, so if the client asks for frames 20-30 we must send a |
| // request for the first 30 and trim them ourselves. |
| |
| // DAP says if numFrames is 0 or missing (which we swap to 0 above) we |
| // should return all. |
| final limit = numFrames == 0 ? null : startFrame + numFrames; |
| final stack = await vmService?.getStack(thread.isolate.id!, limit: limit); |
| final frames = stack?.asyncCausalFrames ?? stack?.frames; |
| |
| if (stack != null && frames != null) { |
| // When the call stack is truncated, we always add [stackFrameBatchSize] |
| // to the count, indicating to the client there are more frames and |
| // the size of the batch they should request when "loading more". |
| // |
| // It's ok to send a number that runs past the actual end of the call |
| // stack and the client should handle this gracefully: |
| // |
| // "a client should be prepared to receive less frames than requested, |
| // which is an indication that the end of the stack has been reached." |
| totalFrames = (stack.truncated ?? false) |
| ? frames.length + stackFrameBatchSize |
| : frames.length; |
| |
| // Find the first async marker, because some functionality only works |
| // up until the first async boundary (e.g. rewind) since we're showing |
| // the user async frames which are out-of-sync with the real frames |
| // past that point. |
| int? firstAsyncMarkerIndex = frames.indexWhere( |
| (frame) => frame.kind == vm.FrameKind.kAsyncSuspensionMarker, |
| ); |
| // indexWhere returns -1 if not found, we treat that as no marker (we |
| // can rewind for all frames in the stack). |
| if (firstAsyncMarkerIndex == -1) { |
| firstAsyncMarkerIndex = null; |
| } |
| |
| // Pre-resolve all URIs in batch so the call below does not trigger |
| // many requests to the server. |
| final allUris = frames |
| .map((frame) => frame.location?.script?.uri) |
| .nonNulls |
| .map(Uri.parse) |
| .toList(); |
| await thread.resolveUrisToPathsBatch(allUris); |
| |
| Future<StackFrame> convert(int index, vm.Frame frame) async { |
| return _converter.convertVmToDapStackFrame( |
| thread, |
| frame, |
| firstAsyncMarkerIndex: firstAsyncMarkerIndex, |
| isTopFrame: startFrame == 0 && index == 0, |
| ); |
| } |
| |
| final frameSubset = frames.sublist(startFrame); |
| stackFrames.addAll(await Future.wait(frameSubset.mapIndexed(convert))); |
| } |
| } |
| |
| sendResponse( |
| StackTraceResponseBody( |
| stackFrames: stackFrames, |
| totalFrames: totalFrames, |
| ), |
| ); |
| } |
| |
| /// Handles the clients "step in" request for the thread in [args.threadId]. |
| @override |
| Future<void> stepInRequest( |
| Request request, |
| StepInArguments args, |
| void Function() sendResponse, |
| ) async { |
| await isolateManager.resumeThread(args.threadId, vm.StepOption.kInto); |
| sendResponse(); |
| } |
| |
| /// Handles the clients "step out" request for the thread in [args.threadId]. |
| @override |
| Future<void> stepOutRequest( |
| Request request, |
| StepOutArguments args, |
| void Function() sendResponse, |
| ) async { |
| await isolateManager.resumeThread(args.threadId, vm.StepOption.kOut); |
| sendResponse(); |
| } |
| |
| /// Stores [evaluateName] as the expression that can be evaluated to get |
| /// [instanceRef]. |
| void storeEvaluateName(vm.InstanceRef instanceRef, String? evaluateName) { |
| if (evaluateName != null) { |
| _evaluateNamesForInstanceRefIds[instanceRef.id!] = evaluateName; |
| } |
| } |
| |
| /// Overridden by sub-classes to handle when the client sends a |
| /// `terminateRequest` (a request for a graceful shut down). |
| Future<void> terminateImpl(); |
| |
| /// [terminateRequest] is called by the client when it wants us to gracefully |
| /// shut down. |
| /// |
| /// It's not very obvious from the names, but `terminateRequest` is sent first |
| /// (a request for a graceful shutdown) and `disconnectRequest` second (a |
| /// request for a forced shutdown). |
| /// |
| /// https://microsoft.github.io/debug-adapter-protocol/overview#debug-session-end |
| @override |
| Future<void> terminateRequest( |
| Request request, |
| TerminateArguments? args, |
| void Function() sendResponse, |
| ) async { |
| isTerminating = true; |
| |
| await terminateImpl(); |
| sendResponse(); |
| |
| await shutdown(); |
| } |
| |
| /// Handles a request from the client for the list of threads. |
| /// |
| /// This is usually called after we sent a [StoppedEvent] to the client |
| /// notifying it that execution of an isolate has paused and it wants to |
| /// populate the threads view. |
| @override |
| Future<void> threadsRequest( |
| Request request, |
| void args, |
| void Function(ThreadsResponseBody) sendResponse, |
| ) async { |
| final threads = [ |
| for (final thread in isolateManager.threads) |
| Thread( |
| id: thread.threadId, |
| name: thread.isolate.name ?? '<unnamed isolate>', |
| ) |
| ]; |
| sendResponse(ThreadsResponseBody(threads: threads)); |
| } |
| |
| /// [variablesRequest] is called by the client to request child variables for |
| /// a given variables variablesReference. |
| /// |
| /// The variablesReference provided by the client will be a reference the |
| /// server has previously provided, for example in response to a scopesRequest |
| /// or an evaluateRequest. |
| /// |
| /// We use the reference to look up the stored data and then create variables |
| /// based on the type of data. For a Frame, we will return the local |
| /// variables, for a List/MapAssociation we will return items from it, and for |
| /// an instance we will return the fields (and possibly getters) for that |
| /// instance. |
| @override |
| Future<void> variablesRequest( |
| Request request, |
| VariablesArguments args, |
| void Function(VariablesResponseBody) sendResponse, |
| ) async { |
| final service = vmService; |
| final childStart = args.start; |
| final childCount = args.count; |
| final storedData = isolateManager.getStoredData(args.variablesReference); |
| if (storedData == null) { |
| throw StateError('variablesReference is no longer valid'); |
| } |
| final thread = storedData.thread; |
| var data = storedData.data; |
| |
| VariableFormat? format; |
| // Unwrap any variable we stored with formatting info. |
| if (data is VariableData) { |
| format = data.format; |
| data = data.data; |
| } |
| |
| // If no explicit formatting, use from args. |
| format ??= VariableFormat.fromDapValueFormat(args.format); |
| |
| final variables = <Variable>[]; |
| |
| if (data is FrameScopeData && data.kind == FrameScopeDataKind.locals) { |
| final vars = data.frame.vars; |
| if (vars != null) { |
| Future<Variable> convert(int index, vm.BoundVariable variable) { |
| // Store the expression that gets this object as we may need it to |
| // compute evaluateNames for child objects later. |
| final value = variable.value; |
| if (value is vm.InstanceRef) { |
| storeEvaluateName(value, variable.name); |
| } |
| return _converter.convertVmResponseToVariable( |
| thread, |
| variable.value, |
| name: variable.name, |
| allowCallingToString: evaluateToStringInDebugViews && |
| index < maxToStringsPerEvaluation, |
| evaluateName: variable.name, |
| format: format, |
| ); |
| } |
| |
| variables.addAll(await Future.wait(vars.mapIndexed(convert))); |
| |
| // Sort the variables by name. |
| variables.sortBy((v) => v.name); |
| } |
| } else if (data is FrameScopeData && |
| data.kind == FrameScopeDataKind.globals) { |
| /// Helper to simplify calling converter. |
| Future<Variable> convert(int index, vm.FieldRef fieldRef) async { |
| return _converter.convertFieldRefToVariable( |
| thread, |
| fieldRef, |
| allowCallingToString: |
| evaluateToStringInDebugViews && index < maxToStringsPerEvaluation, |
| format: format, |
| ); |
| } |
| |
| final globals = await _getFrameGlobals(thread, data.frame); |
| variables.addAll(await Future.wait(globals.mapIndexed(convert))); |
| variables.sortBy((v) => v.name); |
| } else if (data is InspectData) { |
| // When sending variables as part of an OutputEvent, VS Code will only |
| // show the first field, so we wrap the object to ensure there's always |
| // a single field. |
| final instance = data.instance; |
| variables.add(Variable( |
| name: '', // Unused. |
| value: '<inspected variable>', // Shown to user, expandable. |
| variablesReference: instance != null ? thread.storeData(instance) : 0, |
| )); |
| } else if (data is WrappedInstanceVariable) { |
| // WrappedInstanceVariables are used to support DAP-over-DDS clients that |
| // had a VM Instance ID and wanted to convert it to a variable for use in |
| // `variables` requests. |
| try { |
| final response = await isolateManager.getObject( |
| storedData.thread.isolate, |
| vm.ObjRef(id: data.instanceId), |
| offset: childStart, |
| count: childCount, |
| ); |
| // Because `variables` requests are a request for _child_ variables but we |
| // want DAP-over-DDS clients to be able to get the whole variable (eg. |
| // including toe initial string representation of the variable itself) the |
| // initial request will return a list containing a single variable named |
| // `value`. This will contain both the `variablesReference` to get the |
| // children, and also a `value` field with the display string. |
| final variable = await _converter.convertVmResponseToVariable( |
| thread, |
| response, |
| name: 'value', |
| evaluateName: null, |
| allowCallingToString: evaluateToStringInDebugViews, |
| ); |
| variables.add(variable); |
| } on vm.SentinelException catch (e) { |
| variables.add(Variable( |
| name: 'value', |
| value: e.sentinel.valueAsString ?? '<sentinel>', |
| variablesReference: 0, |
| )); |
| } |
| } else if (data is vm.MapAssociation) { |
| final key = data.key; |
| final value = data.value; |
| if (key is vm.InstanceRef && value is vm.InstanceRef) { |
| // For a MapAssociation, we create a dummy set of variables for "key" and |
| // "value" so that each may be expanded if they are complex values. |
| variables.addAll([ |
| Variable( |
| name: 'key', |
| value: await _converter.convertVmInstanceRefToDisplayString( |
| thread, |
| key, |
| allowCallingToString: evaluateToStringInDebugViews, |
| format: format, |
| ), |
| variablesReference: _converter.isSimpleKind(key.kind) |
| ? 0 |
| : thread.storeData(VariableData(key, format)), |
| ), |
| Variable( |
| name: 'value', |
| value: await _converter.convertVmInstanceRefToDisplayString( |
| thread, |
| value, |
| allowCallingToString: evaluateToStringInDebugViews, |
| format: format, |
| ), |
| variablesReference: _converter.isSimpleKind(value.kind) |
| ? 0 |
| : thread.storeData(VariableData(value, format)), |
| evaluateName: |
| buildEvaluateName('', parentInstanceRefId: value.id)), |
| ]); |
| } |
| } else if (data is vm.ObjRef) { |
| try { |
| final object = await isolateManager.getObject( |
| storedData.thread.isolate, |
| data, |
| offset: childStart, |
| count: childCount, |
| ); |
| |
| if (object is vm.Sentinel) { |
| variables.add(Variable( |
| name: '<eval error>', |
| value: object.valueAsString ?? '<sentinel>', |
| variablesReference: 0, |
| )); |
| } else if (object is vm.Instance) { |
| variables.addAll(await _converter.convertVmInstanceToVariablesList( |
| thread, |
| object, |
| evaluateName: buildEvaluateName('', parentInstanceRefId: data.id), |
| allowCallingToString: evaluateToStringInDebugViews, |
| startItem: childStart, |
| numItems: childCount, |
| format: format, |
| )); |
| } else { |
| variables.add(Variable( |
| name: '<eval error>', |
| value: object.runtimeType.toString(), |
| variablesReference: 0, |
| )); |
| } |
| } on vm.SentinelException catch (e) { |
| variables.add(Variable( |
| name: '<eval error>', |
| value: e.sentinel.valueAsString ?? '<sentinel>', |
| variablesReference: 0, |
| )); |
| } |
| } else if (data is VariableGetter && service != null) { |
| final variable = await _converter.createVariableForGetter( |
| service, |
| thread, |
| data.instance, |
| // Empty names for lazy variable values because they were already shown |
| // in the parent object. |
| variableName: '', |
| getterName: data.getterName, |
| evaluateName: data.parentEvaluateName, |
| allowCallingToString: data.allowCallingToString, |
| format: format, |
| ); |
| variables.add(variable); |
| } |
| |
| sendResponse(VariablesResponseBody(variables: variables)); |
| } |
| |
| /// Gets global variables for the library of [frame]. |
| Future<List<vm.FieldRef>> _getFrameGlobals( |
| ThreadInfo thread, |
| vm.Frame frame, |
| ) async { |
| final scriptRef = frame.location?.script; |
| if (scriptRef == null) { |
| return []; |
| } |
| |
| final script = await thread.getScript(scriptRef); |
| final libraryRef = script.library; |
| if (libraryRef == null) { |
| return []; |
| } |
| |
| final library = await thread.getObject(libraryRef); |
| if (library is! vm.Library) { |
| return []; |
| } |
| |
| return library.variables ?? []; |
| } |
| |
| /// Fixes up a VM Service WebSocket URI to not have a trailing /ws |
| /// and use the HTTP scheme which is what DDS expects. |
| Uri vmServiceUriToHttp(Uri uri) { |
| final isSecure = uri.isScheme('https') || uri.isScheme('wss'); |
| uri = uri.replace(scheme: isSecure ? 'https' : 'http'); |
| |
| final segments = uri.pathSegments; |
| if (segments.isNotEmpty && segments.last == 'ws') { |
| uri = uri.replace(pathSegments: segments.take(segments.length - 1)); |
| } |
| |
| return uri; |
| } |
| |
| /// Fixes up a VM Service [uri] to a WebSocket URI with a trailing /ws |
| /// for connecting when not using DDS. |
| /// |
| /// DDS does its own cleaning up of the URI. |
| Uri vmServiceUriToWebSocket(Uri uri) { |
| // The VM Service library always expects the WebSockets URI so fix the |
| // scheme (http -> ws, https -> wss). |
| final isSecure = uri.isScheme('https') || uri.isScheme('wss'); |
| uri = uri.replace(scheme: isSecure ? 'wss' : 'ws'); |
| |
| if (uri.path.endsWith('/ws') || uri.path.endsWith('/ws/')) { |
| return uri; |
| } |
| |
| final append = uri.path.endsWith('/') ? 'ws' : '/ws'; |
| final newPath = '${uri.path}$append'; |
| return uri.replace(path: newPath); |
| } |
| |
| /// Creates one or more OutputEvents for the provided [message]. |
| /// |
| /// Messages that contain stack traces may be split up into separate events |
| /// for each frame to allow location metadata to be attached. |
| Future<List<OutputEventBody>> _buildOutputEvents( |
| String category, |
| String message, { |
| int? variablesReference, |
| }) async { |
| if (variablesReference != null) { |
| return [ |
| OutputEventBody( |
| category: category, |
| output: message, |
| variablesReference: variablesReference, |
| ) |
| ]; |
| } else { |
| try { |
| return await _buildOutputEventsWithSourceReferences(category, message); |
| } catch (e, s) { |
| // Since callers of [sendOutput] may not await it, don't allow unhandled |
| // errors (for example if the VM Service quits while we were trying to |
| // map URIs), just log and return the event without metadata. |
| logger?.call('Failed to build OutputEvent: $e, $s'); |
| return [OutputEventBody(category: category, output: message)]; |
| } |
| } |
| } |
| |
| /// Builds OutputEvents with source references if they contain stack frames. |
| /// |
| /// If a stack trace can be parsed from [message], file/line information will |
| /// be included in the metadata of the event. |
| Future<List<OutputEventBody>> _buildOutputEventsWithSourceReferences( |
| String category, String message) async { |
| final events = <OutputEventBody>[]; |
| |
| // Extract all the URIs so we can send a batch request for resolving them. |
| final lines = message.split('\n'); |
| final frames = lines.map(parseDartStackFrame).toList(); |
| final uris = frames.nonNulls.map((f) => f.uri).toList(); |
| |
| // We need an Isolate to resolve package URIs. Since we don't know what |
| // isolate printed an error to stderr, we just have to use the first one and |
| // hope the packages are available. If one is not available (which should |
| // never be the case), we will just skip resolution. |
| final thread = isolateManager.threads.firstOrNull; |
| |
| // Send a batch request. This will cache the results so we can easily use |
| // them in the loop below by calling the method again. |
| if (uris.isNotEmpty && thread != null) { |
| try { |
| await Future.wait<void>([ |
| // Used to resolve paths to make them clickable. |
| thread.resolveUrisToPathsBatch(uris), |
| // We'll also want to use isExternalPackageLibrary to fade out non-user |
| // stack frames, so cache the result for the lib paths in bulk too. |
| thread.resolveUrisToPackageLibPathsBatch(uris), |
| ]); |
| } catch (e, s) { |
| // Ignore errors that may occur if the VM is shutting down before we got |
| // this request out. In most cases we will have pre-cached the results |
| // when the libraries were loaded (in order to check if they're user code) |
| // so it's likely this won't cause any issues (dart:isolate-patch is an |
| // exception seen that appears in the stack traces but was not previously |
| // seen/cached). |
| logger?.call('Failed to resolve URIs: $e\n$s'); |
| } |
| } |
| |
| // Convert any URIs to paths and if we successfully get a path, check |
| // whether it's inside the users workspace so we can fade out unrelated |
| // frames. |
| final paths = await Future.wait(frames.map((frame) async { |
| final uri = frame?.uri; |
| if (uri == null) return null; |
| if (isSupportedFileScheme(uri)) { |
| return (uri: uri, isUserCode: isInUserProject(uri)); |
| } |
| if (thread == null || !isResolvableUri(uri)) return null; |
| try { |
| final fileLikeUri = await thread.resolveUriToPath(uri); |
| return fileLikeUri != null |
| ? (uri: fileLikeUri, isUserCode: isInUserProject(fileLikeUri)) |
| : null; |
| } catch (e, s) { |
| // Swallow errors for the same reason noted above. |
| logger?.call('Failed to resolve URIs: $e\n$s'); |
| } |
| return null; |
| })); |
| |
| final supportsAnsiColors = args.allowAnsiColorOutput ?? false; |
| for (var i = 0; i < lines.length; i++) { |
| final line = lines[i]; |
| final frame = frames[i]; |
| final uri = frame?.uri; |
| final pathInfo = paths[i]; |
| |
| // A file-like URI ('file://' or 'dart-macro+file://'). |
| final fileLikeUri = pathInfo?.uri; |
| |
| // Default to true so that if we don't know whether this is user-project |
| // then we leave the formatting as-is and don't fade anything out. |
| final isUserProject = pathInfo?.isUserCode ?? true; |
| |
| // For the name, we usually use the package URI, but if we only had a file |
| // URI to begin with, try to make it relative to cwd so it's not so long. |
| final name = uri != null && fileLikeUri != null |
| ? (uri.isScheme('file') |
| ? _converter.convertToRelativePath(uri.toFilePath()) |
| : uri.toString()) |
| : null; |
| |
| // If this is non-user code, fade out the stack frame line so that user |
| // lines are more visible. |
| final linePrefix = |
| !isUserProject && supportsAnsiColors ? '\u001B[2m' : ''; // 2=dim |
| final lineSuffix = |
| !isUserProject && supportsAnsiColors ? '\u001B[0m' : ''; // 0=reset |
| |
| // Because we split on newlines, all items except the last one need to |
| // have their trailing newlines added back. |
| final lineEnd = i != lines.length - 1 ? '\n' : ''; |
| final output = '$linePrefix$line$lineSuffix$lineEnd'; |
| |
| // If the output is empty (for example the output ended with \n so after |
| // splitting by \n, the last iteration is empty) then we don't need |
| // to add any event. |
| if (output.isEmpty) { |
| continue; |
| } |
| |
| final clientPath = |
| fileLikeUri != null ? toClientPathOrUri(fileLikeUri) : null; |
| events.add( |
| OutputEventBody( |
| category: category, |
| output: output, |
| source: |
| clientPath != null ? Source(name: name, path: clientPath) : null, |
| line: frame?.line, |
| column: frame?.column, |
| ), |
| ); |
| } |
| |
| return events; |
| } |
| |
| /// Handles evaluation of an expression that is (or begins with) |
| /// `threadExceptionExpression` which corresponds to the exception at the top |
| /// of [thread]. |
| Future<vm.Response?> _evaluateExceptionExpression( |
| int exceptionReference, |
| String expression, |
| ThreadInfo thread, |
| ) async { |
| final exception = isolateManager.getStoredData(exceptionReference)?.data |
| as vm.InstanceRef?; |
| |
| if (exception == null) { |
| return null; |
| } |
| |
| if (expression == threadExceptionExpression) { |
| return exception; |
| } |
| |
| // Strip the prefix off since we'll evaluate against the exception |
| // by its ID. |
| final expressionWithoutExceptionExpression = |
| expression.substring(threadExceptionExpression.length + 1); |
| |
| return vmEvaluate( |
| thread, |
| exception.id!, |
| expressionWithoutExceptionExpression, |
| ); |
| } |
| |
| /// Sends a VM 'evaluate' request for [thread]. |
| Future<vm.Response?> vmEvaluate( |
| ThreadInfo thread, |
| String targetId, |
| String expression, { |
| bool? disableBreakpoints = true, |
| }) async { |
| final isolateId = thread.isolate.id!; |
| final futureOrEvalZoneId = thread.currentEvaluationZoneId; |
| final evalZoneId = futureOrEvalZoneId is String |
| ? futureOrEvalZoneId |
| : await futureOrEvalZoneId; |
| |
| return vmService?.evaluate( |
| isolateId, |
| targetId, |
| expression, |
| disableBreakpoints: disableBreakpoints, |
| idZoneId: evalZoneId, |
| ); |
| } |
| |
| /// Sends a VM 'evaluateInFrame' request for [thread]. |
| Future<vm.Response?> vmEvaluateInFrame( |
| ThreadInfo thread, |
| int frameIndex, |
| String expression, { |
| bool? disableBreakpoints = true, |
| }) async { |
| final isolateId = thread.isolate.id!; |
| final futureOrEvalZoneId = thread.currentEvaluationZoneId; |
| final evalZoneId = futureOrEvalZoneId is String |
| ? futureOrEvalZoneId |
| : await futureOrEvalZoneId; |
| |
| return vmService?.evaluateInFrame( |
| isolateId, |
| frameIndex, |
| expression, |
| disableBreakpoints: disableBreakpoints, |
| idZoneId: evalZoneId, |
| ); |
| } |
| |
| @protected |
| @mustCallSuper |
| Future<void> handleDebugEvent(vm.Event event) async { |
| // Delay processing any events until the debugger initialization has |
| // finished running, as events may arrive (for ex. IsolateRunnable) while |
| // it's doing is own initialization that this may interfere with. |
| await debuggerInitialized; |
| |
| await isolateManager.handleEvent(event); |
| |
| final eventKind = event.kind; |
| final isolate = event.isolate; |
| // We pause isolates on exit to allow requests for resolving URIs in |
| // stderr call stacks, so when we see an isolate pause, wait for any |
| // pending logs and then resume it (so it exits). |
| if (eventKind == vm.EventKind.kPauseExit && isolate != null) { |
| await _waitForPendingOutputEvents(); |
| await isolateManager.readyToResumeIsolate(isolate); |
| } |
| } |
| |
| @protected |
| @mustCallSuper |
| Future<void> handleExtensionEvent(vm.Event event) async { |
| await debuggerInitialized; |
| |
| // Base Dart does not do anything here, but other DAs (like Flutter) may |
| // override it to do their own handling. |
| } |
| |
| @protected |
| @mustCallSuper |
| Future<void> handleIsolateEvent(vm.Event event) async { |
| // Delay processing any events until the debugger initialization has |
| // finished running, as events may arrive (for ex. IsolateRunnable) while |
| // it's doing is own initialization that this may interfere with. |
| await debuggerInitialized; |
| |
| // Allow IsolateManager to handle any state-related events. |
| await isolateManager.handleEvent(event); |
| |
| switch (event.kind) { |
| // Pass any Service Extension events on to the client so they can enable |
| // functionality based upon them. |
| case vm.EventKind.kServiceExtensionAdded: |
| this._sendServiceExtensionAdded( |
| event.extensionRPC!, |
| event.isolate!.id!, |
| ); |
| break; |
| } |
| } |
| |
| /// Helper to convert to InstanceRef to a complete untruncated unquoted |
| /// String, handling [vm.InstanceKind.kNull] which is the type for the unused |
| /// fields of a log event. |
| Future<String?> getFullString(ThreadInfo thread, vm.InstanceRef? ref) async { |
| if (ref == null || ref.kind == vm.InstanceKind.kNull) { |
| return null; |
| } |
| return _converter |
| .convertVmInstanceRefToDisplayString( |
| thread, |
| ref, |
| // Always allow calling toString() here as the user expects the full |
| // string they logged regardless of the evaluateToStringInDebugViews |
| // setting. |
| allowCallingToString: true, |
| allowTruncatedValue: false, |
| format: VariableFormat.noQuotes(), |
| ) |
| // Fetching strings from the server may throw if they have been |
| // collected since (for example if a Hot Restart occurs while |
| // we're running this) or if the app is terminating. Log the error and |
| // just return null so nothing is shown. |
| .then<String?>( |
| (s) => s, |
| onError: (Object e) { |
| logger?.call('$e'); |
| return null; |
| }, |
| ); |
| } |
| |
| /// Handles a dart:developer log() event, sending output to the client. |
| @protected |
| @mustCallSuper |
| Future<void> handleLoggingEvent(vm.Event event) async { |
| final record = event.logRecord; |
| final thread = isolateManager.threadForIsolate(event.isolate); |
| if (record == null || thread == null) { |
| return; |
| } |
| |
| var loggerName = await getFullString(thread, record.loggerName); |
| if (loggerName?.isEmpty ?? true) { |
| loggerName = 'log'; |
| } |
| final message = await getFullString(thread, record.message); |
| final error = await getFullString(thread, record.error); |
| final stack = await getFullString(thread, record.stackTrace); |
| |
| final prefix = '[$loggerName] '; |
| |
| if (message != null) { |
| sendPrefixedOutput('console', prefix, '$message\n'); |
| } |
| if (error != null) { |
| sendPrefixedOutput('console', prefix, '$error\n'); |
| } |
| if (stack != null) { |
| sendPrefixedOutput('console', prefix, '$stack\n'); |
| } |
| } |
| |
| @protected |
| @mustCallSuper |
| Future<void> handleServiceEvent(vm.Event event) async { |
| await debuggerInitialized; |
| |
| switch (event.kind) { |
| // Service registrations are passed to the client so they can toggle |
| // behaviour based on their presence. |
| case vm.EventKind.kServiceRegistered: |
| this._sendServiceRegistration(event.service!, event.method!); |
| break; |
| case vm.EventKind.kServiceUnregistered: |
| this._sendServiceUnregistration(event.service!, event.method!); |
| break; |
| } |
| } |
| |
| /// Resolves any URI stored in [data] with key [field] to a local file URI via |
| /// the VM Service and adds it to [data] with a 'resolved' prefix. |
| /// |
| /// A resolved URI will not be added if the URI cannot be resolved or is |
| /// already a 'file://' URI. |
| Future<void> resolveToolEventUris( |
| vm.IsolateRef? isolate, |
| Map<String, Object?> data, |
| String field, |
| ) async { |
| final thread = isolateManager.threadForIsolate(isolate); |
| if (thread == null) { |
| return; |
| } |
| |
| final uriString = data[field]; |
| if (uriString is! String) { |
| return; |
| } |
| final uri = Uri.tryParse(uriString); |
| if (uri == null) { |
| return; |
| } |
| |
| // Doesn't need resolving if already file-like. |
| if (isSupportedFileScheme(uri)) { |
| return; |
| } |
| |
| final fileLikeUri = await thread.resolveUriToPath(uri); |
| if (fileLikeUri != null) { |
| // Convert: |
| // uri -> resolvedUri |
| // fileUri -> resolvedFileUri |
| final resolvedFieldName = |
| 'resolved${field.substring(0, 1).toUpperCase()}${field.substring(1)}'; |
| data[resolvedFieldName] = fileLikeUri.toString(); |
| } |
| } |
| |
| @protected |
| @mustCallSuper |
| Future<void> handleToolEvent(vm.Event event) async { |
| await debuggerInitialized; |
| |
| // Some events will contain URIs that need to first be mapped to file URIs |
| // so the IDE can understand them. |
| final data = event.extensionData?.data; |
| if (data is Map<String, Object?>) { |
| const uriFieldNames = ['fileUri', 'uri']; |
| for (final fieldName in uriFieldNames) { |
| await resolveToolEventUris(event.isolate, data, fieldName); |
| } |
| } |
| |
| sendEvent( |
| RawEventBody({ |
| 'kind': event.extensionKind, |
| 'data': data, |
| }), |
| eventType: 'dart.toolEvent', |
| ); |
| } |
| |
| void _handleStderrEvent(vm.Event event) { |
| _sendOutputStreamEvent('stderr', event); |
| } |
| |
| void _handleStdoutEvent(vm.Event event) { |
| _sendOutputStreamEvent('stdout', event); |
| } |
| |
| Future<void> _handleVmServiceClosed() async { |
| isTerminating = true; |
| if (terminateOnVmServiceClose) { |
| handleSessionTerminate(); |
| } |
| } |
| |
| void _logTraffic(String message) { |
| logger?.call(message); |
| if (sendLogsToClient) { |
| sendEvent(RawEventBody({"message": message}), eventType: 'dart.log'); |
| } |
| } |
| |
| /// Performs some setup that is common to both [launchRequest] and |
| /// [attachRequest]. |
| Future<void> _prepareForLaunchOrAttach(bool? noDebug) async { |
| _sendLogsToClient = args.sendLogsToClient ?? false; |
| |
| // Don't start launching until configurationDone. |
| if (!_configurationDoneCompleter.isCompleted) { |
| logger?.call('Waiting for configurationDone request...'); |
| await _configurationDoneCompleter.future; |
| } |
| |
| // Change our current directory to match that of the request. This solves |
| // some issues parsing stack traces because `package:stack_trace` will |
| // convert relative to absolute paths using `path.absolute()`. |
| final cwd = args.cwd; |
| if (cwd != null) { |
| Directory.current = Directory(cwd); |
| } |
| |
| // Notify IsolateManager if we'll be debugging so it knows whether to set |
| // up breakpoints etc. when isolates are registered. |
| final debug = !(noDebug ?? false); |
| isolateManager.debug = debug; |
| isolateManager.debugSdkLibraries = args.debugSdkLibraries ?? true; |
| isolateManager.debugExternalPackageLibraries = |
| args.debugExternalPackageLibraries ?? true; |
| } |
| |
| /// Sends output for a VM WriteEvent to the client. |
| /// |
| /// Used to pass stdout/stderr when there's no access to the streams directly. |
| void _sendOutputStreamEvent(String type, vm.Event event) { |
| final data = event.bytes; |
| if (data == null) { |
| return; |
| } |
| final message = utf8.decode(base64Decode(data)); |
| sendOutput('stdout', message); |
| } |
| |
| void _sendServiceExtensionAdded(String extensionRPC, String isolateId) { |
| sendEvent( |
| RawEventBody({'extensionRPC': extensionRPC, 'isolateId': isolateId}), |
| eventType: 'dart.serviceExtensionAdded', |
| ); |
| } |
| |
| void _sendServiceRegistration(String service, String method) { |
| sendEvent( |
| RawEventBody({'service': service, 'method': method}), |
| eventType: 'dart.serviceRegistered', |
| ); |
| } |
| |
| void _sendServiceUnregistration(String service, String method) { |
| sendEvent( |
| RawEventBody({'service': service, 'method': method}), |
| eventType: 'dart.serviceUnregistered', |
| ); |
| } |
| |
| /// Updates the current debug options for the session. |
| /// |
| /// Clients may not know about all debug options, so anything not included |
| /// in the map will not be updated by this method. |
| Future<void> _updateDebugOptions(Map<String, Object?> args) async { |
| if (args.containsKey('debugSdkLibraries')) { |
| isolateManager.debugSdkLibraries = args['debugSdkLibraries'] as bool; |
| } |
| if (args.containsKey('debugExternalPackageLibraries')) { |
| isolateManager.debugExternalPackageLibraries = |
| args['debugExternalPackageLibraries'] as bool; |
| } |
| await isolateManager.applyDebugOptions(); |
| } |
| |
| /// Configures whether verbose logs should be sent to the client in `dart.log` |
| /// events. |
| Future<void> _updateSendLogsToClient(Map<String, Object?> args) async { |
| if (args.containsKey('enabled')) { |
| _sendLogsToClient = args['enabled'] as bool; |
| } |
| } |
| |
| /// A wrapper around the same name function from package:vm_service that |
| /// allows logging all traffic over the VM Service. |
| Future<vm.VmService> _vmServiceConnectUri(String wsUri) async { |
| final socket = await WebSocket.connect(wsUri); |
| final controller = StreamController(); |
| final streamClosedCompleter = Completer(); |
| final logger = this.logger; |
| |
| socket.listen( |
| (data) { |
| _logTraffic('<== [VM] $data'); |
| controller.add(data); |
| }, |
| onDone: () => streamClosedCompleter.complete(), |
| ); |
| |
| return vm.VmService( |
| controller.stream, |
| (String message) { |
| logger?.call('==> [VM] $message'); |
| _logTraffic('==> [VM] $message'); |
| socket.add(message); |
| }, |
| log: logger != null ? VmServiceLogger(logger) : null, |
| disposeHandler: () => socket.close(), |
| streamClosed: streamClosedCompleter.future, |
| ); |
| } |
| |
| /// Wraps a function with an error handler that handles errors that occur when |
| /// the VM Service/DDS shuts down. |
| /// |
| /// When the debug adapter is terminating, it's possible in-flight requests |
| /// triggered by handlers will fail with "Service Disappeared". This is |
| /// normal and such errors can be ignored, rather than allowed to pass |
| /// uncaught. |
| _StreamEventHandler<T> _wrapHandlerWithErrorHandling<T>( |
| _StreamEventHandler<T> handler, |
| ) { |
| return (data) => _withErrorHandling(() => handler(data)); |
| } |
| |
| /// Waits for any pending async output events that might be in progress. |
| /// |
| /// If another output event is queued while waiting, the new event will be |
| /// waited for, until there are no more. |
| Future<void> _waitForPendingOutputEvents() async { |
| // Keep awaiting it as long as it's changing to allow for other |
| // events being queued up while it runs. |
| var lastEvent = _lastOutputEvent; |
| do { |
| lastEvent = _lastOutputEvent; |
| await lastEvent; |
| } while (lastEvent != _lastOutputEvent); |
| } |
| |
| /// Calls a function with an error handler that handles errors that occur when |
| /// the VM Service/DDS shuts down. |
| /// |
| /// When the debug adapter is terminating, it's possible in-flight requests |
| /// will fail with "Service Disappeared". This is normal and such errors can |
| /// be ignored, rather than allowed to pass uncaught. |
| FutureOr<T?> _withErrorHandling<T>(FutureOr<T> Function() func) async { |
| try { |
| return await func(); |
| } on vm.RPCError catch (e) { |
| // kServiceDisappeared is thrown sometimes when the VM Service is |
| // shutting down. Usually this is because we're shutting down (and |
| // `isTerminating` is true), but it can also happen if the app is closed |
| // outside of the DAP (eg. closing the simulator) so it's possible our |
| // requests will fail in this way before we've handled any event to set |
| // `isTerminating`. |
| if (e.code == RpcErrorCodes.kServiceDisappeared || |
| e.code == RpcErrorCodes.kConnectionDisposed) { |
| return null; |
| } |
| |
| // For any other kind of server error, ignore it if we're shutting down |
| // (because lots of requests can generate all sorts of errors if the VM |
| // and Isolates are shutting down), or if it's a "client closed with |
| // pending request" error (which also indicates a shutdown, but as above, |
| // we might not have set `isTerminating` yet). |
| if (e.code == json_rpc_errors.SERVER_ERROR) { |
| // Ignore all server errors during shutdown. |
| if (isTerminating) { |
| return null; |
| } |
| |
| // Always ignore "client is closed" and "closed with pending request" |
| // errors because these can always occur during shutdown if we were |
| // just starting to send (or had just sent) a request. |
| if (e.message.contains("The client is closed") || |
| e.message.contains("The client closed with pending request") || |
| e.message.contains("Service connection disposed")) { |
| return null; |
| } |
| } |
| |
| // Otherwise, it's an unexpected/unknown failure and should be rethrown. |
| rethrow; |
| } |
| } |
| |
| /// Whether the current client supports URIs in place of file paths, including |
| /// file-like URIs that are not the 'file' scheme (such as 'dart-macro+file'). |
| bool get clientSupportsUri => _initializeArgs?.supportsDartUris ?? false; |
| |
| /// Returns whether [uri] is a file-like URI scheme that is supported by the |
| /// client. |
| /// |
| /// Returning `true` here does not guarantee that the client supports URIs, |
| /// the caller should also check [clientSupportsUri]. |
| bool isSupportedFileScheme(Uri uri) { |
| return uri.isScheme('file') || |
| // Handle all file-like schemes that end '+file' like |
| // 'dart-macro+file://'. |
| (clientSupportsUri && uri.scheme.endsWith('+file')); |
| } |
| |
| /// Converts a URI into a form that can be used by the client. |
| /// |
| /// If the client supports URIs (like VS Code), it will be returned unchanged |
| /// but otherwise it will be the `toFilePath()` equivalent if a 'file://' URI |
| /// and otherwise `null`. |
| String? toClientPathOrUri(Uri? uri) { |
| if (uri == null) { |
| return null; |
| } else if (clientSupportsUri) { |
| return uri.toString(); |
| } else if (uri.isScheme('file')) { |
| return uri.toFilePath(); |
| } else { |
| return null; |
| } |
| } |
| |
| /// Converts a String used by the client as a path/URI into a [Uri]. |
| Uri fromClientPathOrUri(String filePathOrUriString) { |
| var uri = Uri.tryParse(filePathOrUriString); |
| if (uri == null || !isSupportedFileScheme(uri)) { |
| uri = Uri.file(filePathOrUriString); |
| } |
| return uri; |
| } |
| } |
| |
| /// An implementation of [LaunchRequestArguments] that includes all fields used |
| /// by the Dart CLI and test debug adapters. |
| /// |
| /// This class represents the data passed from the client editor to the debug |
| /// adapter in launchRequest, which is a request to start debugging an |
| /// application. |
| /// |
| /// Specialized adapters (such as Flutter) have their own versions of this |
| /// class. |
| class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments |
| implements LaunchRequestArguments { |
| /// A reader for protocol arguments that throws detailed exceptions if |
| /// arguments aren't of the correct type. |
| static final arg = DebugAdapterArgumentReader('launch'); |
| |
| /// If noDebug is true the launch request should launch the program without |
| /// enabling debugging. |
| @override |
| final bool? noDebug; |
| |
| /// The program/Dart script to be run. |
| final String program; |
| |
| /// Arguments to be passed to [program]. |
| final List<String>? args; |
| |
| /// Arguments to be passed to the tool that will run [program] (for example, |
| /// the VM or Flutter tool). |
| final List<String>? toolArgs; |
| |
| /// Arguments to be passed directly to the Dart VM that will run [program]. |
| /// |
| /// Unlike [toolArgs] which always go after the complete tool, these args |
| /// always go directly after `dart`: |
| /// |
| /// - dart {vmAdditionalArgs} {toolArgs} |
| /// - dart {vmAdditionalArgs} run test:test {toolArgs} |
| final List<String>? vmAdditionalArgs; |
| |
| final int? vmServicePort; |
| |
| /// Which console to run the program in. |
| /// |
| /// If "terminal" or "externalTerminal" will cause the program to be run by |
| /// the client by having the server call the `runInTerminal` request on the |
| /// client (as long as the client advertises support for |
| /// `runInTerminalRequest`). |
| /// |
| /// Otherwise will run inside the debug adapter and stdout/stderr will be |
| /// routed to the client using [OutputEvent]s. This is the default (and |
| /// simplest) way, but prevents the user from being able to type into `stdin`. |
| final String? console; |
| |
| /// An optional tool to run instead of "dart". |
| /// |
| /// In combination with [customToolReplacesArgs] allows invoking a custom |
| /// tool instead of "dart" to launch scripts/tests. The custom tool must be |
| /// completely compatible with the tool/command it is replacing. |
| /// |
| /// This field should be a full absolute path if the tool may not be available |
| /// in `PATH`. |
| final String? customTool; |
| |
| /// The number of arguments to delete from the beginning of the argument list |
| /// when invoking [customTool]. |
| /// |
| /// For example, setting [customTool] to `dart_test` and |
| /// `customToolReplacesArgs` to `2` for a test run would invoke |
| /// `dart_test foo_test.dart` instead of `dart run test:test foo_test.dart`. |
| final int? customToolReplacesArgs; |
| |
| DartLaunchRequestArguments({ |
| this.noDebug, |
| required this.program, |
| this.args, |
| this.vmServicePort, |
| this.toolArgs, |
| this.vmAdditionalArgs, |
| this.console, |
| this.customTool, |
| this.customToolReplacesArgs, |
| super.restart, |
| super.name, |
| super.cwd, |
| super.env, |
| super.additionalProjectPaths, |
| super.debugSdkLibraries, |
| super.debugExternalPackageLibraries, |
| super.showGettersInDebugViews, |
| super.evaluateGettersInDebugViews, |
| super.evaluateToStringInDebugViews, |
| super.sendLogsToClient, |
| super.sendCustomProgressEvents = null, |
| super.allowAnsiColorOutput, |
| }); |
| |
| DartLaunchRequestArguments.fromMap(super.obj) |
| : noDebug = arg.read<bool?>(obj, 'noDebug'), |
| program = arg.read<String>(obj, 'program'), |
| args = arg.readOptionalList<String>(obj, 'args'), |
| toolArgs = arg.readOptionalList<String>(obj, 'toolArgs'), |
| vmAdditionalArgs = |
| arg.readOptionalList<String>(obj, 'vmAdditionalArgs'), |
| vmServicePort = arg.read<int?>(obj, 'vmServicePort'), |
| console = arg.read<String?>(obj, 'console'), |
| customTool = arg.read<String?>(obj, 'customTool'), |
| customToolReplacesArgs = arg.read<int?>(obj, 'customToolReplacesArgs'), |
| super.fromMap(); |
| |
| @override |
| Map<String, Object?> toJson() => { |
| ...super.toJson(), |
| if (noDebug != null) 'noDebug': noDebug, |
| 'program': program, |
| if (args != null) 'args': args, |
| if (toolArgs != null) 'toolArgs': toolArgs, |
| if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs, |
| if (vmServicePort != null) 'vmServicePort': vmServicePort, |
| if (console != null) 'console': console, |
| if (customTool != null) 'customTool': customTool, |
| if (customToolReplacesArgs != null) |
| 'customToolReplacesArgs': customToolReplacesArgs, |
| }; |
| |
| static DartLaunchRequestArguments fromJson(Map<String, Object?> obj) => |
| DartLaunchRequestArguments.fromMap(obj); |
| } |
| |
| /// A helper for checking whether the available DDS instance has specific |
| /// capabilities. |
| class _DdsCapabilities { |
| final int major; |
| final int minor; |
| |
| static const empty = _DdsCapabilities(major: 0, minor: 0); |
| |
| const _DdsCapabilities({required this.major, required this.minor}); |
| |
| /// Whether the DDS instance supports custom streams via `dart:developer`'s |
| /// `postEvent`. |
| bool get supportsCustomStreams => _isAtLeast(major: 1, minor: 4); |
| |
| bool _isAtLeast({required major, required minor}) { |
| if (this.major > major) { |
| return true; |
| } else if (this.major == major && this.minor >= minor) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |