| // Copyright 2014 The Flutter Authors. 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 'package:browser_launcher/browser_launcher.dart'; | |
| import 'package:dds/dds.dart'; | |
| import 'package:dds/dds_launcher.dart'; | |
| import 'package:meta/meta.dart'; | |
| import '../artifacts.dart'; | |
| import '../build_info.dart'; | |
| import '../device.dart'; | |
| import '../globals.dart' as globals; | |
| import '../resident_runner.dart'; | |
| import '../vmservice.dart'; | |
| import 'io.dart' as io; | |
| import 'logger.dart'; | |
| import 'utils.dart'; | |
| export 'package:dds/dds.dart' | |
| show DartDevelopmentServiceException, ExistingDartDevelopmentServiceException; | |
| typedef DDSLauncherCallback = Future<DartDevelopmentServiceLauncher> Function({ | |
| required Uri remoteVmServiceUri, | |
| String? appName, | |
| Uri? serviceUri, | |
| bool enableAuthCodes, | |
| bool serveDevTools, | |
| Uri? devToolsServerAddress, | |
| bool enableServicePortFallback, | |
| List<String> cachedUserTags, | |
| String? dartExecutable, | |
| String? google3WorkspaceRoot, | |
| }); | |
| typedef StartChromeCallback = Future<io.Process> Function(List<String> urls, {List<String> args}); | |
| // 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 = DartDevelopmentServiceLauncher.start; | |
| /// Helper class to launch a [DartDevelopmentServiceLauncher]. Allows for us to | |
| /// mock out this functionality for testing purposes. | |
| class DartDevelopmentService with DartDevelopmentServiceLocalOperationsMixin { | |
| DartDevelopmentService({required this.logger}); | |
| DartDevelopmentServiceLauncher? _ddsInstance; | |
| @override | |
| Uri? get uri => _ddsInstance?.uri ?? _existingDdsUri; | |
| Uri? _existingDdsUri; | |
| @override | |
| Uri? get devToolsUri => _ddsInstance?.devToolsUri; | |
| Uri? get dtdUri => _ddsInstance?.dtdUri; | |
| Future<void> get done => _completer.future; | |
| final _completer = Completer<void>(); | |
| @override | |
| final Logger logger; | |
| @override | |
| Future<void> startDartDevelopmentService( | |
| Uri vmServiceUri, { | |
| String? appName, | |
| int? ddsPort, | |
| bool? disableServiceAuthCodes, | |
| bool? ipv6, | |
| bool enableDevTools = true, | |
| bool cacheStartupProfile = false, | |
| String? google3WorkspaceRoot, | |
| Uri? devToolsServerAddress, | |
| }) async { | |
| assert(_ddsInstance == null); | |
| final ddsUri = Uri( | |
| scheme: 'http', | |
| host: ((ipv6 ?? false) ? io.InternetAddress.loopbackIPv6 : io.InternetAddress.loopbackIPv4) | |
| .host, | |
| port: ddsPort ?? 0, | |
| ); | |
| logger.printTrace( | |
| 'Launching a Dart Developer Service (DDS) instance at $ddsUri, ' | |
| 'connecting to VM service at $vmServiceUri.', | |
| ); | |
| void completeFuture() { | |
| if (!_completer.isCompleted) { | |
| _completer.complete(); | |
| } | |
| } | |
| try { | |
| _ddsInstance = await ddsLauncherCallback( | |
| appName: appName, | |
| remoteVmServiceUri: vmServiceUri, | |
| serviceUri: ddsUri, | |
| enableAuthCodes: disableServiceAuthCodes != true, | |
| // Enables caching of CPU samples collected during application startup. | |
| cachedUserTags: cacheStartupProfile ? const <String>['AppStartUp'] : const <String>[], | |
| serveDevTools: enableDevTools, | |
| devToolsServerAddress: devToolsServerAddress, | |
| google3WorkspaceRoot: google3WorkspaceRoot, | |
| dartExecutable: globals.artifacts!.getArtifactPath(Artifact.engineDartBinary), | |
| ); | |
| unawaited(_ddsInstance!.done.whenComplete(completeFuture)); | |
| } on DartDevelopmentServiceException catch (e) { | |
| logger.printTrace('Warning: Failed to start DDS: ${e.message}'); | |
| if (e is ExistingDartDevelopmentServiceException) { | |
| _existingDdsUri = e.ddsUri; | |
| } | |
| completeFuture(); | |
| rethrow; | |
| } | |
| } | |
| Future<void> shutdown() async { | |
| await _ddsInstance?.shutdown(); | |
| } | |
| } | |
| /// Contains common functionality that can be used with any implementation of | |
| /// [DartDevelopmentService]. | |
| mixin DartDevelopmentServiceLocalOperationsMixin { | |
| Uri? get uri; | |
| Uri? get devToolsUri; | |
| Logger get logger; | |
| /// Used to confirm `launchDevToolsInBrowser` is called in tests. | |
| @visibleForTesting | |
| bool get calledLaunchDevToolsInBrowser => _calledLaunchDevToolsInBrowser; | |
| var _calledLaunchDevToolsInBrowser = false; | |
| Future<void> startDartDevelopmentService( | |
| Uri vmServiceUri, { | |
| String? appName, | |
| int? ddsPort, | |
| bool? disableServiceAuthCodes, | |
| bool? ipv6, | |
| bool enableDevTools = true, | |
| bool cacheStartupProfile = false, | |
| String? google3WorkspaceRoot, | |
| Uri? devToolsServerAddress, | |
| }); | |
| /// A convenience method used to create a [DartDevelopmentService] instance | |
| /// from a [DebuggingOptions] instance. | |
| Future<void> startDartDevelopmentServiceFromDebuggingOptions( | |
| Uri vmServiceUri, { | |
| String? appName, | |
| required DebuggingOptions debuggingOptions, | |
| }) => startDartDevelopmentService( | |
| vmServiceUri, | |
| appName: appName, | |
| ddsPort: debuggingOptions.ddsPort, | |
| disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes, | |
| ipv6: debuggingOptions.ipv6, | |
| enableDevTools: debuggingOptions.enableDevTools, | |
| cacheStartupProfile: debuggingOptions.cacheStartupProfile, | |
| google3WorkspaceRoot: debuggingOptions.google3WorkspaceRoot, | |
| devToolsServerAddress: debuggingOptions.devToolsServerAddress, | |
| ); | |
| /// Launches a DevTools instance connected to the DDS instance connected to | |
| /// [device] in Chrome. | |
| bool launchDevToolsInBrowser( | |
| FlutterDevice device, { | |
| @visibleForTesting StartChromeCallback startChrome = Chrome.start, | |
| }) { | |
| _calledLaunchDevToolsInBrowser = true; | |
| if (devToolsUri == null) { | |
| return false; | |
| } | |
| assert(devToolsUri != null); | |
| logger.printStatus('Launching Flutter DevTools for ${device.device!.name} at $devToolsUri'); | |
| Future<void> launchChrome() async { | |
| try { | |
| await startChrome(<String>[devToolsUri!.toString()]); | |
| } on Exception catch (error) { | |
| logger.printError('Failed to launch DevTools in browser: $error'); | |
| } | |
| } | |
| unawaited(launchChrome()); | |
| return true; | |
| } | |
| /// Re-initializes Flutter framework service extension state after a hot | |
| /// restart. | |
| Future<void> handleHotRestart(FlutterDevice? device) => invokeServiceExtensions(device); | |
| /// Initializes Flutter framework service extension state related to DevTools | |
| /// and VM service connection information. | |
| Future<void> invokeServiceExtensions(FlutterDevice? device) async { | |
| await Future.wait(<Future<void>>[ | |
| maybeCallDevToolsUriServiceExtension(device: device, uri: devToolsUri), | |
| _callConnectedVmServiceUriExtension(device), | |
| ]); | |
| } | |
| /// Returns null if the service extension cannot be found on the device. | |
| Future<bool> _waitForExtensionsForDevice(FlutterDevice flutterDevice, String extension) async { | |
| try { | |
| await flutterDevice.vmService?.findExtensionIsolate(extension); | |
| return true; | |
| } on VmServiceDisappearedException { | |
| logger.printTrace( | |
| 'The VM Service for ${flutterDevice.device} disappeared while trying to' | |
| ' find the $extension service extension. Skipping subsequent DevTools ' | |
| 'setup for this device.', | |
| ); | |
| return false; | |
| } | |
| } | |
| /// Sets the DevTools URI in the Flutter framework, used for deep linking | |
| /// support. | |
| Future<void> maybeCallDevToolsUriServiceExtension({ | |
| required FlutterDevice? device, | |
| required Uri? uri, | |
| }) async { | |
| if (uri != null && device?.vmService != null) { | |
| // We're only setting the URI pointing to where DevTools is being served from. Don't include | |
| // any query parameters, including those used to automatically connect to the application. | |
| if (uri.hasQuery) { | |
| uri = uri.withoutQueryParameters(); | |
| } | |
| await _callDevToolsUriExtension(device!, uri); | |
| } | |
| } | |
| Future<void> _callDevToolsUriExtension(FlutterDevice device, Uri uri) async { | |
| try { | |
| await _invokeRpcOnFirstView( | |
| 'ext.flutter.activeDevToolsServerAddress', | |
| device: device, | |
| params: <String, dynamic>{'value': uri.toString()}, | |
| ); | |
| } on Exception catch (e) { | |
| logger.printError( | |
| 'Failed to set DevTools server address: $e. Deep links to' | |
| ' DevTools will not show in Flutter errors.', | |
| ); | |
| } | |
| } | |
| Future<void> _callConnectedVmServiceUriExtension(FlutterDevice? device) async { | |
| if (device == null || uri == null) { | |
| return; | |
| } | |
| try { | |
| await _invokeRpcOnFirstView( | |
| 'ext.flutter.connectedVmServiceUri', | |
| device: device, | |
| params: <String, dynamic>{'value': uri.toString()}, | |
| ); | |
| } on Exception catch (e) { | |
| logger.printError(e.toString()); | |
| logger.printError( | |
| 'Failed to set vm service URI: $e. Deep links to DevTools' | |
| ' will not show in Flutter errors.', | |
| ); | |
| } | |
| } | |
| Future<void> _invokeRpcOnFirstView( | |
| String method, { | |
| required FlutterDevice device, | |
| required Map<String, dynamic> params, | |
| }) async { | |
| if (!(await _waitForExtensionsForDevice(device, method))) { | |
| return; | |
| } | |
| if (device.targetPlatform == TargetPlatform.web_javascript) { | |
| await device.vmService!.callMethodWrapper(method, args: params); | |
| return; | |
| } | |
| final List<FlutterView> views = await device.vmService!.getFlutterViews(); | |
| if (views.isEmpty) { | |
| return; | |
| } | |
| await device.vmService!.invokeFlutterExtensionRpcRaw( | |
| method, | |
| args: params, | |
| isolateId: views.first.uiIsolate!.id, | |
| ); | |
| } | |
| } |