[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:
Danny Tuppeny 2021-08-23 23:22:48 +00:00 committed by commit-bot@chromium.org
parent cccc9f93b2
commit 63f1fb02a4
9 changed files with 487 additions and 151 deletions

View file

@ -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) =>

View file

@ -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) {

View file

@ -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,

View 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);
}

View file

@ -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)!);
}

View file

@ -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(

View file

@ -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

View file

@ -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 = '''

View file

@ -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;