Add debug adapter to DAP

Change-Id: I5bacd460175a5b2e86326f7501d7f250bbe3ab0c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/292060
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Helin Shiah <helinx@google.com>
This commit is contained in:
Helin Shiah 2023-04-27 01:19:57 +00:00 committed by Commit Queue
parent 86d754690a
commit 5ef021b116
10 changed files with 435 additions and 18 deletions

View file

@ -11,6 +11,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
import '../dds.dart';
import 'constants.dart';
import 'dap/adapters/dds_hosted_adapter.dart';
import 'dds_impl.dart';
import 'rpc_error_codes.dart';
import 'stream_manager.dart';
@ -252,6 +253,11 @@ class DartDevelopmentServiceClient {
dds.packageUriConverter.convert,
);
_clientPeer.registerMethod(
'handleDap',
(parameters) => dds.dapHandler.handle(adapter, parameters),
);
// When invoked within a fallback, the next fallback will start executing.
// The final fallback forwards the request to the VM service directly.
Never nextFallback() => throw json_rpc.RpcException.methodNotFound('');
@ -334,4 +340,5 @@ class DartDevelopmentServiceClient {
final Set<String> profilerUserTagFilters = {};
final json_rpc.Peer _vmServicePeer;
late json_rpc.Peer _clientPeer;
final DdsHostedAdapter adapter = DdsHostedAdapter();
}

View file

@ -631,7 +631,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
}
logger?.call('Connecting to debugger at $uri');
sendOutput('console', 'Connecting to VM Service at $uri\n');
sendConsoleOutput('Connecting to VM Service at $uri\n');
final vmService = await _vmServiceConnectUri(uri.toString());
logger?.call('Connected to debugger at $uri!');
@ -1272,6 +1272,11 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
sendResponse(ScopesResponseBody(scopes: scopes));
}
/// Sends an OutputEvent with a newline to the console.
void sendConsoleOutput(String message) {
sendOutput('console', '\n$message');
}
/// Sends an OutputEvent (without a newline, since calls to this method
/// may be using buffered data that is not split cleanly on newlines).
///

View file

@ -0,0 +1,124 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../exceptions.dart';
import '../protocol_generated.dart';
import '../protocol_stream.dart';
import 'dart.dart';
import 'mixins.dart';
/// A DAP Debug Adapter for attaching to already-running Dart and Flutter applications.
class DdsHostedAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
DartAttachRequestArguments>
with PidTracker, VmServiceInfoFileUtils, PackageConfigUtils, TestAdapter {
Uri? ddsUri;
@override
final parseLaunchArgs = DartLaunchRequestArguments.fromJson;
@override
final parseAttachArgs = DartAttachRequestArguments.fromJson;
DdsHostedAdapter()
: super(
// TODO(helin24): Make channel optional for base adapter class.
ByteStreamServerChannel(
Stream.empty(),
NullStreamSink(),
(message) {},
),
ipv6: true,
enableDds: false,
);
/// Whether the VM Service closing should be used as a signal to terminate the
/// debug session.
///
/// True here because we no longer need this adapter once the VM service has closed.
@override
bool get terminateOnVmServiceClose => true;
@override
Future<void> debuggerConnected(vm.VM vmInfo) async {}
/// Called by [disconnectRequest] to request that we forcefully shut down the
/// app being run (or in the case of an attach, disconnect).
@override
Future<void> disconnectImpl() async {
await handleDetach();
}
/// Called by [launchRequest] to request that we actually start the app to be
/// run/debugged.
///
/// For debugging, this should start paused, connect to the VM Service, set
/// breakpoints, and resume.
@override
Future<void> launchImpl() async {
sendConsoleOutput(
'Launch is not supported for the attach only adapter',
);
handleSessionTerminate();
}
/// Called by [attachRequest] to request that we actually connect to the app
/// to be debugged.
@override
Future<void> attachImpl() async {
final args = this.args as DartAttachRequestArguments;
final vmServiceUri = args.vmServiceUri;
if (vmServiceUri == null) {
sendConsoleOutput(
'To attach, provide vmServiceUri',
);
handleSessionTerminate();
return;
}
if (vmServiceUri != ddsUri.toString()) {
sendConsoleOutput(
'To use the attach-only adapter, VM service URI must match DDS URI',
);
handleSessionTerminate();
}
// TODO(helin24): In this method, we only need to verify that the DDS URI
// matches the VM service URI. The DDS URI isn't really needed because this
// adapter is running in the same process. We need to refactor so that we
// call DDS/VM service methods directly instead of using the websocket.
unawaited(connectDebugger(ddsUri!));
}
/// Called by [terminateRequest] to request that we gracefully shut down the
/// app being run (or in the case of an attach, disconnect).
@override
Future<void> terminateImpl() async {
await handleDetach();
terminatePids(ProcessSignal.sigterm);
}
Future<Response> handleMessage(String message) async {
final potentialException =
DebugAdapterException('Message does not conform to DAP spec: $message');
try {
final Map<String, Object?> json = jsonDecode(message);
final type = json['type'] as String;
if (type == 'request') {
return handleIncomingRequest(Request.fromJson(json));
// TODO(helin24): Handle event and response?
}
throw potentialException;
} catch (e) {
throw potentialException;
}
}
}

