| // 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); |
| } |
| } |