// Copyright (c) 2024, 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 'package:path/path.dart' as path; import 'dds.dart' hide DartDevelopmentService; import 'src/arg_parser.dart'; import 'src/dds_impl.dart'; /// Spawns a Dart Development Service instance which will communicate with a /// VM service. Requires the target VM service to have no other connected /// clients. /// /// [remoteVmServiceUri] is the address of the VM service that this /// development service will communicate with. /// /// If provided, [serviceUri] will determine the address and port of the /// spawned Dart Development Service. /// /// [enableAuthCodes] controls whether or not an authentication code must /// be provided by clients when communicating with this instance of /// DDS. Authentication codes take the form of a base64 /// encoded string provided as the first element of the DDS path and is meant /// to make it more difficult for unintended clients to connect to this /// service. Authentication codes are enabled by default. /// /// If [serveDevTools] is enabled, DDS will serve a DevTools instance and act /// as a DevTools Server. If not specified, [devToolsServerAddress] is ignored. /// /// If provided, DDS will redirect DevTools requests to an existing DevTools /// server hosted at [devToolsServerAddress]. Ignored if [serveDevTools] is not /// true. /// /// If [enableServicePortFallback] is enabled, DDS will attempt to bind to any /// available port if the specified port is unavailable. /// /// [cachedUserTags] is deprecated and supplying an argument to it will cause no /// effect. /// /// If provided, [dartExecutable] is the path to the 'dart' executable that /// should be used to spawn the DDS instance. By default, `Platform.executable` /// is used. class DartDevelopmentServiceLauncher { static Future start({ required Uri remoteVmServiceUri, Uri? serviceUri, bool enableAuthCodes = true, bool serveDevTools = false, Uri? devToolsServerAddress, bool enableServicePortFallback = false, List cachedUserTags = const [], String? dartExecutable, String? google3WorkspaceRoot, }) async { var args = [ '--${DartDevelopmentServiceOptions.vmServiceUriOption}=$remoteVmServiceUri', if (serviceUri != null) ...[ '--${DartDevelopmentServiceOptions.bindAddressOption}=${serviceUri.host}', '--${DartDevelopmentServiceOptions.bindPortOption}=${serviceUri.port}', ], if (!enableAuthCodes) '--${DartDevelopmentServiceOptions.disableServiceAuthCodesFlag}', if (serveDevTools) '--${DartDevelopmentServiceOptions.serveDevToolsFlag}', if (devToolsServerAddress != null) '--${DartDevelopmentServiceOptions.devToolsServerAddressOption}=$devToolsServerAddress', if (enableServicePortFallback) '--${DartDevelopmentServiceOptions.enableServicePortFallbackFlag}', if (google3WorkspaceRoot != null) '--${DartDevelopmentServiceOptions.google3WorkspaceRootOption}=$google3WorkspaceRoot', ]; late String executable; if (dartExecutable == null) { // If a dart executable is not specified and we are able to locate // the 'dartaotruntime' executable and the AOT snapshot for dds // then invoke it directly as it would avoid the additional hop // of going through the dart CLI process to invoke dds. executable = Platform.executable; var sdkPath = path.absolute(path.dirname(path.dirname(executable)), 'bin'); var snapshotsDir = path.join(sdkPath, 'snapshots'); final type = FileSystemEntity.typeSync(snapshotsDir); if (type != FileSystemEntityType.directory && type != FileSystemEntityType.link) { // This is the less common case where the user is in // the checked out Dart SDK, and is executing `dart` via: // ./out/ReleaseX64/dart ... or in google3. sdkPath = path.absolute(path.dirname(executable)); snapshotsDir = sdkPath; } final dartAotRuntime = path.absolute( sdkPath, Platform.isWindows ? 'dartaotruntime.exe' : 'dartaotruntime'); final ddsAotSnapshot = path.absolute(snapshotsDir, 'dds_aot.dart.snapshot'); if (File(dartAotRuntime).existsSync() && File(ddsAotSnapshot).existsSync()) { executable = dartAotRuntime; args = [ddsAotSnapshot, ...args]; } else { args = ['development-service', ...args]; } } else { executable = dartExecutable; args = ['development-service', ...args]; } final process = await Process.start(executable, args); final completer = Completer(); late StreamSubscription stderrSub; stderrSub = process.stderr .transform(utf8.decoder) .transform(json.decoder) .listen((Object? result) { if (result case { 'state': 'started', 'ddsUri': final String ddsUriStr, }) { final ddsUri = Uri.parse(ddsUriStr); final devToolsUriStr = result['devToolsUri'] as String?; final devToolsUri = devToolsUriStr == null ? null : Uri.parse(devToolsUriStr); final dtdUriStr = (result['dtd'] as Map?)?['uri'] as String?; final dtdUri = dtdUriStr == null ? null : Uri.parse(dtdUriStr); completer.complete( DartDevelopmentServiceLauncher._( process: process, uri: ddsUri, devToolsUri: devToolsUri, dtdUri: dtdUri, ), ); } else if (result case { 'state': 'error', 'error': final String error, }) { final Map? exceptionDetails = result['ddsExceptionDetails'] as Map?; completer.completeError( exceptionDetails != null ? DartDevelopmentServiceException.fromJson(exceptionDetails) : StateError(error), ); } else { throw StateError('Unexpected result from DDS: $result'); } stderrSub.cancel(); }); return completer.future; } DartDevelopmentServiceLauncher._({ required Process process, required this.uri, required this.devToolsUri, required this.dtdUri, }) : _ddsInstance = process; final Process _ddsInstance; /// The [Uri] VM service clients can use to communicate with this /// DDS instance via HTTP. final Uri uri; /// The HTTP [Uri] of the hosted DevTools instance. /// /// Returns `null` if DevTools is not running. final Uri? devToolsUri; /// The [Uri] of the Dart Tooling Daemon instance that is hosted by DevTools. /// /// This will be null if DTD was not started by the DevTools server. For /// example, it may have been started by an IDE. final Uri? dtdUri; /// The [Uri] VM service clients can use to communicate with this /// DDS instance via server-sent events (SSE). Uri get sseUri => _toSse(uri)!; /// The [Uri] VM service clients can use to communicate with this /// DDS instance via a [WebSocket]. Uri get wsUri => _toWebSocket(uri)!; List _cleanupPathSegments(Uri uri) { final pathSegments = []; if (uri.pathSegments.isNotEmpty) { pathSegments.addAll( uri.pathSegments.where( // Strip out the empty string that appears at the end of path segments. // Empty string elements will result in an extra '/' being added to the // URI. (s) => s.isNotEmpty, ), ); } return pathSegments; } Uri? _toWebSocket(Uri? uri) { if (uri == null) { return null; } final pathSegments = _cleanupPathSegments(uri); pathSegments.add('ws'); return uri.replace(scheme: 'ws', pathSegments: pathSegments); } Uri? _toSse(Uri? uri) { if (uri == null) { return null; } final pathSegments = _cleanupPathSegments(uri); pathSegments.add(DartDevelopmentServiceImpl.kSseHandlerPath); return uri.replace(scheme: 'sse', pathSegments: pathSegments); } /// Completes when the DDS instance has shutdown. Future get done => _ddsInstance.exitCode; /// Shutdown the DDS instance. Future shutdown() { _ddsInstance.kill(); return _ddsInstance.exitCode; } }