From 5ef021b116bc9bf08daf447b79dfe7815bd29490 Mon Sep 17 00:00:00 2001 From: Helin Shiah Date: Thu, 27 Apr 2023 01:19:57 +0000 Subject: [PATCH] Add debug adapter to DAP Change-Id: I5bacd460175a5b2e86326f7501d7f250bbe3ab0c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/292060 Reviewed-by: Ben Konyi Commit-Queue: Helin Shiah --- pkg/dds/lib/src/client.dart | 7 + pkg/dds/lib/src/dap/adapters/dart.dart | 7 +- .../src/dap/adapters/dds_hosted_adapter.dart | 124 ++++++++++++++++++ pkg/dds/lib/src/dap/base_debug_adapter.dart | 105 ++++++++++++--- pkg/dds/lib/src/dap/constants.dart | 9 ++ pkg/dds/lib/src/dap/protocol_stream.dart | 2 +- pkg/dds/lib/src/dap_handler.dart | 80 +++++++++++ pkg/dds/lib/src/dds_impl.dart | 5 + pkg/dds/test/dap_handler_test.dart | 64 +++++++++ pkg/dds_service_extensions/lib/src/dap.dart | 50 +++++++ 10 files changed, 435 insertions(+), 18 deletions(-) create mode 100644 pkg/dds/lib/src/dap/adapters/dds_hosted_adapter.dart create mode 100644 pkg/dds/lib/src/dap/constants.dart create mode 100644 pkg/dds/lib/src/dap_handler.dart create mode 100644 pkg/dds/test/dap_handler_test.dart create mode 100644 pkg/dds_service_extensions/lib/src/dap.dart diff --git a/pkg/dds/lib/src/client.dart b/pkg/dds/lib/src/client.dart index 6d714301300..ab1ac25e5d3 100644 --- a/pkg/dds/lib/src/client.dart +++ b/pkg/dds/lib/src/client.dart @@ -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 profilerUserTagFilters = {}; final json_rpc.Peer _vmServicePeer; late json_rpc.Peer _clientPeer; + final DdsHostedAdapter adapter = DdsHostedAdapter(); } diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart index 6578db4dd8c..0d6b9a4949f 100644 --- a/pkg/dds/lib/src/dap/adapters/dart.dart +++ b/pkg/dds/lib/src/dap/adapters/dart.dart @@ -631,7 +631,7 @@ abstract class DartDebugAdapter + 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 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 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 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 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 terminateImpl() async { + await handleDetach(); + terminatePids(ProcessSignal.sigterm); + } + + Future handleMessage(String message) async { + final potentialException = + DebugAdapterException('Message does not conform to DAP spec: $message'); + + try { + final Map 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; + } + } +} diff --git a/pkg/dds/lib/src/dap/base_debug_adapter.dart b/pkg/dds/lib/src/dap/base_debug_adapter.dart index e20e494f1c6..26bbe413eec 100644 --- a/pkg/dds/lib/src/dap/base_debug_adapter.dart +++ b/pkg/dds/lib/src/dap/base_debug_adapter.dart @@ -107,6 +107,7 @@ abstract class BaseDebugAdapter handler, TArg Function(Map) fromJson, + Completer completer, ) async { try { final args = request.arguments != null @@ -130,7 +131,7 @@ abstract class BaseDebugAdapter handleIncomingRequest(Request request) { + final completer = Completer(); + 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) { diff --git a/pkg/dds/lib/src/dap/constants.dart b/pkg/dds/lib/src/dap/constants.dart new file mode 100644 index 00000000000..7f24e6e8e71 --- /dev/null +++ b/pkg/dds/lib/src/dap/constants.dart @@ -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'; +} diff --git a/pkg/dds/lib/src/dap/protocol_stream.dart b/pkg/dds/lib/src/dap/protocol_stream.dart index a3694dcce69..dcd3e5f8a88 100644 --- a/pkg/dds/lib/src/dap/protocol_stream.dart +++ b/pkg/dds/lib/src/dap/protocol_stream.dart @@ -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]. diff --git a/pkg/dds/lib/src/dap_handler.dart b/pkg/dds/lib/src/dap_handler.dart new file mode 100644 index 00000000000..f1cdfc04ccc --- /dev/null +++ b/pkg/dds/lib/src/dap_handler.dart @@ -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> 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 { + 'type': 'DapResponse', + 'message': result.toJson(), + }; + } + + Future _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() {} diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart index 73c0fa4e708..faf19dfad0e 100644 --- a/pkg/dds/lib/src/dds_impl.dart +++ b/pkg/dds/lib/src/dds_impl.dart @@ -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; diff --git a/pkg/dds/test/dap_handler_test.dart b/pkg/dds/test/dap_handler_test.dart new file mode 100644 index 00000000000..51d8c092417 --- /dev/null +++ b/pkg/dds/test/dap_handler_test.dart @@ -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); + }); +} diff --git a/pkg/dds_service_extensions/lib/src/dap.dart b/pkg/dds_service_extensions/lib/src/dap.dart new file mode 100644 index 00000000000..c55726259b8 --- /dev/null +++ b/pkg/dds_service_extensions/lib/src/dap.dart @@ -0,0 +1,50 @@ +// ignore: implementation_imports +import 'package:vm_service/src/vm_service.dart'; + +extension DapExtension on VmService { + static bool _factoriesRegistered = false; + Future handleDap(String message) async { + return _callHelper( + 'handleDap', + args: {'message': message}, + ); + } + + Future _callHelper(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? json) => + json == null ? null : DapResponse._fromJson(json); + + DapResponse({ + required this.message, + }); + + DapResponse._fromJson(Map json) : message = json['message']; + + @override + String get type => 'DapResponse'; + + @override + String toString() => '[DapResponse]'; + + final Map message; +}