| // 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 | |
| > { | |
| BaseDebugAdapter(this._channel, {Function? onError}) { | |
| _channel.listen(_handleIncomingMessage, onError: onError); | |
| } | |
| 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?>>{}; | |
| /// 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, | |
| ) { | |
| 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)); | |
| } | |
| } |