| // 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:meta/meta.dart'; | |
| import 'base/logger.dart'; | |
| import 'build_info.dart'; | |
| import 'resident_runner.dart'; | |
| import 'vmservice.dart'; | |
| typedef ResidentDevtoolsHandlerFactory = ResidentDevtoolsHandler Function(DevtoolsLauncher?, ResidentRunner, Logger); | |
| ResidentDevtoolsHandler createDefaultHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) { | |
| return FlutterResidentDevtoolsHandler(launcher, runner, logger); | |
| } | |
| /// Helper class to manage the life-cycle of devtools and its interaction with | |
| /// the resident runner. | |
| abstract class ResidentDevtoolsHandler { | |
| /// The current devtools server, or null if one is not running. | |
| DevToolsServerAddress? get activeDevToolsServer; | |
| /// Whether it's ok to announce the [activeDevToolsServer]. | |
| /// | |
| /// This should only return true once all the devices have been notified | |
| /// of the DevTools. | |
| bool get readyToAnnounce; | |
| Future<void> hotRestart(List<FlutterDevice?> flutterDevices); | |
| Future<void> serveAndAnnounceDevTools({ | |
| Uri? devToolsServerAddress, | |
| required List<FlutterDevice?> flutterDevices, | |
| bool isStartPaused = false, | |
| }); | |
| bool launchDevToolsInBrowser({required List<FlutterDevice?> flutterDevices}); | |
| Future<void> shutdown(); | |
| } | |
| class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { | |
| FlutterResidentDevtoolsHandler(this._devToolsLauncher, this._residentRunner, this._logger); | |
| static const Duration launchInBrowserTimeout = Duration(seconds: 15); | |
| final DevtoolsLauncher? _devToolsLauncher; | |
| final ResidentRunner _residentRunner; | |
| final Logger _logger; | |
| bool _shutdown = false; | |
| bool _served = false; | |
| @visibleForTesting | |
| bool launchedInBrowser = false; | |
| @override | |
| DevToolsServerAddress? get activeDevToolsServer { | |
| assert(!_readyToAnnounce || _devToolsLauncher?.activeDevToolsServer != null); | |
| return _devToolsLauncher?.activeDevToolsServer; | |
| } | |
| @override | |
| bool get readyToAnnounce => _readyToAnnounce; | |
| bool _readyToAnnounce = false; | |
| // This must be guaranteed not to return a Future that fails. | |
| @override | |
| Future<void> serveAndAnnounceDevTools({ | |
| Uri? devToolsServerAddress, | |
| required List<FlutterDevice?> flutterDevices, | |
| bool isStartPaused = false, | |
| }) async { | |
| assert(!_readyToAnnounce); | |
| if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) { | |
| return; | |
| } | |
| if (devToolsServerAddress != null) { | |
| _devToolsLauncher!.devToolsUrl = devToolsServerAddress; | |
| } else { | |
| await _devToolsLauncher!.serve(); | |
| _served = true; | |
| } | |
| await _devToolsLauncher!.ready; | |
| // Do not attempt to print debugger list if the connection has failed or if we're shutting down. | |
| if (_devToolsLauncher!.activeDevToolsServer == null || _shutdown) { | |
| assert(!_readyToAnnounce); | |
| return; | |
| } | |
| final Uri? devToolsUrl = _devToolsLauncher!.devToolsUrl; | |
| if (devToolsUrl != null) { | |
| for (final FlutterDevice? device in flutterDevices) { | |
| if (device == null) { | |
| continue; | |
| } | |
| // Notify the DDS instances that there's a DevTools instance available so they can correctly | |
| // redirect DevTools related requests. | |
| device.device?.dds.setExternalDevToolsUri(devToolsUrl); | |
| } | |
| } | |
| Future<void> callServiceExtensions() async { | |
| final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices); | |
| await Future.wait( | |
| <Future<void>>[ | |
| _maybeCallDevToolsUriServiceExtension(devicesWithExtension), | |
| _callConnectedVmServiceUriExtension(devicesWithExtension) | |
| ] | |
| ); | |
| } | |
| // If the application is starting paused, we can't invoke service extensions | |
| // as they're handled on the target app's paused isolate. Since invoking | |
| // service extensions will block in this situation, we should wait to invoke | |
| // them until after we've output the DevTools connection details. | |
| if (!isStartPaused) { | |
| await callServiceExtensions(); | |
| } | |
| // This check needs to happen after the possible asynchronous call above, | |
| // otherwise a shutdown event might be missed and the DevTools launcher may | |
| // no longer be initialized. | |
| if (_shutdown) { | |
| // If we're shutting down, no point reporting the debugger list. | |
| return; | |
| } | |
| _readyToAnnounce = true; | |
| assert(_devToolsLauncher!.activeDevToolsServer != null); | |
| if (_residentRunner.reportedDebuggers) { | |
| // Since the DevTools only just became available, we haven't had a chance to | |
| // report their URLs yet. Do so now. | |
| _residentRunner.printDebuggerList(includeVmService: false); | |
| } | |
| if (isStartPaused) { | |
| await callServiceExtensions(); | |
| } | |
| } | |
| // This must be guaranteed not to return a Future that fails. | |
| @override | |
| bool launchDevToolsInBrowser({required List<FlutterDevice?> flutterDevices}) { | |
| if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) { | |
| return false; | |
| } | |
| if (_devToolsLauncher!.devToolsUrl == null) { | |
| _logger.startProgress('Waiting for Flutter DevTools to be served...'); | |
| unawaited(_devToolsLauncher!.ready.then((_) { | |
| _launchDevToolsForDevices(flutterDevices); | |
| })); | |
| } else { | |
| _launchDevToolsForDevices(flutterDevices); | |
| } | |
| return true; | |
| } | |
| void _launchDevToolsForDevices(List<FlutterDevice?> flutterDevices) { | |
| assert(activeDevToolsServer != null); | |
| for (final FlutterDevice? device in flutterDevices) { | |
| final String devToolsUrl = activeDevToolsServer!.uri!.replace( | |
| queryParameters: <String, dynamic>{'uri': '${device!.vmService!.httpAddress}'}, | |
| ).toString(); | |
| _logger.printStatus('Launching Flutter DevTools for ${device.device!.name} at $devToolsUrl'); | |
| unawaited(Chrome.start(<String>[devToolsUrl])); | |
| } | |
| launchedInBrowser = true; | |
| } | |
| Future<void> _maybeCallDevToolsUriServiceExtension( | |
| List<FlutterDevice?> flutterDevices, | |
| ) async { | |
| if (_devToolsLauncher?.activeDevToolsServer == null) { | |
| return; | |
| } | |
| await Future.wait(<Future<void>>[ | |
| for (final FlutterDevice? device in flutterDevices) | |
| if (device?.vmService != null) _callDevToolsUriExtension(device!), | |
| ]); | |
| } | |
| Future<void> _callDevToolsUriExtension( | |
| FlutterDevice device, | |
| ) async { | |
| try { | |
| await _invokeRpcOnFirstView( | |
| 'ext.flutter.activeDevToolsServerAddress', | |
| device: device, | |
| params: <String, dynamic>{ | |
| 'value': _devToolsLauncher!.activeDevToolsServer!.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<List<FlutterDevice?>> _devicesWithExtensions(List<FlutterDevice?> flutterDevices) async { | |
| return Future.wait(<Future<FlutterDevice?>>[ | |
| for (final FlutterDevice? device in flutterDevices) _waitForExtensionsForDevice(device!), | |
| ]); | |
| } | |
| /// Returns null if the service extension cannot be found on the device. | |
| Future<FlutterDevice?> _waitForExtensionsForDevice(FlutterDevice flutterDevice) async { | |
| const String extension = 'ext.flutter.connectedVmServiceUri'; | |
| try { | |
| await flutterDevice.vmService?.findExtensionIsolate( | |
| extension, | |
| ); | |
| return flutterDevice; | |
| } 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 null; | |
| } | |
| } | |
| Future<void> _callConnectedVmServiceUriExtension(List<FlutterDevice?> flutterDevices) async { | |
| await Future.wait(<Future<void>>[ | |
| for (final FlutterDevice? device in flutterDevices) | |
| if (device?.vmService != null) _callConnectedVmServiceExtension(device!), | |
| ]); | |
| } | |
| Future<void> _callConnectedVmServiceExtension(FlutterDevice device) async { | |
| final Uri? uri = device.vmService!.httpAddress ?? device.vmService!.wsAddress; | |
| if (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 (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!, | |
| ); | |
| } | |
| @override | |
| Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async { | |
| final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices); | |
| await Future.wait(<Future<void>>[ | |
| _maybeCallDevToolsUriServiceExtension(devicesWithExtension), | |
| _callConnectedVmServiceUriExtension(devicesWithExtension), | |
| ]); | |
| } | |
| @override | |
| Future<void> shutdown() async { | |
| if (_devToolsLauncher == null || _shutdown || !_served) { | |
| return; | |
| } | |
| _shutdown = true; | |
| _readyToAnnounce = false; | |
| await _devToolsLauncher!.close(); | |
| } | |
| } | |
| @visibleForTesting | |
| NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) { | |
| return NoOpDevtoolsHandler(); | |
| } | |
| @visibleForTesting | |
| class NoOpDevtoolsHandler implements ResidentDevtoolsHandler { | |
| bool wasShutdown = false; | |
| @override | |
| DevToolsServerAddress? get activeDevToolsServer => null; | |
| @override | |
| bool get readyToAnnounce => false; | |
| @override | |
| Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async { | |
| return; | |
| } | |
| @override | |
| Future<void> serveAndAnnounceDevTools({ | |
| Uri? devToolsServerAddress, | |
| List<FlutterDevice?>? flutterDevices, | |
| bool isStartPaused = false, | |
| }) async { | |
| return; | |
| } | |
| @override | |
| bool launchDevToolsInBrowser({List<FlutterDevice?>? flutterDevices}) { | |
| return false; | |
| } | |
| @override | |
| Future<void> shutdown() async { | |
| wasShutdown = true; | |
| return; | |
| } | |
| } | |
| /// Convert a [URI] with query parameters into a display format instead | |
| /// of the default URI encoding. | |
| String urlToDisplayString(Uri uri) { | |
| final StringBuffer base = StringBuffer(uri.replace( | |
| queryParameters: <String, String>{}, | |
| ).toString()); | |
| base.write(uri.queryParameters.keys.map((String key) => '$key=${uri.queryParameters[key]}').join('&')); | |
| return base.toString(); | |
| } |