[dds] Add support for services/service extensions to DAP

Change-Id: I4f63fd60bf2128c8e88c999c0963b8a1e5304178
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/214281
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Danny Tuppeny 2021-09-24 11:53:15 +00:00 committed by commit-bot@chromium.org
parent 637fb323d9
commit 4389cf7bba
8 changed files with 358 additions and 59 deletions

View file

@ -67,22 +67,17 @@ var _subscribeToOutputStreams = false;
/// Pattern for a trailing semicolon.
final _trailingSemicolonPattern = RegExp(r';$');
/// An implementation of [LaunchRequestArguments] that includes all fields used
/// An implementation of [AttachRequestArguments] that includes all fields used
/// by the base Dart debug adapter.
///
/// This class represents the data passed from the client editor to the debug
/// adapter in launchRequest, which is a request to start debugging an
/// adapter in attachRequest, which is a request to start debugging an
/// application.
///
/// Specialised adapters (such as Flutter) will likely extend this class with
/// their own additional fields.
/// Specialised adapters (such as Flutter) will likely have their own versions
/// of this class.
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;
/// The VM Service URI to attach to.
///
/// Either this or [vmServiceInfoFile] must be supplied.
@ -94,9 +89,9 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
final String? vmServiceInfoFile;
DartAttachRequestArguments({
this.restart,
this.vmServiceUri,
this.vmServiceInfoFile,
Object? restart,
String? name,
String? cwd,
List<String>? additionalProjectPaths,
@ -108,6 +103,7 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
}) : super(
name: name,
cwd: cwd,
restart: restart,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
@ -117,15 +113,13 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
);
DartAttachRequestArguments.fromMap(Map<String, Object?> obj)
: restart = obj['restart'],
vmServiceUri = obj['vmServiceUri'] as String?,
: vmServiceUri = obj['vmServiceUri'] as String?,
vmServiceInfoFile = obj['vmServiceInfoFile'] as String?,
super.fromMap(obj);
@override
Map<String, Object?> toJson() => {
...super.toJson(),
if (restart != null) 'restart': restart,
if (vmServiceUri != null) 'vmServiceUri': vmServiceUri,
if (vmServiceInfoFile != null) 'vmServiceInfoFile': vmServiceInfoFile,
};
@ -137,6 +131,11 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
/// A common base for [DartLaunchRequestArguments] and
/// [DartAttachRequestArguments] for fields that are common to both.
class DartCommonLaunchAttachRequestArguments extends RequestArguments {
/// Optional data from the previous, restarted session.
/// The data is sent as the 'restart' attribute of the 'terminated' event.
/// The client should leave the data intact.
final Object? restart;
final String? name;
final String? cwd;
@ -187,6 +186,7 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
final bool? sendLogsToClient;
DartCommonLaunchAttachRequestArguments({
required this.restart,
required this.name,
required this.cwd,
required this.additionalProjectPaths,
@ -198,7 +198,8 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
});
DartCommonLaunchAttachRequestArguments.fromMap(Map<String, Object?> obj)
: name = obj['name'] as String?,
: restart = obj['restart'],
name = obj['name'] as String?,
cwd = obj['cwd'] as String?,
additionalProjectPaths =
(obj['additionalProjectPaths'] as List?)?.cast<String>(),
@ -212,6 +213,7 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
sendLogsToClient = obj['sendLogsToClient'] as bool?;
Map<String, Object?> toJson() => {
if (restart != null) 'restart': restart,
if (name != null) 'name': name,
if (cwd != null) 'cwd': cwd,
if (additionalProjectPaths != null)
@ -263,8 +265,8 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
/// an expression into an evaluation console) or to events sent by the server
/// (for example when the server sends a `StoppedEvent` it may cause the client
/// to then send a `stackTraceRequest` or `scopesRequest` to get variables).
abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
TA extends DartAttachRequestArguments> extends BaseDebugAdapter<TL, TA> {
abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
TA extends AttachRequestArguments> extends BaseDebugAdapter<TL, TA> {
late final DartCommonLaunchAttachRequestArguments args;
final _debuggerInitializedCompleter = Completer<void>();
final _configurationDoneCompleter = Completer<void>();
@ -378,6 +380,14 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
/// `null` if the `initialize` request has not yet been made.
InitializeRequestArguments? get initializeArgs => _initializeArgs;
/// Whether or not this adapter can handle the restartRequest.
///
/// If false, the editor will just terminate the debug session and start a new
/// one when the user asks to restart. If true, the adapter must implement
/// the [restartRequest] method and handle its own restart (for example the
/// Flutter adapter will perform a Hot Restart).
bool get supportsRestartRequest => false;
/// Whether the VM Service closing should be used as a signal to terminate the
/// debug session.
///
@ -407,7 +417,7 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
TA args,
void Function() sendResponse,
) async {
this.args = args;
this.args = args as DartCommonLaunchAttachRequestArguments;
isAttach = true;
_subscribeToOutputStreams = true;
@ -480,7 +490,7 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
logger?.call('Starting a DDS instance for $uri');
try {
final dds = await DartDevelopmentService.startDartDevelopmentService(
uri,
vmServiceUriToHttp(uri),
enableAuthCodes: enableAuthCodes,
ipv6: ipv6,
);
@ -492,13 +502,13 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
// instance.
if (e.errorCode ==
DartDevelopmentServiceException.existingDdsInstanceError) {
uri = _cleanVmServiceUri(uri);
uri = vmServiceUriToWebSocket(uri);
} else {
rethrow;
}
}
} else {
uri = _cleanVmServiceUri(uri);
uri = vmServiceUriToWebSocket(uri);
}
logger?.call('Connecting to debugger at $uri');
@ -506,21 +516,25 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
final vmService = await _vmServiceConnectUri(uri.toString());
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).
// Send a custom event with the VM Service URI as the editor might want to
// know about this (for example so it can connect an embedded DevTools to
// this app).
sendEvent(
RawEventBody({
'vmServiceUri': uri.toString(),
}),
eventType: 'dart.debuggerUris',
);
this.vmService = vmService;
unawaited(vmService.onDone.then((_) => _handleVmServiceClosed()));
_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.onIsolateEvent.listen(handleIsolateEvent),
vmService.onDebugEvent.listen(handleDebugEvent),
vmService.onLoggingEvent.listen(handleLoggingEvent),
vmService.onExtensionEvent.listen(handleExtensionEvent),
vmService.onServiceEvent.listen(handleServiceEvent),
if (_subscribeToOutputStreams)
vmService.onStdoutEvent.listen(_handleStdoutEvent),
if (_subscribeToOutputStreams)
@ -530,8 +544,8 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
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.kExtension),
vmService.streamListen(vm.EventStreams.kService),
vmService.streamListen(vm.EventStreams.kStdout),
vmService.streamListen(vm.EventStreams.kStderr),
]);
@ -633,6 +647,24 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
sendResponse(null);
break;
/// Allows an editor to call a service/service extension that it was told
/// about via a custom 'dart.serviceRegistered' or
/// 'dart.serviceExtensionAdded' event.
case 'callService':
final method = args?.args['method'] as String?;
if (method == null) {
throw DebugAdapterException(
'Method is required to call services/service extensions',
);
}
final params = args?.args['params'] as Map<String, Object?>?;
final response = await vmService?.callServiceExtension(
method,
args: params,
);
sendResponse(response?.json);
break;
default:
await super.customRequest(request, args, sendResponse);
}
@ -848,6 +880,7 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
supportsDelayedStackTraceLoading: true,
supportsEvaluateForHovers: true,
supportsLogPoints: true,
supportsRestartRequest: supportsRestartRequest,
// TODO(dantup): All of these...
// supportsRestartFrame: true,
supportsTerminateRequest: true,
@ -903,7 +936,7 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
TL args,
void Function() sendResponse,
) async {
this.args = args;
this.args = args as DartCommonLaunchAttachRequestArguments;
isAttach = false;
// Common setup.
@ -947,6 +980,23 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
/// not in the package mapping file.
String? resolvePackageUri(Uri uri) => _converter.resolvePackageUri(uri);
/// restart is called by the client when the user invokes a restart (for
/// example with the button on the debug toolbar).
///
/// The base implementation of this method throws. It is up to a debug adapter
/// that advertises `supportsRestartRequest` to override this method.
@override
Future<void> restartRequest(
Request request,
RestartArguments? args,
void Function() sendResponse,
) async {
throw DebugAdapterException(
'restartRequest was called on an adapter that '
'does not provide an implementation',
);
}
/// [scopesRequest] is called by the client to request all of the variables
/// scopes available for a given stack frame.
@override
@ -1417,11 +1467,25 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
sendResponse(VariablesResponseBody(variables: variables));
}
/// Fixes up a VM Service WebSocket URI to not have a trailing /ws
/// and use the HTTP scheme which is what DDS expects.
Uri vmServiceUriToHttp(Uri uri) {
final isSecure = uri.isScheme('https') || uri.isScheme('wss');
uri = uri.replace(scheme: isSecure ? 'https' : 'http');
final segments = uri.pathSegments;
if (segments.isNotEmpty && segments.last == 'ws') {
uri = uri.replace(pathSegments: segments.take(segments.length - 1));
}
return uri;
}
/// Fixes up an Observatory [uri] to a WebSocket URI with a trailing /ws
/// for connecting when not using DDS.
///
/// DDS does its own cleaning up of the URI.
Uri _cleanVmServiceUri(Uri uri) {
Uri vmServiceUriToWebSocket(Uri uri) {
// The VM Service library always expects the WebSockets URI so fix the
// scheme (http -> ws, https -> wss).
final isSecure = uri.isScheme('https') || uri.isScheme('wss');
@ -1468,7 +1532,9 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
);
}
Future<void> _handleDebugEvent(vm.Event event) async {
@protected
@mustCallSuper
Future<void> handleDebugEvent(vm.Event event) async {
// Delay processing any events until the debugger initialization has
// finished running, as events may arrive (for ex. IsolateRunnable) while
// it's doing is own initialization that this may interfere with.
@ -1477,17 +1543,42 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
await _isolateManager.handleEvent(event);
}
Future<void> _handleIsolateEvent(vm.Event event) async {
@protected
@mustCallSuper
Future<void> handleExtensionEvent(vm.Event event) async {
await debuggerInitialized;
// Base Dart does not do anything here, but other DAs (like Flutter) may
// override it to do their own handling.
}
@protected
@mustCallSuper
Future<void> handleIsolateEvent(vm.Event event) async {
// Delay processing any events until the debugger initialization has
// finished running, as events may arrive (for ex. IsolateRunnable) while
// it's doing is own initialization that this may interfere with.
await debuggerInitialized;
// Allow IsolateManager to handle any state-related events.
await _isolateManager.handleEvent(event);
switch (event.kind) {
// Pass any Service Extension events on to the client so they can enable
// functionality based upon them.
case vm.EventKind.kServiceExtensionAdded:
this._sendServiceExtensionAdded(
event.extensionRPC!,
event.isolate!.id!,
);
break;
}
}
/// Handles a dart:developer log() event, sending output to the client.
Future<void> _handleLoggingEvent(vm.Event event) async {
@protected
@mustCallSuper
Future<void> handleLoggingEvent(vm.Event event) async {
final record = event.logRecord;
final thread = _isolateManager.threadForIsolate(event.isolate);
if (record == null || thread == null) {
@ -1501,7 +1592,8 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
if (ref == null || ref.kind == vm.InstanceKind.kNull) {
return null;
}
return _converter.convertVmInstanceRefToDisplayString(
return _converter
.convertVmInstanceRefToDisplayString(
thread,
ref,
// Always allow calling toString() here as the user expects the full
@ -1510,7 +1602,14 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
allowCallingToString: true,
allowTruncatedValue: false,
includeQuotesAroundString: false,
);
)
.catchError((e) {
// Fetching strings from the server may throw if they have been
// collected since (for example if a Hot Restart occurs while
// we're running this). Log the error and just return null so
// nothing is shown.
logger?.call('$e');
});
}
var loggerName = await asString(record.loggerName);
@ -1534,6 +1633,23 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
}
}
@protected
@mustCallSuper
Future<void> handleServiceEvent(vm.Event event) async {
await debuggerInitialized;
switch (event.kind) {
// Service registrations are passed to the client so they can toggle
// behaviour based on their presence.
case vm.EventKind.kServiceRegistered:
this._sendServiceRegistration(event.service!, event.method!);
break;
case vm.EventKind.kServiceUnregistered:
this._sendServiceUnregistration(event.service!, event.method!);
break;
}
}
void _handleStderrEvent(vm.Event event) {
_sendOutputStreamEvent('stderr', event);
}
@ -1585,6 +1701,27 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
sendOutput('stdout', message);
}
void _sendServiceExtensionAdded(String extensionRPC, String isolateId) {
sendEvent(
RawEventBody({'extensionRPC': extensionRPC, 'isolateId': isolateId}),
eventType: 'dart.serviceExtensionAdded',
);
}
void _sendServiceRegistration(String service, String method) {
sendEvent(
RawEventBody({'service': service, 'method': method}),
eventType: 'dart.serviceRegistered',
);
}
void _sendServiceUnregistration(String service, String method) {
sendEvent(
RawEventBody({'service': service, 'method': method}),
eventType: 'dart.serviceUnregistered',
);
}
/// Updates the current debug options for the session.
///
/// Clients may not know about all debug options, so anything not included
@ -1641,15 +1778,10 @@ abstract class DartDebugAdapter<TL extends DartLaunchRequestArguments,
/// 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.
/// Specialised adapters (such as Flutter) will likely have their own versions
/// of this class.
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;
@ -1681,7 +1813,6 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
final String? console;
DartLaunchRequestArguments({
this.restart,
this.noDebug,
required this.program,
this.args,
@ -1689,6 +1820,7 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
this.toolArgs,
this.console,
this.enableAsserts,
Object? restart,
String? name,
String? cwd,
List<String>? additionalProjectPaths,
@ -1698,6 +1830,7 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
bool? evaluateToStringInDebugViews,
bool? sendLogsToClient,
}) : super(
restart: restart,
name: name,
cwd: cwd,
additionalProjectPaths: additionalProjectPaths,
@ -1709,8 +1842,7 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
);
DartLaunchRequestArguments.fromMap(Map<String, Object?> obj)
: restart = obj['restart'],
noDebug = obj['noDebug'] as bool?,
: noDebug = obj['noDebug'] as bool?,
program = obj['program'] as String,
args = (obj['args'] as List?)?.cast<String>(),
toolArgs = (obj['toolArgs'] as List?)?.cast<String>(),
@ -1722,7 +1854,6 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
@override
Map<String, Object?> toJson() => {
...super.toJson(),
if (restart != null) 'restart': restart,
if (noDebug != null) 'noDebug': noDebug,
'program': program,
if (args != null) 'args': args,

View file

@ -145,6 +145,11 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
} else {
await launchAsProcess(vmPath, processArgs);
}
// Delay responding until the debugger is connected.
if (debug) {
await debuggerInitialized;
}
}
/// Called by [attachRequest] to request that we actually connect to the app
@ -249,7 +254,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
/// Called by [terminateRequest] to request that we gracefully shut down the
/// app being run (or in the case of an attach, disconnect).
Future<void> terminateImpl() async {
terminatePids(ProcessSignal.sigint);
terminatePids(ProcessSignal.sigterm);
await _process?.exitCode;
}

