// 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 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, }) 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 List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices); await _maybeCallDevToolsUriServiceExtension(devicesWithExtension); await _callConnectedVmServiceUriExtension(devicesWithExtension); 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(includeObservatory: false); } } // 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}) 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(); }