diff --git a/BUILD.gn b/BUILD.gn index c1179533453..f8411f94213 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -61,6 +61,7 @@ group("runtime") { deps += [ "runtime/bin:dartaotruntime", "runtime/bin:dartaotruntime_product", + "utils/dart_runtime_service_vm", "utils/dart_runtime_service_vm:dart_runtime_service_vm_aot_snapshot", "utils/dartdev:dartdev_aot_snapshot", "utils/dds:dds_aot", diff --git a/build/rbe/rewrapper_dart.py b/build/rbe/rewrapper_dart.py index bf172049492..012e990c309 100644 --- a/build/rbe/rewrapper_dart.py +++ b/build/rbe/rewrapper_dart.py @@ -360,6 +360,9 @@ trace to find the place to insert the appropriate support. elif arg == 'gen/utils/bazel/kernel_worker.dart.dill': self.extra_paths.add(self.rebase(arg)) return self.parse_kernel_worker() + elif arg == 'gen/utils/dart_runtime_service_vm/generate_dart_runtime_service_vm_snapshot.dart.dill': + self.extra_paths.add(self.rebase(arg)) + return self.parse_generate_dart_runtime_service_vm_snapshot() elif arg == 'gen/utils/dartdev/generate_dartdev_snapshot.dart.dill': self.extra_paths.add(self.rebase(arg)) return self.parse_generate_dartdev_snapshot() @@ -677,6 +680,15 @@ trace to find the place to insert the appropriate support. else: self.unsupported('kernel_worker', arg) + def parse_generate_dart_runtime_service_vm_snapshot(self): + while self.has_next_arg: + arg = self.next_arg() + if arg in ['--help']: + pass + else: + self.unsupported('generate_dart_runtime_service_vm_snapshot', + arg) + def parse_generate_dartdev_snapshot(self): while self.has_next_arg: arg = self.next_arg() diff --git a/pkg/dart_runtime_service/lib/dart_runtime_service.dart b/pkg/dart_runtime_service/lib/dart_runtime_service.dart index 5d7cd085781..0b65a39168a 100644 --- a/pkg/dart_runtime_service/lib/dart_runtime_service.dart +++ b/pkg/dart_runtime_service/lib/dart_runtime_service.dart @@ -5,5 +5,7 @@ export 'src/dart_runtime_service.dart'; export 'src/dart_runtime_service_backend.dart'; export 'src/dart_runtime_service_options.dart'; +export 'src/event_streams.dart'; export 'src/exceptions.dart'; +export 'src/isolate_manager.dart'; export 'src/rpc_exceptions.dart'; diff --git a/pkg/dart_runtime_service/lib/src/clients.dart b/pkg/dart_runtime_service/lib/src/clients.dart index 2e8060af345..af9a8062996 100644 --- a/pkg/dart_runtime_service/lib/src/clients.dart +++ b/pkg/dart_runtime_service/lib/src/clients.dart @@ -25,7 +25,9 @@ base class Client { required UnmodifiableNamedLookup clients, required EventStreamMethods eventStreamMethods, required this.backend, + String? name, }) { + _name = name ?? defaultClientName; _clientPeer = json_rpc.Peer(connection, strictProtocolChecks: false); _internalRpcs = DartRuntimeServiceRpcs( clients: clients, @@ -47,12 +49,15 @@ base class Client { late final Future done; Future initialize({required String namespace}) { + 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); }); + logger.info('Initialization complete.'); return done; } @@ -64,6 +69,7 @@ base class Client { /// Called if the connection to the client should be closed. @mustCallSuper Future close() async { + logger.info('Cleaning up.'); await _clientPeer.close(); } @@ -157,8 +163,13 @@ base class Client { /// Sets the name associated with this client. /// /// If [n] is null, the client name is reset to [defaultClientName]. - void setName(String? n) => _name = n ?? defaultClientName; - late String _name = defaultClientName; + void setName(String? n) { + final updated = n ?? defaultClientName; + logger.info('Changing client name to $updated.'); + _name = updated; + } + + late String _name; } /// Used for keeping track and managing clients that are connected to a given @@ -185,12 +196,13 @@ base class ClientManager { /// /// This should be called when a client connects to the service. @mustCallSuper - Client addClient(StreamChannel connection) { + Client addClient({required StreamChannel connection, String? name}) { final client = Client( connection: connection, clients: UnmodifiableNamedLookup(clients), eventStreamMethods: eventStreamMethods, backend: backend, + name: name, ); final namespace = clients.add(client); client.initialize(namespace: namespace).then((_) { 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 754883d9ad6..2c5d2319bd4 100644 --- a/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart +++ b/pkg/dart_runtime_service/lib/src/dart_runtime_service.dart @@ -3,12 +3,14 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:developer'; import 'dart:io'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as io; +import 'package:stream_channel/stream_channel.dart'; import 'clients.dart'; import 'dart_runtime_service_backend.dart'; @@ -19,32 +21,43 @@ import 'handlers.dart'; import 'utils.dart'; typedef RpcResponse = Map; +typedef DartRuntimeServiceBackendBuilder = + DartRuntimeServiceBackend Function(DartRuntimeService); class DartRuntimeService { - DartRuntimeService._({required this.config, required this.backend}) - : authCode = config.disableAuthCodes ? null : generateSecret() { + DartRuntimeService._({ + required this.config, + required DartRuntimeServiceBackendBuilder backendBuilder, + }) : authCode = config.disableAuthCodes ? null : generateSecret() { if (config.enableLogging) { _logger.onRecord.listen(stdout.writeln); } + backend = backendBuilder(this); } static Future initialize({ required DartRuntimeServiceOptions config, - required DartRuntimeServiceBackend backend, + required DartRuntimeServiceBackendBuilder backendBuilder, }) async { - final service = DartRuntimeService._(config: config, backend: backend); + final service = DartRuntimeService._( + config: config, + backendBuilder: backendBuilder, + ); await service._initialize(); return service; } final DartRuntimeServiceOptions config; - final DartRuntimeServiceBackend backend; + late final DartRuntimeServiceBackend backend; /// The ws:// URI pointing to this [DartRuntimeService]'s server. /// /// Throws [DartRuntimeServiceServerNotRunning] if the HTTP server is not /// active. + /// + /// It's possible that the returned [Uri] is no longer valid if the server + /// was recently shut down. Uri get uri { if (_server == null) { throw const DartRuntimeServiceServerNotRunning(); @@ -58,6 +71,9 @@ class DartRuntimeService { /// /// Throws [DartRuntimeServiceServerNotRunning] if the HTTP server is not /// active. + /// + /// It's possible that the returned [Uri] is no longer valid if the server + /// was recently shut down. Uri get httpUri => uri.replace(scheme: 'http'); /// The sse:// URI pointing to this [DartRuntimeService]'s server. @@ -67,6 +83,9 @@ class DartRuntimeService { /// /// Throws [DartRuntimeServiceServerNotRunning] if the HTTP server is not /// active. + /// + /// It's possible that the returned [Uri] is no longer valid if the server + /// was recently shut down. Uri get sseUri { if (config.sseHandlerPath == null) { throw StateError('SSE handler path not configured.'); @@ -92,11 +111,16 @@ class DartRuntimeService { @visibleForTesting late final eventStreamManager = EventStreamManager( + backend: backend, clientsGetter: () => UnmodifiableNamedLookup(clientManager.clients), ); + // TODO(bkonyi): this should be protected by a mutex. HttpServer? _server; + /// Returns true if the HTTP server is active. + bool get isServerRunning => _server != null; + /// Initializes the service's state without starting the web server. Future _initialize() async { await backend.initialize(); @@ -117,15 +141,46 @@ class DartRuntimeService { Logger.root.clearListeners(); } + /// Enables or disables the server based on the value of [enable]. + /// + /// [silenceOutput] is used to determine if the service will output + /// non-logging information to the terminal. + /// + /// This is called when `dart:developer`'s [Service.controlWebServer] is + /// invoked. + // TODO(bkonyi): respect silenceOutput + Future serverControl({ + required bool enable, + bool? silenceOutput, + }) async { + // TODO(bkonyi): verify there's no race conditions + if (!enable && isServerRunning) { + await _shutdownServer(); + } else if (enable && !isServerRunning) { + await _startServer(); + } + } + + /// Toggles the state of the HTTP server, enabling it if it's not running and + /// disabling it if it is running. Future toggleServer() async { // TODO(bkonyi): verify there's no race conditions - if (_server != null) { + if (isServerRunning) { await _shutdownServer(); } else { await _startServer(); } } + /// Creates an artificial client to process JSON-RPC requests from + /// non-standard sources (e.g., from native code). + void addArtificialClient({ + required StreamChannel connection, + required String name, + }) { + clientManager.addClient(connection: connection, name: name); + } + Future _startServer() async { if (_server != null) { _logger.warning( @@ -193,6 +248,11 @@ class DartRuntimeService { await server.close(); } + /// Send a [StreamEvent] to subscribed clients. + void sendEvent({required StreamEvent event}) { + event.send(eventStreamMethods: eventStreamManager); + } + shelf.Handler _handlers() { _logger.info('Building Shelf handlers.'); var pipeline = const shelf.Pipeline(); 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 fc814d307ba..29d3c94efc3 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 @@ -3,16 +3,28 @@ // BSD-style license that can be found in the LICENSE file. import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; +import 'package:meta/meta.dart'; import 'dart_runtime_service.dart'; +import 'event_streams.dart'; +import 'isolate_manager.dart'; /// A backend implementation of a service used to inject non-common /// functionality into a [DartRuntimeService]. -abstract class DartRuntimeServiceBackend { +abstract class DartRuntimeServiceBackend { + DartRuntimeServiceBackend({required this.frontend}); + + /// The active service frontend hosting this [DartRuntimeServiceBackend]. + final DartRuntimeService frontend; + + /// Manages and tracks the lifecycle of isolates for the backend. + IM get isolateManager; + /// Invoked by the [DartRuntimeService] when the service is initializing, /// before the service's HTTP server is started. /// /// The backend should not expect for this to be invoked more than once. + @mustCallSuper Future initialize(); /// Invoked by the [DartRuntimeService] once it has completely finished @@ -38,6 +50,24 @@ abstract class DartRuntimeServiceBackend { /// started. Future onServerStarted({required Uri httpUri, required Uri wsUri}); + /// Invoked when [EventStreamManager.streamListen] is called and the first + /// client has subscribed to [streamId]. + /// + /// [params] contains all of the parameters sent as part of the + /// `streamListen` request. + /// + /// Returns true when the stream was successfully listened to. + bool onStreamListen({ + required String streamId, + required Map params, + }) { + return true; + } + + /// Invoked when [EventStreamManager.streamCancel] is called and there are no + /// more clients listening to [streamId]. + void onStreamCancel({required String streamId}) {} + /// Invoked by the [DartRuntimeService] to register handlers for the RPCs /// provided by the backend. void registerRpcs(json_rpc.Peer clientPeer); diff --git a/pkg/dart_runtime_service/lib/src/dart_runtime_service_rpcs.dart b/pkg/dart_runtime_service/lib/src/dart_runtime_service_rpcs.dart index ff9066cc517..91e1dde9182 100644 --- a/pkg/dart_runtime_service/lib/src/dart_runtime_service_rpcs.dart +++ b/pkg/dart_runtime_service/lib/src/dart_runtime_service_rpcs.dart @@ -201,9 +201,13 @@ final class DartRuntimeServiceRpcs { /// /// If the stream ID does not correspond with a known stream, an error /// response may be returned. - RpcResponse streamListen(json_rpc.Parameters parameters) { + Future streamListen(json_rpc.Parameters parameters) async { final stream = parameters[_kStreamId].asString; - eventStreamMethods.streamListen(client: client, streamId: stream); + eventStreamMethods.streamListen( + client: client, + streamId: stream, + params: parameters.asMap.cast(), + ); return Success().toJson(); } @@ -214,7 +218,7 @@ final class DartRuntimeServiceRpcs { /// /// If the stream ID does not correspond with a known stream, an error /// response may be returned. - RpcResponse streamCancel(json_rpc.Parameters parameters) { + Future streamCancel(json_rpc.Parameters parameters) async { final streamId = parameters[_kStreamId].asString; eventStreamMethods.streamCancel(client: client, streamId: streamId); return Success().toJson(); diff --git a/pkg/dart_runtime_service/lib/src/event_streams.dart b/pkg/dart_runtime_service/lib/src/event_streams.dart index 80975f23d9b..1eec13bf733 100644 --- a/pkg/dart_runtime_service/lib/src/event_streams.dart +++ b/pkg/dart_runtime_service/lib/src/event_streams.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import 'package:vm_service/vm_service.dart'; import 'clients.dart'; +import 'dart_runtime_service_backend.dart'; import 'rpc_exceptions.dart'; import 'utils.dart'; @@ -109,8 +110,16 @@ abstract interface class EventStreamMethods { ServiceAlias alias, ); - /// Subscribes `client` to a stream. - void streamListen({required Client client, required String streamId}); + /// Subscribes [client] to a stream. + /// + /// [params] is the unaltered set of parameters included when `streamListen` + /// is invoked. Backends may use these additional parameters for special + /// behavior (e.g., changing the verbosity of responses). + void streamListen({ + required Client client, + required String streamId, + required Map params, + }); /// Unsubscribes `client` from a stream. void streamCancel({required Client client, required String streamId}); @@ -122,13 +131,12 @@ abstract interface class EventStreamMethods { /// Used for keeping track of stream subscription state and sending events to /// clients subscribed to individual streams. class EventStreamManager implements EventStreamMethods { - EventStreamManager({ - required UnmodifiableNamedLookup Function() clientsGetter, - }) : _clientsGetter = clientsGetter; + EventStreamManager({required this._clientsGetter, required this._backend}); static const kStreamNotify = 'streamNotify'; final UnmodifiableNamedLookup Function() _clientsGetter; + final DartRuntimeServiceBackend _backend; late final clients = _clientsGetter(); @visibleForTesting @@ -225,14 +233,23 @@ class EventStreamManager implements EventStreamMethods { /// Subscribes `client` to a stream. @override - void streamListen({required Client client, required String streamId}) { + void streamListen({ + required Client client, + required String streamId, + required Map params, + }) { assert(streamId.isNotEmpty); - // TODO(bkonyi): invoke backend stream handling logic. final listeners = streamListeners.putIfAbsent(streamId, () => []); if (listeners.contains(client)) { RpcException.streamAlreadySubscribed.throwException(); } listeners.add(client); + + // Tell the backend to start sending events for this stream if this is the + // first listener. + if (listeners.length == 1) { + _backend.onStreamListen(streamId: streamId, params: params); + } if (streamId == EventStreams.kService) { // Send all previously registered service extensions when a client // subscribes to the Service stream. @@ -265,7 +282,11 @@ class EventStreamManager implements EventStreamMethods { } listeners.remove(client); - // TODO(bkonyi): invoke backend stream handling logic. + // Tell the backend to stop sending events for this stream if there's no + // more listeners. + if (listeners.isEmpty) { + _backend.onStreamCancel(streamId: streamId); + } } /// Cleanup stream subscriptions for `client` when it has disconnected. diff --git a/pkg/dart_runtime_service/lib/src/handlers.dart b/pkg/dart_runtime_service/lib/src/handlers.dart index 46ab0553e19..a3aae226053 100644 --- a/pkg/dart_runtime_service/lib/src/handlers.dart +++ b/pkg/dart_runtime_service/lib/src/handlers.dart @@ -61,7 +61,7 @@ Handler webSocketClientHandler({required ClientManager clientManager}) => // package:shelf_web_socket v2. final logger = Logger('WebSocketHandler'); logger.info('New web socket connection. Creating $Client.'); - clientManager.addClient(ws.cast()); + clientManager.addClient(connection: ws.cast()); }); /// Creates a [Handler] for incoming SSE connections. @@ -82,7 +82,7 @@ Handler sseClientHandler({ handler.connections.rest.listen((sseConnection) { logger.info('New SSE connection. Creating $Client.'); - clientManager.addClient(sseConnection); + clientManager.addClient(connection: sseConnection); }); return handler.handler; diff --git a/pkg/dart_runtime_service/lib/src/isolate_manager.dart b/pkg/dart_runtime_service/lib/src/isolate_manager.dart new file mode 100644 index 00000000000..5208dd63b6a --- /dev/null +++ b/pkg/dart_runtime_service/lib/src/isolate_manager.dart @@ -0,0 +1,178 @@ +// 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 'dart:async'; + +import 'package:json_rpc_2/error_code.dart' as json_rpc_error; +import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; + +import 'dart_runtime_service.dart'; + +enum IsolateState { + start, + running, + pauseStart, + pauseExit, + pausePostRequest, + unknown, +} + +/// Base class for representing the state of a running isolate. +base class RunningIsolate { + RunningIsolate({required this.id, required this.name}) + : _state = IsolateState.unknown; + + late final _logger = Logger('Isolate ($name)'); + final String name; + final int id; + // ignore: unused_field, will be used for resume permission logic. + IsolateState _state; + + /// Invoked when the isolate has shutdown. + /// + /// Override this to clean up any state associated with this isolate. + @mustCallSuper + void shutdown() { + _logger.info('Shutting down.'); + } + + // State setters. + void pausedOnExit() => _stateChange(IsolateState.pauseExit); + + void pausedOnStart() => _stateChange(IsolateState.pauseStart); + + void pausedPostRequest() => _stateChange(IsolateState.pausePostRequest); + + void resumed() => running(); + + void running() => _stateChange(IsolateState.running); + + void started() => _stateChange(IsolateState.start); + + void _stateChange(IsolateState updated) { + _logger.info('${_state.name} => ${updated.name}'); + _state = updated; + } + + @override + String toString() => 'Isolate(name: $name id: $id)'; +} + +/// This file contains functionality used to track the running state of +/// all isolates in a given Dart process. +/// +/// [RunningIsolate] is a representation of a single live isolate and contains +/// running state information for that isolate. In addition, approvals from +/// clients used to synchronize isolate resuming across multiple clients are +/// tracked in this class. +/// +/// The [IsolateManager] keeps track of all the isolates in the +/// target process and handles isolate lifecycle events including: +/// - Startup +/// - Shutdown +/// - Pauses +/// +/// The [IsolateManager] also handles the `resume` RPC, which checks the +/// resume approvals in the target [RunningIsolate] to determine if the +/// isolate should be resumed or wait for additional approvals to be granted. +abstract base class IsolateManager { + final _logger = Logger('$IsolateManager'); + final isolates = {}; + + /// The ID of the root isolate. + /// + /// Used to support the `isolates/root` isolate ID. + int? _rootIsolateId; + + @mustCallSuper + Future shutdown() async { + _logger.info('Shutting down.'); + } + + /// Forwards the RPC request for [method] to be handled in the context of an + /// isolate. + /// + /// [params] is the set of parameters for the RPC and must include a valid + /// `isolateId`. + Future sendToIsolate({ + required String method, + required Map params, + }); + + /// Initializes state for a newly started isolate. + void isolateStarted({required RunningIsolate isolate}) { + _logger.info('Starting isolate: $isolate'); + if (_rootIsolateId == null) { + // TODO(bkonyi): ensure this is a non-system isolate + _logger.info('$isolate is the root isolate.'); + _rootIsolateId = isolate.id; + } + isolate.running(); + isolates[isolate.id] = isolate; + } + + /// Cleans up state for an isolate that has exited. + void isolateExited({required int id}) { + final isolate = isolates.remove(id); + if (isolate == null) { + _logger.warning( + 'isolateExited called with id: $id, but the isolate is not registered. ' + 'Ignoring.', + ); + return; + } + _logger.info('Isolate exited: $isolate'); + isolate.shutdown(); + } + + /// Looks up a [RunningIsolate] based on the `isolateId` entry in [params]. + /// + /// If the isolate ID is malformed, a [json_rpc.RpcException] is thrown with + /// an invalid parameters error. + /// + /// If the isolate ID is not associated with a running isolate, null is + /// returned. + RunningIsolate? lookupIsolateFromParams({ + required String method, + required Map params, + }) { + const kIsolateId = 'isolateId'; + const kIsolateIdPrefix = 'isolates/'; + assert(params.containsKey(kIsolateId)); + final isolateIdParam = params[kIsolateId] as String; + + Never throwInvalidIsolateId() => throw json_rpc.RpcException( + json_rpc_error.INVALID_PARAMS, + 'Invalid params', + data: { + 'details': + "$method: invalid '$kIsolateId' parameter: " + '$isolateIdParam', + }, + ); + + if (!isolateIdParam.startsWith(kIsolateIdPrefix)) { + _logger.warning('Malformed $kIsolateId: $isolateIdParam'); + throwInvalidIsolateId(); + } + + final isolateId = isolateIdParam.substring(kIsolateIdPrefix.length); + int id; + if (isolateId == 'root') { + if (_rootIsolateId == null) { + throwInvalidIsolateId(); + } + id = _rootIsolateId!; + } else { + try { + id = int.parse(isolateId); + } on FormatException { + throwInvalidIsolateId(); + } + } + return isolates[id]; + } +} diff --git a/pkg/dart_runtime_service/lib/src/rpc_exceptions.dart b/pkg/dart_runtime_service/lib/src/rpc_exceptions.dart index 05252cbae07..9693dea9b16 100644 --- a/pkg/dart_runtime_service/lib/src/rpc_exceptions.dart +++ b/pkg/dart_runtime_service/lib/src/rpc_exceptions.dart @@ -21,7 +21,11 @@ enum RpcException { const RpcException({required this.code, required this.message}); /// Throws a [json_rpc.RpcException] with [code] and [message]. - Never throwException() => throw json_rpc.RpcException(code, message); + Never throwException() => throw toException(); + + /// Builds a [json_rpc.RpcException] with [code] and [message] without + /// throwing. + json_rpc.RpcException toException() => json_rpc.RpcException(code, message); /// The JSON-RPC error code. final int code; diff --git a/pkg/dart_runtime_service/pubspec.yaml b/pkg/dart_runtime_service/pubspec.yaml index bf57ca029b7..e0ed77ed3b8 100644 --- a/pkg/dart_runtime_service/pubspec.yaml +++ b/pkg/dart_runtime_service/pubspec.yaml @@ -3,7 +3,7 @@ name: dart_runtime_service publish_to: none environment: - sdk: ^3.8.0 + sdk: ^3.12.0-edge resolution: workspace diff --git a/pkg/dart_runtime_service/test/utils/mocks.dart b/pkg/dart_runtime_service/test/utils/mocks.dart index b55dc26ebc2..4c5df770ca8 100644 --- a/pkg/dart_runtime_service/test/utils/mocks.dart +++ b/pkg/dart_runtime_service/test/utils/mocks.dart @@ -36,4 +36,21 @@ base class FakeDartRuntimeServiceBackend extends Fake @override void registerFallbacks(json_rpc.Peer clientPeer) {} + + @override + DartRuntimeService get frontend => throw UnimplementedError(); + + @override + IsolateManager get isolateManager => throw UnimplementedError(); + + @override + void onStreamCancel({required String streamId}) {} + + @override + bool onStreamListen({ + required String streamId, + required Map params, + }) { + return true; + } } diff --git a/pkg/dart_runtime_service/test/utils/utilities.dart b/pkg/dart_runtime_service/test/utils/utilities.dart index 7f25c231fc9..6c2a2cd0df0 100644 --- a/pkg/dart_runtime_service/test/utils/utilities.dart +++ b/pkg/dart_runtime_service/test/utils/utilities.dart @@ -24,7 +24,7 @@ Future createDartRuntimeServiceForTest({ service = await DartRuntimeService.initialize( config: config, - backend: FakeDartRuntimeServiceBackend(), + backendBuilder: (_) => FakeDartRuntimeServiceBackend(), ); 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 2afe4c906a1..4ee611d44bc 100644 --- a/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart +++ b/pkg/dart_runtime_service_vm/bin/vm_service_entrypoint.dart @@ -4,9 +4,11 @@ import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; import 'package:dart_runtime_service/dart_runtime_service.dart'; import 'package:dart_runtime_service_vm/dart_runtime_service_vm.dart'; +import 'package:dart_runtime_service_vm/src/vm_isolate_manager.dart'; // ignore: unreachable_from_main const entrypoint = pragma( @@ -63,6 +65,19 @@ bool _isFuchsia = false; Stream Function(ProcessSignal signal)? _signalWatch; @entrypoint +RawReceivePort boot() => DartRuntimeServiceVMBackend.isolateControlPort; + +final _isolateRegistrationStreamController = StreamController( + sync: true, +); + +@entrypoint +// ignore: unused_element +void _registerIsolate(int portId, SendPort sendPort, String name) => + _isolateRegistrationStreamController.sink.add( + VmRunningIsolate(id: portId, name: name, sendPort: sendPort), + ); + // ignore: unused_element StreamSubscription? _signalSubscription; @@ -108,6 +123,10 @@ Future main([List args = const []]) async { disableAuthCodes: _authCodesDisabled, autoStart: _autoStart, ), - backend: DartRuntimeServiceVMBackend(signalWatch: _signalWatch!), + backendBuilder: (frontend) => DartRuntimeServiceVMBackend( + frontend: frontend, + signalWatch: _signalWatch!, + runningIsolatesStream: _isolateRegistrationStreamController.stream, + ), ); } 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 0404404818f..15d26fcbb2b 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 @@ -3,36 +3,99 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:dart_runtime_service/dart_runtime_service.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; import 'package:logging/logging.dart'; +import 'package:stream_channel/stream_channel.dart'; import 'src/native_bindings.dart'; +import 'src/vm_isolate_manager.dart'; -class DartRuntimeServiceVMBackend extends DartRuntimeServiceBackend { +class DartRuntimeServiceVMBackend + extends DartRuntimeServiceBackend { /// The backend implementation for the Dart VM Service. /// /// [signalWatch] is the internal implementation of [ProcessSignal.watch], /// which bypasses checks that prevent [ProcessSignal.sigquit] from being /// watched. - DartRuntimeServiceVMBackend({required this.signalWatch}); + DartRuntimeServiceVMBackend({ + required super.frontend, + required this.signalWatch, + required Stream runningIsolatesStream, + }) : isolateManager = VmIsolateManager( + runningIsolatesStream: runningIsolatesStream, + ); + + static const int _kServiceExitMessageId = 0; + static const int _kIsolateStartupMessageId = 1; + static const int _kIsolateShutdownMessageId = 2; + static const int _kWebServerControlMessageId = 3; + static const int _kServerInfoMessageId = 4; + + /// Signals an RPC coming from native code (instead of from a websocket + /// connection). These calls are limited to simple request-response and do + /// not allow arbitrary json-rpc messages. + /// + /// The messages are an array of length 3: + /// (kMethodCallFromNative, String jsonRequest, PortId replyPort). + static const int _kMethodCallFromNativeId = 5; /// The internal implementation of [ProcessSignal.watch]. final Stream Function(ProcessSignal signal) signalWatch; + StreamSubscription? _sigquitSubscription; + + /// The port the VM uses to send messages to the VM service. + static final isolateControlPort = RawReceivePort(); + + /// Used to create an artificial client from within the VM service, allowing + /// for VM service requests to be made from within the VM or through the Dart + /// embedding API. + final _nativeRpcClientStreamChannelController = + StreamChannelController(sync: true); + + /// Iterator for RPC responses sent to the artificial client. + late final _nativeRpcClientResponseStream = StreamIterator( + _nativeRpcClientStreamChannelController.local.stream, + ); final _nativeBindings = NativeBindings(); - final _logger = Logger('VM Backend'); + final _logger = Logger('$DartRuntimeServiceVMBackend'); - StreamSubscription? _sigquitSubscription; + @override + final VmIsolateManager isolateManager; @override Future initialize() async { _logger.info('Initializing...'); + isolateControlPort.handler = _vmMessageHandler; + frontend.addArtificialClient( + name: 'native-rpc-client', + connection: _nativeRpcClientStreamChannelController.foreign, + ); _nativeBindings.onStart(); - _logger.info('Initialized!'); + _logger.info('Initialized.'); + } + + @override + Future clearState() async { + // Do nothing for now. + } + + @override + Future shutdown() async { + _logger.info('Shutting down...'); + await Future.wait([ + _sigquitSubscription?.cancel() ?? Future.value(), + _nativeRpcClientStreamChannelController.local.sink.close(), + ]); + isolateControlPort.close(); + _nativeBindings.onExit(); + _logger.info('Shutdown.'); } @override @@ -53,19 +116,28 @@ class DartRuntimeServiceVMBackend extends DartRuntimeServiceBackend { required Uri wsUri, }) async { // TODO(bkonyi): handle DDS connection case. - stdout.writeln('The Dart VM service is listening on $httpUri'); + stdout.writeln('The Dart VM service is listening on $httpUri/'); _nativeBindings.onServerAddressChange(httpUri.toString()); } @override - Future clearState() async { - // Do nothing for now. + bool onStreamListen({ + required String streamId, + required Map params, + }) { + var includePrivates = false; + if (params case {'includePrivates': final bool value}) { + includePrivates = value; + } + return _nativeBindings.streamListen( + streamId: streamId, + includePrivates: includePrivates, + ); } @override - Future shutdown() async { - await _sigquitSubscription?.cancel(); - _nativeBindings.onExit(); + void onStreamCancel({required String streamId}) { + _nativeBindings.streamCancel(streamId: streamId); } @override @@ -85,9 +157,168 @@ class DartRuntimeServiceVMBackend extends DartRuntimeServiceBackend { final method = request.method; final params = request.asMap.cast(); if (params case {'isolateId': final String _}) { - // TODO(bkonyi): handle isolate requests - RpcException.serverError.throwException(); + _logger.info( + 'Sending request to isolate. Method: $method Params: $params', + ); + return await isolateManager.sendToIsolate(method: method, params: params); } + _logger.info('Sending request to VM. Method: $method Params: $params'); return await _nativeBindings.sendToVM(method: method, params: params); } + + /// Handles messages sent directly from the VM via the isolate control port. + /// + /// Messages sent over the isolate control port include: + /// - Stream events + /// - Request to shutdown the service + /// - RPC invocations from the VM or VM's embedder + /// - dart:developer API invocations (e.g. Service.getInfo() and + /// Service.toggleWebServer()) + /// - Isolate startup and shutdown notifications + void _vmMessageHandler(List message) { + _logger.info('VM message: $message'); + switch (message) { + case [final String streamId, final String eventJsonString]: + // This is an event. + _eventMessageHandler( + streamId, + json.decode(eventJsonString) as Map, + ); + case [final int opcode]: + // This is a control message directing the vm service to exit. + assert(opcode == _kServiceExitMessageId); + frontend.shutdown(); + case [ + final int opcode, + final List messageBytes, + final SendPort replyPort, + ] + when opcode == _kMethodCallFromNativeId: + _handleNativeRpcCall(messageBytes, replyPort); + case [ + final int opcode, + final SendPort sendPort, + final bool enable, + final bool? silenceOutput, + ] + when opcode == _kWebServerControlMessageId || + opcode == _kServerInfoMessageId: + // This is a message interacting with the web server. + _serverMessageHandler(opcode, sendPort, enable, silenceOutput); + case [ + final int opcode, + final int portId, + final SendPort sendPort, + final String name, + ] + when opcode == _kIsolateStartupMessageId || + opcode == _kIsolateShutdownMessageId: + // This is a message informing us of the birth or death of an + // isolate. + _isolateControlMessageHandler(opcode, portId, sendPort, name); + default: + print('Internal vm-service error: ignoring illegal message: $message'); + } + } + + /// Forward VM service events sent from the VM. + void _eventMessageHandler(String streamId, Map event) { + frontend.sendEvent( + event: ForwardingStreamEvent(streamId: streamId, event: event), + ); + } + + /// Handle notifications from the VM related to isolate startup and shutdown. + void _isolateControlMessageHandler( + int code, + int portId, + SendPort sp, + String name, + ) { + switch (code) { + case _kIsolateStartupMessageId: + isolateManager.onIsolateStartupMessage( + id: portId, + sendPort: sp, + name: name, + ); + case _kIsolateShutdownMessageId: + isolateManager.onIsolateShutdownMessage(id: portId); + } + } + + /// Handle requests from the VM related to the state of the service's HTTP + /// server. + Future _serverMessageHandler( + int code, + SendPort sp, + bool enable, + bool? silenceOutput, + ) async { + void sendServerInfo() { + try { + sp.send(frontend.httpUri.toString()); + } on DartRuntimeServiceServerNotRunning { + sp.send(null); + } + } + + switch (code) { + case _kWebServerControlMessageId: + await frontend.serverControl( + enable: enable, + silenceOutput: silenceOutput, + ); + sendServerInfo(); + case _kServerInfoMessageId: + sendServerInfo(); + } + } + + /// Handle VM service requests from native code (see + /// `Dart_InvokeVMServiceMethod` in `dart_tools_api.h`). + Future _handleNativeRpcCall( + List message, + SendPort replyPort, + ) async { + final messageStr = utf8.decode(message); + _logger.info('Native RPC request: $messageStr'); + _nativeRpcClientStreamChannelController.local.sink.add(messageStr); + + // TODO(bkonyi): handle non-string results + /* + late List bytes; + switch (response.kind) { + case ResponsePayloadKind.String: + bytes = utf8.encode(response.payload as String); + bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); + case ResponsePayloadKind.Binary: + case ResponsePayloadKind.Utf8String: + bytes = response.payload as Uint8List; + } + */ + + if (!await _nativeRpcClientResponseStream.moveNext()) { + _logger.warning('Native RPC client stream has closed.'); + return; + } + _logger.info( + 'Response received: ${_nativeRpcClientResponseStream.current}', + ); + replyPort.send(utf8.encode(_nativeRpcClientResponseStream.current)); + } +} + +final class ForwardingStreamEvent extends StreamEvent { + ForwardingStreamEvent({required super.streamId, required this.event}) + : super(kind: ''); + + static const kParams = 'params'; + + Map event; + + @override + Map toJson() { + return event[kParams] as Map; + } } diff --git a/pkg/dart_runtime_service_vm/lib/src/native_bindings.dart b/pkg/dart_runtime_service_vm/lib/src/native_bindings.dart index ee400b34102..5bf667ffb82 100644 --- a/pkg/dart_runtime_service_vm/lib/src/native_bindings.dart +++ b/pkg/dart_runtime_service_vm/lib/src/native_bindings.dart @@ -16,11 +16,20 @@ import 'dart:typed_data'; import 'package:dart_runtime_service/dart_runtime_service.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; +import 'package:logging/logging.dart'; + +import 'vm_isolate_manager.dart'; /// Allows for sending messages to the native VM service implementation. class NativeBindings { + factory NativeBindings() => _instance; + NativeBindings._(); + + static final NativeBindings _instance = NativeBindings._(); static final jsonUtf8Decoder = json.fuse(utf8); + final _logger = Logger('$NativeBindings'); + /// Sends a general RPC to the VM for processing. /// /// The RPC is not executed in the scope of any particular isolate. @@ -44,6 +53,66 @@ class NativeBindings { return completer.future; } + /// Sends an RPC to a specific isolate for processing. + /// + /// The RPC is not executed in the scope of any particular isolate. + Future sendToIsolate({ + required VmRunningIsolate isolate, + required String method, + required Map params, + }) { + final receivePort = RawReceivePort( + null, + 'Isolate Message (${isolate.name})', + ); + // Keep track of receive port associated with the request so we can close + // it if isolate exits before sending a response. + isolate.outstandingRequestPorts.add(receivePort); + final completer = Completer(); + receivePort.handler = (Object value) { + receivePort.close(); + isolate.outstandingRequestPorts.remove(receivePort); + try { + completer.complete(_toResponse(value: value)); + } on json_rpc.RpcException catch (e) { + completer.completeError(e); + } + }; + if (!vm_service_natives.sendIsolateServiceMessage( + isolate.sendPort, + _toRequest(responsePort: receivePort, method: method, params: params), + )) { + receivePort.close(); + isolate.outstandingRequestPorts.remove(receivePort); + _logger.warning('Could not send message to $isolate.'); + completer.completeError(RpcException.internalError.toException()); + } + return completer.future; + } + + /// Notifies the VM to start sending events for [streamId]. + /// + /// This only needs to be called once the first client has subscribed to the + /// stream. Subsequent subscriptions to the stream are handled by the + /// [EventStreamManager]. + /// + /// If [includePrivates] is true, private event properties starting with '_' + /// will be included in events. This is false by default to reduce the size of + /// events for clients that don't rely on private properties. + bool streamListen({required String streamId, bool includePrivates = false}) => + // TODO(bkonyi): handle case where some clients want privates included + // and others don't. + vm_service_natives.vmListenStream(streamId, includePrivates); + + /// Notifies the VM to stop sending events for [streamId]. + /// + /// This only needs to be called once the last client subscribed to the + /// stream has cancelled its subscription or disconnected. While clients have + /// active subscriptions to this stream, stream cancellation requests are + /// handled by the [EventStreamManager]. + void streamCancel({required String streamId}) => + vm_service_natives.vmCancelStream(streamId); + /// Notifies the VM that the VM service server has finished initializing. void onStart() => vm_service_natives.onStart(); diff --git a/pkg/dart_runtime_service_vm/lib/src/vm_isolate_manager.dart b/pkg/dart_runtime_service_vm/lib/src/vm_isolate_manager.dart new file mode 100644 index 00000000000..094c944c57b --- /dev/null +++ b/pkg/dart_runtime_service_vm/lib/src/vm_isolate_manager.dart @@ -0,0 +1,102 @@ +// 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 'dart:async'; +import 'dart:isolate'; + +import 'package:dart_runtime_service/dart_runtime_service.dart'; +import 'package:logging/logging.dart'; +import 'package:vm_service/vm_service.dart'; + +import 'native_bindings.dart'; + +/// A running isolate for the Dart VM. +final class VmRunningIsolate extends RunningIsolate { + VmRunningIsolate({ + required super.id, + required super.name, + required this.sendPort, + }); + + /// The port used to send service requests to the isolate within the VM. + final SendPort sendPort; + + /// The set of ports for outstanding requests that are used by the VM to send + /// responses. + final outstandingRequestPorts = {}; + + @override + void shutdown() { + for (final requestPort in outstandingRequestPorts) { + requestPort.close(); + } + outstandingRequestPorts.clear(); + super.shutdown(); + } +} + +/// Manages and tracks running isolates in the Dart VM. +final class VmIsolateManager extends IsolateManager { + /// Initializes the [VmIsolateManager]. + /// + /// [runningIsolatesStream] should be a stream of [VmRunningIsolate]s reported + /// by the Dart VM as started once the VM service has finished initializing. + VmIsolateManager({required Stream runningIsolatesStream}) { + _runningIsolatesStreamSub = runningIsolatesStream.listen( + (isolate) => isolateStarted(isolate: isolate), + ); + } + + final _logger = Logger('$VmIsolateManager'); + final _nativeBindings = NativeBindings(); + late final StreamSubscription _runningIsolatesStreamSub; + + @override + Future shutdown() async { + await _runningIsolatesStreamSub.cancel(); + await super.shutdown(); + } + + /// Registers a newly started isolate reported via a message over the + /// service's control port. + void onIsolateStartupMessage({ + required int id, + required SendPort sendPort, + required String name, + }) { + final isolate = VmRunningIsolate(id: id, name: name, sendPort: sendPort); + _logger.info('Isolate startup message received for $isolate'); + isolateStarted(isolate: isolate); + } + + /// Reports that an isolate is shutting down based on a message over the + /// service's control port. + void onIsolateShutdownMessage({required int id}) { + _logger.info('Isolate startup message received for isolate ID $id'); + isolateExited(id: id); + } + + @override + Future sendToIsolate({ + required String method, + required Map params, + }) async { + final isolate = + lookupIsolateFromParams(method: method, params: params) + as VmRunningIsolate?; + if (isolate == null) { + // There is some chance that this isolate may have lived before, + // so return a sentinel rather than an error. + return Sentinel( + kind: SentinelKind.kCollected, + valueAsString: '', + ).toJson(); + } + return _nativeBindings.sendToIsolate( + isolate: isolate, + method: method, + params: params, + ); + } +} diff --git a/pkg/dart_runtime_service_vm/pubspec.yaml b/pkg/dart_runtime_service_vm/pubspec.yaml index 0f7be90657d..a2a044092d6 100644 --- a/pkg/dart_runtime_service_vm/pubspec.yaml +++ b/pkg/dart_runtime_service_vm/pubspec.yaml @@ -3,7 +3,7 @@ name: dart_runtime_service_vm publish_to: none environment: - sdk: ^3.8.0 + sdk: ^3.12.0-edge resolution: workspace @@ -12,3 +12,5 @@ dependencies: dart_runtime_service: any json_rpc_2: any logging: any + stream_channel: any + vm_service: any diff --git a/runtime/bin/run_vm_tests.cc b/runtime/bin/run_vm_tests.cc index 661267100ce..5bc2f56e823 100644 --- a/runtime/bin/run_vm_tests.cc +++ b/runtime/bin/run_vm_tests.cc @@ -110,8 +110,14 @@ static Dart_Isolate CreateAndSetupServiceIsolate(const char* script_uri, // vm/cc tests to randomly time out due to inability to shut service-isolate // down. // Issue(https://dartbug.com/37741): - if ((strcmp(run_filter, "DartAPI_InvokeVMServiceMethod") != 0) && - (strcmp(run_filter, "DartAPI_InvokeVMServiceMethod_Loop") != 0)) { + const bool is_service_test = + (strcmp(run_filter, "DartAPI_InvokeVMServiceMethod") == 0) || + (strcmp(run_filter, "DartAPI_InvokeVMServiceMethod_Loop") == 0); + const bool is_exp_service_test = + (strcmp(run_filter, "DartAPI_InvokeVMServiceMethod_Exp") == 0) || + (strcmp(run_filter, "DartAPI_InvokeVMServiceMethod_Loop_Exp") == 0); + + if (!is_service_test && !is_exp_service_test) { return nullptr; } @@ -122,21 +128,50 @@ static Dart_Isolate CreateAndSetupServiceIsolate(const char* script_uri, packages_config, /*app_snapshot=*/nullptr, /*isolate_run_app_snapshot=*/false); - const uint8_t* kernel_buffer = nullptr; - intptr_t kernel_buffer_size = 0; - - bin::dfe.Init(); - bin::dfe.LoadPlatform(&kernel_buffer, &kernel_buffer_size); - RELEASE_ASSERT(kernel_buffer != nullptr); - flags->load_vmservice_library = true; flags->is_service_isolate = true; - isolate_group_data->SetKernelBufferUnowned( - const_cast(kernel_buffer), kernel_buffer_size); - isolate = Dart_CreateIsolateGroupFromKernel( - script_uri, DART_VM_SERVICE_ISOLATE_NAME, kernel_buffer, - kernel_buffer_size, flags, isolate_group_data, /*isolate_data=*/nullptr, - error); + +#if defined(EXPERIMENTAL_VM_SERVICE) + if (is_exp_service_test) { + ASSERT(!is_service_test); + const uint8_t* isolate_snapshot_data = nullptr; + const uint8_t* isolate_snapshot_instructions = nullptr; + + bin::VmService::enable_experimental_vm_service = true; + auto [app_snapshot, script_name] = bin::Snapshot::TryReadSDKSnapshot( + "dart_runtime_service_vm.dart.snapshot"); + if (app_snapshot == nullptr) { + return nullptr; + } + const uint8_t* ignore_vm_snapshot_data; + const uint8_t* ignore_vm_snapshot_instructions; + app_snapshot->SetBuffers( + &ignore_vm_snapshot_data, &ignore_vm_snapshot_instructions, + &isolate_snapshot_data, &isolate_snapshot_instructions); + isolate = Dart_CreateIsolateGroup( + script_uri, DART_VM_SERVICE_ISOLATE_NAME, isolate_snapshot_data, + isolate_snapshot_instructions, flags, isolate_group_data, + /*isolate_data=*/nullptr, error); + } +#endif // defined(EXPERIMENTAL_VM_SERVICE) + + if (is_service_test) { + ASSERT(!is_exp_service_test); + const uint8_t* kernel_buffer = nullptr; + intptr_t kernel_buffer_size = 0; + + bin::dfe.Init(); + bin::dfe.LoadPlatform(&kernel_buffer, &kernel_buffer_size); + RELEASE_ASSERT(kernel_buffer != nullptr); + + isolate_group_data->SetKernelBufferUnowned( + const_cast(kernel_buffer), kernel_buffer_size); + isolate = Dart_CreateIsolateGroupFromKernel( + script_uri, DART_VM_SERVICE_ISOLATE_NAME, kernel_buffer, + kernel_buffer_size, flags, isolate_group_data, /*isolate_data=*/nullptr, + error); + } + if (isolate == nullptr) { delete isolate_group_data; return nullptr; diff --git a/runtime/vm/dart_api_impl_test.cc b/runtime/vm/dart_api_impl_test.cc index 3eb7ae70c4a..2af17157851 100644 --- a/runtime/vm/dart_api_impl_test.cc +++ b/runtime/vm/dart_api_impl_test.cc @@ -10559,11 +10559,11 @@ TEST_CASE(DartAPI_InvokeImportedFunction) { "NoSuchMethodError: No top-level method 'getCurrentTag' declared."); } -TEST_CASE(DartAPI_InvokeVMServiceMethod) { +static void InvokeVMServiceMethodCommon() { char buffer[1024]; Utils::SNPrint(buffer, sizeof(buffer), R"({ - "jsonrpc": 2.0, + "jsonrpc": "2.0", "id": "foo", "method": "getVM", "params": { } @@ -10618,6 +10618,16 @@ TEST_CASE(DartAPI_InvokeVMServiceMethod) { EXPECT(result == Dart_True()); } +TEST_CASE(DartAPI_InvokeVMServiceMethod) { + InvokeVMServiceMethodCommon(); +} + +#if defined(EXPERIMENTAL_VM_SERVICE) +TEST_CASE(DartAPI_InvokeVMServiceMethod_Exp) { + InvokeVMServiceMethodCommon(); +} +#endif // defined(EXPERIMENTAL_VM_SERVICE) + static Monitor* loop_test_lock = new Monitor(); static bool loop_test_exit = false; static bool loop_reset_count = false; @@ -10630,7 +10640,7 @@ static void InvokeServiceMessages(uword param) { char buffer[1024]; Utils::SNPrint(buffer, sizeof(buffer), R"({ - "jsonrpc": 2.0, + "jsonrpc": "2.0", "id": "foo", "method": "getVM", "params": { } @@ -10661,7 +10671,7 @@ static void InvokeServiceMessages(uword param) { } while (count < 100); } -TEST_CASE(DartAPI_InvokeVMServiceMethod_Loop) { +static void InvokeVMServiceMethodLoopCommon() { { MonitorLocker ml(loop_test_lock); loop_test_exit = false; @@ -10673,6 +10683,16 @@ TEST_CASE(DartAPI_InvokeVMServiceMethod_Loop) { } OSThread::Join(loop_test_join_id); } + +TEST_CASE(DartAPI_InvokeVMServiceMethod_Loop) { + InvokeVMServiceMethodLoopCommon(); +} + +#if defined(EXPERIMENTAL_VM_SERVICE) +TEST_CASE(DartAPI_InvokeVMServiceMethod_Loop_Exp) { + InvokeVMServiceMethodLoopCommon(); +} +#endif // defined(EXPERIMENTAL_VM_SERVICE) #endif // !defined(PRODUCT) static void HandleResponse(Dart_Port dest_port_id, Dart_CObject* message) { diff --git a/runtime/vm/service_isolate.cc b/runtime/vm/service_isolate.cc index b680bf8f559..fd6613600b8 100644 --- a/runtime/vm/service_isolate.cc +++ b/runtime/vm/service_isolate.cc @@ -578,12 +578,18 @@ void ServiceIsolate::Shutdown() { void ServiceIsolate::BootVmServiceLibrary() { Thread* thread = Thread::Current(); - const Library& vmservice_library = - Library::Handle(Library::LookupLibrary(thread, Symbols::DartVMService())); - ASSERT(!vmservice_library.IsNull()); + Library& lib = + Library::Handle(thread->isolate_group()->object_store()->root_library()); const String& boot_function_name = String::Handle(String::New("boot")); - const Function& boot_function = Function::Handle( - vmservice_library.LookupFunctionAllowPrivate(boot_function_name)); + Function& boot_function = + Function::Handle(lib.LookupFunctionAllowPrivate(boot_function_name)); + + if (boot_function.IsNull()) { + lib ^= Library::LookupLibrary(thread, Symbols::DartVMService()); + ASSERT(!lib.IsNull()); + boot_function ^= lib.LookupFunctionAllowPrivate(boot_function_name); + } + ASSERT(!boot_function.IsNull()); const Object& result = Object::Handle( DartEntry::InvokeFunction(boot_function, Object::empty_array())); @@ -608,16 +614,18 @@ void ServiceIsolate::RegisterRunningIsolates( ASSERT(thread->isolate()->is_service_isolate()); // Obtain "_registerIsolate" function to call. - const String& library_url = Symbols::DartVMService(); - ASSERT(!library_url.IsNull()); - const Library& library = - Library::Handle(zone, Library::LookupLibrary(thread, library_url)); - ASSERT(!library.IsNull()); - const String& function_name = - String::Handle(zone, String::New("_registerIsolate")); - ASSERT(!function_name.IsNull()); - const Function& register_function_ = - Function::Handle(zone, library.LookupFunctionAllowPrivate(function_name)); + Library& lib = + Library::Handle(thread->isolate_group()->object_store()->root_library()); + const String& function_name = String::Handle(String::New("_registerIsolate")); + Function& register_function_ = + Function::Handle(lib.LookupFunctionAllowPrivate(function_name)); + + if (register_function_.IsNull()) { + lib ^= Library::LookupLibrary(thread, Symbols::DartVMService()); + ASSERT(!lib.IsNull()); + register_function_ ^= lib.LookupFunctionAllowPrivate(function_name); + } + ASSERT(!register_function_.IsNull()); Integer& port_int = Integer::Handle(zone);