blob: f7d3bfecbcab200b96d29d72f7b8c8f7ba8db60f [file] [log] [blame]
// 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:dap/dap.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../constants.dart';
import '../protocol_stream.dart';
import '../variables.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;
final _dapEventsController = StreamController<Event>();
@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!));
}
/// Handles a request from the client for the list of threads.
///
/// Unlike the base implementation, the DDS version includes additional fields
/// in the response for `isolateId`.
@override
Future<void> threadsRequest(
Request request,
void args,
void Function(ThreadsResponseBody) sendResponse,
) async {
final threads = [
for (final thread in isolateManager.threads)
ThreadWithIsolateId(
id: thread.threadId,
name: thread.isolate.name ?? '<unnamed isolate>',
isolateId:
thread.isolate.id ?? '<unknown isolate ${thread.threadId}>',
)
];
sendResponse(ThreadsResponseBody(threads: threads));
}
/// Handles custom requests that are specific to the DDS-hosted adapter, such
/// as translating between VM IDs and DAP IDs.
@override
Future<void> customRequest(
Request request,
RawRequestArguments? args,
void Function(Object?) sendResponse,
) async {
switch (request.command) {
case Command.createVariableForInstance:
sendResponse(_createVariableForInstance(request.arguments));
break;
case Command.getVariablesInstanceId:
sendResponse(_getVariablesInstanceId(request.arguments));
break;
default:
await super.customRequest(request, args, sendResponse);
}
}
/// Creates a DAP variablesReference for a VM Instance ID.
Map<String, Object?> _createVariableForInstance(Object? arguments) {
if (arguments is! Map<String, Object?>) {
throw DebugAdapterException(
'${Command.createVariableForInstance} arguments must be Map<String, Object?>',
);
}
final isolateId = arguments[Parameters.isolateId];
final instanceId = arguments[Parameters.instanceId];
if (isolateId is! String) {
throw DebugAdapterException(
'createVariableForInstance requires a valid String ${Parameters.isolateId}',
);
}
if (instanceId is! String) {
throw DebugAdapterException(
'createVariableForInstance requires a value String ${Parameters.instanceId}',
);
}
final thread = isolateManager.threadForIsolateId(isolateId);
if (thread == null) {
throw DebugAdapterException('Isolate $isolateId is not valid');
}
// Create a new reference for this instance ID.
final variablesReference =
thread.storeData(WrappedInstanceVariable(instanceId));
return {
Parameters.variablesReference: variablesReference,
};
}
/// Tries to extract a VM Instance ID from a DAP variablesReference.
Map<String, Object?> _getVariablesInstanceId(Object? arguments) {
if (arguments is! Map<String, Object?>) {
throw DebugAdapterException(
'${Command.getVariablesInstanceId} arguments must be Map<String, Object?>',
);
}
final variablesReference = arguments[Parameters.variablesReference];
if (variablesReference is! int) {
throw DebugAdapterException(
'${Command.getVariablesInstanceId} requires a valid int ${Parameters.variablesReference}',
);
}
// Extract the stored data. This should generally always be a
// `WrappedInstanceVariable` (created by `_createVariableForInstance`) but
// for possible future compatibility, we'll also handle `VariableData` and
// other variables we can extract IDs for.
var data = isolateManager.getStoredData(variablesReference)?.data;
// Unwrap if it was wrapped for formatting.
if (data is VariableData) {
data = data.data;
}
// Extract the ID.
final instanceId = data is WrappedInstanceVariable
? data.instanceId
: data is vm.ObjRef
? data.id
: null;
return {
Parameters.instanceId: instanceId,
};
}
/// 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);
}
void handleMessage(String message, void Function(Response) responseWriter) {
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') {
handleIncomingRequest(Request.fromJson(json), responseWriter);
// TODO(helin24): Handle event and response?
} else {
throw potentialException;
}
} catch (e) {
throw potentialException;
}
}
@override
void sendEventToChannel(Event event) {
_dapEventsController.add(event);
}
void setEventHandler(void Function(Event) eventHandler) {
_dapEventsController.stream.listen(eventHandler);
}
}
/// Extends [Thread] with [isolateId] for easier mapping for clients using both
/// DAP and VM Service.
class ThreadWithIsolateId extends Thread {
/// The ID of the Isolate this thread represents.
final String isolateId;
ThreadWithIsolateId({
required super.id,
required super.name,
required this.isolateId,
});
@override
Map<String, Object?> toJson() => {
...super.toJson(),
'isolateId': isolateId,
};
}