| // 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; | |
| } |