blob: 5208dd63b6ae587ac6c20d1333cfe03b52614f9c [file] [log] [blame] [edit]
// 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 = <int, RunningIsolate>{};
/// The ID of the root isolate.
///
/// Used to support the `isolates/root` isolate ID.
int? _rootIsolateId;
@mustCallSuper
Future<void> 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<RpcResponse> sendToIsolate({
required String method,
required Map<String, Object?> 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<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];
}
}