blob: 2dedda6065a136e1a93487699ab945dbff3fbf36 [file]
// 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:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:devtools_shared/devtools_shared.dart' show DtdInfo;
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'utils.dart';
const _kDtdAotSnapshotName = 'dart_tooling_daemon_aot.dart.snapshot';
const _kDtdAotProductSnapshotName =
'dart_tooling_daemon_aot_product.dart.snapshot';
const _kDtdJitSnapshotName = 'dart_tooling_daemon.dart.snapshot';
const _kMachineFlag = '--machine';
const _kDtdDdsStartedEvent = 'server.dtdStarted';
final _logger = Logger('DTD');
/// Information about a Dart Tooling Daemon instance hosted by the current
/// process, including a [shutdown] method to terminate the underlying
/// [Isolate] or child [Process].
class HostedDtdInfo extends DtdInfo {
HostedDtdInfo(
super.localUri, {
super.exposedUri,
this._isolate,
this._process,
super.secret,
});
final Isolate? _isolate;
final Process? _process;
/// Terminates the hosted DTD isolate or process.
Future<void> shutdown() async {
_isolate?.kill(priority: Isolate.immediate);
_process?.kill();
}
}
List<String> _findDtdSnapshots(
String snapshotDir, {
required bool runFromBuildRoot,
}) {
// In product builds (e.g., prebuilt SDKs and google3), the AOT snapshot is
// built with the product-mode AOT runtime
// (`dart_tooling_daemon_aot_product.dart.snapshot`), whereas local
// non-product SDK builds produce `dart_tooling_daemon_aot.dart.snapshot`.
// Check the AOT snapshot matching the current runtime mode first, falling
// back to the other AOT variant if only one was built, and finally falling
// back to the JIT snapshot (`dart_tooling_daemon.dart.snapshot`) when running
// on a JIT VM.
final isProduct = const bool.fromEnvironment('dart.vm.product');
final aotSnapshots = isProduct
? <String>[_kDtdAotProductSnapshotName, _kDtdAotSnapshotName]
: <String>[_kDtdAotSnapshotName, _kDtdAotProductSnapshotName];
final candidateNames = <String>[...aotSnapshots, _kDtdJitSnapshotName];
final results = <String>[];
for (final name in candidateNames) {
final directPath = path.join(snapshotDir, name);
if (File(directPath).existsSync() && !results.contains(directPath)) {
results.add(directPath);
}
if (runFromBuildRoot) {
final genPath = path.join(snapshotDir, 'gen', name);
if (File(genPath).existsSync() && !results.contains(genPath)) {
results.add(genPath);
}
}
}
return results;
}
/// Locates the directory containing the Dart Tooling Daemon (DTD) snapshot
/// and whether the SDK is running from the build root.
(String, {bool runFromBuildRoot}) getDTDSnapshotDirInfo() {
// Find SDK path.
(String, {bool runFromBuildRoot})? trySDKPath(String executablePath) {
// The common case: [path.dirname] called twice on Platform.executable.
// Confirm by asserting that `./bin/snapshots/` exists.
var sdkPath = path.absolute(path.dirname(path.dirname(executablePath)));
var snapshotsDir = path.join(sdkPath, 'bin', 'snapshots');
var runFromBuildRoot = false;
final type = FileSystemEntity.typeSync(snapshotsDir);
if (type != FileSystemEntityType.directory &&
type != FileSystemEntityType.link) {
// Checked out Dart SDK (e.g., ./out/ReleaseX64/dart ... or in google3).
sdkPath = path.absolute(path.dirname(executablePath));
snapshotsDir = sdkPath;
runFromBuildRoot = true;
}
// Try to locate the DTD snapshot to determine if we're able to find
// the SDK snapshots with this SDK path.
final snapshots = _findDtdSnapshots(
snapshotsDir,
runFromBuildRoot: runFromBuildRoot,
);
if (snapshots.isEmpty) {
return null;
}
return (snapshotsDir, runFromBuildRoot: runFromBuildRoot);
}
final info =
trySDKPath(Platform.resolvedExecutable) ??
trySDKPath(Platform.executable);
if (info == null) {
throw StateError(
'Unable to locate DTD snapshots directory from '
'${Platform.resolvedExecutable} or ${Platform.executable}',
);
}
return info;
}
/// Returns the directory containing the Dart Tooling Daemon snapshot.
String getDTDSnapshotDir() => getDTDSnapshotDirInfo().$1;
/// Parses the JSON details emitted by DTD when started with `--machine`
/// (see `DartToolingDaemon.startService` in `pkg/dtd_impl/lib/src/dart_tooling_daemon.dart`).
///
/// Returns [DtdInfo] if [message] contains valid tooling daemon details, or
/// `null` otherwise.
DtdInfo? _parseDtdDetails(
String message, {
required bool machineMode,
required bool printDtdUri,
}) {
try {
final json = jsonDecode(message) as Map<String, Object?>;
if (json case {
'tooling_daemon_details': {
'uri': final String uri,
'trusted_client_secret': final String secret,
},
}) {
if (printDtdUri || machineMode) {
DevToolsUtils.printOutput('Serving the Dart Tooling Daemon at $uri', {
'event': _kDtdDdsStartedEvent,
'params': {'uri': uri},
}, machineMode: machineMode);
}
return DtdInfo(Uri.parse(uri), secret: secret);
}
} catch (_) {
// Ignore non-json or malformed output lines.
}
return null;
}
/// Starts the Dart Tooling Daemon as a child process using `dartaotruntime`.
///
/// * [machineMode]: Whether the output should be machine-readable (JSON).
/// * [printDtdUri]: Whether to print the DTD serving URI to stdout.
/// * [snapshotPath]: The file path to the DTD AOT snapshot.
Future<HostedDtdInfo?> _startDtdProcess({
required bool machineMode,
required bool printDtdUri,
required String snapshotPath,
}) async {
final execDir = path.dirname(Platform.resolvedExecutable);
final execName = Platform.isWindows ? 'dartaotruntime.exe' : 'dartaotruntime';
final candidateExes = <String>[
path.join(execDir, execName),
path.join(path.dirname(execDir), 'bin', execName),
path.join(path.dirname(execDir), execName),
];
String? dartAotRuntime;
for (final exe in candidateExes) {
if (File(exe).existsSync()) {
dartAotRuntime = exe;
break;
}
}
if (dartAotRuntime == null) {
_logger.warning(
'Could not find dartaotruntime executable to run $snapshotPath',
);
return null;
}
StreamSubscription<String>? stdoutSub;
StreamSubscription<String>? stderrSub;
try {
// Run in the system temp directory so a detached child process does not
// hold a Windows directory handle on the caller's working directory.
final process = await Process.start(
dartAotRuntime,
[snapshotPath, _kMachineFlag],
mode: ProcessStartMode.detachedWithStdio,
workingDirectory: Directory.systemTemp.path,
);
final completer = Completer<DtdInfo?>();
stdoutSub = process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(line) {
final dtdInfo = _parseDtdDetails(
line,
machineMode: machineMode,
printDtdUri: printDtdUri,
);
if (dtdInfo != null && !completer.isCompleted) {
completer.complete(dtdInfo);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
onError: (_) {
if (!completer.isCompleted) {
completer.complete();
}
},
);
stderrSub = process.stderr.transform(utf8.decoder).listen((data) {
if (data.isNotEmpty) {
_logger.warning('DTD process stderr: $data');
}
});
final result = await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
process.kill();
return null;
},
);
if (result == null) {
process.kill();
return null;
}
return HostedDtdInfo(
result.localUri,
exposedUri: result.exposedUri,
process: process,
secret: result.secret,
);
} catch (e, st) {
_logger.warning('Failed to start DTD process from $snapshotPath', e, st);
return null;
} finally {
await stdoutSub?.cancel();
await stderrSub?.cancel();
}
}
/// Starts a Dart Tooling Daemon instance as a separate isolate or process.
Future<HostedDtdInfo?> startDtd({
required bool machineMode,
required bool printDtdUri,
}) async {
final (snapshotDir, :runFromBuildRoot) = getDTDSnapshotDirInfo();
final snapshotPaths = _findDtdSnapshots(
snapshotDir,
runFromBuildRoot: runFromBuildRoot,
);
for (final snapshotPath in snapshotPaths) {
final completer = Completer<DtdInfo?>();
final exitPort = ReceivePort()
..listen((_) {
if (!completer.isCompleted) {
completer.complete();
}
});
final errorPort = ReceivePort()
..listen((message) {
if (message case [final Object error, final Object stackTrace]) {
_logger.warning('DTD isolate error: $error\n$stackTrace');
} else {
_logger.warning('DTD isolate error: $message');
}
if (!completer.isCompleted) {
completer.complete();
}
});
final receivePort = ReceivePort()
..listen((message) {
if (message is String) {
final dtdInfo = _parseDtdDetails(
message,
machineMode: machineMode,
printDtdUri: printDtdUri,
);
if (dtdInfo != null && !completer.isCompleted) {
completer.complete(dtdInfo);
}
}
});
Isolate? isolate;
try {
isolate = await Isolate.spawnUri(
Uri.file(snapshotPath),
[_kMachineFlag],
receivePort.sendPort,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
);
final result = await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
isolate?.kill(priority: Isolate.immediate);
return null;
},
);
if (result != null) {
return HostedDtdInfo(
result.localUri,
exposedUri: result.exposedUri,
isolate: isolate,
secret: result.secret,
);
}
isolate.kill(priority: Isolate.immediate);
} catch (e, st) {
_logger.fine('Failed to spawn DTD isolate from $snapshotPath', e, st);
} finally {
receivePort.close();
errorPort.close();
exitPort.close();
}
}
// If spawning an isolate failed for all snapshots (for example, when running
// on a JIT VM in an environment where only AOT snapshots were built), fall
// back to launching the AOT snapshot in a child dartaotruntime process.
for (final snapshotPath in snapshotPaths) {
final isAotSnapshot =
snapshotPath.endsWith(_kDtdAotSnapshotName) ||
snapshotPath.endsWith(_kDtdAotProductSnapshotName);
if (!isAotSnapshot) {
continue;
}
final dtdInfo = await _startDtdProcess(
snapshotPath: snapshotPath,
machineMode: machineMode,
printDtdUri: printDtdUri,
);
if (dtdInfo != null) {
return dtdInfo;
}
}
_logger.warning(
'Failed to start DTD from any candidate snapshot: $snapshotPaths',
);
return null;
}