[ 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 <yjessy@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
Ben Konyi
2026-04-10 12:04:28 -07:00
committed by Commit Queue
parent cb72e886d5
commit d2d28414ef
11 changed files with 217 additions and 21 deletions
+35 -10
View File
@@ -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<BE extends DartRuntimeServiceBackend> {
@protected
Client({
required this.connection,
required UnmodifiableClientNamedLookup clients,
@@ -63,7 +64,7 @@ base class Client {
final StreamChannel<Object?> 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<void> 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<BE extends DartRuntimeServiceBackend>
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<Object?> 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,
@@ -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;
@@ -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<IM extends IsolateManager> {
/// 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<DartRuntimeServiceBackend<IM>> 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.
///
+11 -1
View File
@@ -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<void> 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<DartRuntimeServiceBackend> clientManagerBuilder() {
return ClientManager(
backend: this,
eventStreamMethods: frontend.eventStreams,
);
}
@override
void onStreamCancel({required String streamId}) {}
@@ -24,7 +24,8 @@ Future<DartRuntimeService> createDartRuntimeServiceForTest({
service = await DartRuntimeService.initialize(
config: config,
backendBuilder: (_) => FakeDartRuntimeServiceBackend(),
backendBuilder: (frontend) =>
FakeDartRuntimeServiceBackend(frontend: frontend),
);
return service;
}
@@ -113,7 +113,7 @@ Future<void> main([List<String> args = const []]) async {
}
await DartRuntimeService.initialize(
config: DartRuntimeServiceOptions(
enableLogging: true,
enableLogging: Platform.environment.containsKey('VM_SERVICE_LOGGING'),
port: _port,
disableAuthCodes: _authCodesDisabled,
disableOriginCheck: _originCheckDisabled,
@@ -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<void> initialize() async {
_logger.info('Initializing...');
@@ -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<ServiceRpcHandler>([
('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<RpcResponse> 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<RpcResponse> 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;
}
}
@@ -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<DartRuntimeServiceVMBackend> {
VmClient({
required super.connection,
required super.clients,
required super.eventStreamMethods,
required super.backend,
required super.artificial,
super.name,
});
final _idZones = <ServiceIDZone>{};
@override
@protected
Future<void> 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<void> _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<DartRuntimeServiceVMBackend> {
VmClientManager({required super.backend, required super.eventStreamMethods});
@override
VmClient clientBuilder({
required StreamChannel<Object?> 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,
);
}
}
@@ -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<String, String>? 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 = <String, Object?>{
@@ -215,6 +224,7 @@ final class VmExpressionEvaluator extends ExpressionEvaluator {
kFrameIndex: ?frameIndex,
kTargetId: ?targetId,
kDisableBreakpoints: ?disableBreakpoints,
kIdZoneId: ?idZoneId,
kKernelBytes: kernelBase64,
};
return backend.sendToRuntime(
+1
View File
@@ -13,5 +13,6 @@ dependencies:
file: any
json_rpc_2: any
logging: any
meta: any
stream_channel: any
vm_service: any