Launch DDS using DartDevelopmentServiceLauncher (#154015)

`DartDevelopmentServiceLauncher` was created to share the DDS launch
logic from flutter_tools with other Dart tooling.

---------

Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
Ben Konyi 2024-08-29 16:16:27 -04:00 committed by GitHub
parent 055350f84a
commit 04595bc088
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 104 additions and 245 deletions

View File

@ -4,220 +4,46 @@
import 'dart:async';
import 'package:dds/dds.dart';
import 'package:dds/dds_launcher.dart';
import 'package:meta/meta.dart';
import '../artifacts.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart' as globals;
import 'io.dart' as io;
import 'logger.dart';
/// A representation of the current DDS state including:
///
/// - The process the external DDS instance is running in
/// - The service URI DDS is being served on
/// - The URI DevTools is being served on, if applicable
/// - The URI DTD is being served on, if applicable
typedef DartDevelopmentServiceInstance = ({
io.Process? process,
Uri? serviceUri,
Uri? devToolsUri,
Uri? dtdUri,
});
export 'package:dds/dds.dart'
show
DartDevelopmentServiceException,
ExistingDartDevelopmentServiceException;
/// The default DDSLauncherCallback used to spawn DDS.
Future<DartDevelopmentServiceInstance> defaultStartDartDevelopmentService(
Uri remoteVmServiceUri, {
required bool enableAuthCodes,
required bool ipv6,
required bool enableDevTools,
required List<String> cachedUserTags,
typedef DDSLauncherCallback = Future<DartDevelopmentServiceLauncher> Function({
required Uri remoteVmServiceUri,
Uri? serviceUri,
String? google3WorkspaceRoot,
Uri? devToolsServerAddress,
}) async {
final String exe = globals.artifacts!.getArtifactPath(
Artifact.engineDartBinary,
);
final io.Process process = await io.Process.start(
exe,
<String>[
'development-service',
'--vm-service-uri=$remoteVmServiceUri',
if (serviceUri != null) ...<String>[
'--bind-address=${serviceUri.host}',
'--bind-port=${serviceUri.port}',
],
if (!enableAuthCodes) '--disable-service-auth-codes',
if (google3WorkspaceRoot != null)
'--google3-workspace-root=$google3WorkspaceRoot',
for (final String tag in cachedUserTags) '--cached-user-tags=$tag',
],
);
final Completer<DartDevelopmentServiceInstance> completer =
Completer<DartDevelopmentServiceInstance>();
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 Uri ddsUri = Uri.parse(ddsUriStr);
final String? devToolsUriStr = result['devToolsUri'] as String?;
final Uri? devToolsUri =
devToolsUriStr == null ? null : Uri.parse(devToolsUriStr);
final String? dtdUriStr =
(result['dtd'] as Map<String, Object?>?)?['uri'] as String?;
final Uri? dtdUri = dtdUriStr == null ? null : Uri.parse(dtdUriStr);
completer.complete((
process: process,
serviceUri: 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;
}
typedef DDSLauncherCallback = Future<DartDevelopmentServiceInstance> Function(
Uri remoteVmServiceUri, {
required bool enableAuthCodes,
required bool ipv6,
required bool enableDevTools,
required List<String> cachedUserTags,
Uri? serviceUri,
String? google3WorkspaceRoot,
bool enableAuthCodes,
bool serveDevTools,
Uri? devToolsServerAddress,
bool enableServicePortFallback,
List<String> cachedUserTags,
String? dartExecutable,
Uri? google3WorkspaceRoot,
});
// TODO(fujino): This should be direct injected, rather than mutable global state.
/// Used by tests to override the DDS spawn behavior for mocking purposes.
@visibleForTesting
DDSLauncherCallback ddsLauncherCallback = defaultStartDartDevelopmentService;
/// 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 {
factory DartDevelopmentServiceException.fromJson(Map<String, Object?> json) {
if (json
case {
'error_code': final int errorCode,
'message': final String message,
}) {
return switch (errorCode) {
existingDdsInstanceError =>
DartDevelopmentServiceException.existingDdsInstance(
message,
ddsUri: Uri.parse(json['uri']! as String),
),
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,
}) {
return ExistingDartDevelopmentServiceException._(
message,
ddsUri: ddsUri,
);
}
/// 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);
}
DartDevelopmentServiceException._(this.errorCode, this.message);
/// Set when `DartDeveloperService.startDartDevelopmentService` is called and
/// the target VM service already has a Dart Developer Service instance
/// connected.
static const int existingDdsInstanceError = 1;
/// Set when the connection to the remote VM service terminates unexpectedly
/// during Dart Development Service startup.
static const int failedToStartError = 2;
/// Set when a connection error has occurred after startup.
static const int connectionError = 3;
@override
String toString() => 'DartDevelopmentServiceException: $message';
final int errorCode;
final String message;
}
/// Thrown when attempting to start a new DDS instance when one already exists.
class ExistingDartDevelopmentServiceException
extends DartDevelopmentServiceException {
ExistingDartDevelopmentServiceException._(
String message, {
this.ddsUri,
}) : super._(
DartDevelopmentServiceException.existingDdsInstanceError,
message,
);
/// The URI of the existing DDS instance, if available.
///
/// This URI is the base HTTP URI such as `http://127.0.0.1:1234/AbcDefg=/`,
/// not the WebSocket URI (which can be obtained by mapping the scheme to
/// `ws` (or `wss`) and appending `ws` to the path segments).
final Uri? ddsUri;
}
DDSLauncherCallback ddsLauncherCallback = DartDevelopmentServiceLauncher.start;
/// Helper class to launch a [dds.DartDevelopmentService]. Allows for us to
/// mock out this functionality for testing purposes.
class DartDevelopmentService with DartDevelopmentServiceLocalOperationsMixin {
DartDevelopmentService({required Logger logger}) : _logger = logger;
DartDevelopmentServiceInstance? _ddsInstance;
DartDevelopmentServiceLauncher? _ddsInstance;
Uri? get uri => _ddsInstance?.serviceUri ?? _existingDdsUri;
Uri? get uri => _ddsInstance?.uri ?? _existingDdsUri;
Uri? _existingDdsUri;
Future<void> get done => _completer.future;
@ -257,25 +83,25 @@ class DartDevelopmentService with DartDevelopmentServiceLocalOperationsMixin {
try {
_ddsInstance = await ddsLauncherCallback(
vmServiceUri,
remoteVmServiceUri: vmServiceUri,
serviceUri: ddsUri,
enableAuthCodes: disableServiceAuthCodes != true,
ipv6: ipv6 ?? false,
enableDevTools: enableDevTools,
// Enables caching of CPU samples collected during application startup.
cachedUserTags: cacheStartupProfile
? const <String>['AppStartUp']
: const <String>[],
google3WorkspaceRoot: google3WorkspaceRoot,
devToolsServerAddress: devToolsServerAddress,
google3WorkspaceRoot: google3WorkspaceRoot != null
? Uri.parse(google3WorkspaceRoot)
: null,
dartExecutable: globals.artifacts!.getArtifactPath(
Artifact.engineDartBinary,
),
);
final io.Process? process = _ddsInstance?.process;
// Complete the future if the DDS process is null, which happens in
// testing.
if (process != null) {
unawaited(process.exitCode.whenComplete(completeFuture));
}
unawaited(_ddsInstance!.done.whenComplete(completeFuture));
} on DartDevelopmentServiceException catch (e) {
_logger.printTrace('Warning: Failed to start DDS: ${e.message}');
if (e is ExistingDartDevelopmentServiceException) {
@ -294,7 +120,7 @@ class DartDevelopmentService with DartDevelopmentServiceLocalOperationsMixin {
}
}
void shutdown() => _ddsInstance?.process?.kill();
void shutdown() => _ddsInstance?.shutdown();
}
/// Contains common functionality that can be used with any implementation of

View File

@ -23,6 +23,7 @@ import 'package:test/fake.dart';
import '../src/context.dart';
import '../src/fake_process_manager.dart';
import '../src/fake_vm_services.dart';
import '../src/fakes.dart';
void main() {
late FakePlatform platform;
@ -267,16 +268,18 @@ void main() {
]);
device = createDevice(enableVmService: true);
originalDdsLauncher = ddsLauncherCallback;
ddsLauncherCallback = (Uri remoteVmServiceUri, {
required bool enableAuthCodes,
required bool ipv6,
required bool enableDevTools,
required List<String> cachedUserTags,
ddsLauncherCallback = ({
required Uri remoteVmServiceUri,
Uri? serviceUri,
String? google3WorkspaceRoot,
bool enableAuthCodes = true,
bool serveDevTools = false,
Uri? devToolsServerAddress,
bool enableServicePortFallback = false,
List<String> cachedUserTags = const <String>[],
String? dartExecutable,
Uri? google3WorkspaceRoot,
}) async {
return (process: null, serviceUri: Uri.parse('http://localhost:1234'), devToolsUri: null, dtdUri: null);
return FakeDartDevelopmentServiceLauncher(uri: Uri.parse('http://localhost:1234'));
};
});

View File

@ -138,13 +138,6 @@ const FakeVmServiceRequest evictShader = FakeVmServiceRequest(
}
);
const DartDevelopmentServiceInstance fakeDartDevelopmentServiceInstance = (
process: null,
serviceUri: null,
devToolsUri: null,
dtdUri: null,
);
final Uri testUri = Uri.parse('foo://bar');
class FakeDartDevelopmentService extends Fake with DartDevelopmentServiceLocalOperationsMixin implements DartDevelopmentService {
@ -164,6 +157,11 @@ class FakeDartDevelopmentServiceException implements DartDevelopmentServiceExcep
@override
final String message;
static const String defaultMessage = 'A DDS instance is already connected at http://localhost:8181';
@override
Map<String, Object?> toJson() {
throw UnimplementedError();
}
}
class TestFlutterDevice extends FlutterDevice {

View File

@ -1938,13 +1938,16 @@ flutter:
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
final FakeDevice device = FakeDevice()
..dds = DartDevelopmentService(logger: testLogger);
ddsLauncherCallback = (Uri uri,
{bool enableDevTools = false,
ddsLauncherCallback = ({
required Uri remoteVmServiceUri,
Uri? serviceUri,
bool enableAuthCodes = true,
bool ipv6 = false, Uri? serviceUri,
List<String> cachedUserTags = const <String>[],
String? google3WorkspaceRoot,
bool serveDevTools = false,
Uri? devToolsServerAddress,
bool enableServicePortFallback = false,
List<String> cachedUserTags = const <String>[],
String? dartExecutable,
Uri? google3WorkspaceRoot,
}) {
throw DartDevelopmentServiceException.existingDdsInstance(
'Existing DDS at http://localhost/existingDdsInMessage.',
@ -1984,21 +1987,23 @@ flutter:
final FakeDevice device = FakeDevice()
..dds = DartDevelopmentService(logger: testLogger);
final Completer<void>done = Completer<void>();
ddsLauncherCallback = (Uri uri,
{bool enableDevTools = false,
ddsLauncherCallback = ({
required Uri remoteVmServiceUri,
Uri? serviceUri,
bool enableAuthCodes = true,
bool ipv6 = false, Uri? serviceUri,
List<String> cachedUserTags = const <String>[],
String? google3WorkspaceRoot,
bool serveDevTools = false,
Uri? devToolsServerAddress,
bool enableServicePortFallback = false,
List<String> cachedUserTags = const <String>[],
String? dartExecutable,
Uri? google3WorkspaceRoot,
}) async {
expect(uri, Uri(scheme: 'foo', host: 'bar'));
expect(remoteVmServiceUri, Uri(scheme: 'foo', host: 'bar'));
expect(enableAuthCodes, isFalse);
expect(ipv6, isTrue);
expect(serviceUri, Uri(scheme: 'http', host: '::1', port: 0));
expect(cachedUserTags, isEmpty);
done.complete();
return fakeDartDevelopmentServiceInstance;
return FakeDartDevelopmentServiceLauncher(uri: remoteVmServiceUri);
};
final TestFlutterDevice flutterDevice = TestFlutterDevice(
device,
@ -2029,19 +2034,19 @@ flutter:
// See https://github.com/flutter/flutter/issues/72385 for context.
final FakeDevice device = FakeDevice()
..dds = DartDevelopmentService(logger: testLogger);
ddsLauncherCallback = (
Uri uri, {
bool enableDevTools = false,
bool enableAuthCodes = false,
bool ipv6 = false,
ddsLauncherCallback = ({
required Uri remoteVmServiceUri,
Uri? serviceUri,
List<String> cachedUserTags = const <String>[],
String? google3WorkspaceRoot,
bool enableAuthCodes = true,
bool serveDevTools = false,
Uri? devToolsServerAddress,
bool enableServicePortFallback = false,
List<String> cachedUserTags = const <String>[],
String? dartExecutable,
Uri? google3WorkspaceRoot,
}) {
expect(uri, Uri(scheme: 'foo', host: 'bar'));
expect(remoteVmServiceUri, Uri(scheme: 'foo', host: 'bar'));
expect(enableAuthCodes, isTrue);
expect(ipv6, isFalse);
expect(serviceUri, Uri(scheme: 'http', host: '127.0.0.1', port: 0));
expect(cachedUserTags, isEmpty);
throw FakeDartDevelopmentServiceException(message: 'No URI');

View File

@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException;
import 'package:dds/dds_launcher.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
@ -696,6 +697,32 @@ class FakeJava extends Fake implements Java {
}
}
class FakeDartDevelopmentServiceLauncher extends Fake
implements DartDevelopmentServiceLauncher {
FakeDartDevelopmentServiceLauncher({
required this.uri,
this.devToolsUri,
this.dtdUri,
});
@override
final Uri uri;
@override
final Uri? devToolsUri;
@override
final Uri? dtdUri;
@override
Future<void> get done => _completer.future;
@override
Future<void> shutdown() async => _completer.complete();
final Completer<void> _completer = Completer<void>();
}
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
FakeDevtoolsLauncher({DevToolsServerAddress? serverAddress})
: _serverAddress = serverAddress;