blob: bbfd62692dcc9151a140990b52884d00f618578e [file] [log] [blame]
// 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: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 '../exceptions.dart';
import '../isolate_manager.dart';
import '../logging.dart';
import '../protocol_common.dart';
import '../protocol_converter.dart';
import '../protocol_generated.dart';
import '../protocol_stream.dart';
import '../utils.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 = 10;
/// 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(.*)');
/// 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;
/// Pattern for a trailing semicolon.
final _trailingSemicolonPattern = RegExp(r';$');
/// An implementation of [AttachRequestArguments] that includes all fields used
/// by the base Dart debug adapter.
///
/// 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.
///
/// Specialised adapters (such as Flutter) will likely 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;
DartAttachRequestArguments({
this.vmServiceUri,
this.vmServiceInfoFile,
Object? restart,
String? name,
String? cwd,
List<String>? additionalProjectPaths,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
bool? evaluateToStringInDebugViews,
bool? sendLogsToClient,
}) : super(
name: name,
cwd: cwd,
restart: restart,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
sendLogsToClient: sendLogsToClient,
);
DartAttachRequestArguments.fromMap(Map<String, Object?> obj)
: vmServiceUri = obj['vmServiceUri'] as String?,
vmServiceInfoFile = obj['vmServiceInfoFile'] as String?,
super.fromMap(obj);
@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 {
/// 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 `false` if null, which means "step in" will not step into SDK
/// libraries.
final bool? debugSdkLibraries;
/// Whether external package libraries should be marked as debuggable.
///
/// Treated as `false` if null, which means "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 evaluate getters in debug views like hovers and the variables
/// list.
///
/// Invoking getters 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? 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;
DartCommonLaunchAttachRequestArguments({
required this.restart,
required this.name,
required this.cwd,
// TODO(dantup): This can be made required after Flutter DAP is passing it.
this.env,
required this.additionalProjectPaths,
required this.debugSdkLibraries,
required this.debugExternalPackageLibraries,
required this.evaluateGettersInDebugViews,
required this.evaluateToStringInDebugViews,
required this.sendLogsToClient,
});
DartCommonLaunchAttachRequestArguments.fromMap(Map<String, Object?> obj)
: restart = obj['restart'],
name = obj['name'] as String?,
cwd = obj['cwd'] as String?,
env = (obj['env'] as Map<String, Object?>?)?.cast<String, String>(),
additionalProjectPaths =
(obj['additionalProjectPaths'] as List?)?.cast<String>(),
debugSdkLibraries = obj['debugSdkLibraries'] as bool?,
debugExternalPackageLibraries =
obj['debugExternalPackageLibraries'] as bool?,
evaluateGettersInDebugViews =
obj['evaluateGettersInDebugViews'] as bool?,
evaluateToStringInDebugViews =
obj['evaluateToStringInDebugViews'] as bool?,
sendLogsToClient = obj['sendLogsToClient'] as bool?;
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 (evaluateGettersInDebugViews != null)
'evaluateGettersInDebugViews': evaluateGettersInDebugViews,
if (evaluateToStringInDebugViews != null)
'evaluateToStringInDebugViews': evaluateToStringInDebugViews,
if (sendLogsToClient != null) 'sendLogsToClient': sendLogsToClient,
};
}
/// 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> {
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 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.VmServiceInterface? vmService;
/// The DDS instance that was started and that [vmService] is connected to.
///
/// `null` if the session is running in noDebug mode of the connection has not
/// yet been made.
DartDevelopmentService? _dds;
/// The [InitializeRequestArguments] provided by the client in the
/// `initialize` request.
///
/// `null` if the `initialize` request has not yet been made.
InitializeRequestArguments? _initializeArgs;
/// Whether to use IPv6 for DAP/Debugger services.
final bool ipv6;
/// Whether to enable DDS for launched applications.
final bool enableDds;
/// Whether to enable authentication codes for the VM Service/DDS.
final bool enableAuthCodes;
/// A logger for printing diagnostic information.
final Logger? logger;
/// Whether the current debug session is an attach request (as opposed to a
/// launch request). Not available until after launchRequest or attachRequest
/// have been called.
late final bool isAttach;
/// 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,
].whereNotNull().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;
late final sendLogsToClient = args.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 isolates that pause in the PauseExit state should be automatically
/// resumed after any in-process log events have completed.
///
/// Normally this will be true, but it may be set to false if the user
/// also manually passes pause-isolates-on-exit.
bool resumeIsolatesAfterPauseExit = 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;
/// 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 {
// 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,
this.enableDds = true,
this.enableAuthCodes = true,
this.logger,
Function? onError,
}) : super(channel, onError: onError) {
channel.closed.then((_) => shutdown());
_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.
InitializeRequestArguments? 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;
/// 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 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 {
this.args = args as DartCommonLaunchAttachRequestArguments;
isAttach = true;
_subscribeToOutputStreams = true;
// When attaching to a process, suppress auto-resuming isolates until the
// first time the user resumes anything.
_isolateManager.autoResumeStartingIsolates = false;
// Common setup.
await _prepareForLaunchOrAttach(null);
// Delegate to the sub-class to attach to the process.
await attachImpl();
sendResponse();
}
/// 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 normalised.
/// The caller should handle any other normalisation (such as adding /ws to
/// the end if required).
Future<void> connectDebugger(
Uri uri, {
// TODO(dantup): Remove this after parameter after updating the Flutter
// DAP to not pass it.
bool? resumeIfStarting,
}) async {
// Start up a DDS instance for this VM.
if (enableDds) {
logger?.call('Starting a DDS instance for $uri');
try {
final dds = await DartDevelopmentService.startDartDevelopmentService(
vmServiceUriToHttp(uri),
enableAuthCodes: enableAuthCodes,
ipv6: ipv6,
);
_dds = dds;
uri = dds.wsUri!;
} on DartDevelopmentServiceException catch (e) {
// If there's already a DDS instance, then just continue. This is common
// when attaching, as the program may have already been run with a DDS
// instance.
if (e.errorCode ==
DartDevelopmentServiceException.existingDdsInstanceError) {
uri = vmServiceUriToWebSocket(uri);
} else {
rethrow;
}
}
} else {
uri = vmServiceUriToWebSocket(uri);
}
logger?.call('Connecting to debugger at $uri');
sendOutput('console', 'Connecting to VM Service at $uri\n');
final vmService = await _vmServiceConnectUri(uri.toString());
logger?.call('Connected to debugger at $uri!');
// 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 (_subscribeToOutputStreams)
vmService.onStdoutEvent.listen(wrap(_handleStdoutEvent)),
if (_subscribeToOutputStreams)
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),
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 _withErrorHandling(
() => _configureExistingIsolates(vmService, vmInfo),
);
_debuggerInitializedCompleter.complete();
}
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',
);
}
/// 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)
.whereNotNull()
.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;
final thread =
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) {
// If requested, automatically resume. Otherwise send a Stopped event to
// inform the client UI the thread is paused.
if (_isolateManager.autoResumeStartingIsolates) {
await _isolateManager.resumeIsolate(isolate);
} else {
_isolateManager.sendStoppedOnEntryEvent(thread.threadId);
}
}
}));
}
/// 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 {
await _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;
// 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;
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();
await shutdown();
sendResponse();
}
/// 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;
// TODO(dantup): Special handling for clipboard/watch (see Dart-Code DAP) to
// avoid wrapping strings in quotes, etc.
// 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;
}
}
if (thread == null || frameIndex == null) {
// TODO(dantup): Dart-Code evaluates these in the context of the rootLib
// rather than just not supporting it. Consider something similar (or
// better here).
throw UnimplementedError('Global evaluation not currently supported');
}
// 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 expression = 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 exceptionReference = thread.exceptionReference;
final isExceptionExpression = expression == threadExceptionExpression ||
expression.startsWith('$threadExceptionExpression.');
vm.Response? result;
try {
if (exceptionReference != null && isExceptionExpression) {
result = await _evaluateExceptionExpression(
exceptionReference,
expression,
thread,
);
} else {
result = await vmService?.evaluateInFrame(
thread.isolate.id!,
frameIndex,
expression,
disableBreakpoints: true,
);
}
} 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) {
final resultString = await _converter.convertVmInstanceRefToDisplayString(
thread,
result,
allowCallingToString: evaluateToStringInDebugViews,
);
final variablesReference =
_converter.isSimpleKind(result.kind) ? 0 : thread.storeData(result);
// 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 != null ? match.group(1)! : null;
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 != null ? match.group(1)! : null;
return shortError ?? rawError;
}
/// 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. Send directly via sendEvent and not sendOutput to ensure no
// async since we're about to terminate.
sendEvent(OutputEventBody(output: '\nExited$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,
InitializeRequestArguments 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,
supportsLogPoints: true,
supportsRestartRequest: supportsRestartRequest,
// TODO(dantup): All of these...
// 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 packagePath = await thread.resolveUriToPackageLibPath(uri);
if (packagePath == null) {
return false;
}
// 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.
final packagePathLower = packagePath.toLowerCase();
return !projectPaths
.map((projectPath) => projectPath.toLowerCase())
.any((projectPath) => path.isWithin(projectPath, packagePathLower));
}
/// 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 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 {
this.args = args as DartCommonLaunchAttachRequestArguments;
isAttach = false;
// Common setup.
await _prepareForLaunchOrAttach(args.noDebug);
// Delegate to the sub-class to launch the process.
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 (await isExternalPackageLibrary(thread, uri)) {
return _isolateManager.debugExternalPackageLibraries;
} 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();
}
/// 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 scopes = <Scope>[];
// For local variables, we can just reuse the frameId as variablesReference
// as variablesRequest handles stored data of type `Frame` directly.
scopes.add(Scope(
name: 'Locals',
presentationHint: 'locals',
variablesReference: args.frameId,
expensive: false,
));
// If the top frame has an exception, add an additional section to allow
// that to be inspected.
final data = _isolateManager.getStoredData(args.frameId);
final exceptionReference = data?.thread.exceptionReference;
if (exceptionReference != null) {
scopes.add(Scope(
name: 'Exceptions',
variablesReference: exceptionReference,
expensive: false,
));
}
sendResponse(ScopesResponseBody(scopes: scopes));
}
/// Sends an OutputEvent (without a newline, since calls to this method
/// may be using buffered data that is not split cleanly on newlines).
///
/// If [category] is `stderr`, will also look for stack traces and extract
/// file/line information to add to the metadata of the event.
///
/// 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) 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);
// 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 ? Uri.file(path).toString() : name!;
await _isolateManager.setBreakpoints(uri, breakpoints);
// TODO(dantup): Handle breakpoint resolution rather than pretending all
// breakpoints are verified immediately.
sendResponse(SetBreakpointsResponseBody(
breakpoints: breakpoints.map((e) => Breakpoint(verified: true)).toList(),
));
}
/// 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 and cleans up.
///
/// This is called by [disconnectRequest] and [terminateRequest] but may also
/// be called if the client just disconnects from the server without calling
/// either.
///
/// This method must tolerate being called multiple times.
@mustCallSuper
Future<void> shutdown() async {
await _dds?.shutdown();
}
/// [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) {
throw DebugAdapterException('No thread with threadId $threadId');
}
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 bounday (e.g. rewind) since we're showing
// the user async frames which are out-of-sync with the real frames
// past that point.
final firstAsyncMarkerIndex = frames.indexWhere(
(frame) => frame.kind == vm.FrameKind.kAsyncSuspensionMarker,
);
// 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)
.whereNotNull()
.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();
await shutdown();
sendResponse();
}
/// 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));
}
/// Sets the package config file to use for `package: URI` resolution.
///
/// It is no longer necessary to call this method as the package config file
/// is no longer used. URI lookups are done via the VM Service.
@Deprecated('No longer necessary, URI lookups are done via VM Service')
void usePackageConfigFile(File packageConfig) {
// TODO(dantup): Remove this method after Flutter DA is updated not to use
// it.
}
/// [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 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;
final data = storedData.data;
final vmData = data is vm.Response ? data : null;
final variables = <Variable>[];
if (vmData is vm.Frame) {
final vars = vmData.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.
storeEvaluateName(variable.value, variable.name);
return _converter.convertVmResponseToVariable(
thread,
variable.value,
name: variable.name,
allowCallingToString: evaluateToStringInDebugViews &&
index <= maxToStringsPerEvaluation,
evaluateName: variable.name,
);
}
variables.addAll(await Future.wait(vars.mapIndexed(convert)));
// Sort the variables by name.
variables.sortBy((v) => v.name);
}
} 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,
),
variablesReference:
_converter.isSimpleKind(key.kind) ? 0 : thread.storeData(key),
),
Variable(
name: 'value',
value: await _converter.convertVmInstanceRefToDisplayString(
thread,
value,
allowCallingToString: evaluateToStringInDebugViews,
),
variablesReference: _converter.isSimpleKind(value.kind)
? 0
: thread.storeData(value),
evaluateName:
buildEvaluateName('', parentInstanceRefId: value.id)),
]);
}
} else if (vmData is vm.ObjRef) {
final object =
await _isolateManager.getObject(storedData.thread.isolate, vmData);
if (object is vm.Sentinel) {
variables.add(Variable(
name: '<eval error>',
value: object.valueAsString.toString(),
variablesReference: 0,
));
} else if (object is vm.Instance) {
variables.addAll(await _converter.convertVmInstanceToVariablesList(
thread,
object,
evaluateName: buildEvaluateName('', parentInstanceRefId: vmData.id),
allowCallingToString: evaluateToStringInDebugViews,
startItem: childStart,
numItems: childCount,
));
} else {
variables.add(Variable(
name: '<eval error>',
value: object.runtimeType.toString(),
variablesReference: 0,
));
}
}
sendResponse(VariablesResponseBody(variables: 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 an Observatory [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,
) async {
try {
if (category == 'stderr') {
return await _buildStdErrOutputEvents(message);
} else {
return [OutputEventBody(category: category, output: 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 for stderr.
///
/// If a stack trace can be parsed from [message], file/line information will
/// be included in the metadata of the event.
Future<List<OutputEventBody>> _buildStdErrOutputEvents(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(parseStackFrame).toList();
final uris = frames.whereNotNull().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) {
try {
await thread?.resolveUrisToPathsBatch(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.
final paths = await Future.wait(frames.map((frame) async {
final uri = frame?.uri;
if (uri == null) return null;
if (uri.isScheme('file')) return uri.toFilePath();
if (isResolvableUri(uri)) {
try {
return await thread?.resolveUriToPath(uri);
} catch (e, s) {
// Swallow errors for the same reason noted above.
logger?.call('Failed to resolve URIs: $e\n$s');
}
}
return null;
}));
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
final frame = frames[i];
final uri = frame?.uri;
final path = paths[i];
// For the name, we usually use the package URI, but if we only ended up
// with a file URI, try to make it relative to cwd so it's not so long.
final name = uri != null && path != null
? (uri.isScheme('file')
? _converter.convertToRelativePath(path)
: uri.toString())
: null;
// Because we split on newlines, all items exept the last one need to
// have their trailing newlines added back.
final output = i == lines.length - 1 ? line : '$line\n';
events.add(
OutputEventBody(
category: 'stderr',
output: output,
source: path != null ? Source(name: name, path: path) : 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 vmService?.evaluate(
thread.isolate.id!,
exception.id!,
expressionWithoutExceptionExpression,
disableBreakpoints: true,
);
}
@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 (resumeIsolatesAfterPauseExit &&
eventKind == vm.EventKind.kPauseExit &&
isolate != null) {
await _waitForPendingOutputEvents();
await _isolateManager.resumeIsolate(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 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,
includeQuotesAroundString: false,
)
.catchError((e) {
// 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). Log the error and just return null so
// nothing is shown.
logger?.call('$e');
});
}
/// 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;
}
}
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 {
// Don't start launching until configurationDone.
if (!_configurationDoneCompleter.isCompleted) {
logger?.call('Waiting for configurationDone request...');
await _configurationDoneCompleter.future;
}
// 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();
}
/// 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) {
// If we're been asked to shut down while this request was occurring,
// it's normal to get kServiceDisappeared so we should handle this
// silently.
if (isTerminating && e.code == RpcErrorCodes.kServiceDisappeared) {
return null;
}
rethrow;
}
}
}
/// An implementation of [LaunchRequestArguments] that includes all fields used
/// by the base Dart debug adapter.
///
/// 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.
///
/// Specialised adapters (such as Flutter) will likely have their own versions
/// of this class.
class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
implements LaunchRequestArguments {
/// If noDebug is true the launch request should launch the program without
/// enabling debugging.
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,
Object? restart,
String? name,
String? cwd,
Map<String, String>? env,
List<String>? additionalProjectPaths,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
bool? evaluateToStringInDebugViews,
bool? sendLogsToClient,
}) : super(
restart: restart,
name: name,
cwd: cwd,
env: env,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
sendLogsToClient: sendLogsToClient,
);
DartLaunchRequestArguments.fromMap(Map<String, Object?> obj)
: noDebug = obj['noDebug'] as bool?,
program = obj['program'] as String,
args = (obj['args'] as List?)?.cast<String>(),
toolArgs = (obj['toolArgs'] as List?)?.cast<String>(),
vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
vmServicePort = obj['vmServicePort'] as int?,
console = obj['console'] as String?,
customTool = obj['customTool'] as String?,
customToolReplacesArgs = obj['customToolReplacesArgs'] as int?,
super.fromMap(obj);
@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);
}