blob: 12fc7691b399ce7724fcccd75f9ffe7d3e85127a [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:io';
import 'package:collection/collection.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../base_debug_adapter.dart';
import '../exceptions.dart';
import '../isolate_manager.dart';
import '../logging.dart';
import '../protocol_converter.dart';
import '../protocol_generated.dart';
import '../protocol_stream.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';
/// 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<T extends DartLaunchRequestArguments>
extends BaseDebugAdapter<T> {
late final T 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;
/// 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;
DartDebugAdapter(ByteStreamServerChannel channel, Logger? logger)
: super(channel, logger) {
_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;
/// [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,
T args,
void Function() sendResponse,
) async {
this.args = args;
isAttach = true;
// Common setup.
await _prepareForLaunchOrAttach();
// TODO(dantup): Implement attach support.
throw UnimplementedError();
// Delegate to the sub-class to attach to the process.
// await attachImpl();
//
// sendResponse();
}
/// 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) async {
// 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');
logger?.call('Connecting to debugger at $uri');
sendOutput('console', 'Connecting to VM Service at $uri\n');
final vmService =
await _vmServiceConnectUri(uri.toString(), logger: logger);
logger?.call('Connected to debugger at $uri!');
// TODO(dantup): VS Code currently depends on a custom dart.debuggerUris
// event to notify it of VM Services that become available (for example to
// register with the DevTools server). If this is still required, it will
// need implementing here (and also documented as a customisation and
// perhaps gated on a capability/argument).
this.vmService = vmService;
_subscriptions.addAll([
vmService.onIsolateEvent.listen(_handleIsolateEvent),
vmService.onDebugEvent.listen(_handleDebugEvent),
vmService.onLoggingEvent.listen(_handleLoggingEvent),
// TODO(dantup): Implement these.
// vmService.onExtensionEvent.listen(_handleExtensionEvent),
// vmService.onServiceEvent.listen(_handleServiceEvent),
// vmService.onStdoutEvent.listen(_handleStdoutEvent),
// vmService.onStderrEvent.listen(_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);
// Process any existing isolates that may have been created before the
// streams above were set up.
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;
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.resumeIsolate(isolate);
}
}));
_debuggerInitializedCompleter.complete();
}
/// 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));
}
/// 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 {
await disconnectImpl();
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();
final exceptionReference = thread.exceptionReference;
final isExceptionExpression = expression == threadExceptionExpression ||
expression.startsWith('$threadExceptionExpression.');
vm.Response? result;
if (exceptionReference != null && isExceptionExpression) {
result = await _evaluateExceptionExpression(
exceptionReference,
expression,
thread,
);
} else {
result = await vmService?.evaluateInFrame(
thread.isolate.id!,
frameIndex,
expression,
disableBreakpoints: true,
);
}
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: true,
);
// TODO(dantup): We may need to store `expression` with this data
// to allow building nested evaluateNames.
final variablesReference =
_converter.isSimpleKind(result.kind) ? 0 : thread.storeData(result);
sendResponse(EvaluateResponseBody(
result: resultString,
variablesReference: variablesReference,
));
} else {
throw DebugAdapterException(
'Unknown evaluation response type: ${result?.runtimeType}',
);
}
}
/// [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 {
// 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,
// TODO(dantup): All of these...
// supportsConditionalBreakpoints: true,
supportsConfigurationDoneRequest: true,
supportsDelayedStackTraceLoading: true,
supportsEvaluateForHovers: true,
// supportsLogPoints: true,
// supportsRestartFrame: true,
supportsTerminateRequest: true,
));
// This must only be sent AFTER the response!
sendEvent(InitializedEventBody());
}
/// 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,
T args,
void Function() sendResponse,
) async {
this.args = args;
isAttach = false;
// Common setup.
await _prepareForLaunchOrAttach();
// Delegate to the sub-class to launch the process.
await launchImpl();
sendResponse();
}
/// 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();
}
/// [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: 'Variables',
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 used by buffered data).
void sendOutput(String category, String message) {
sendEvent(OutputEventBody(category: category, output: message));
}
/// 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());
}
/// 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.
final limit = startFrame + numFrames;
final stack = await vmService?.getStack(thread.isolate.id!, limit: limit);
final frames = 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;
Future<StackFrame> convert(int index, vm.Frame frame) async {
return _converter.convertVmToDapStackFrame(
thread,
frame,
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();
}
/// 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 {
await terminateImpl();
sendResponse();
}
/// [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) {
return _converter.convertVmResponseToVariable(
thread,
variable.value,
name: variable.name,
allowCallingToString: index <= maxToStringsPerEvaluation,
);
}
variables.addAll(await Future.wait(vars.mapIndexed(convert)));
}
} else if (vmData is vm.MapAssociation) {
// TODO(dantup): Maps
} 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) {
// TODO(dantup): evaluateName
// should be built taking the parent into account, for ex. if
// args.variablesReference == thread.exceptionReference then we need to
// use some sythensized variable name like `frameExceptionExpression`.
variables.addAll(await _converter.convertVmInstanceToVariablesList(
thread,
object,
startItem: childStart,
numItems: childCount,
));
} else {
variables.add(Variable(
name: '<eval error>',
value: object.runtimeType.toString(),
variablesReference: 0,
));
}
}
variables.sortBy((v) => v.name);
sendResponse(VariablesResponseBody(variables: variables));
}
/// 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,
);
}
void _handleDebugEvent(vm.Event event) {
_isolateManager.handleEvent(event);
}
void _handleIsolateEvent(vm.Event event) {
_isolateManager.handleEvent(event);
}
/// Handles a dart:developer log() event, sending output to the client.
Future<void> _handleLoggingEvent(vm.Event event) async {
final record = event.logRecord;
final thread = _isolateManager.threadForIsolate(event.isolate);
if (record == null || thread == null) {
return;
}
/// Helper to convert to InstanceRef to a String, taking into account
/// [vm.InstanceKind.kNull] which is the type for the unused fields of a
/// log event.
Future<String?> asString(vm.InstanceRef? ref) async {
if (ref == null || ref.kind == vm.InstanceKind.kNull) {
return null;
}
// TODO(dantup): This should handle truncation and complex types.
return ref.valueAsString;
}
var loggerName = await asString(record.loggerName);
if (loggerName?.isEmpty ?? true) {
loggerName = 'log';
}
final message = await asString(record.message);
final error = await asString(record.error);
final stack = await asString(record.stackTrace);
final prefix = '[$loggerName] ';
if (message != null) {
sendPrefixedOutput('stdout', prefix, '$message\n');
}
if (error != null) {
sendPrefixedOutput('stderr', prefix, '$error\n');
}
if (stack != null) {
sendPrefixedOutput('stderr', prefix, '$stack\n');
}
}
/// Performs some setup that is common to both [launchRequest] and
/// [attachRequest].
Future<void> _prepareForLaunchOrAttach() 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 = !(args.noDebug ?? false);
_isolateManager.setDebugEnabled(debug);
}
/// 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, {
Logger? logger,
}) async {
final socket = await WebSocket.connect(wsUri);
final controller = StreamController();
final streamClosedCompleter = Completer();
socket.listen(
(data) {
logger?.call('<== [VM] $data');
controller.add(data);
},
onDone: () => streamClosedCompleter.complete(),
);
return vm.VmService(
controller.stream,
(String message) {
logger?.call('==> [VM] $message');
socket.add(message);
},
log: logger != null ? VmServiceLogger(logger) : null,
disposeHandler: () => socket.close(),
streamClosed: streamClosedCompleter.future,
);
}
}
/// 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 extend this class with
/// their own additional fields.
class DartLaunchRequestArguments extends LaunchRequestArguments {
final String program;
final List<String>? args;
final String? cwd;
final String? vmServiceInfoFile;
final int? vmServicePort;
final List<String>? vmAdditionalArgs;
final bool? enableAsserts;
/// 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;
DartLaunchRequestArguments({
Object? restart,
bool? noDebug,
required this.program,
this.args,
this.cwd,
this.vmServiceInfoFile,
this.vmServicePort,
this.vmAdditionalArgs,
this.enableAsserts,
this.debugSdkLibraries,
this.debugExternalPackageLibraries,
this.evaluateGettersInDebugViews,
this.evaluateToStringInDebugViews,
this.sendLogsToClient,
}) : super(restart: restart, noDebug: noDebug);
DartLaunchRequestArguments.fromMap(Map<String, Object?> obj)
: program = obj['program'] as String,
args = (obj['args'] as List?)?.cast<String>(),
cwd = obj['cwd'] as String?,
vmServiceInfoFile = obj['vmServiceInfoFile'] as String?,
vmServicePort = obj['vmServicePort'] as int?,
vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
enableAsserts = obj['enableAsserts'] as bool?,
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?,
super.fromMap(obj);
@override
Map<String, Object?> toJson() => {
...super.toJson(),
'program': program,
if (args != null) 'args': args,
if (cwd != null) 'cwd': cwd,
if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile,
if (vmServicePort != null) 'vmServicePort': vmServicePort,
if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs,
if (enableAsserts != null) 'enableAsserts': enableAsserts,
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,
};
static DartLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
DartLaunchRequestArguments.fromMap(obj);
}