View file

@ -159,7 +159,7 @@ class DartTestDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
/// Called by [terminateRequest] to request that we gracefully shut down the
/// app being run (or in the case of an attach, disconnect).
Future<void> terminateImpl() async {
terminatePids(ProcessSignal.sigint);
terminatePids(ProcessSignal.sigterm);
await _process?.exitCode;
}

View file

@ -77,7 +77,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
RawRequestArguments? args,
void Function(Object?) sendResponse,
) async {
throw DebugAdapterException('Unknown command ${request.command}');
throw DebugAdapterException('Unknown command ${request.command}');
}
Future<void> disconnectRequest(
@ -167,6 +167,12 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
void Function() sendResponse,
);
Future<void> restartRequest(
Request request,
RestartArguments? args,
void Function() sendResponse,
);
Future<void> scopesRequest(
Request request,
ScopesArguments args,
@ -280,6 +286,12 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
handle(request, _withVoidResponse(launchRequest), parseLaunchArgs);
} else if (request.command == 'attach') {
handle(request, _withVoidResponse(attachRequest), parseAttachArgs);
} else if (request.command == 'restart') {
handle(
request,
_withVoidResponse(restartRequest),
_allowNullArg(RestartArguments.fromJson),
);
} else if (request.command == 'terminate') {
handle(
request,

View file

@ -558,11 +558,18 @@ class IsolateManager {
// Set new breakpoints.
final newBreakpoints = _clientBreakpointsByUri[uri] ?? const [];
await Future.forEach<SourceBreakpoint>(newBreakpoints, (bp) async {
final vmBp = await service.addBreakpointWithScriptUri(
isolateId, uri, bp.line,
column: bp.column);
existingBreakpointsForIsolateAndUri.add(vmBp);
_clientBreakpointsByVmId[vmBp.id!] = bp;
try {
final vmBp = await service.addBreakpointWithScriptUri(
isolateId, uri, bp.line,
column: bp.column);
existingBreakpointsForIsolateAndUri.add(vmBp);
_clientBreakpointsByVmId[vmBp.id!] = bp;
} catch (e) {
// Swallow errors setting breakpoints rather than failing the whole
// request as it's very easy for editors to send us breakpoints that
// aren't valid any more.
_adapter.logger?.call('Failed to add breakpoint $e');
}
});
}
}

View file

@ -0,0 +1,98 @@
// 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 'package:vm_service/vm_service_io.dart';
import 'test_client.dart';
import 'test_scripts.dart';
import 'test_support.dart';
main() {
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('debug mode', () {
test('reports the VM Service URI to the client', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
final vmServiceUri = (await client.vmServiceUri)!;
expect(vmServiceUri.scheme, anyOf('ws', 'wss'));
await client.terminate();
});
test('exposes VM services to the client', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
// Capture our test service registration.
final myServiceRegistrationFuture = client.serviceRegisteredEvents
.firstWhere((event) => event['service'] == 'myService');
await client.hitBreakpoint(testFile, breakpointLine);
final vmServiceUri = await client.vmServiceUri;
// Register a service that echos back its params.
final vmService = await vmServiceConnectUri(vmServiceUri.toString());
// A service seems mandatory for this to work, even though it's unused.
await vmService.registerService('myService', 'myServiceAlias');
vmService.registerServiceCallback('myService', (params) async {
return {'result': params};
});
// Ensure the service registration event is emitted and includes the
// method to call it.
final myServiceRegistration = await myServiceRegistrationFuture;
final myServiceRegistrationMethod =
myServiceRegistration['method'] as String;
// Call the method and expect it to return the same values.
final response = await client.callService(
myServiceRegistrationMethod,
{'foo': 'bar'},
);
final result = response.body as Map<String, Object?>;
expect(result['foo'], equals('bar'));
await vmService.dispose();
await client.terminate();
});
test('exposes VM service extensions to the client', () async {
final client = dap.client;
final testFile = await dap.createTestFile(serviceExtensionProgram);
// Capture our test service registration.
final serviceExtensionAddedFuture = client.serviceExtensionAddedEvents
.firstWhere(
(event) => event['extensionRPC'] == 'ext.service.extension');
await client.start(file: testFile);
// Ensure the service registration event is emitted and includes the
// method to call it.
final serviceExtensionAdded = await serviceExtensionAddedFuture;
final extensionRPC = serviceExtensionAdded['extensionRPC'] as String;
final isolateId = serviceExtensionAdded['isolateId'] as String;
// Call the method and expect it to return the same values.
final response = await client.callService(
extensionRPC,
{
'isolateId': isolateId,
'foo': 'bar',
},
);
final result = response.body as Map<String, Object?>;
expect(result['foo'], equals('bar'));
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -36,11 +36,20 @@ class DapTestClient {
final _serverRequestHandlers =
<String, FutureOr<Object?> Function(Object?)>{};
late final Future<Uri?> vmServiceUri;
DapTestClient._(
this._channel,
this._logger, {
this.captureVmServiceTraffic = false,
}) {
// Set up a future that will complete when the 'dart.debuggerUris' event is
// emitted by the debug adapter so tests have easy access to it.
vmServiceUri = event('dart.debuggerUris').then<Uri?>((event) {
final body = event.body as Map<String, Object?>;
return Uri.parse(body['vmServiceUri'] as String);
}).catchError((e) => null);
_subscription = _channel.listen(
_handleMessage,
onDone: () {
@ -59,6 +68,16 @@ class DapTestClient {
Stream<OutputEventBody> get outputEvents => events('output')
.map((e) => OutputEventBody.fromJson(e.body as Map<String, Object?>));
/// Returns a stream of custom 'dart.serviceExtensionAdded' events.
Stream<Map<String, Object?>> get serviceExtensionAddedEvents =>
events('dart.serviceExtensionAdded')
.map((e) => e.body as Map<String, Object?>);
/// Returns a stream of custom 'dart.serviceRegistered' events.
Stream<Map<String, Object?>> get serviceRegisteredEvents =>
events('dart.serviceRegistered')
.map((e) => e.body as Map<String, Object?>);
/// Returns a stream of 'dart.testNotification' custom events from the
/// package:test JSON reporter.
Stream<Map<String, Object?>> get testNotificationEvents =>
@ -116,6 +135,11 @@ class DapTestClient {
return attachResponse;
}
/// Calls a service method via a custom request.
Future<Response> callService(String name, Object? params) {
return custom('callService', {'method': name, 'params': params});
}
/// Sends a continue request for the given thread.
///
/// Returns a Future that completes when the server returns a corresponding

View file

@ -20,6 +20,28 @@ const sdkStackFrameProgram = '''
}
''';
/// A simple Dart script that registers a simple service extension that returns
/// its params and waits until it is called before exiting.
const serviceExtensionProgram = '''
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
void main(List<String> args) async {
// Using a completer here causes the VM to quit when the extension is called
// so use a flag.
// https://github.com/dart-lang/sdk/issues/47279
var wasCalled = false;
registerExtension('ext.service.extension', (method, params) async {
wasCalled = true;
return ServiceExtensionResponse.result(jsonEncode(params));
});
while (!wasCalled) {
await Future.delayed(const Duration(milliseconds: 100));
}
}
''';
/// A simple Dart script that prints its arguments.
const simpleArgPrintingProgram = r'''
void main(List<String> args) async {