blob: d83e3f070e555803b06cda4708f8f7205a43b4bd [file] [log] [blame]
// 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 '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.
///
/// If set, the set of [cachedUserTags] will be used to determine which CPU
/// samples should be cached by DDS.
///
/// 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<DartDevelopmentServiceLauncher> start({
required Uri remoteVmServiceUri,
Uri? serviceUri,
bool enableAuthCodes = true,
bool serveDevTools = false,
Uri? devToolsServerAddress,
bool enableServicePortFallback = false,
List<String> cachedUserTags = const <String>[],
String? dartExecutable,
String? google3WorkspaceRoot,
}) async {
final process = await Process.start(
dartExecutable ?? Platform.executable,
<String>[
'development-service',
'--${DartDevelopmentServiceOptions.vmServiceUriOption}=$remoteVmServiceUri',
if (serviceUri != null) ...<String>[
'--${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}',
for (final String tag in cachedUserTags)
'--${DartDevelopmentServiceOptions.cachedUserTagsOption}=$tag',
if (google3WorkspaceRoot != null)
'--${DartDevelopmentServiceOptions.google3WorkspaceRootOption}=$google3WorkspaceRoot',
],
);
final completer = Completer<DartDevelopmentServiceLauncher>();
late StreamSubscription<Object?> 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<String, Object?>?)?['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<String, Object?>? exceptionDetails =
result['ddsExceptionDetails'] as Map<String, Object?>?;
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<String> _cleanupPathSegments(Uri uri) {
final pathSegments = <String>[];
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<void> get done => _ddsInstance.exitCode;
/// Shutdown the DDS instance.
Future<void> shutdown() {
_ddsInstance.kill();
return _ddsInstance.exitCode;
}
}