mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 16:41:07 +00:00
[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:
parent
637fb323d9
commit
4389cf7bba
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
98
pkg/dds/test/dap/integration/debug_services.dart
Normal file
98
pkg/dds/test/dap/integration/debug_services.dart
Normal 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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue