From d2d28414ef769b60b538577aeb707f3c40de9f4c Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 10 Apr 2026 12:04:28 -0700 Subject: [PATCH] [ Service ] Add support for ID zones to package:dart_runtime_service_vm Also disables logging by default, which can be enabled by specifying the `VM_SERVICE_LOGGING` environment variable. package:vm_service test suite is ~99% passing with this change. Change-Id: I41578cc8b39afac93589b8792861be34b848f558 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/490921 Reviewed-by: Jessy Yameogo Reviewed-by: Nicholas Shahan --- pkg/dart_runtime_service/lib/src/clients.dart | 45 +++++++--- .../lib/src/dart_runtime_service.dart | 5 +- .../lib/src/dart_runtime_service_backend.dart | 13 +++ .../test/utils/mocks.dart | 12 ++- .../test/utils/utilities.dart | 3 +- .../bin/vm_service_entrypoint.dart | 2 +- .../lib/dart_runtime_service_vm.dart | 7 +- .../lib/src/dart_runtime_service_vm_rpcs.dart | 54 +++++++++++- .../lib/src/vm_clients.dart | 82 +++++++++++++++++++ .../lib/src/vm_expression_evaluator.dart | 14 +++- pkg/dart_runtime_service_vm/pubspec.yaml | 1 + 11 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 pkg/dart_runtime_service_vm/lib/src/vm_clients.dart diff --git a/pkg/dart_runtime_service/lib/src/clients.dart b/pkg/dart_runtime_service/lib/src/clients.dart index 438907cc6ff..a36c3a42055 100644 --- a/pkg/dart_runtime_service/lib/src/clients.dart +++ b/pkg/dart_runtime_service/lib/src/clients.dart @@ -22,7 +22,8 @@ typedef ServiceAlias = String; typedef ServiceNameAliasPair = ({ServiceName service, ServiceAlias alias}); /// Represents a client that is connected to a service. -base class Client { +base class Client { + @protected Client({ required this.connection, required UnmodifiableClientNamedLookup clients, @@ -63,7 +64,7 @@ base class Client { final StreamChannel connection; late json_rpc.Peer _clientPeer; late final DartRuntimeServiceRpcs _internalRpcs; - final DartRuntimeServiceBackend backend; + final BE backend; /// If `true`, this client was created via /// [DartRuntimeService.addArtificialClient]. @@ -86,11 +87,7 @@ base class Client { logger.info('Initializing...'); this.namespace = namespace; registerRpcHandlers(); - done = _listen().then((_) { - logger.info('Client connection closed.'); - // Cleanup stream subscription state when the client disconnects. - _internalRpcs.eventStreamMethods.onClientDisconnect(this); - }); + done = _listen().then((_) => cleanup()); logger.info('Initialization complete.'); return done; } @@ -107,6 +104,14 @@ base class Client { await _clientPeer.close(); } + @protected + @mustCallSuper + Future cleanup() async { + logger.info('Client connection closed.'); + // Cleanup stream subscription state when the client disconnects. + _internalRpcs.eventStreamMethods.onClientDisconnect(this); + } + @mustCallSuper void registerRpcHandlers() { _internalRpcs @@ -242,11 +247,12 @@ abstract interface class ClientConnectionController { /// service. /// /// Call [addClient] when a client connects to your service. -base class ClientManager implements ClientConnectionController { +base class ClientManager + implements ClientConnectionController { ClientManager({required this.backend, required this.eventStreamMethods}); static const _kServicePrologue = 's'; - final DartRuntimeServiceBackend backend; + final BE backend; final EventStreamMethods eventStreamMethods; final _logger = Logger('$ClientManager'); @@ -289,6 +295,25 @@ base class ClientManager implements ClientConnectionController { ); } + @visibleForOverriding + Client clientBuilder({ + required StreamChannel connection, + required UnmodifiableClientNamedLookup clients, + required EventStreamMethods eventStreamMethods, + required BE backend, + required bool artificial, + String? name, + }) { + return Client( + connection: connection, + clients: clients, + eventStreamMethods: eventStreamMethods, + backend: backend, + name: name, + artificial: artificial, + ); + } + /// Creates a [Client] from [connection] and adds it to the list of connected /// clients. /// @@ -299,7 +324,7 @@ base class ClientManager implements ClientConnectionController { String? name, bool artificial = false, }) { - final client = Client( + final client = clientBuilder( connection: connection, clients: clients, eventStreamMethods: eventStreamMethods, diff --git a/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart b/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart index e09f01a04be..d1dfd7b44aa 100644 --- a/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart +++ b/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart @@ -107,10 +107,7 @@ class DartRuntimeService { ClientConnectionController get clientConnectionController => clientManager; @visibleForTesting - late final ClientManager clientManager = ClientManager( - backend: backend, - eventStreamMethods: eventStreamManager, - ); + late final ClientManager clientManager = backend.clientManagerBuilder(); /// The set of currently connected [Client]s. UnmodifiableClientNamedLookup get clients => clientManager.clients; diff --git a/pkg/dart_runtime_service/lib/src/dart_runtime_service_backend.dart b/pkg/dart_runtime_service/lib/src/dart_runtime_service_backend.dart index acdf0d0330d..3a5df1b8a72 100644 --- a/pkg/dart_runtime_service/lib/src/dart_runtime_service_backend.dart +++ b/pkg/dart_runtime_service/lib/src/dart_runtime_service_backend.dart @@ -8,6 +8,7 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart'; +import 'clients.dart'; import 'dart_runtime_service.dart'; import 'dart_runtime_service_rpcs.dart'; import 'event_streams.dart'; @@ -30,6 +31,18 @@ abstract class DartRuntimeServiceBackend { /// Adds support for expression evaluation if non-null. ExpressionEvaluator? get expressionEvaluator => null; + /// Used by [DartRuntimeService] to create a [ClientManager]. + /// + /// Backend implementations should override this method and return their own + /// [ClientManager] implementation if they require tracking additional state + /// for each [Client]. + ClientManager> clientManagerBuilder() { + return ClientManager( + backend: this, + eventStreamMethods: frontend.eventStreams, + ); + } + /// Invoked by the [DartRuntimeService] when the service is initializing, /// before the service's HTTP server is started. /// diff --git a/pkg/dart_runtime_service/test/utils/mocks.dart b/pkg/dart_runtime_service/test/utils/mocks.dart index e4613613064..6cc35cac8fc 100644 --- a/pkg/dart_runtime_service/test/utils/mocks.dart +++ b/pkg/dart_runtime_service/test/utils/mocks.dart @@ -14,6 +14,8 @@ import 'package:test/fake.dart'; /// a backend implementation. base class FakeDartRuntimeServiceBackend extends Fake implements DartRuntimeServiceBackend { + FakeDartRuntimeServiceBackend({required this.frontend}); + @override Future initialize() async {} @@ -41,7 +43,7 @@ base class FakeDartRuntimeServiceBackend extends Fake UnmodifiableListView(const []); @override - DartRuntimeService get frontend => throw UnimplementedError(); + final DartRuntimeService frontend; @override IsolateManager get isolateManager => throw UnimplementedError(); @@ -49,6 +51,14 @@ base class FakeDartRuntimeServiceBackend extends Fake @override ExpressionEvaluator? get expressionEvaluator => null; + @override + ClientManager clientManagerBuilder() { + return ClientManager( + backend: this, + eventStreamMethods: frontend.eventStreams, + ); + } + @override void onStreamCancel({required String streamId}) {} diff --git a/pkg/dart_runtime_service/test/utils/utilities.dart b/pkg/dart_runtime_service/test/utils/utilities.dart index 6c2a2cd0df0..a15c0d310e3 100644 --- a/pkg/dart_runtime_service/test/utils/utilities.dart +++ b/pkg/dart_runtime_service/test/utils/utilities.dart @@ -24,7 +24,8 @@ Future createDartRuntimeServiceForTest({ service = await DartRuntimeService.initialize( config: config, - backendBuilder: (_) => FakeDartRuntimeServiceBackend(), + backendBuilder: (frontend) => + FakeDartRuntimeServiceBackend(frontend: frontend), ); return service; } diff --git a/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart b/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart index 2410356c4c4..f6d63b1d4b7 100644 --- a/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart +++ b/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart @@ -113,7 +113,7 @@ Future main([List args = const []]) async { } await DartRuntimeService.initialize( config: DartRuntimeServiceOptions( - enableLogging: true, + enableLogging: Platform.environment.containsKey('VM_SERVICE_LOGGING'), port: _port, disableAuthCodes: _authCodesDisabled, disableOriginCheck: _originCheckDisabled, diff --git a/pkg/dart_runtime_service_vm/lib/dart_runtime_service_vm.dart b/pkg/dart_runtime_service_vm/lib/dart_runtime_service_vm.dart index 2df2d05bf0c..9ddad4cf9a0 100644 --- a/pkg/dart_runtime_service_vm/lib/dart_runtime_service_vm.dart +++ b/pkg/dart_runtime_service_vm/lib/dart_runtime_service_vm.dart @@ -17,6 +17,7 @@ import 'package:stream_channel/stream_channel.dart'; import 'src/dart_runtime_service_vm_rpcs.dart'; import 'src/native_bindings.dart'; +import 'src/vm_clients.dart'; import 'src/vm_dev_fs.dart'; import 'src/vm_expression_evaluator.dart'; import 'src/vm_isolate_manager.dart'; @@ -79,7 +80,7 @@ class DartRuntimeServiceVMBackend @override late final VmExpressionEvaluator expressionEvaluator; - final _vmServiceRpcs = DartRuntimeServiceVmRpcs(); + late final _vmServiceRpcs = DartRuntimeServiceVmRpcs(backend: this); /// Adds support for launching and accepting connections from the /// Dart Development Service. @@ -103,6 +104,10 @@ class DartRuntimeServiceVMBackend @override OptionalHandler get httpHandler => _devFs.handlePutStreamRequest; + @override + VmClientManager clientManagerBuilder() => + VmClientManager(backend: this, eventStreamMethods: frontend.eventStreams); + @override Future initialize() async { _logger.info('Initializing...'); diff --git a/pkg/dart_runtime_service_vm/lib/src/dart_runtime_service_vm_rpcs.dart b/pkg/dart_runtime_service_vm/lib/src/dart_runtime_service_vm_rpcs.dart index 959b7cd42b1..f045fce2116 100644 --- a/pkg/dart_runtime_service_vm/lib/src/dart_runtime_service_vm_rpcs.dart +++ b/pkg/dart_runtime_service_vm/lib/src/dart_runtime_service_vm_rpcs.dart @@ -5,18 +5,34 @@ import 'dart:collection'; import 'package:dart_runtime_service/dart_runtime_service.dart'; +import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; +import '../dart_runtime_service_vm.dart'; import 'native_bindings.dart'; +import 'vm_clients.dart'; /// Implementations of RPCs specific to the VM service that are not handled /// in runtime/vm/service.cc. final class DartRuntimeServiceVmRpcs { + DartRuntimeServiceVmRpcs({required this.backend}); + final _logger = Logger('$DartRuntimeServiceVmRpcs'); final _nativeBindings = NativeBindings(); + final DartRuntimeServiceVMBackend backend; + + static const _kGetSupportedProtocols = 'getSupportedProtocols'; + static const _kCreateIdZone = 'createIdZone'; + static const _kDeleteIdZone = 'deleteIdZone'; + + static const _kIsolateId = 'isolateId'; + static const _kIdZoneId = 'idZoneId'; + late final rpcs = UnmodifiableListView([ - ('getSupportedProtocols', getSupportedProtocols), + (_kGetSupportedProtocols, getSupportedProtocols), + (_kCreateIdZone, createIdZone), + (_kDeleteIdZone, deleteIdZone), ]); /// Returns the list of protocols implemented by the service. @@ -42,4 +58,40 @@ final class DartRuntimeServiceVmRpcs { ], ).toJson(); } + + /// Creates a new [IdZone] where temporary IDs for instances in the specified + /// isolate may be allocated for [client]. + Future createIdZone( + json_rpc_2.Parameters parameters, + Client client, + ) async { + // The implementation of this RPC is in the VM, but we track which zones + // have been created by individual clients so we can clean them up when the + // clients disconnect. + final result = await backend.sendToRuntime(parameters); + final idZone = IdZone.parse(result); + if (idZone != null) { + final isolateId = parameters[_kIsolateId].asString; + final vmClient = client as VmClient; + vmClient.registerIdZone(isolateId: isolateId, idZone: idZone); + } + return result; + } + + /// Destroys an [IdZone] owned by [client]. + Future deleteIdZone( + json_rpc_2.Parameters parameters, + Client client, + ) async { + // The implementation of this RPC is in the VM, but we track which zones + // have been created by individual clients so we can clean them up when the + // clients disconnect. + final result = await backend.sendToRuntime(parameters); + final vmClient = client as VmClient; + vmClient.unregisterIdZone( + isolateId: parameters[_kIsolateId].asString, + idZoneId: parameters[_kIdZoneId].asString, + ); + return result; + } } diff --git a/pkg/dart_runtime_service_vm/lib/src/vm_clients.dart b/pkg/dart_runtime_service_vm/lib/src/vm_clients.dart new file mode 100644 index 00000000000..eec91d323c1 --- /dev/null +++ b/pkg/dart_runtime_service_vm/lib/src/vm_clients.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2026, 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 'package:dart_runtime_service/dart_runtime_service.dart'; +import 'package:json_rpc_2/json_rpc_2.dart' hide Client; +import 'package:meta/meta.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../dart_runtime_service_vm.dart'; + +typedef ServiceIDZone = ({IdZone idZone, String isolateId}); + +/// A [Client] of the VM service. +final class VmClient extends Client { + VmClient({ + required super.connection, + required super.clients, + required super.eventStreamMethods, + required super.backend, + required super.artificial, + super.name, + }); + + final _idZones = {}; + + @override + @protected + Future cleanup() async { + await _cleanupIdZones(); + await super.cleanup(); + } + + /// Track a newly created [IdZone]. + void registerIdZone({required String isolateId, required IdZone idZone}) { + _idZones.add((idZone: idZone, isolateId: isolateId)); + } + + /// Stop tracking a recently destroyed [IdZone]. + void unregisterIdZone({required String isolateId, required String idZoneId}) { + _idZones.removeWhere( + (e) => e.isolateId == isolateId && e.idZone.id == idZoneId, + ); + } + + Future _cleanupIdZones() async { + await Future.wait([ + for (final (:idZone, :isolateId) in _idZones) + backend.sendToRuntime( + Parameters('deleteIdZone', { + 'isolateId': isolateId, + 'idZoneId': idZone.id!, + }), + ), + ]); + } +} + +/// Manages and tracks clients of the VM service. +final class VmClientManager extends ClientManager { + VmClientManager({required super.backend, required super.eventStreamMethods}); + + @override + VmClient clientBuilder({ + required StreamChannel connection, + required UnmodifiableClientNamedLookup clients, + required EventStreamMethods eventStreamMethods, + required DartRuntimeServiceVMBackend backend, + required bool artificial, + String? name, + }) { + return VmClient( + connection: connection, + clients: clients, + eventStreamMethods: eventStreamMethods, + backend: backend, + name: name, + artificial: artificial, + ); + } +} diff --git a/pkg/dart_runtime_service_vm/lib/src/vm_expression_evaluator.dart b/pkg/dart_runtime_service_vm/lib/src/vm_expression_evaluator.dart index 28c3daa54dd..d2d3131bc6a 100644 --- a/pkg/dart_runtime_service_vm/lib/src/vm_expression_evaluator.dart +++ b/pkg/dart_runtime_service_vm/lib/src/vm_expression_evaluator.dart @@ -29,8 +29,8 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { static const kScope = 'scope'; static const kDisableBreakpoints = 'disableBreakpoints'; - // TODO(bkonyi): add ID zone support. - // static const kIdZoneId = 'idZoneId'; + // ID zone support. + static const kIdZoneId = 'idZoneId'; // `evaluate` specific parameters. static const kTargetId = 'targetId'; @@ -74,6 +74,9 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { disableBreakpoints: parameters[kDisableBreakpoints].exists ? parameters[kDisableBreakpoints].asBool : null, + idZoneId: parameters[kIdZoneId].exists + ? parameters[kIdZoneId].asString + : null, ); } @@ -90,6 +93,9 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { disableBreakpoints: parameters[kDisableBreakpoints].exists ? parameters[kDisableBreakpoints].asBool : null, + idZoneId: parameters[kIdZoneId].exists + ? parameters[kIdZoneId].asString + : null, ); } @@ -105,6 +111,7 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { required String? targetId, required Map? scope, required bool? disableBreakpoints, + required String? idZoneId, }) async { final buildScopeResponse = await _buildScope( isolateId: isolateId, @@ -124,6 +131,7 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { targetId: targetId, scope: scope, disableBreakpoints: disableBreakpoints, + idZoneId: idZoneId, kernelBase64: kernelBase64, ); } @@ -206,6 +214,7 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { required int? frameIndex, required String? targetId, required bool? disableBreakpoints, + required String? idZoneId, required String kernelBase64, }) { final params = { @@ -215,6 +224,7 @@ final class VmExpressionEvaluator extends ExpressionEvaluator { kFrameIndex: ?frameIndex, kTargetId: ?targetId, kDisableBreakpoints: ?disableBreakpoints, + kIdZoneId: ?idZoneId, kKernelBytes: kernelBase64, }; return backend.sendToRuntime( diff --git a/pkg/dart_runtime_service_vm/pubspec.yaml b/pkg/dart_runtime_service_vm/pubspec.yaml index 37312f95850..b09b942eb58 100644 --- a/pkg/dart_runtime_service_vm/pubspec.yaml +++ b/pkg/dart_runtime_service_vm/pubspec.yaml @@ -13,5 +13,6 @@ dependencies: file: any json_rpc_2: any logging: any + meta: any stream_channel: any vm_service: any