From 04595bc088309c3c177f99acaeb3218952bc88f5 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Thu, 29 Aug 2024 16:16:27 -0400 Subject: [PATCH] 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 --- packages/flutter_tools/lib/src/base/dds.dart | 226 ++---------------- .../flutter_tester_device_test.dart | 17 +- .../resident_runner_helpers.dart | 12 +- .../general.shard/resident_runner_test.dart | 67 +++--- packages/flutter_tools/test/src/fakes.dart | 27 +++ 5 files changed, 104 insertions(+), 245 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/dds.dart b/packages/flutter_tools/lib/src/base/dds.dart index 75b91bf412..6b9524224f 100644 --- a/packages/flutter_tools/lib/src/base/dds.dart +++ b/packages/flutter_tools/lib/src/base/dds.dart @@ -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 defaultStartDartDevelopmentService( - Uri remoteVmServiceUri, { - required bool enableAuthCodes, - required bool ipv6, - required bool enableDevTools, - required List cachedUserTags, +typedef DDSLauncherCallback = Future 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, - [ - 'development-service', - '--vm-service-uri=$remoteVmServiceUri', - if (serviceUri != null) ...[ - '--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 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 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?)?['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? 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; -} - -typedef DDSLauncherCallback = Future Function( - Uri remoteVmServiceUri, { - required bool enableAuthCodes, - required bool ipv6, - required bool enableDevTools, - required List cachedUserTags, - Uri? serviceUri, - String? google3WorkspaceRoot, + bool enableAuthCodes, + bool serveDevTools, Uri? devToolsServerAddress, + bool enableServicePortFallback, + List 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 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 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 ['AppStartUp'] : const [], - 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 diff --git a/packages/flutter_tools/test/general.shard/flutter_tester_device_test.dart b/packages/flutter_tools/test/general.shard/flutter_tester_device_test.dart index 2639e38816..bbb0bfd09e 100644 --- a/packages/flutter_tools/test/general.shard/flutter_tester_device_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_tester_device_test.dart @@ -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 cachedUserTags, + ddsLauncherCallback = ({ + required Uri remoteVmServiceUri, Uri? serviceUri, - String? google3WorkspaceRoot, + bool enableAuthCodes = true, + bool serveDevTools = false, Uri? devToolsServerAddress, + bool enableServicePortFallback = false, + List cachedUserTags = const [], + 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')); }; }); diff --git a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart index 165c00d234..428b342340 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart @@ -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 toJson() { + throw UnimplementedError(); + } } class TestFlutterDevice extends FlutterDevice { diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index ea85a78f27..5518ef1a6e 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -1938,14 +1938,17 @@ flutter: fakeVmServiceHost = FakeVmServiceHost(requests: []); final FakeDevice device = FakeDevice() ..dds = DartDevelopmentService(logger: testLogger); - ddsLauncherCallback = (Uri uri, - {bool enableDevTools = false, - bool enableAuthCodes = true, - bool ipv6 = false, Uri? serviceUri, - List cachedUserTags = const [], - String? google3WorkspaceRoot, - Uri? devToolsServerAddress, - }) { + ddsLauncherCallback = ({ + required Uri remoteVmServiceUri, + Uri? serviceUri, + bool enableAuthCodes = true, + bool serveDevTools = false, + Uri? devToolsServerAddress, + bool enableServicePortFallback = false, + List cachedUserTags = const [], + String? dartExecutable, + Uri? google3WorkspaceRoot, + }) { throw DartDevelopmentServiceException.existingDdsInstance( 'Existing DDS at http://localhost/existingDdsInMessage.', ddsUri: Uri.parse('http://localhost/existingDdsInField'), @@ -1984,21 +1987,23 @@ flutter: final FakeDevice device = FakeDevice() ..dds = DartDevelopmentService(logger: testLogger); final Completerdone = Completer(); - ddsLauncherCallback = (Uri uri, - {bool enableDevTools = false, - bool enableAuthCodes = true, - bool ipv6 = false, Uri? serviceUri, - List cachedUserTags = const [], - String? google3WorkspaceRoot, - Uri? devToolsServerAddress, - }) async { - expect(uri, Uri(scheme: 'foo', host: 'bar')); + ddsLauncherCallback = ({ + required Uri remoteVmServiceUri, + Uri? serviceUri, + bool enableAuthCodes = true, + bool serveDevTools = false, + Uri? devToolsServerAddress, + bool enableServicePortFallback = false, + List cachedUserTags = const [], + String? dartExecutable, + Uri? google3WorkspaceRoot, + }) async { + 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, - Uri? serviceUri, - List cachedUserTags = const [], - String? google3WorkspaceRoot, - Uri? devToolsServerAddress, - }) { - expect(uri, Uri(scheme: 'foo', host: 'bar')); + ddsLauncherCallback = ({ + required Uri remoteVmServiceUri, + Uri? serviceUri, + bool enableAuthCodes = true, + bool serveDevTools = false, + Uri? devToolsServerAddress, + bool enableServicePortFallback = false, + List cachedUserTags = const [], + String? dartExecutable, + Uri? google3WorkspaceRoot, + }) { + 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'); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index cc6d456b44..71ef35c426 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -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 get done => _completer.future; + + @override + Future shutdown() async => _completer.complete(); + + final Completer _completer = Completer(); +} + class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher { FakeDevtoolsLauncher({DevToolsServerAddress? serverAddress}) : _serverAddress = serverAddress;