[ 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:
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -13,5 +13,6 @@ dependencies:
|
||||
file: any
|
||||
json_rpc_2: any
|
||||
logging: any
|
||||
meta: any
|
||||
stream_channel: any
|
||||
vm_service: any
|
||||
|
||||
Reference in New Issue
Block a user