| // 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 'dart:async'; |
| |
| import 'package:dap/dap.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import 'constants.dart'; |
| import 'protocol_stream.dart'; |
| |
| typedef _FromJsonHandler<T> = T Function(Map<String, Object?>); |
| typedef _NullableFromJsonHandler<T> = T? Function(Map<String, Object?>?); |
| typedef RequestHandler<TArg, TResp> = Future<void> Function( |
| Request, TArg, void Function(TResp)); |
| typedef _VoidArgRequestHandler<TArg> = Future<void> Function( |
| Request, TArg, void Function(void)); |
| typedef _VoidNoArgRequestHandler<TArg> = Future<void> Function( |
| Request, TArg, void Function()); |
| |
| /// A base class for debug adapters. |
| /// |
| /// Communicates over a [ByteStreamServerChannel] and turns messages into |
| /// appropriate method calls/events. |
| /// |
| /// This class does not implement any DA functionality, only message handling. |
| abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments, |
| TAttachArgs extends AttachRequestArguments> { |
| int _sequence = 1; |
| final ByteStreamServerChannel _channel; |
| |
| /// Completers for requests that are sent from the server back to the editor |
| /// such as `runInTerminal`. |
| final _serverToClientRequestCompleters = <int, Completer<Object?>>{}; |
| |
| BaseDebugAdapter(this._channel, {Function? onError}) { |
| _channel.listen(_handleIncomingMessage, onError: onError); |
| } |
| |
| /// Parses arguments for [attachRequest] into a type of [TAttachArgs]. |
| /// |
| /// This method must be implemented by the implementing class using a class |
| /// that corresponds to the arguments it expects (these may differ between |
| /// Dart CLI, Dart tests, Flutter, Flutter tests). |
| TAttachArgs Function(Map<String, Object?>) get parseAttachArgs; |
| |
| /// Parses arguments for [launchRequest] into a type of [TLaunchArgs]. |
| /// |
| /// This method must be implemented by the implementing class using a class |
| /// that corresponds to the arguments it expects (these may differ between |
| /// Dart CLI, Dart tests, Flutter, Flutter tests). |
| TLaunchArgs Function(Map<String, Object?>) get parseLaunchArgs; |
| |
| Future<void> attachRequest( |
| Request request, |
| TAttachArgs args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> configurationDoneRequest( |
| Request request, |
| ConfigurationDoneArguments? args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> continueRequest( |
| Request request, |
| ContinueArguments args, |
| void Function(ContinueResponseBody) sendResponse, |
| ); |
| |
| @mustCallSuper |
| Future<void> customRequest( |
| Request request, |
| RawRequestArguments? args, |
| void Function(Object?) sendResponse, |
| ) async { |
| throw DebugAdapterException('Unknown command ${request.command}'); |
| } |
| |
| Future<void> disconnectRequest( |
| Request request, |
| DisconnectArguments? args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> evaluateRequest( |
| Request request, |
| EvaluateArguments args, |
| void Function(EvaluateResponseBody) sendResponse, |
| ); |
| |
| /// Calls [handler] for an incoming request, using [fromJson] to parse its |
| /// arguments from the request. |
| /// |
| /// [handler] will be provided a function [sendResponse] that it can use to |
| /// sends its response without needing to build a [Response] from fields on |
| /// the request. |
| /// |
| /// [handler] must _always_ call [sendResponse], even if the response does not |
| /// require a body. |
| /// |
| /// [responseWriter] is a function that will be provided the response to be |
| /// sent on the stream. It is critical that this sends the response |
| /// synchronously because additional events may be sent by handlers that must |
| /// come after it. |
| /// |
| /// If [handler] throws, its exception will be sent as an error response. |
| Future<void> handle<TArg, TResp>( |
| Request request, |
| RequestHandler<TArg, TResp> handler, |
| TArg Function(Map<String, Object?>) fromJson, |
| void Function(Response) responseWriter, |
| ) async { |
| try { |
| final args = request.arguments != null |
| ? fromJson(request.arguments as Map<String, Object?>) |
| // arguments are only valid to be null then TArg is nullable. |
| : null as TArg; |
| |
| // Because handlers may need to send responses before they have finished |
| // executing (for example, initializeRequest needs to send its response |
| // before sending InitializedEvent()), we pass in a function `sendResponse` |
| // rather than using a return value. |
| var sendResponseCalled = false; |
| void sendResponse(TResp responseBody) { |
| assert(!sendResponseCalled, |
| 'sendResponse was called multiple times by ${request.command}'); |
| sendResponseCalled = true; |
| final response = Response( |
| success: true, |
| requestSeq: request.seq, |
| seq: _sequence++, |
| command: request.command, |
| body: responseBody, |
| ); |
| responseWriter(response); |
| } |
| |
| await handler(request, args, sendResponse); |
| assert(sendResponseCalled, |
| 'sendResponse was not called in ${request.command}'); |
| } catch (e, s) { |
| // TODO(helin24): Consider adding an error type to DebugAdapterException. |
| final isDapException = e is DebugAdapterException; |
| final messageText = isDapException ? e.message : '$e'; |
| final errorMessage = Message( |
| id: ErrorMessageType.general, |
| format: '{message}', |
| // We include stack in the payload for debugging, but we don't include |
| // it in format above because we don't want it used to build the error |
| // shown to the user. |
| variables: {'message': messageText, 'stack': '$s'}, |
| // DAP specification did not specify how to handle the case where |
| // showUser does not exist. VSCode defaults to true, but some other |
| // systems might default it to false. |
| // Only show errors that are intended to be shown to the user. These |
| // include any errors in LaunchRequest or AttachRequest. |
| showUser: isDapException && e.showToUser == true, |
| ); |
| final response = Response( |
| success: false, |
| requestSeq: request.seq, |
| seq: _sequence++, |
| command: request.command, |
| message: messageText, |
| body: ErrorResponseBody(error: errorMessage), |
| ); |
| responseWriter(response); |
| } |
| } |
| |
| Future<void> initializeRequest( |
| Request request, |
| DartInitializeRequestArguments args, |
| void Function(Capabilities) sendResponse, |
| ); |
| |
| Future<void> launchRequest( |
| Request request, |
| TLaunchArgs args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> nextRequest( |
| Request request, |
| NextArguments args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> pauseRequest( |
| Request request, |
| PauseArguments args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> restartFrameRequest( |
| Request request, |
| RestartFrameArguments args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> restartRequest( |
| Request request, |
| RestartArguments? args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> scopesRequest( |
| Request request, |
| ScopesArguments args, |
| void Function(ScopesResponseBody) sendResponse, |
| ); |
| |
| /// Sends an event, lookup up the event type based on the runtimeType of |
| /// [body]. |
| void sendEvent(EventBody body, {String? eventType}) { |
| final event = Event( |
| seq: _sequence++, |
| event: eventType ?? eventTypes[body.runtimeType]!, |
| body: body, |
| ); |
| sendEventToChannel(event); |
| } |
| |
| void sendEventToChannel(Event event) { |
| _channel.sendEvent(event); |
| } |
| |
| /// Sends a request to the client, looking up the request type based on the |
| /// runtimeType of [arguments]. |
| Future<Object?> sendRequest(RequestArguments arguments) { |
| final request = Request( |
| seq: _sequence++, |
| command: commandTypes[arguments.runtimeType]!, |
| arguments: arguments, |
| ); |
| |
| // Store a completer to be used when a response comes back. |
| final completer = Completer<Object?>(); |
| _serverToClientRequestCompleters[request.seq] = completer; |
| _channel.sendRequest(request); |
| |
| return completer.future; |
| } |
| |
| Future<void> setBreakpointsRequest( |
| Request request, |
| SetBreakpointsArguments args, |
| void Function(SetBreakpointsResponseBody) sendResponse); |
| |
| Future<void> setExceptionBreakpointsRequest( |
| Request request, |
| SetExceptionBreakpointsArguments args, |
| void Function(SetExceptionBreakpointsResponseBody) sendResponse, |
| ); |
| |
| Future<void> sourceRequest( |
| Request request, |
| SourceArguments args, |
| void Function(SourceResponseBody) sendResponse, |
| ); |
| |
| Future<void> stackTraceRequest( |
| Request request, |
| StackTraceArguments args, |
| void Function(StackTraceResponseBody) sendResponse, |
| ); |
| |
| Future<void> stepInRequest( |
| Request request, |
| StepInArguments args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> stepOutRequest( |
| Request request, |
| StepOutArguments args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> terminateRequest( |
| Request request, |
| TerminateArguments? args, |
| void Function() sendResponse, |
| ); |
| |
| Future<void> threadsRequest( |
| Request request, |
| void args, |
| void Function(ThreadsResponseBody) sendResponse, |
| ); |
| |
| Future<void> variablesRequest( |
| Request request, |
| VariablesArguments args, |
| void Function(VariablesResponseBody) sendResponse, |
| ); |
| |
| /// Wraps a fromJson handler for requests that allow null arguments. |
| _NullableFromJsonHandler<T> _allowNullArg<T extends RequestArguments>( |
| _FromJsonHandler<T> fromJson, |
| ) { |
| return (data) => data == null ? null : fromJson(data); |
| } |
| |
| /// Handles incoming messages from the client editor. |
| void _handleIncomingMessage(ProtocolMessage message) { |
| if (message is Request) { |
| handleIncomingRequest(message, _channel.sendResponse); |
| } else if (message is Response) { |
| _handleIncomingResponse(message); |
| } else { |
| throw DebugAdapterException('Unknown Protocol message ${message.type}'); |
| } |
| } |
| |
| /// Handles an incoming request, calling the appropriate method to handle it. |
| /// |
| /// [responseWriter] is a function that will be provided the response to be |
| /// sent on the stream. It is critical that this sends the response |
| /// synchronously because additional events may be sent by handlers that must |
| /// come after it. |
| void handleIncomingRequest( |
| Request request, |
| void Function(Response) responseWriter, |
| ) { |
| if (request.command == 'initialize') { |
| handle( |
| request, |
| initializeRequest, |
| DartInitializeRequestArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'launch') { |
| handle( |
| request, |
| _withVoidResponse(launchRequest), |
| parseLaunchArgs, |
| responseWriter, |
| ); |
| } else if (request.command == 'attach') { |
| handle( |
| request, |
| _withVoidResponse(attachRequest), |
| parseAttachArgs, |
| responseWriter, |
| ); |
| } else if (request.command == 'restart') { |
| handle( |
| request, |
| _withVoidResponse(restartRequest), |
| _allowNullArg(RestartArguments.fromJson), |
| responseWriter, |
| ); |
| } else if (request.command == 'terminate') { |
| handle( |
| request, |
| _withVoidResponse(terminateRequest), |
| _allowNullArg(TerminateArguments.fromJson), |
| responseWriter, |
| ); |
| } else if (request.command == 'disconnect') { |
| handle( |
| request, |
| _withVoidResponse(disconnectRequest), |
| _allowNullArg(DisconnectArguments.fromJson), |
| responseWriter, |
| ); |
| } else if (request.command == 'configurationDone') { |
| handle( |
| request, |
| _withVoidResponse(configurationDoneRequest), |
| _allowNullArg(ConfigurationDoneArguments.fromJson), |
| responseWriter, |
| ); |
| } else if (request.command == 'setBreakpoints') { |
| handle( |
| request, |
| setBreakpointsRequest, |
| SetBreakpointsArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'setExceptionBreakpoints') { |
| handle( |
| request, |
| setExceptionBreakpointsRequest, |
| SetExceptionBreakpointsArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'pause') { |
| handle( |
| request, |
| _withVoidResponse(pauseRequest), |
| PauseArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'continue') { |
| handle( |
| request, |
| continueRequest, |
| ContinueArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'next') { |
| handle( |
| request, |
| _withVoidResponse(nextRequest), |
| NextArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'stepIn') { |
| handle( |
| request, |
| _withVoidResponse(stepInRequest), |
| StepInArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'stepOut') { |
| handle( |
| request, |
| _withVoidResponse(stepOutRequest), |
| StepOutArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'restartFrame') { |
| handle( |
| request, |
| _withVoidResponse(restartFrameRequest), |
| RestartFrameArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'threads') { |
| handle( |
| request, |
| threadsRequest, |
| _voidArgs, |
| responseWriter, |
| ); |
| } else if (request.command == 'stackTrace') { |
| handle( |
| request, |
| stackTraceRequest, |
| StackTraceArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'source') { |
| handle( |
| request, |
| sourceRequest, |
| SourceArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'scopes') { |
| handle( |
| request, |
| scopesRequest, |
| ScopesArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'variables') { |
| handle( |
| request, |
| variablesRequest, |
| VariablesArguments.fromJson, |
| responseWriter, |
| ); |
| } else if (request.command == 'evaluate') { |
| handle( |
| request, |
| evaluateRequest, |
| EvaluateArguments.fromJson, |
| responseWriter, |
| ); |
| } else { |
| handle( |
| request, |
| customRequest, |
| _allowNullArg(RawRequestArguments.fromJson), |
| responseWriter, |
| ); |
| } |
| } |
| |
| void _handleIncomingResponse(Response response) { |
| final completer = |
| _serverToClientRequestCompleters.remove(response.requestSeq); |
| |
| if (response.success) { |
| completer?.complete(response.body); |
| } else { |
| completer?.completeError( |
| response.message ?? 'Request ${response.requestSeq} failed', |
| ); |
| } |
| } |
| |
| /// Shuts down the debug adapter by closing the underlying communication |
| /// channel. |
| void shutdown() { |
| _channel.close(); |
| } |
| |
| /// Helpers for requests that have `void` arguments. The supplied args are |
| /// ignored. |
| void _voidArgs(Map<String, Object?>? args) {} |
| |
| /// Helper that converts a handler with no response value to one that has |
| /// passes an unused arg so that `Function()` can be passed to a function |
| /// accepting `Function<T>(T x)` where `T` happens to be `void`. |
| /// |
| /// This allows handlers to simply call sendResponse() where they have no |
| /// return value but need to send a valid response. |
| _VoidArgRequestHandler<TArg> _withVoidResponse<TArg>( |
| _VoidNoArgRequestHandler<TArg> handler, |
| ) { |
| return (request, arg, sendResponse) => handler( |
| request, |
| arg, |
| () => sendResponse(null), |
| ); |
| } |
| } |