| // 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; | |
| IsolateState get state => _state; | |
| 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 = <int, RunningIsolate>{}; | |
| /// The ID of the root isolate. | |
| /// | |
| /// Used to support the `isolates/root` isolate ID. | |
| int? _rootIsolateId; | |
| @protected | |
| int? get rootIsolateId => _rootIsolateId; | |
| @protected | |
| set rootIsolateId(int? value) => _rootIsolateId = value; | |
| @mustCallSuper | |
| Future<void> shutdown() async { | |
| _logger.info('Shutting down.'); | |
| for (final isolate in isolates.values) { | |
| isolate.shutdown(); | |
| } | |
| isolates.clear(); | |
| } | |
| /// 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<RpcResponse> sendToIsolate({ | |
| required String method, | |
| required Map<String, Object?> params, | |
| }); | |
| /// Initializes state for a newly started isolate. | |
| void isolateStarted({ | |
| required RunningIsolate isolate, | |
| bool isSystemIsolate = false, | |
| }) { | |
| if (_logger.isLoggable(Level.FINE)) { | |
| _logger.fine('Starting isolate: $isolate'); | |
| } | |
| if (_rootIsolateId == null && !isSystemIsolate) { | |
| if (_logger.isLoggable(Level.FINE)) { | |
| _logger.fine('$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; | |
| } | |
| if (_logger.isLoggable(Level.FINE)) { | |
| _logger.fine('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<String, Object?> 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]; | |
| } | |
| } |