[ package:dds ] Add DartDevelopmentServiceLauncher class
Provides shared logic for launching DDS using `dart development-service`. Change-Id: I934c777ada23206b498731ffb0611ed3bd383f1c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378320 Commit-Queue: Ben Konyi <bkonyi@google.com> Reviewed-by: Derek Xu <derekx@google.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 4.2.5
|
||||
- Fixed DevTools URI not including a trailing '/' before the query parameters, which could prevent DevTools from loading properly.
|
||||
- [DAP] Fixed an issue where format specifiers and `format.hex` in `variablesRequest` would not apply to values from lists such as `Uint8List` from `dart:typed_data`.
|
||||
- Added `package:dds/dds_launcher.dart`, a library which can be used to launch DDS instances using `dart development-service`.
|
||||
|
||||
# 4.2.4+1
|
||||
- Added missing type to `Event` in `postEvent`.
|
||||
|
||||
@@ -169,6 +169,8 @@ abstract class DartDevelopmentService {
|
||||
static const String protocolVersion = '2.0';
|
||||
}
|
||||
|
||||
/// Thrown by DDS during initialization failures, unexpected connection issues,
|
||||
/// and when attempting to spawn DDS when an existing DDS instance exists.
|
||||
class DartDevelopmentServiceException implements Exception {
|
||||
/// Set when `DartDeveloperService.startDartDevelopmentService` is called and
|
||||
/// the target VM service already has a Dart Developer Service instance
|
||||
@@ -182,6 +184,33 @@ class DartDevelopmentServiceException implements Exception {
|
||||
/// Set when a connection error has occurred after startup.
|
||||
static const int connectionError = 3;
|
||||
|
||||
factory DartDevelopmentServiceException.fromJson(Map<String, Object?> json) {
|
||||
if (json
|
||||
case {
|
||||
'error_code': final int errorCode,
|
||||
'message': final String message,
|
||||
'uri': final String? uri
|
||||
}) {
|
||||
return switch (errorCode) {
|
||||
existingDdsInstanceError =>
|
||||
DartDevelopmentServiceException.existingDdsInstance(
|
||||
message,
|
||||
ddsUri: Uri.parse(uri!),
|
||||
),
|
||||
failedToStartError => DartDevelopmentServiceException.failedToStart(),
|
||||
connectionError =>
|
||||
DartDevelopmentServiceException.connectionIssue(message),
|
||||
_ => throw StateError(
|
||||
'Invalid DartDevelopmentServiceException error_code: $errorCode',
|
||||
),
|
||||
};
|
||||
}
|
||||
throw StateError('Invalid DartDevelopmentServiceException JSON: $json');
|
||||
}
|
||||
|
||||
/// Thrown when `DartDeveloperService.startDartDevelopmentService` is called
|
||||
/// and the target VM service already has a Dart Developer Service instance
|
||||
/// connected.
|
||||
factory DartDevelopmentServiceException.existingDdsInstance(
|
||||
String message, {
|
||||
Uri? ddsUri,
|
||||
@@ -192,11 +221,14 @@ class DartDevelopmentServiceException implements Exception {
|
||||
);
|
||||
}
|
||||
|
||||
/// Thrown when the connection to the remote VM service terminates unexpectedly
|
||||
/// during Dart Development Service startup.
|
||||
factory DartDevelopmentServiceException.failedToStart() {
|
||||
return DartDevelopmentServiceException._(
|
||||
failedToStartError, 'Failed to start Dart Development Service');
|
||||
}
|
||||
|
||||
/// Thrown when a connection error has occurred after startup.
|
||||
factory DartDevelopmentServiceException.connectionIssue(String message) {
|
||||
return DartDevelopmentServiceException._(connectionError, message);
|
||||
}
|
||||
@@ -215,6 +247,7 @@ class DartDevelopmentServiceException implements Exception {
|
||||
final String message;
|
||||
}
|
||||
|
||||
/// Thrown when attempting to start a new DDS instance when one already exists.
|
||||
class ExistingDartDevelopmentServiceException
|
||||
extends DartDevelopmentServiceException {
|
||||
ExistingDartDevelopmentServiceException._(
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// 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,
|
||||
}) 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',
|
||||
],
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -347,8 +347,8 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService {
|
||||
Handler _sseHandler() {
|
||||
final handler = SseHandler(
|
||||
authCodesEnabled
|
||||
? Uri.parse('/$authCode/$_kSseHandlerPath')
|
||||
: Uri.parse('/$_kSseHandlerPath'),
|
||||
? Uri.parse('/$authCode/$kSseHandlerPath')
|
||||
: Uri.parse('/$kSseHandlerPath'),
|
||||
keepAlive: sseKeepAlive,
|
||||
);
|
||||
|
||||
@@ -441,7 +441,7 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService {
|
||||
return null;
|
||||
}
|
||||
final pathSegments = _cleanupPathSegments(uri);
|
||||
pathSegments.add(_kSseHandlerPath);
|
||||
pathSegments.add(kSseHandlerPath);
|
||||
return uri.replace(scheme: 'sse', pathSegments: pathSegments);
|
||||
}
|
||||
|
||||
@@ -552,7 +552,7 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService {
|
||||
StreamManager get streamManager => _streamManager;
|
||||
late StreamManager _streamManager;
|
||||
|
||||
static const _kSseHandlerPath = '\$debugHandler';
|
||||
static const kSseHandlerPath = '\$debugHandler';
|
||||
|
||||
late json_rpc.Peer vmServiceClient;
|
||||
late WebSocketChannel _vmServiceSocket;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dds/dds_launcher.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service_io.dart';
|
||||
|
||||
import 'common/test_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('DDS', () {
|
||||
late Process process;
|
||||
late DartDevelopmentServiceLauncher dds;
|
||||
|
||||
setUp(() async {
|
||||
process = await spawnDartProcess('smoke.dart');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await dds.shutdown();
|
||||
process.kill();
|
||||
});
|
||||
|
||||
void createSmokeTest(bool useAuthCodes) {
|
||||
test(
|
||||
'Launcher Smoke Test with ${useAuthCodes ? "" : "no"} authentication codes',
|
||||
() async {
|
||||
dds = await DartDevelopmentServiceLauncher.start(
|
||||
remoteVmServiceUri: remoteVmServiceUri,
|
||||
enableAuthCodes: useAuthCodes,
|
||||
);
|
||||
|
||||
// Ensure basic websocket requests are forwarded correctly to the VM service.
|
||||
final service = await vmServiceConnectUri(dds.wsUri.toString());
|
||||
final version = await service.getVersion();
|
||||
expect(version.major! > 0, true);
|
||||
expect(version.minor! >= 0, true);
|
||||
|
||||
expect(
|
||||
dds.uri.pathSegments,
|
||||
useAuthCodes ? isNotEmpty : isEmpty,
|
||||
);
|
||||
|
||||
// Ensure we can still make requests of the VM service via HTTP.
|
||||
HttpClient client = HttpClient();
|
||||
final request = await client.getUrl(remoteVmServiceUri.replace(
|
||||
pathSegments: [
|
||||
if (remoteVmServiceUri.pathSegments.isNotEmpty)
|
||||
remoteVmServiceUri.pathSegments.first,
|
||||
'getVersion',
|
||||
],
|
||||
));
|
||||
final response = await request.close();
|
||||
final Map<String, dynamic> jsonResponse = (await response
|
||||
.transform(utf8.decoder)
|
||||
.transform(json.decoder)
|
||||
.single) as Map<String, dynamic>;
|
||||
expect(jsonResponse['result']['type'], 'Version');
|
||||
expect(jsonResponse['result']['major'] > 0, true);
|
||||
expect(jsonResponse['result']['minor'] >= 0, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
createSmokeTest(true);
|
||||
createSmokeTest(false);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user