mirror of
https://github.com/dart-lang/sdk
synced 2024-10-04 16:04:53 +00:00
[dds] Support attachRequest in Dart CLI DAP
Change-Id: I4bcd7483ab9f3ed8dd839bd69b59d21f8139c582 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/210724 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
cccc9f93b2
commit
63f1fb02a4
|
@ -3,6 +3,7 @@
|
|||
// 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';
|
||||
|
@ -54,9 +55,173 @@ 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 [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 DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
|
||||
implements AttachRequestArguments {
|
||||
/// 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 vmServiceUri;
|
||||
|
||||
DartAttachRequestArguments({
|
||||
this.restart,
|
||||
required this.vmServiceUri,
|
||||
String? name,
|
||||
String? cwd,
|
||||
String? vmServiceInfoFile,
|
||||
List<String>? additionalProjectPaths,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
bool? sendLogsToClient,
|
||||
}) : super(
|
||||
name: name,
|
||||
cwd: cwd,
|
||||
vmServiceInfoFile: vmServiceInfoFile,
|
||||
additionalProjectPaths: additionalProjectPaths,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
sendLogsToClient: sendLogsToClient,
|
||||
);
|
||||
|
||||
DartAttachRequestArguments.fromMap(Map<String, Object?> obj)
|
||||
: restart = obj['restart'],
|
||||
vmServiceUri = obj['vmServiceUri'] as String,
|
||||
super.fromMap(obj);
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
...super.toJson(),
|
||||
if (restart != null) 'restart': restart,
|
||||
'vmServiceUri': vmServiceUri,
|
||||
};
|
||||
|
||||
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 {
|
||||
final String? name;
|
||||
final String? cwd;
|
||||
final String? vmServiceInfoFile;
|
||||
|
||||
/// 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.name,
|
||||
required this.cwd,
|
||||
required this.vmServiceInfoFile,
|
||||
required this.additionalProjectPaths,
|
||||
required this.debugSdkLibraries,
|
||||
required this.debugExternalPackageLibraries,
|
||||
required this.evaluateGettersInDebugViews,
|
||||
required this.evaluateToStringInDebugViews,
|
||||
required this.sendLogsToClient,
|
||||
});
|
||||
|
||||
DartCommonLaunchAttachRequestArguments.fromMap(Map<String, Object?> obj)
|
||||
: name = obj['name'] as String?,
|
||||
cwd = obj['cwd'] as String?,
|
||||
vmServiceInfoFile = obj['vmServiceInfoFile'] as 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 (name != null) 'name': name,
|
||||
if (cwd != null) 'cwd': cwd,
|
||||
if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile,
|
||||
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).
|
||||
///
|
||||
|
@ -93,9 +258,9 @@ final _trailingSemicolonPattern = RegExp(r';$');
|
|||
/// 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;
|
||||
abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
|
||||
TA extends DartAttachRequestArguments> extends BaseDebugAdapter<TL, TA> {
|
||||
late final DartCommonLaunchAttachRequestArguments args;
|
||||
final _debuggerInitializedCompleter = Completer<void>();
|
||||
final _configurationDoneCompleter = Completer<void>();
|
||||
|
||||
|
@ -168,7 +333,8 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// 'cwd' and any 'additionalProjectPaths' from the launch arguments.
|
||||
late final List<String> projectPaths = [
|
||||
args.cwd,
|
||||
path.dirname(args.program),
|
||||
if (args is DartLaunchRequestArguments)
|
||||
path.dirname((args as DartLaunchRequestArguments).program),
|
||||
...?args.additionalProjectPaths,
|
||||
].whereNotNull().toList();
|
||||
|
||||
|
@ -220,28 +386,33 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// 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,
|
||||
T args,
|
||||
TA args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
this.args = args;
|
||||
isAttach = true;
|
||||
_subscribeToOutputStreams = true;
|
||||
|
||||
// Common setup.
|
||||
await _prepareForLaunchOrAttach();
|
||||
|
||||
// TODO(dantup): Implement attach support.
|
||||
throw UnimplementedError();
|
||||
await _prepareForLaunchOrAttach(null);
|
||||
|
||||
// Delegate to the sub-class to attach to the process.
|
||||
// await attachImpl();
|
||||
//
|
||||
// sendResponse();
|
||||
await attachImpl();
|
||||
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Builds an evaluateName given a parent VM InstanceRef ID and a suffix.
|
||||
|
@ -297,13 +468,25 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
// TODO(dantup): Do we need to worry about there already being one connected
|
||||
// if this URL came from another service that may have started one?
|
||||
logger?.call('Starting a DDS instance for $uri');
|
||||
final dds = await DartDevelopmentService.startDartDevelopmentService(
|
||||
uri,
|
||||
enableAuthCodes: enableAuthCodes,
|
||||
ipv6: ipv6,
|
||||
);
|
||||
_dds = dds;
|
||||
uri = dds.wsUri!;
|
||||
try {
|
||||
final dds = await DartDevelopmentService.startDartDevelopmentService(
|
||||
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 = _cleanVmServiceUri(uri);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uri = _cleanVmServiceUri(uri);
|
||||
}
|
||||
|
@ -328,8 +511,10 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
// TODO(dantup): Implement these.
|
||||
// vmService.onExtensionEvent.listen(_handleExtensionEvent),
|
||||
// vmService.onServiceEvent.listen(_handleServiceEvent),
|
||||
// vmService.onStdoutEvent.listen(_handleStdoutEvent),
|
||||
// vmService.onStderrEvent.listen(_handleStderrEvent),
|
||||
if (_subscribeToOutputStreams) ...[
|
||||
vmService.onStdoutEvent.listen(_handleStdoutEvent),
|
||||
vmService.onStderrEvent.listen(_handleStderrEvent),
|
||||
],
|
||||
]);
|
||||
await Future.wait([
|
||||
vmService.streamListen(vm.EventStreams.kIsolate),
|
||||
|
@ -337,8 +522,8 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
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),
|
||||
vmService.streamListen(vm.EventStreams.kStdout),
|
||||
vmService.streamListen(vm.EventStreams.kStderr),
|
||||
]);
|
||||
|
||||
final vmInfo = await vmService.getVM();
|
||||
|
@ -595,12 +780,15 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
}
|
||||
|
||||
/// Sends a [TerminatedEvent] if one has not already been sent.
|
||||
void handleSessionTerminate() {
|
||||
void handleSessionTerminate([String exitSuffix = '']) {
|
||||
if (_hasSentTerminatedEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
_hasSentTerminatedEvent = true;
|
||||
// Always add a leading newline since the last written text might not have
|
||||
// had one.
|
||||
sendOutput('console', '\nExited$exitSuffix.');
|
||||
sendEvent(TerminatedEventBody());
|
||||
}
|
||||
|
||||
|
@ -688,18 +876,18 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
|
||||
/// [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).
|
||||
/// [attachRequest] will be called).
|
||||
@override
|
||||
Future<void> launchRequest(
|
||||
Request request,
|
||||
T args,
|
||||
TL args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
this.args = args;
|
||||
isAttach = false;
|
||||
|
||||
// Common setup.
|
||||
await _prepareForLaunchOrAttach();
|
||||
await _prepareForLaunchOrAttach(args.noDebug);
|
||||
|
||||
// Delegate to the sub-class to launch the process.
|
||||
await launchImpl();
|
||||
|
@ -1326,6 +1514,14 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
}
|
||||
}
|
||||
|
||||
void _handleStderrEvent(vm.Event event) {
|
||||
_sendOutputStreamEvent('stderr', event);
|
||||
}
|
||||
|
||||
void _handleStdoutEvent(vm.Event event) {
|
||||
_sendOutputStreamEvent('stdout', event);
|
||||
}
|
||||
|
||||
Future<void> _handleVmServiceClosed() async {
|
||||
if (terminateOnVmServiceClose) {
|
||||
handleSessionTerminate();
|
||||
|
@ -1341,7 +1537,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
|
||||
/// Performs some setup that is common to both [launchRequest] and
|
||||
/// [attachRequest].
|
||||
Future<void> _prepareForLaunchOrAttach() async {
|
||||
Future<void> _prepareForLaunchOrAttach(bool? noDebug) async {
|
||||
// Don't start launching until configurationDone.
|
||||
if (!_configurationDoneCompleter.isCompleted) {
|
||||
logger?.call('Waiting for configurationDone request...');
|
||||
|
@ -1350,13 +1546,25 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
|
||||
/// Updates the current debug options for the session.
|
||||
///
|
||||
/// Clients may not know about all debug options, so anything not included
|
||||
|
@ -1415,24 +1623,23 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
///
|
||||
/// Specialised adapters (such as Flutter) will likely extend this class with
|
||||
/// their own additional fields.
|
||||
class DartLaunchRequestArguments extends LaunchRequestArguments {
|
||||
final String? name;
|
||||
class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
|
||||
implements LaunchRequestArguments {
|
||||
/// 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;
|
||||
|
||||
/// If noDebug is true the launch request should launch the program without
|
||||
/// enabling debugging.
|
||||
final bool? noDebug;
|
||||
|
||||
final String program;
|
||||
final List<String>? args;
|
||||
final String? cwd;
|
||||
final String? vmServiceInfoFile;
|
||||
final int? vmServicePort;
|
||||
final List<String>? vmAdditionalArgs;
|
||||
final bool? enableAsserts;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Which console to run the program in.
|
||||
///
|
||||
/// If "terminal" or "externalTerminal" will cause the program to be run by
|
||||
|
@ -1445,108 +1652,58 @@ class DartLaunchRequestArguments extends LaunchRequestArguments {
|
|||
/// simplest) way, but prevents the user from being able to type into `stdin`.
|
||||
final String? console;
|
||||
|
||||
/// 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,
|
||||
this.name,
|
||||
this.restart,
|
||||
this.noDebug,
|
||||
required this.program,
|
||||
this.args,
|
||||
this.cwd,
|
||||
this.vmServiceInfoFile,
|
||||
this.vmServicePort,
|
||||
this.vmAdditionalArgs,
|
||||
this.console,
|
||||
this.enableAsserts,
|
||||
this.additionalProjectPaths,
|
||||
this.debugSdkLibraries,
|
||||
this.debugExternalPackageLibraries,
|
||||
this.evaluateGettersInDebugViews,
|
||||
this.evaluateToStringInDebugViews,
|
||||
this.sendLogsToClient,
|
||||
}) : super(restart: restart, noDebug: noDebug);
|
||||
String? name,
|
||||
String? cwd,
|
||||
String? vmServiceInfoFile,
|
||||
List<String>? additionalProjectPaths,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
bool? sendLogsToClient,
|
||||
}) : super(
|
||||
name: name,
|
||||
cwd: cwd,
|
||||
vmServiceInfoFile: vmServiceInfoFile,
|
||||
additionalProjectPaths: additionalProjectPaths,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
sendLogsToClient: sendLogsToClient,
|
||||
);
|
||||
|
||||
DartLaunchRequestArguments.fromMap(Map<String, Object?> obj)
|
||||
: name = obj['name'] as String?,
|
||||
: restart = obj['restart'],
|
||||
noDebug = obj['noDebug'] as bool?,
|
||||
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>(),
|
||||
vmServicePort = obj['vmServicePort'] as int?,
|
||||
console = obj['console'] as String?,
|
||||
enableAsserts = obj['enableAsserts'] as bool?,
|
||||
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?,
|
||||
super.fromMap(obj);
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
...super.toJson(),
|
||||
if (name != null) 'name': name,
|
||||
if (restart != null) 'restart': restart,
|
||||
if (noDebug != null) 'noDebug': noDebug,
|
||||
'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 (vmServicePort != null) 'vmServicePort': vmServicePort,
|
||||
if (console != null) 'console': console,
|
||||
if (enableAsserts != null) 'enableAsserts': enableAsserts,
|
||||
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,
|
||||
};
|
||||
|
||||
static DartLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
|
||||
|
|
|
@ -16,7 +16,8 @@ import '../protocol_stream.dart';
|
|||
import 'dart.dart';
|
||||
|
||||
/// A DAP Debug Adapter for running and debugging Dart CLI scripts.
|
||||
class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
||||
class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
|
||||
DartAttachRequestArguments> {
|
||||
Process? _process;
|
||||
|
||||
/// The location of the vm-service-info file (if debugging).
|
||||
|
@ -41,6 +42,9 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
@override
|
||||
final parseLaunchArgs = DartLaunchRequestArguments.fromJson;
|
||||
|
||||
@override
|
||||
final parseAttachArgs = DartAttachRequestArguments.fromJson;
|
||||
|
||||
DartCliDebugAdapter(
|
||||
ByteStreamServerChannel channel, {
|
||||
bool ipv6 = false,
|
||||
|
@ -94,6 +98,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
/// For debugging, this should start paused, connect to the VM Service, set
|
||||
/// breakpoints, and resume.
|
||||
Future<void> launchImpl() async {
|
||||
final args = this.args as DartLaunchRequestArguments;
|
||||
final vmPath = Platform.resolvedExecutable;
|
||||
|
||||
final debug = !(args.noDebug ?? false);
|
||||
|
@ -143,7 +148,10 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
// TODO(dantup): Remove this once
|
||||
// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
|
||||
// necessary.
|
||||
final packageConfig = _findPackageConfigFile();
|
||||
var possibleRoot = path.isAbsolute(args.program)
|
||||
? path.dirname(args.program)
|
||||
: path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
|
||||
final packageConfig = _findPackageConfigFile(possibleRoot);
|
||||
if (packageConfig != null) {
|
||||
this.usePackageConfigFile(packageConfig);
|
||||
}
|
||||
|
@ -173,6 +181,26 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Called by [attachRequest] to request that we actually connect to the app
|
||||
/// to be debugged.
|
||||
Future<void> attachImpl() async {
|
||||
final args = this.args as DartAttachRequestArguments;
|
||||
|
||||
// Find the package_config file for this script.
|
||||
// TODO(dantup): Remove this once
|
||||
// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
|
||||
// necessary.
|
||||
final cwd = args.cwd;
|
||||
if (cwd != null) {
|
||||
final packageConfig = _findPackageConfigFile(cwd);
|
||||
if (packageConfig != null) {
|
||||
this.usePackageConfigFile(packageConfig);
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(connectDebugger(Uri.parse(args.vmServiceUri)));
|
||||
}
|
||||
|
||||
/// Calls the client (via a `runInTerminal` request) to spawn the process so
|
||||
/// that it can run in a local terminal that the user can interact with.
|
||||
Future<void> launchInEditorTerminal(
|
||||
|
@ -181,6 +209,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
String vmPath,
|
||||
List<String> processArgs,
|
||||
) async {
|
||||
final args = this.args as DartLaunchRequestArguments;
|
||||
logger?.call('Spawning $vmPath with $processArgs in ${args.cwd}'
|
||||
' via client ${terminalKind} terminal');
|
||||
|
||||
|
@ -241,11 +270,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
/// TODO(dantup): Remove this once
|
||||
/// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
|
||||
/// necessary.
|
||||
File? _findPackageConfigFile() {
|
||||
var possibleRoot = path.isAbsolute(args.program)
|
||||
? path.dirname(args.program)
|
||||
: path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
|
||||
|
||||
File? _findPackageConfigFile(String possibleRoot) {
|
||||
File? packageConfig;
|
||||
while (true) {
|
||||
packageConfig =
|
||||
|
@ -282,10 +307,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
void _handleExitCode(int code) {
|
||||
final codeSuffix = code == 0 ? '' : ' ($code)';
|
||||
logger?.call('Process exited ($code)');
|
||||
// Always add a leading newline since the last written text might not have
|
||||
// had one.
|
||||
sendOutput('console', '\nExited$codeSuffix.');
|
||||
handleSessionTerminate();
|
||||
handleSessionTerminate(codeSuffix);
|
||||
}
|
||||
|
||||
void _handleStderr(List<int> data) {
|
||||
|
|
|
@ -26,7 +26,8 @@ typedef _VoidNoArgRequestHandler<TArg> = Future<void> Function(
|
|||
/// appropriate method calls/events.
|
||||
///
|
||||
/// This class does not implement any DA functionality, only message handling.
|
||||
abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
||||
abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
|
||||
TAttachArgs extends AttachRequestArguments> {
|
||||
int _sequence = 1;
|
||||
final ByteStreamServerChannel _channel;
|
||||
|
||||
|
@ -38,6 +39,13 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
_channel.listen(_handleIncomingMessage);
|
||||
}
|
||||
|
||||
/// Parses arguments for [attachRequest] into a type of [TAttachArgs].
|
||||
///
|
||||
/// This method must be implemented by the implementing class using a class
|
||||
/// that corresponds to the arguments it expects (these may differ between
|
||||
/// Dart CLI, Dart tests, Flutter, Flutter tests).
|
||||
TAttachArgs Function(Map<String, Object?>) get parseAttachArgs;
|
||||
|
||||
/// Parses arguments for [launchRequest] into a type of [TLaunchArgs].
|
||||
///
|
||||
/// This method must be implemented by the implementing class using a class
|
||||
|
@ -47,7 +55,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
|
||||
Future<void> attachRequest(
|
||||
Request request,
|
||||
TLaunchArgs args,
|
||||
TAttachArgs args,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
|
@ -271,7 +279,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
} else if (request.command == 'launch') {
|
||||
handle(request, _withVoidResponse(launchRequest), parseLaunchArgs);
|
||||
} else if (request.command == 'attach') {
|
||||
handle(request, _withVoidResponse(attachRequest), parseLaunchArgs);
|
||||
handle(request, _withVoidResponse(attachRequest), parseAttachArgs);
|
||||
} else if (request.command == 'terminate') {
|
||||
handle(
|
||||
request,
|
||||
|
|
66
pkg/dds/test/dap/integration/debug_attach_test.dart
Normal file
66
pkg/dds/test/dap/integration/debug_attach_test.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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 'package:test/test.dart';
|
||||
|
||||
import 'test_client.dart';
|
||||
import 'test_scripts.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
main() {
|
||||
group('debug mode', () {
|
||||
late DapTestSession dap;
|
||||
setUp(() async {
|
||||
dap = await DapTestSession.setUp();
|
||||
});
|
||||
tearDown(() => dap.tearDown());
|
||||
|
||||
test('can attach to a simple script using vmServiceUri', () async {
|
||||
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
||||
|
||||
final args = ['one', 'two'];
|
||||
final proc = await startDartProcessPaused(
|
||||
testFile.path,
|
||||
args,
|
||||
cwd: dap.testAppDir.path,
|
||||
);
|
||||
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
|
||||
|
||||
final outputEvents = await dap.client.collectOutput(
|
||||
launch: () => dap.client.attach(
|
||||
vmServiceUri: vmServiceUri.toString(),
|
||||
cwd: dap.testAppDir.path,
|
||||
),
|
||||
);
|
||||
|
||||
// Expect a "console" output event that prints the URI of the VM Service
|
||||
// the debugger connects to.
|
||||
final vmConnection = outputEvents.first;
|
||||
expect(vmConnection.output,
|
||||
startsWith('Connecting to VM Service at ws://127.0.0.1:'));
|
||||
expect(vmConnection.category, equals('console'));
|
||||
|
||||
// Expect the normal applications output.
|
||||
final output = outputEvents
|
||||
.skip(1)
|
||||
.map((e) => e.output)
|
||||
// The stdout also contains the Observatory+DevTools banners.
|
||||
.where(
|
||||
(line) =>
|
||||
!line.startsWith('Observatory listening on') &&
|
||||
!line.startsWith(
|
||||
'The Dart DevTools debugger and profiler is available at'),
|
||||
)
|
||||
.join();
|
||||
expectLines(output, [
|
||||
'Hello!',
|
||||
'World!',
|
||||
'args: [one, two]',
|
||||
'',
|
||||
'Exited.',
|
||||
]);
|
||||
});
|
||||
// These tests can be slow due to starting up the external server process.
|
||||
}, timeout: Timeout.none);
|
||||
}
|
|
@ -20,13 +20,7 @@ main() {
|
|||
tearDown(() => dap.tearDown());
|
||||
|
||||
test('runs a simple script', () async {
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!');
|
||||
print('World!');
|
||||
print('args: $args');
|
||||
}
|
||||
''');
|
||||
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
||||
|
||||
final outputEvents = await dap.client.collectOutput(
|
||||
launch: () => dap.client.launch(
|
||||
|
@ -212,6 +206,6 @@ void main(List<String> args) async {
|
|||
Uri _extractVmServiceUri(OutputEventBody vmConnectionBanner) {
|
||||
// TODO(dantup): Change this to use the dart.debuggerUris custom event
|
||||
// if implemented (whch VS Code also needs).
|
||||
final match = vmServiceUriPattern.firstMatch(vmConnectionBanner.output);
|
||||
final match = dapVmServiceBannerPattern.firstMatch(vmConnectionBanner.output);
|
||||
return Uri.parse(match!.group(1)!);
|
||||
}
|
||||
|
|
|
@ -20,13 +20,7 @@ main() {
|
|||
|
||||
group('noDebug mode', () {
|
||||
test('runs a simple script', () async {
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!');
|
||||
print('World!');
|
||||
print('args: $args');
|
||||
}
|
||||
''');
|
||||
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
||||
|
||||
final outputEvents = await dap.client.collectOutput(
|
||||
launch: () => dap.client.launch(
|
||||
|
|
|
@ -59,6 +59,37 @@ class DapTestClient {
|
|||
Stream<OutputEventBody> get outputEvents => events('output')
|
||||
.map((e) => OutputEventBody.fromJson(e.body as Map<String, Object?>));
|
||||
|
||||
/// Send an attachRequest to the server, asking it to attach to an existing
|
||||
/// Dart program.
|
||||
Future<Response> attach({
|
||||
required String vmServiceUri,
|
||||
String? cwd,
|
||||
List<String>? additionalProjectPaths,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
}) {
|
||||
return sendRequest(
|
||||
DartAttachRequestArguments(
|
||||
vmServiceUri: vmServiceUri,
|
||||
cwd: cwd,
|
||||
additionalProjectPaths: additionalProjectPaths,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
// When running out of process, VM Service traffic won't be available
|
||||
// to the client-side logger, so force logging on which sends VM Service
|
||||
// traffic in a custom event.
|
||||
sendLogsToClient: captureVmServiceTraffic,
|
||||
),
|
||||
// We can't automatically pick the command when using a custom type
|
||||
// (DartAttachRequestArguments).
|
||||
overrideCommand: 'attach',
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends a continue request for the given thread.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
|
|
|
@ -20,6 +20,15 @@ const sdkStackFrameProgram = '''
|
|||
}
|
||||
''';
|
||||
|
||||
/// A simple Dart script that prints its arguments.
|
||||
const simpleArgPrintingProgram = r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!');
|
||||
print('World!');
|
||||
print('args: $args');
|
||||
}
|
||||
''';
|
||||
|
||||
/// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
|
||||
/// will contain multiple stack frames across some async boundaries.
|
||||
const simpleAsyncProgram = '''
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dds/src/dap/logging.dart';
|
||||
|
@ -14,6 +15,11 @@ import 'package:test/test.dart';
|
|||
import 'test_client.dart';
|
||||
import 'test_server.dart';
|
||||
|
||||
/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
|
||||
/// by the DAP adapter as the first output event for a debug session.
|
||||
final dapVmServiceBannerPattern =
|
||||
RegExp(r'Connecting to VM Service at ([^\s]+)\s');
|
||||
|
||||
/// Whether to run the DAP server in-process with the tests, or externally in
|
||||
/// another process.
|
||||
///
|
||||
|
@ -34,9 +40,9 @@ final verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true';
|
|||
/// an authentication token.
|
||||
final vmServiceAuthCodePathPattern = RegExp(r'^/[\w_\-=]{5,15}/ws$');
|
||||
|
||||
/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
|
||||
/// as the first output event for a debug session.
|
||||
final vmServiceUriPattern = RegExp(r'Connecting to VM Service at ([^\s]+)\s');
|
||||
/// A [RegExp] that matches the "Observatory listening on" banner that is sent
|
||||
/// by the VM when not using --write-service-info.
|
||||
final vmServiceBannerPattern = RegExp(r'Observatory listening on ([^\s]+)\s');
|
||||
|
||||
/// Expects [actual] to equal the lines [expected], ignoring differences in line
|
||||
/// endings and trailing whitespace.
|
||||
|
@ -72,6 +78,55 @@ expectResponseError<T>(Future<T> response, Matcher messageMatcher) {
|
|||
int lineWith(File file, String searchText) =>
|
||||
file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
|
||||
|
||||
Future<Process> startDartProcessPaused(
|
||||
String script,
|
||||
List<String> args, {
|
||||
required String cwd,
|
||||
List<String>? vmArgs,
|
||||
}) async {
|
||||
final vmPath = Platform.resolvedExecutable;
|
||||
vmArgs ??= [];
|
||||
vmArgs.addAll([
|
||||
'--enable-vm-service=0',
|
||||
'--pause_isolates_on_start',
|
||||
]);
|
||||
final processArgs = [
|
||||
...vmArgs,
|
||||
script,
|
||||
...args,
|
||||
];
|
||||
|
||||
return Process.start(
|
||||
vmPath,
|
||||
processArgs,
|
||||
workingDirectory: cwd,
|
||||
);
|
||||
}
|
||||
|
||||
/// Monitors [process] for the Observatory/VM Service banner and extracts the
|
||||
/// VM Service URI.
|
||||
Future<Uri> waitForStdoutVmServiceBanner(Process process) {
|
||||
final _vmServiceUriCompleter = Completer<Uri>();
|
||||
|
||||
late StreamSubscription<String> vmServiceBannerSub;
|
||||
vmServiceBannerSub = process.stdout.transform(utf8.decoder).listen(
|
||||
(line) {
|
||||
final match = vmServiceBannerPattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
_vmServiceUriCompleter.complete(Uri.parse(match.group(1)!));
|
||||
vmServiceBannerSub.cancel();
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!_vmServiceUriCompleter.isCompleted) {
|
||||
_vmServiceUriCompleter.completeError('Stream ended');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return _vmServiceUriCompleter.future;
|
||||
}
|
||||
|
||||
/// A helper class containing the DAP server/client for DAP integration tests.
|
||||
class DapTestSession {
|
||||
DapTestServer server;
|
||||
|
|
Loading…
Reference in a new issue