blob: 3e69ca79d021d448f6e04e136580c6f1fe7df503 [file] [log] [blame] [edit]
// Copyright (c) 2025, 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 'package:dwds/data/debug_event.dart';
import 'package:dwds/data/hot_reload_response.dart';
import 'package:dwds/data/hot_restart_response.dart';
import 'package:dwds/data/register_event.dart';
import 'package:dwds/data/service_extension_response.dart';
import 'package:dwds/src/connections/app_connection.dart';
import 'package:dwds/src/debugging/inspector.dart';
import 'package:dwds/src/events.dart';
import 'package:dwds/src/services/debug_service.dart';
import 'package:dwds/src/utilities/dart_uri.dart';
import 'package:dwds/src/utilities/shared.dart';
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart' as semver;
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:vm_service/vm_service.dart';
import 'package:vm_service_interface/vm_service_interface.dart';
// This event is identical to the one sent by the VM service from
// sdk/lib/vmservice/vmservice.dart before existing VM service clients are
// disconnected.
final class DartDevelopmentServiceConnectedEvent extends Event {
DartDevelopmentServiceConnectedEvent({
required super.timestamp,
required this.uri,
}) : message =
'A Dart Developer Service instance has connected and this direct '
'connection to the VM service will now be closed. Please reconnect to '
'the Dart Development Service at $uri.',
super(kind: 'DartDevelopmentServiceConnected');
final String message;
final String uri;
@override
Map<String, Object?> toJson() => {
...super.toJson(),
'uri': uri,
'message': message,
};
}
final class DisconnectNonDartDevelopmentServiceClients extends RPCError {
DisconnectNonDartDevelopmentServiceClients()
: super('_yieldControlToDDS', kErrorCode);
// Arbitrary error code that's unlikely to be used elsewhere.
static const kErrorCode = -199328;
}
/// Abstract base class for VM service proxy implementations.
abstract base class ProxyService<InspectorT extends AppInspector>
implements VmServiceInterface {
/// Cache of all existing StreamControllers.
///
/// These are all created through [onEvent].
final Map<String, StreamController<vm_service.Event>> _streamControllers = {};
/// Provides variable inspection functionality.
InspectorT get inspector {
if (_inspector == null) {
throw StateError('No running isolate (inspector is not set).');
}
return _inspector!;
}
@protected
set inspector(InspectorT? inspector) {
_inspector = inspector;
}
InspectorT? _inspector;
/// Determines if there an isolate running currently.
///
/// [_inspector] is `null` iff the isolate is not running,
/// for example, before the first isolate starts or during
/// a hot restart.
bool get isIsolateRunning => _inspector != null;
/// The [DebugService] implementation.
final DebugService debugService;
/// The root `VM` instance.
final vm_service.VM vm;
/// Signals when isolate is initialized.
Future<void> get isInitialized => initializedCompleter.future;
Completer<void> initializedCompleter = Completer<void>();
static const _kPauseIsolatesOnStartFlag = 'pause_isolates_on_start';
/// The flags that can be set at runtime via [setFlag] and their respective
/// values.
final Map<String, bool> _currentVmServiceFlags = {
_kPauseIsolatesOnStartFlag: false,
};
/// The value of the [_kPauseIsolatesOnStartFlag].
///
/// This value can be updated at runtime via [setFlag].
bool get pauseIsolatesOnStart =>
_currentVmServiceFlags[_kPauseIsolatesOnStartFlag] ?? false;
/// Stream controller for resume events after restart.
final _resumeAfterRestartEventsController =
StreamController<String>.broadcast();
/// A global stream of resume events for hot restart.
///
/// The values in the stream are the isolates IDs for the resume event.
///
/// IMPORTANT: This should only be listened to during a hot-restart or page
/// refresh. The debugger ignores any resume events as long as there is a
/// subscriber to this stream.
Stream<String> get resumeAfterRestartEventsStream =>
_resumeAfterRestartEventsController.stream;
/// Whether or not the connected app has a pending restart.
bool get hasPendingRestart => _resumeAfterRestartEventsController.hasListener;
// Protected accessors for subclasses
Map<String, StreamController<vm_service.Event>> get streamControllers =>
_streamControllers;
StreamController<String> get resumeAfterRestartEventsController =>
_resumeAfterRestartEventsController;
Map<String, bool> get currentVmServiceFlags => _currentVmServiceFlags;
/// The root at which we're serving.
final String root;
ProxyService({
required this.vm,
required this.root,
required this.debugService,
});
/// Sends events to stream controllers.
void streamNotify(String streamId, vm_service.Event event) {
final controller = _streamControllers[streamId];
if (controller == null) return;
controller.add(event);
}
/// Returns a broadcast stream for the given streamId.
@override
Stream<vm_service.Event> onEvent(String streamId) {
return _streamControllers.putIfAbsent(streamId, () {
return StreamController<vm_service.Event>.broadcast();
}).stream;
}
@override
Future<void> yieldControlToDDS(String uri) async {
// This will throw an RPCError if there's already an existing DDS instance.
debugService.yieldControlToDDS(uri);
// Notify existing clients that DDS has connected and they're about to be
// disconnected.
final event = DartDevelopmentServiceConnectedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
uri: uri,
);
streamNotify(EventStreams.kService, event);
// We throw since we have no other way to control what the response content
// is for this RPC. The debug service will check for this particular
// exception as a signal to close connections to all other clients.
throw DisconnectNonDartDevelopmentServiceClients();
}
@override
Future<vm_service.Success> streamListen(String streamId) =>
wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId));
Future<vm_service.Success> _streamListen(String streamId) async {
// TODO: This should return an error if the stream is already being listened
// to.
onEvent(streamId);
return vm_service.Success();
}
@override
Future<vm_service.Success> streamCancel(String streamId) {
// TODO: We should implement this (as we've already implemented
// streamListen).
return _rpcNotSupportedFuture('streamCancel');
}
@override
Future<vm_service.VM> getVM() => wrapInErrorHandlerAsync('getVM', _getVM);
Future<vm_service.VM> _getVM() {
return captureElapsedTime(() async {
return vm;
}, (result) => DwdsEvent.getVM());
}
@override
Future<Isolate> getIsolate(String isolateId) =>
wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId));
Future<Isolate> _getIsolate(String isolateId) {
return captureElapsedTime(() async {
await isInitialized;
checkIsolate('getIsolate', isolateId);
return inspector.isolate;
}, (result) => DwdsEvent.getIsolate());
}
@override
Future<Success> setName(String isolateId, String name) =>
wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name));
Future<Success> _setName(String isolateId, String name) async {
await isInitialized;
checkIsolate('setName', isolateId);
inspector.isolate.name = name;
return Success();
}
@override
Future<Success> setVMName(String name) =>
wrapInErrorHandlerAsync('setVMName', () => _setVMName(name));
Future<Success> _setVMName(String name) async {
vm.name = name;
streamNotify(
'VM',
Event(
kind: EventKind.kVMUpdate,
timestamp: DateTime.now().millisecondsSinceEpoch,
// We are not guaranteed to have an isolate at this point in time.
isolate: null,
vm: toVMRef(vm),
),
);
return Success();
}
@override
Future<vm_service.FlagList> getFlagList() =>
wrapInErrorHandlerAsync('getFlagList', _getFlagList);
Future<vm_service.FlagList> _getFlagList() async {
final flags = _currentVmServiceFlags.entries.map<vm_service.Flag>(
(entry) =>
vm_service.Flag(name: entry.key, valueAsString: '${entry.value}'),
);
return vm_service.FlagList(flags: flags.toList());
}
@override
Future<vm_service.Success> setFlag(String name, String value) =>
wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value));
Future<vm_service.Success> _setFlag(String name, String value) async {
if (!_currentVmServiceFlags.containsKey(name)) {
throw vm_service.RPCError(
'setFlag',
vm_service.RPCErrorKind.kInvalidRequest.code,
'Cannot set flag "$name" (invalid flag)',
);
}
if (value != 'true' && value != 'false') {
throw vm_service.RPCError(
'setFlag',
vm_service.RPCErrorKind.kInvalidParams.code,
'Cannot set flag "$name" to "$value" (boolean value expected)',
);
}
_currentVmServiceFlags[name] = value == 'true';
return vm_service.Success();
}
@override
Future<vm_service.ProtocolList> getSupportedProtocols() =>
wrapInErrorHandlerAsync('getSupportedProtocols', _getSupportedProtocols);
Future<vm_service.ProtocolList> _getSupportedProtocols() async {
final version = semver.Version.parse(vm_service.vmServiceVersion);
return vm_service.ProtocolList(
protocols: [
vm_service.Protocol(
protocolName: 'VM Service',
major: version.major,
minor: version.minor,
),
],
);
}
@override
Future<vm_service.Version> getVersion() =>
wrapInErrorHandlerAsync('getVersion', _getVersion);
Future<vm_service.Version> _getVersion() async {
final version = semver.Version.parse(vm_service.vmServiceVersion);
return vm_service.Version(major: version.major, minor: version.minor);
}
// Note: Ignore the optional local parameter, when it is set to `true` the
// request is intercepted and handled by DDS.
@override
Future<UriList> lookupResolvedPackageUris(
String isolateId,
List<String> uris, {
bool? local,
}) => wrapInErrorHandlerAsync(
'lookupResolvedPackageUris',
() => _lookupResolvedPackageUris(isolateId, uris),
);
Future<UriList> _lookupResolvedPackageUris(
String isolateId,
List<String> uris,
) async {
await isInitialized;
checkIsolate('lookupResolvedPackageUris', isolateId);
return UriList(uris: uris.map(DartUri.toResolvedUri).toList());
}
@override
Future<UriList> lookupPackageUris(String isolateId, List<String> uris) =>
wrapInErrorHandlerAsync(
'lookupPackageUris',
() => _lookupPackageUris(isolateId, uris),
);
Future<UriList> _lookupPackageUris(
String isolateId,
List<String> uris,
) async {
await isInitialized;
checkIsolate('lookupPackageUris', isolateId);
return UriList(uris: uris.map(DartUri.toPackageUri).toList());
}
@override
Future<ScriptList> getScripts(String isolateId) =>
wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId));
Future<ScriptList> _getScripts(String isolateId) {
return captureElapsedTime(() async {
await isInitialized;
checkIsolate('getScripts', isolateId);
return inspector.getScripts();
}, (result) => DwdsEvent.getScripts());
}
/// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service
/// protocol [Event]s.
void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) {
for (final debugEvent in debugEvents.events) {
parseDebugEvent(debugEvent);
}
}
/// Parses the [DebugEvent] and emits a corresponding Dart VM Service
/// protocol [Event].
@mustCallSuper
void parseDebugEvent(DebugEvent debugEvent) {
if (!isIsolateRunning) return;
final isolateRef = inspector.isolateRef;
streamNotify(
EventStreams.kExtension,
Event(
kind: EventKind.kExtension,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef,
)
..extensionKind = debugEvent.kind
..extensionData = ExtensionData.parse(
jsonDecode(debugEvent.eventData) as Map<String, dynamic>,
),
);
}
/// Parses the [RegisterEvent] and emits a corresponding Dart VM Service
/// protocol [Event].
@mustCallSuper
void parseRegisterEvent(RegisterEvent registerEvent) {
if (!isIsolateRunning) return;
final isolate = inspector.isolate;
final isolateRef = inspector.isolateRef;
final service = registerEvent.eventData;
isolate.extensionRPCs?.add(service);
streamNotify(
EventStreams.kIsolate,
Event(
kind: EventKind.kServiceExtensionAdded,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef,
)..extensionRPC = service,
);
}
/// Completes hot reload with response from client.
///
/// Default implementation throws [UnimplementedError].
/// Override in subclasses that support hot reload completion.
void completeHotReload(HotReloadResponse response) {
throw UnimplementedError('completeHotReload not supported');
}
/// Completes hot restart with response from client.
///
/// Default implementation throws [UnimplementedError].
/// Override in subclasses that support hot restart completion.
void completeHotRestart(HotRestartResponse response) {
throw UnimplementedError('completeHotRestart not supported');
}
/// Completes service extension with response from client.
///
/// Default implementation throws [UnimplementedError].
/// Override in subclasses that support service extension completion.
void completeServiceExtension(ServiceExtensionResponse response) {
throw UnimplementedError('completeServiceExtension not supported');
}
/// Sends `ServiceExtensionAdded` events for each currently registered
/// service extension.
Future<void> sendServiceExtensionRegisteredEvents() async {
final timestamp = DateTime.now().millisecondsSinceEpoch;
for (final extensionRpc in await inspector.getExtensionRpcs()) {
streamNotify(
'Isolate',
vm_service.Event(
kind: vm_service.EventKind.kServiceExtensionAdded,
timestamp: timestamp,
isolate: inspector.isolateRef,
)..extensionRPC = extensionRpc,
);
}
}
/// Standard RPC error for unsupported methods.
static vm_service.RPCError _rpcNotSupported(String method) {
return vm_service.RPCError(
method,
vm_service.RPCErrorKind.kMethodNotFound.code,
'$method: Not supported on web devices',
);
}
/// Standard future error for unsupported methods.
static Future<T> _rpcNotSupportedFuture<T>(String method) {
return Future.error(_rpcNotSupported(method));
}
/// Protected accessor for _rpcNotSupportedFuture for subclasses
Future<T> rpcNotSupportedFuture<T>(String method) {
return _rpcNotSupportedFuture<T>(method);
}
// Default implementations for unsupported methods
@override
Future<vm_service.AllocationProfile> getAllocationProfile(
String isolateId, {
bool? gc,
bool? reset,
}) {
return _rpcNotSupportedFuture('getAllocationProfile');
}
@override
Future<vm_service.ClassList> getClassList(String isolateId) {
return _rpcNotSupportedFuture('getClassList');
}
@override
Future<vm_service.InstanceSet> getInstances(
String isolateId,
String classId,
int limit, {
bool? includeImplementers,
bool? includeSubclasses,
String? idZoneId,
}) {
return _rpcNotSupportedFuture('getInstances');
}
@override
Future<vm_service.Success> kill(String isolateId) {
return _rpcNotSupportedFuture('kill');
}
@override
Future<vm_service.Success> clearVMTimeline() {
return _rpcNotSupportedFuture('clearVMTimeline');
}
@override
Future<vm_service.Timeline> getVMTimeline({
int? timeOriginMicros,
int? timeExtentMicros,
}) {
return _rpcNotSupportedFuture('getVMTimeline');
}
@override
Future<vm_service.TimelineFlags> getVMTimelineFlags() {
return _rpcNotSupportedFuture('getVMTimelineFlags');
}
@override
Future<vm_service.Success> setVMTimelineFlags(List<String> recordedStreams) {
return _rpcNotSupportedFuture('setVMTimelineFlags');
}
@override
Future<vm_service.Timestamp> getVMTimelineMicros() {
return _rpcNotSupportedFuture('getVMTimelineMicros');
}
@override
Future<vm_service.InboundReferences> getInboundReferences(
String isolateId,
String targetId,
int limit, {
String? idZoneId,
}) {
return _rpcNotSupportedFuture('getInboundReferences');
}
@override
Future<vm_service.RetainingPath> getRetainingPath(
String isolateId,
String targetId,
int limit, {
String? idZoneId,
}) {
return _rpcNotSupportedFuture('getRetainingPath');
}
@override
Future<vm_service.Success> requestHeapSnapshot(String isolateId) {
return _rpcNotSupportedFuture('requestHeapSnapshot');
}
@override
Future<vm_service.IsolateGroup> getIsolateGroup(String isolateGroupId) {
return _rpcNotSupportedFuture('getIsolateGroup');
}
@override
Future<vm_service.MemoryUsage> getIsolateGroupMemoryUsage(
String isolateGroupId,
) {
return _rpcNotSupportedFuture('getIsolateGroupMemoryUsage');
}
@override
Future<vm_service.ProcessMemoryUsage> getProcessMemoryUsage() =>
_rpcNotSupportedFuture('getProcessMemoryUsage');
@override
Future<Success> setLibraryDebuggable(
String isolateId,
String libraryId,
bool isDebuggable,
) {
return rpcNotSupportedFuture('setLibraryDebuggable');
}
@override
Future<vm_service.PortList> getPorts(String isolateId) =>
throw UnimplementedError();
@override
Future<vm_service.CpuSamples> getAllocationTraces(
String isolateId, {
int? timeOriginMicros,
int? timeExtentMicros,
String? classId,
}) => throw UnimplementedError();
@override
Future<vm_service.Success> setTraceClassAllocation(
String isolateId,
String classId,
bool enable,
) => throw UnimplementedError();
@override
Future<vm_service.Breakpoint> setBreakpointState(
String isolateId,
String breakpointId,
bool enable,
) => throw UnimplementedError();
@override
Future<vm_service.Success> streamCpuSamplesWithUserTag(
List<String> userTags,
) => _rpcNotSupportedFuture('streamCpuSamplesWithUserTag');
@override
Future<vm_service.CpuSamples> getCpuSamples(
String isolateId,
int timeOriginMicros,
int timeExtentMicros,
) {
return _rpcNotSupportedFuture('getCpuSamples');
}
@override
Future<vm_service.Success> clearCpuSamples(String isolateId) {
return _rpcNotSupportedFuture('clearCpuSamples');
}
@override
Future<Success> registerService(String service, String alias) {
return rpcNotSupportedFuture('registerService');
}
/// Creates a new isolate for debugging.
///
/// Implementations should handle isolate lifecycle management according to
/// their specific debugging mode (Chrome vs WebSocket).
Future<void> createIsolate(
AppConnection appConnection, {
bool newConnection = false,
});
/// Destroys the isolate and cleans up debugging state.
///
/// Implementations should handle cleanup according to their specific
/// debugging mode and connection management strategy.
void destroyIsolate();
/// Validate that isolateId matches the current isolate we're connected to and
/// return that isolate.
///
/// This is useful to call at the beginning of API methods that are passed an
/// isolate id.
@protected
Isolate checkIsolate(String methodName, String? isolateId) {
final currentIsolateId = inspector.isolate.id;
if (currentIsolateId == null) {
throw StateError('No running isolate ID');
}
if (isolateId != currentIsolateId) {
_throwSentinel(
methodName,
SentinelKind.kCollected,
'Unrecognized isolateId: $isolateId',
);
}
return inspector.isolate;
}
static Never _throwSentinel(String method, String kind, String message) {
final data = <String, String>{'kind': kind, 'valueAsString': message};
throw SentinelException.parse(method, data);
}
/// Prevent DWDS from blocking Dart SDK rolls if changes in package:vm_service
/// are unimplemented in DWDS.
@override
dynamic noSuchMethod(Invocation invocation) {
return super.noSuchMethod(invocation);
}
}