View file

@ -107,6 +107,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
Request request,
RequestHandler<TArg, TResp> handler,
TArg Function(Map<String, Object?>) fromJson,
Completer<Response> completer,
) async {
try {
final args = request.arguments != null
@ -130,7 +131,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
command: request.command,
body: responseBody,
);
_channel.sendResponse(response);
completer.complete(response);
}
await handler(request, args, sendResponse);
@ -145,7 +146,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
message: e is DebugAdapterException ? e.message : '$e',
body: '$s',
);
_channel.sendResponse(response);
completer.complete(response);
}
}
@ -270,7 +271,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
/// Handles incoming messages from the client editor.
void _handleIncomingMessage(ProtocolMessage message) {
if (message is Request) {
_handleIncomingRequest(message);
handleIncomingRequest(message).then(_channel.sendResponse);
} else if (message is Response) {
_handleIncomingResponse(message);
} else {
@ -279,80 +280,152 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments,
}
/// Handles an incoming request, calling the appropriate method to handle it.
void _handleIncomingRequest(Request request) {
Future<Response> handleIncomingRequest(Request request) {
final completer = Completer<Response>();
if (request.command == 'initialize') {
handle(request, initializeRequest, InitializeRequestArguments.fromJson);
handle(
request,
initializeRequest,
InitializeRequestArguments.fromJson,
completer,
);
} else if (request.command == 'launch') {
handle(request, _withVoidResponse(launchRequest), parseLaunchArgs);
handle(
request,
_withVoidResponse(launchRequest),
parseLaunchArgs,
completer,
);
} else if (request.command == 'attach') {
handle(request, _withVoidResponse(attachRequest), parseAttachArgs);
handle(
request,
_withVoidResponse(attachRequest),
parseAttachArgs,
completer,
);
} else if (request.command == 'restart') {
handle(
request,
_withVoidResponse(restartRequest),
_allowNullArg(RestartArguments.fromJson),
completer,
);
} else if (request.command == 'terminate') {
handle(
request,
_withVoidResponse(terminateRequest),
_allowNullArg(TerminateArguments.fromJson),
completer,
);
} else if (request.command == 'disconnect') {
handle(
request,
_withVoidResponse(disconnectRequest),
_allowNullArg(DisconnectArguments.fromJson),
completer,
);
} else if (request.command == 'configurationDone') {
handle(
request,
_withVoidResponse(configurationDoneRequest),
_allowNullArg(ConfigurationDoneArguments.fromJson),
completer,
);
} else if (request.command == 'setBreakpoints') {
handle(request, setBreakpointsRequest, SetBreakpointsArguments.fromJson);
handle(
request,
setBreakpointsRequest,
SetBreakpointsArguments.fromJson,
completer,
);
} else if (request.command == 'setExceptionBreakpoints') {
handle(
request,
setExceptionBreakpointsRequest,
SetExceptionBreakpointsArguments.fromJson,
completer,
);
} else if (request.command == 'continue') {
handle(request, continueRequest, ContinueArguments.fromJson);
handle(
request,
continueRequest,
ContinueArguments.fromJson,
completer,
);
} else if (request.command == 'next') {
handle(request, _withVoidResponse(nextRequest), NextArguments.fromJson);
handle(
request,
_withVoidResponse(nextRequest),
NextArguments.fromJson,
completer,
);
} else if (request.command == 'stepIn') {
handle(
request,
_withVoidResponse(stepInRequest),
StepInArguments.fromJson,
completer,
);
} else if (request.command == 'stepOut') {
handle(
request,
_withVoidResponse(stepOutRequest),
StepOutArguments.fromJson,
completer,
);
} else if (request.command == 'threads') {
handle(request, threadsRequest, _voidArgs);
handle(
request,
threadsRequest,
_voidArgs,
completer,
);
} else if (request.command == 'stackTrace') {
handle(request, stackTraceRequest, StackTraceArguments.fromJson);
handle(
request,
stackTraceRequest,
StackTraceArguments.fromJson,
completer,
);
} else if (request.command == 'source') {
handle(request, sourceRequest, SourceArguments.fromJson);
handle(
request,
sourceRequest,
SourceArguments.fromJson,
completer,
);
} else if (request.command == 'scopes') {
handle(request, scopesRequest, ScopesArguments.fromJson);
handle(
request,
scopesRequest,
ScopesArguments.fromJson,
completer,
);
} else if (request.command == 'variables') {
handle(request, variablesRequest, VariablesArguments.fromJson);
handle(
request,
variablesRequest,
VariablesArguments.fromJson,
completer,
);
} else if (request.command == 'evaluate') {
handle(request, evaluateRequest, EvaluateArguments.fromJson);
handle(
request,
evaluateRequest,
EvaluateArguments.fromJson,
completer,
);
} else {
handle(
request,
customRequest,
_allowNullArg(RawRequestArguments.fromJson),
completer,
);
}
return completer.future;
}
void _handleIncomingResponse(Response response) {

View file

@ -0,0 +1,9 @@
// Copyright (c) 2023, 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.
class Command {
static const initialize = 'initialize';
static const configurationDone = 'configurationDone';
static const attach = 'attach';
}

View file

@ -114,7 +114,7 @@ class ByteStreamServerChannel {
void _sendParseError(String data) {
// TODO(dantup): Review LSP implementation of this when consolidating classes.
throw DebugAdapterException('Message does not confirm to DAP spec: $data');
throw DebugAdapterException('Message does not conform to DAP spec: $data');
}
/// Send [bytes] to [_output].

View file

@ -0,0 +1,80 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import '../dap.dart';
import 'dap/adapters/dds_hosted_adapter.dart';
import 'dap/constants.dart';
import 'dds_impl.dart';
/// Responds to incoming DAP messages using a debug adapter connected to DDS.
class DapHandler {
DapHandler(this.dds);
Future<Map<String, dynamic>> handle(
DdsHostedAdapter adapter,
json_rpc.Parameters parameters,
) async {
if (adapter.ddsUri == null) {
_startAdapter(adapter);
}
// TODO(helin24): Consider a sequence offset for incoming messages to avoid
// overlapping sequence numbers with startup requests.
final message = parameters['message'].asString;
final result = await adapter.handleMessage(message);
return <String, dynamic>{
'type': 'DapResponse',
'message': result.toJson(),
};
}
Future<void> _startAdapter(DdsHostedAdapter adapter) async {
adapter.ddsUri = dds.uri;
// TODO(helin24): Most likely we'll want the client to do these
// initialization steps so that clients can differentiate capabilities. This
// may require a custom stream for the debug adapter.
int seq = 1;
// TODO(helin24): Add waiting for `InitializedEvent`.
await adapter.initializeRequest(
Request(
command: Command.initialize,
seq: seq,
),
InitializeRequestArguments(
adapterID: 'dds-dap-handler',
),
(capabilities) {},
);
await adapter.configurationDoneRequest(
Request(
arguments: const {},
command: Command.configurationDone,
seq: seq++,
),
ConfigurationDoneArguments(),
noopCallback,
);
await adapter.attachRequest(
Request(
arguments: const {},
command: Command.attach,
seq: seq++,
),
DartAttachRequestArguments(
vmServiceUri: dds.remoteVmServiceUri.toString(),
),
noopCallback,
);
}
final DartDevelopmentServiceImpl dds;
}
void noopCallback() {}

View file

@ -23,6 +23,7 @@ import 'binary_compatible_peer.dart';
import 'client.dart';
import 'client_manager.dart';
import 'constants.dart';
import 'dap_handler.dart';
import 'devtools/handler.dart';
import 'expression_evaluator.dart';
import 'isolate_manager.dart';
@ -68,6 +69,7 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService {
_isolateManager = IsolateManager(this);
_streamManager = StreamManager(this);
_packageUriConverter = PackageUriConverter(this);
_dapHandler = DapHandler(this);
_authCode = _authCodesEnabled ? _makeAuthToken() : '';
}
@ -486,6 +488,9 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService {
PackageUriConverter get packageUriConverter => _packageUriConverter;
late PackageUriConverter _packageUriConverter;
DapHandler get dapHandler => _dapHandler;
late DapHandler _dapHandler;
ClientManager get clientManager => _clientManager;
late ClientManager _clientManager;

View file

@ -0,0 +1,64 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:dds/dds.dart';
import 'package:dds/src/dap/protocol_generated.dart';
import 'package:dds_service_extensions/src/dap.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service_io.dart';
import 'common/test_helper.dart';
void main() {
late Process process;
DartDevelopmentService? dds;
setUp(() async {
process = await spawnDartProcess(
'get_cached_cpu_samples_script.dart',
disableServiceAuthCodes: true,
);
});
tearDown(() async {
await dds?.shutdown();
process.kill();
});
test('DDS responds to DAP message', () async {
Uri serviceUri = remoteVmServiceUri;
dds = await DartDevelopmentService.startDartDevelopmentService(
remoteVmServiceUri,
);
serviceUri = dds!.wsUri!;
expect(dds!.isRunning, true);
final service = await vmServiceConnectUri(serviceUri.toString());
final setBreakpointsRequest = Request(
command: 'setBreakpoints',
seq: 9,
arguments: SetBreakpointsArguments(
breakpoints: [
SourceBreakpoint(line: 20),
SourceBreakpoint(line: 30),
],
source: Source(
name: 'main.dart',
path: '/file/to/main.dart',
),
),
);
// TODO(helinx): Check result format after using better typing from JSON.
final result = await service.handleDap(jsonEncode(setBreakpointsRequest));
expect(result.message, isNotNull);
expect(result.message['type'], 'response');
expect(result.message['success'], true);
expect(result.message['command'], 'setBreakpoints');
expect(result.message['body'], isNotNull);
});
}

View file

@ -0,0 +1,50 @@
// ignore: implementation_imports
import 'package:vm_service/src/vm_service.dart';
extension DapExtension on VmService {
static bool _factoriesRegistered = false;
Future<DapResponse> handleDap(String message) async {
return _callHelper<DapResponse>(
'handleDap',
args: {'message': message},
);
}
Future<T> _callHelper<T>(String method,
{String? isolateId, Map args = const {}}) {
if (!_factoriesRegistered) {
_registerFactories();
}
return callMethod(
method,
args: {
if (isolateId != null) 'isolateId': isolateId,
...args,
},
).then((e) => e as T);
}
static void _registerFactories() {
addTypeFactory('DapResponse', DapResponse.parse);
_factoriesRegistered = true;
}
}
class DapResponse extends Response {
static DapResponse? parse(Map<String, dynamic>? json) =>
json == null ? null : DapResponse._fromJson(json);
DapResponse({
required this.message,
});
DapResponse._fromJson(Map<String, dynamic> json) : message = json['message'];
@override
String get type => 'DapResponse';
@override
String toString() => '[DapResponse]';
final Map<String, Object?> message;
}