diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 26e74d855e3..660c494662d 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -156,6 +156,8 @@ Future main(List args) async { processManager: globals.processManager, pubExecutable: globals.artifacts.getArtifactPath(Artifact.pubExecutable), logger: globals.logger, + platform: globals.platform, + persistentToolState: globals.persistentToolState, ), Logger: () { final LoggerFactory loggerFactory = LoggerFactory( diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index d10a7000e21..ddc877b4a80 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -101,6 +101,7 @@ class AttachCommand extends FlutterCommand { ); usesTrackWidgetCreation(verboseHelp: verboseHelp); addDdsOptions(verboseHelp: verboseHelp); + addDevToolsOptions(); usesDeviceTimeoutOption(); hotRunnerFactory ??= HotRunnerFactory(); } @@ -405,7 +406,11 @@ known, it can be explicitly provided to attach via the command-line, e.g. ); flutterDevice.observatoryUris = observatoryUris; final List flutterDevices = [flutterDevice]; - final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(buildInfo, disableDds: boolArg('disable-dds')); + final DebuggingOptions debuggingOptions = DebuggingOptions.enabled( + buildInfo, + disableDds: boolArg('disable-dds'), + devToolsServerAddress: devToolsServerAddress, + ); return buildInfo.isDebug ? hotRunnerFactory.build( diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index a72e7763a41..16c457b2aee 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -871,8 +871,7 @@ class DevToolsDomain extends Domain { Future> serve([ Map args ]) async { _devtoolsLauncher ??= DevtoolsLauncher.instance; - final bool openInBrowser = args != null && (args['openInBrowser'] == 'true'); - final DevToolsServerAddress server = await _devtoolsLauncher.serve(openInBrowser: openInBrowser); + final DevToolsServerAddress server = await _devtoolsLauncher.serve(); return{ 'host': server?.host, 'port': server?.port, diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 7b2af9f3dcc..e9cf68b40e4 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -134,6 +134,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment usesDeviceUserOption(); usesDeviceTimeoutOption(); addDdsOptions(verboseHelp: verboseHelp); + addDevToolsOptions(); addAndroidSpecificBuildOptions(hide: !verboseHelp); } @@ -195,6 +196,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment hostVmServicePort: hostVmservicePort, disablePortPublication: disablePortPublication, ddsPort: ddsPort, + devToolsServerAddress: devToolsServerAddress, verboseSystemLogs: boolArg('verbose-system-logs'), hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', port: featureFlags.isWebEnabled ? stringArg('web-port') : '', diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 02ca1a7f7fa..baad4222df0 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -184,6 +184,8 @@ Future runInContext( processManager: globals.processManager, pubExecutable: globals.artifacts.getArtifactPath(Artifact.pubExecutable), logger: globals.logger, + platform: globals.platform, + persistentToolState: globals.persistentToolState, ), Doctor: () => Doctor(logger: globals.logger), DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 53c8861f5a8..cbbf9e5fee9 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -852,6 +852,7 @@ class DebuggingOptions { this.disablePortPublication = false, this.deviceVmServicePort, this.ddsPort, + this.devToolsServerAddress, this.hostname, this.port, this.webEnableExposeUrl, @@ -895,6 +896,7 @@ class DebuggingOptions { disablePortPublication = false, deviceVmServicePort = null, ddsPort = null, + devToolsServerAddress = null, vmserviceOutFile = null, fastStart = false, webEnableExpressionEvaluation = false, @@ -924,6 +926,7 @@ class DebuggingOptions { final int deviceVmServicePort; final bool disablePortPublication; final int ddsPort; + final Uri devToolsServerAddress; final String port; final String hostname; final bool webEnableExposeUrl; diff --git a/packages/flutter_tools/lib/src/devtools_launcher.dart b/packages/flutter_tools/lib/src/devtools_launcher.dart index c553a997df5..c4480181433 100644 --- a/packages/flutter_tools/lib/src/devtools_launcher.dart +++ b/packages/flutter_tools/lib/src/devtools_launcher.dart @@ -4,13 +4,15 @@ import 'dart:async'; -import 'package:browser_launcher/browser_launcher.dart'; +import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'base/io.dart' as io; import 'base/logger.dart'; +import 'base/platform.dart'; import 'convert.dart'; +import 'persistent_tool_state.dart'; import 'resident_runner.dart'; /// An implementation of the devtools launcher that uses the server package. @@ -19,59 +21,68 @@ import 'resident_runner.dart'; /// a devtools dep in google3. class DevtoolsServerLauncher extends DevtoolsLauncher { DevtoolsServerLauncher({ + @required Platform platform, @required ProcessManager processManager, @required String pubExecutable, @required Logger logger, + @required PersistentToolState persistentToolState, }) : _processManager = processManager, _pubExecutable = pubExecutable, - _logger = logger; + _logger = logger, + _platform = platform, + _persistentToolState = persistentToolState; final ProcessManager _processManager; final String _pubExecutable; final Logger _logger; + final Platform _platform; + final PersistentToolState _persistentToolState; io.Process _devToolsProcess; - Uri _devToolsUri; static final RegExp _serveDevToolsPattern = RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)'); @override - Future launch(Uri vmServiceUri, {bool openInBrowser = false}) async { - if (_devToolsProcess != null && _devToolsUri != null) { - // DevTools is already running. - if (openInBrowser) { - await Chrome.start([_devToolsUri.toString()]); - } - return; - } - - final Status status = _logger.startProgress( - 'Activating Dart DevTools...', - ); + Future launch(Uri vmServiceUri) async { + // Place this entire method in a try/catch that swallows exceptions because + // we do not want to block Flutter run/attach operations on a DevTools + // failure. try { - // TODO(kenz): https://github.com/dart-lang/pub/issues/2791 - calling `pub - // global activate` adds ~ 4.5 seconds of latency. - final io.ProcessResult _devToolsActivateProcess = await _processManager.run([ - _pubExecutable, - 'global', - 'activate', - 'devtools' - ]); - if (_devToolsActivateProcess.exitCode != 0) { - status.cancel(); - _logger.printError('Error running `pub global activate ' - 'devtools`:\n${_devToolsActivateProcess.stderr}'); - return; + bool offline = false; + try { + const String pubHostedUrlKey = 'PUB_HOSTED_URL'; + if (_platform.environment.containsKey(pubHostedUrlKey)) { + await http.head(_platform.environment[pubHostedUrlKey]); + } else { + await http.head('https://pub.dev'); + } + } on Exception { + offline = true; + } + + if (offline) { + // TODO(kenz): we should launch an already activated version of DevTools + // here, if available, once DevTools has offline support. DevTools does + // not work without internet currently due to the failed request of a + // couple scripts. See https://github.com/flutter/devtools/issues/2420. + return; + } else { + final bool didActivateDevTools = await _activateDevTools(); + final bool devToolsActive = await _checkForActiveDevTools(); + if (!didActivateDevTools && !devToolsActive) { + // At this point, we failed to activate the DevTools package and the + // package is not already active. + return; + } } - status.stop(); _devToolsProcess = await _processManager.start([ _pubExecutable, 'global', 'run', 'devtools', - if (!openInBrowser) '--no-launch-browser', + '--no-launch-browser', if (vmServiceUri != null) '--vm-uri=$vmServiceUri', ]); final Completer completer = Completer(); @@ -91,30 +102,83 @@ class DevtoolsServerLauncher extends DevtoolsLauncher { } completer.complete(Uri.parse(uri)); } - _logger.printStatus(line); }); _devToolsProcess.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(_logger.printError); - _devToolsUri = await completer.future; + devToolsUri = await completer.future + .timeout(const Duration(seconds: 10)); } on Exception catch (e, st) { - status.cancel(); _logger.printError('Failed to launch DevTools: $e', stackTrace: st); } } - @override - Future serve({bool openInBrowser = false}) async { - await launch(null, openInBrowser: openInBrowser); - if (_devToolsUri == null) { - return null; + Future _checkForActiveDevTools() async { + // We are offline, and cannot activate DevTools, so check if the DevTools + // package is already active. + final io.ProcessResult _pubGlobalListProcess = await _processManager.run([ + _pubExecutable, + 'global', + 'list', + ]); + + if (_pubGlobalListProcess.stdout.toString().contains('devtools ')) { + return true; } - return DevToolsServerAddress(_devToolsUri.host, _devToolsUri.port); + return false; + } + + /// Helper method to activate the DevTools pub package. + /// + /// Returns a bool indicating whether or not the package was successfully + /// activated from pub. + Future _activateDevTools() async { + final DateTime now = DateTime.now(); + // Only attempt to activate DevTools twice a day. + final bool shouldActivate = + _persistentToolState.lastDevToolsActivationTime == null || + now.difference(_persistentToolState.lastDevToolsActivationTime).inHours >= 12; + if (!shouldActivate) { + return false; + } + + final Status status = _logger.startProgress( + 'Activating Dart DevTools...', + ); + try { + final io.ProcessResult _devToolsActivateProcess = await _processManager + .run([ + _pubExecutable, + 'global', + 'activate', + 'devtools' + ]); + if (_devToolsActivateProcess.exitCode != 0) { + status.cancel(); + _logger.printError('Error running `pub global activate ' + 'devtools`:\n${_devToolsActivateProcess.stderr}'); + return false; + } + status.stop(); + _persistentToolState.lastDevToolsActivationTime = DateTime.now(); + return true; + } on Exception catch (e, _) { + status.stop(); + _logger.printError('Error running `pub global activate devtools`: $e'); + return false; + } + } + + @override + Future serve() async { + await launch(null); + return activeDevToolsServer; } @override Future close() async { + devToolsUri = null; if (_devToolsProcess != null) { _devToolsProcess.kill(); await _devToolsProcess.exitCode; diff --git a/packages/flutter_tools/lib/src/persistent_tool_state.dart b/packages/flutter_tools/lib/src/persistent_tool_state.dart index 2fc16994e7a..f0bfbf39481 100644 --- a/packages/flutter_tools/lib/src/persistent_tool_state.dart +++ b/packages/flutter_tools/lib/src/persistent_tool_state.dart @@ -52,6 +52,9 @@ abstract class PersistentToolState { /// Whether this client was already determined to be or not be a bot. bool isRunningOnBot; + + /// The last time the the DevTools package was activated from pub. + DateTime lastDevToolsActivationTime; } class _DefaultPersistentToolState implements PersistentToolState { @@ -85,6 +88,7 @@ class _DefaultPersistentToolState implements PersistentToolState { Channel.stable: 'last-active-stable-version' }; static const String _kBotKey = 'is-bot'; + static const String _kLastDevToolsActivationTimeKey = 'last-devtools-activation-time'; static const String _kLicenseHash = 'license-hash'; final Config _config; @@ -131,4 +135,14 @@ class _DefaultPersistentToolState implements PersistentToolState { @override set isRunningOnBot(bool value) => _config.setValue(_kBotKey, value); + + @override + DateTime get lastDevToolsActivationTime { + final String value = _config.getValue(_kLastDevToolsActivationTimeKey) as String; + return value != null ? DateTime.parse(value) : null; + } + + @override + set lastDevToolsActivationTime(DateTime time) => + _config.setValue(_kLastDevToolsActivationTimeKey, time.toString()); } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index c99b7d4c366..59243f68b20 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -774,7 +774,7 @@ abstract class ResidentRunner { final CommandHelp commandHelp; final bool machine; - DevtoolsLauncher _devtoolsLauncher; + DevtoolsLauncher _devToolsLauncher; bool _exited = false; Completer _finished = Completer(); @@ -926,7 +926,7 @@ abstract class ResidentRunner { } @protected - void writeVmserviceFile() { + void writeVmServiceFile() { if (debuggingOptions.vmserviceOutFile != null) { try { final String address = flutterDevices.first.vmService.wsAddress.toString(); @@ -941,7 +941,7 @@ abstract class ResidentRunner { Future exit() async { _exited = true; - await shutdownDevtools(); + await shutdownDevTools(); await stopEchoingDeviceLog(); await preExit(); await exitApp(); @@ -949,7 +949,7 @@ abstract class ResidentRunner { } Future detach() async { - await shutdownDevtools(); + await shutdownDevTools(); await stopEchoingDeviceLog(); await preExit(); await shutdownDartDevelopmentService(); @@ -1250,22 +1250,29 @@ abstract class ResidentRunner { } } - Future launchDevTools({bool openInBrowser = false}) async { - if (!supportsServiceProtocol) { - return false; - } - assert(supportsServiceProtocol); - _devtoolsLauncher ??= DevtoolsLauncher.instance; - unawaited(_devtoolsLauncher.launch( - flutterDevices.first.vmService.httpAddress, - openInBrowser: openInBrowser, - )); - return true; + DevToolsServerAddress activeDevToolsServer() { + _devToolsLauncher ??= DevtoolsLauncher.instance; + return _devToolsLauncher.activeDevToolsServer; } - Future shutdownDevtools() async { - await _devtoolsLauncher?.close(); - _devtoolsLauncher = null; + Future serveDevToolsGracefully({ + Uri devToolsServerAddress + }) async { + if (!supportsServiceProtocol) { + return; + } + + _devToolsLauncher ??= DevtoolsLauncher.instance; + if (devToolsServerAddress != null) { + _devToolsLauncher.devToolsUri = devToolsServerAddress; + } else { + await _devToolsLauncher.serve(); + } + } + + Future shutdownDevTools() async { + await _devToolsLauncher?.close(); + _devToolsLauncher = null; } Future _serviceProtocolDone(dynamic object) async { @@ -1551,8 +1558,6 @@ class TerminalHandler { return residentRunner.debugDumpRenderTree(); case 'U': return residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); - case 'v': - return residentRunner.launchDevTools(openInBrowser: true); case 'w': case 'W': return residentRunner.debugDumpApp(); @@ -1645,16 +1650,25 @@ String nextPlatform(String currentPlatform, FeatureFlags featureFlags) { /// A launcher for the devtools debugger and analysis tool. abstract class DevtoolsLauncher { + Uri devToolsUri; + /// Launch a Dart DevTools process, optionally targeting a specific VM Service /// URI if [vmServiceUri] is non-null. - Future launch(Uri vmServiceUri, {bool openInBrowser = false}); + Future launch(Uri vmServiceUri); /// Serve Dart DevTools and return the host and port they are available on. - Future serve({bool openInBrowser = false}); + Future serve(); Future close(); static DevtoolsLauncher get instance => context.get(); + + DevToolsServerAddress get activeDevToolsServer { + if (devToolsUri == null) { + return null; + } + return DevToolsServerAddress(devToolsUri.host, devToolsUri.port); + } } class DevToolsServerAddress { @@ -1662,4 +1676,11 @@ class DevToolsServerAddress { final String host; final int port; + + Uri get uri { + if (host == null || port == null) { + return null; + } + return Uri(scheme: 'http', host: host, port: port); + } } diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index 6ce44a13c56..8410f8ede2b 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart @@ -72,7 +72,12 @@ class ColdRunner extends ResidentRunner { // Connect to observatory. if (debuggingOptions.debuggingEnabled) { try { - await connectToServiceProtocol(); + await Future.wait(>[ + connectToServiceProtocol(), + serveDevToolsGracefully( + devToolsServerAddress: debuggingOptions.devToolsServerAddress, + ), + ]); } on String catch (message) { globals.printError(message); appFailedToStart(); @@ -115,7 +120,7 @@ class ColdRunner extends ResidentRunner { appStartedCompleter?.complete(); - writeVmserviceFile(); + writeVmServiceFile(); if (stayResident && !traceStartup) { return waitForAppToFinish(); @@ -132,10 +137,15 @@ class ColdRunner extends ResidentRunner { }) async { _didAttach = true; try { - await connectToServiceProtocol( - getSkSLMethod: writeSkSL, - allowExistingDdsInstance: allowExistingDdsInstance, - ); + await Future.wait(>[ + connectToServiceProtocol( + getSkSLMethod: writeSkSL, + allowExistingDdsInstance: allowExistingDdsInstance, + ), + serveDevToolsGracefully( + devToolsServerAddress: debuggingOptions.devToolsServerAddress, + ), + ]); } on Exception catch (error) { globals.printError('Error connecting to the service protocol: $error'); return 2; @@ -195,6 +205,19 @@ class ColdRunner extends ResidentRunner { 'An Observatory debugger and profiler on $dname is available at: ' '${device.vmService.httpAddress}', ); + + final DevToolsServerAddress devToolsServerAddress = activeDevToolsServer(); + if (devToolsServerAddress != null) { + final Uri uri = devToolsServerAddress.uri?.replace( + queryParameters: {'uri': '${device.vmService.httpAddress}'}, + ); + if (uri != null) { + globals.printStatus( + '\nFlutter DevTools, a Flutter debugger and profiler, on ' + '${device.device.name} is available at: $uri', + ); + } + } } } } diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 443a7c2c31a..c8ec0de38fe 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -174,13 +174,18 @@ class HotRunner extends ResidentRunner { }) async { _didAttach = true; try { - await connectToServiceProtocol( - reloadSources: _reloadSourcesService, - restart: _restartService, - compileExpression: _compileExpressionService, - getSkSLMethod: writeSkSL, - allowExistingDdsInstance: allowExistingDdsInstance, - ); + await Future.wait(>[ + connectToServiceProtocol( + reloadSources: _reloadSourcesService, + restart: _restartService, + compileExpression: _compileExpressionService, + getSkSLMethod: writeSkSL, + allowExistingDdsInstance: allowExistingDdsInstance, + ), + serveDevToolsGracefully( + devToolsServerAddress: debuggingOptions.devToolsServerAddress, + ), + ]); // Catches all exceptions, non-Exception objects are rethrown. } catch (error) { // ignore: avoid_catches_without_on_clauses if (error is! Exception && error is! String) { @@ -280,7 +285,7 @@ class HotRunner extends ResidentRunner { benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData)); return 0; } - writeVmserviceFile(); + writeVmServiceFile(); int result = 0; if (stayResident) { @@ -1087,6 +1092,19 @@ class HotRunner extends ResidentRunner { 'An Observatory debugger and profiler on ${device.device.name} is available at: ' '${device.vmService.httpAddress}', ); + + final DevToolsServerAddress devToolsServerAddress = activeDevToolsServer(); + if (devToolsServerAddress != null) { + final Uri uri = devToolsServerAddress.uri?.replace( + queryParameters: {'uri': '${device.vmService.httpAddress}'}, + ); + if (uri != null) { + globals.printStatus( + '\nFlutter DevTools, a Flutter debugger and profiler, on ' + '${device.device.name} is available at: $uri', + ); + } + } } globals.printStatus(''); if (debuggingOptions.buildInfo.nullSafetyMode == NullSafetyMode.sound) { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index bf4247ae405..b9adae8710c 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -128,6 +128,9 @@ abstract class FlutterCommand extends Command { /// The option name for a custom observatory port. static const String observatoryPortOption = 'observatory-port'; + /// The option name for a custom DevTools server address. + static const String kDevToolsServerAddress = 'devtools-server-address'; + /// The flag name for whether or not to use ipv6. static const String ipv6Flag = 'ipv6'; @@ -322,6 +325,13 @@ abstract class FlutterCommand extends Command { _usesPortOption = true; } + void addDevToolsOptions() { + argParser.addOption(kDevToolsServerAddress, + help: 'When this value is provided, the Flutter tool will not spin up a ' + 'new DevTools server instance, but instead will use the one provided ' + 'at this address.'); + } + void addDdsOptions({@required bool verboseHelp}) { argParser.addOption('dds-port', help: 'When this value is provided, the Dart Development Service (DDS) will be ' @@ -365,6 +375,16 @@ abstract class FlutterCommand extends Command { return 0; } + Uri get devToolsServerAddress { + if (argResults.wasParsed(kDevToolsServerAddress)) { + final Uri uri = Uri.tryParse(stringArg(kDevToolsServerAddress)); + if (uri != null && uri.host.isNotEmpty && uri.port != 0) { + return uri; + } + } + return null; + } + /// Gets the vmservice port provided to in the 'observatory-port' or /// 'host-vmservice-port option. /// diff --git a/packages/flutter_tools/test/general.shard/devtools_launcher_test.dart b/packages/flutter_tools/test/general.shard/devtools_launcher_test.dart index c7cf5704976..43ccb8a48c8 100644 --- a/packages/flutter_tools/test/general.shard/devtools_launcher_test.dart +++ b/packages/flutter_tools/test/general.shard/devtools_launcher_test.dart @@ -4,20 +4,41 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/devtools_launcher.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/persistent_tool_state.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import '../src/common.dart'; import '../src/context.dart'; void main() { + BufferLogger logger; + FakePlatform platform; + PersistentToolState persistentToolState; + + setUp(() { + logger = BufferLogger.test(); + platform = FakePlatform(environment: {}); + + final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('devtools_launcher_test'); + persistentToolState = PersistentToolState.test( + directory: tempDir, + logger: logger, + ); + }); + testWithoutContext('DevtoolsLauncher launches DevTools through pub and saves the URI', () async { final Completer completer = Completer(); final DevtoolsLauncher launcher = DevtoolsServerLauncher( pubExecutable: 'pub', - logger: BufferLogger.test(), + logger: logger, + platform: platform, + persistentToolState: persistentToolState, processManager: FakeProcessManager.list([ const FakeCommand( command: [ @@ -28,6 +49,14 @@ void main() { ], stdout: 'Activated DevTools 0.9.5', ), + const FakeCommand( + command: [ + 'pub', + 'global', + 'list', + ], + stdout: 'devtools 0.9.6', + ), FakeCommand( command: const [ 'pub', @@ -51,7 +80,9 @@ void main() { final Completer completer = Completer(); final DevtoolsLauncher launcher = DevtoolsServerLauncher( pubExecutable: 'pub', - logger: BufferLogger.test(), + logger: logger, + platform: platform, + persistentToolState: persistentToolState, processManager: FakeProcessManager.list([ const FakeCommand( command: [ @@ -62,12 +93,21 @@ void main() { ], stdout: 'Activated DevTools 0.9.5', ), + const FakeCommand( + command: [ + 'pub', + 'global', + 'list', + ], + stdout: 'devtools 0.9.6', + ), FakeCommand( command: const [ 'pub', 'global', 'run', 'devtools', + '--no-launch-browser', ], stdout: 'Serving DevTools at http://127.0.0.1:9100\n', completer: completer, @@ -75,16 +115,49 @@ void main() { ]), ); - final DevToolsServerAddress address = await launcher.serve(openInBrowser: true); + final DevToolsServerAddress address = await launcher.serve(); expect(address.host, '127.0.0.1'); expect(address.port, 9100); }); - testWithoutContext('DevtoolsLauncher prints error if exception is thrown during activate', () async { - final BufferLogger logger = BufferLogger.test(); + testWithoutContext('DevtoolsLauncher does not activate DevTools if it was recently activated', () async { + persistentToolState.lastDevToolsActivationTime = DateTime.now(); final DevtoolsLauncher launcher = DevtoolsServerLauncher( pubExecutable: 'pub', logger: logger, + platform: platform, + persistentToolState: persistentToolState, + processManager: FakeProcessManager.list([ + const FakeCommand( + command: [ + 'pub', + 'global', + 'list', + ], + stdout: 'devtools 0.9.6', + ), + const FakeCommand( + command: [ + 'pub', + 'global', + 'run', + 'devtools', + '--no-launch-browser', + ], + stdout: 'Serving DevTools at http://127.0.0.1:9100\n', + ), + ]), + ); + + await launcher.serve(); + }); + + testWithoutContext('DevtoolsLauncher prints error if exception is thrown during activate', () async { + final DevtoolsLauncher launcher = DevtoolsServerLauncher( + pubExecutable: 'pub', + logger: logger, + platform: platform, + persistentToolState: persistentToolState, processManager: FakeProcessManager.list([ const FakeCommand( command: [ @@ -96,6 +169,14 @@ void main() { stderr: 'Error - could not activate devtools', exitCode: 1, ), + const FakeCommand( + command: [ + 'pub', + 'global', + 'list', + ], + stdout: 'devtools 0.9.6', + ), FakeCommand( command: const [ 'pub', @@ -118,10 +199,11 @@ void main() { }); testWithoutContext('DevtoolsLauncher prints error if exception is thrown during launch', () async { - final BufferLogger logger = BufferLogger.test(); final DevtoolsLauncher launcher = DevtoolsServerLauncher( pubExecutable: 'pub', logger: logger, + platform: platform, + persistentToolState: persistentToolState, processManager: FakeProcessManager.list([ const FakeCommand( command: [ @@ -132,6 +214,14 @@ void main() { ], stdout: 'Activated DevTools 0.9.5', ), + const FakeCommand( + command: [ + 'pub', + 'global', + 'list', + ], + stdout: 'devtools 0.9.6', + ), FakeCommand( command: const [ 'pub', diff --git a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart index 2823c515a75..94b32d36b0a 100644 --- a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart +++ b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart @@ -57,4 +57,23 @@ void main() { expect(state2.lastActiveVersion(Channel.beta), 'ghi'); expect(state2.lastActiveVersion(Channel.stable), 'jkl'); }); + + testWithoutContext('lastDevToolsActivationTime can be cached and stored', () { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final Directory directory = fileSystem.directory('state_dir')..createSync(); + final PersistentToolState state1 = PersistentToolState.test( + directory: directory, + logger: BufferLogger.test(), + ); + + final DateTime time = DateTime.now(); + state1.lastDevToolsActivationTime = time; + + final PersistentToolState state2 = PersistentToolState.test( + directory: directory, + logger: BufferLogger.test(), + ); + + expect(state2.lastDevToolsActivationTime, equals(time)); + }); } 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 bc1ac3574b6..91d4c585b65 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -229,7 +229,9 @@ void main() { expect((await connectionInfo).baseUri, 'foo://bar'); expect(onAppStart.isCompleted, true); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner suppresses errors for the initial compilation', () => testbed.run(() async { globals.fs.file(globals.fs.path.join('lib', 'main.dart')) @@ -274,7 +276,9 @@ void main() { suppressErrors: true, )).called(1); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); // Regression test for https://github.com/flutter/flutter/issues/60613 testUsingContext('ResidentRunner calls appFailedToStart if initial compilation fails', () => testbed.run(() async { @@ -406,7 +410,9 @@ void main() { suppressErrors: false, )).called(1); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -486,7 +492,9 @@ void main() { expect((await connectionInfo).baseUri, 'foo://bar'); expect(onAppStart.isCompleted, true); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -539,7 +547,9 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { Usage: () => MockUsage(), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner fails its operation if the device initialization is not complete', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -624,7 +634,9 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { Usage: () => MockUsage(), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -688,6 +700,7 @@ void main() { })).called(1); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, Usage: () => MockUsage(), })); @@ -756,7 +769,9 @@ void main() { expect(result.code, 0); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner reports error with missing entrypoint file', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -840,7 +855,9 @@ void main() { expect(testLogger.errorText, contains('The entrypoint file (i.e. the file with main())')); expect(result.fatal, false); expect(result.code, 0); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner resets compilation time on reload reject', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -928,7 +945,9 @@ void main() { expect(result.message, contains('Reload rejected: Failed to hot reload')); // contains error message from reload report. expect(result.code, 1); verify(mockDevFS.resetLastCompiled()).called(1); // compilation time is reset. - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1000,7 +1019,9 @@ void main() { ); }, overrides: { Usage: () => MockUsage(), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1114,7 +1135,9 @@ void main() { ProjectFileInvalidator: () => FakeProjectFileInvalidator(), Usage: () => MockUsage(), FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1184,7 +1207,9 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { Usage: () => MockUsage(), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner can remove breakpoints from paused isolate during hot restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1260,7 +1285,9 @@ void main() { expect(result.isOk, true); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner will alternative the name of the dill file uploaded for a hot restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1392,7 +1419,9 @@ void main() { await residentRunner.restart(fullRestart: true); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -1445,7 +1474,9 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { Usage: () => MockUsage(), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ResidentRunner uses temp directory when there is no output dill path', () => testbed.run(() { fakeVmServiceHost = FakeVmServiceHost(requests: []); @@ -1650,13 +1681,88 @@ void main() { ) })); - testUsingContext('ResidentRunner invokes DevtoolsLauncher when launching and shutting down Devtools', () => testbed.run(() async { - when(mockFlutterDevice.vmService).thenReturn(fakeVmServiceHost.vmService); - setHttpAddress(testUri, fakeVmServiceHost.vmService); - await residentRunner.launchDevTools(); - verify(mockDevtoolsLauncher.launch(testUri)).called(1); + testUsingContext('ResidentRunner invokes DevToolsLauncher when attaching and shutting down DevTools', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + listViews, + setAssetBundlePath, + ]); + final Future result = residentRunner.attach(); + expect(await result, 0); - await residentRunner.shutdownDevtools(); + // Verify DevTools was served. + verify(mockDevtoolsLauncher.serve()).called(1); + + // Shutdown + await residentRunner.shutdownDevTools(); + verify(mockDevtoolsLauncher.close()).called(1); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); + + testUsingContext('ResidentRunner invokes DevtoolsLauncher when attaching and shutting down - cold mode', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + listViews, + setAssetBundlePath, + ]); + residentRunner = ColdRunner( + [ + mockFlutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, vmserviceOutFile: 'foo'), + target: 'main.dart', + ); + when(mockFlutterDevice.runCold( + coldRunner: anyNamed('coldRunner'), + route: anyNamed('route'), + )).thenAnswer((Invocation invocation) async { + return 0; + }); + + final Future result = residentRunner.attach(); + expect(await result, 0); + + // Verify DevTools was served. + verify(mockDevtoolsLauncher.serve()).called(1); + + // Shutdown + await residentRunner.shutdownDevTools(); + verify(mockDevtoolsLauncher.close()).called(1); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); + + testUsingContext('ResidentRunner invokes DevtoolsLauncher when running and shutting down - cold mode', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + listViews, + setAssetBundlePath, + ]); + residentRunner = ColdRunner( + [ + mockFlutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, vmserviceOutFile: 'foo'), + target: 'main.dart', + ); + when(mockFlutterDevice.runCold( + coldRunner: anyNamed('coldRunner'), + route: anyNamed('route'), + )).thenAnswer((Invocation invocation) async { + return 0; + }); + + final Future result = residentRunner.run(); + expect(await result, 0); + + // Verify DevTools was served. + verify(mockDevtoolsLauncher.serve()).called(1); + + // Shutdown + await residentRunner.shutdownDevTools(); verify(mockDevtoolsLauncher.close()).called(1); }), overrides: { DevtoolsLauncher: () => mockDevtoolsLauncher, @@ -2299,7 +2405,9 @@ void main() { await residentRunner.run(); expect(await globals.fs.file('foo').readAsString(), testUri.toString()); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner copies compiled app.dill to cache during startup', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2327,7 +2435,9 @@ void main() { await residentRunner.run(); expect(await globals.fs.file(globals.fs.path.join('build', 'cache.dill')).readAsString(), 'ABC'); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner copies compiled app.dill to cache during startup with dart defines', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2363,7 +2473,9 @@ void main() { expect(await globals.fs.file(globals.fs.path.join( 'build', '187ef4436122d1cc2f40dc2b92f0eba0.cache.dill')).readAsString(), 'ABC'); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner copies compiled app.dill to cache during startup with null safety', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2399,7 +2511,9 @@ void main() { expect(await globals.fs.file(globals.fs.path.join( 'build', '3416d3007730479552122f01c01e326d.cache.dill')).readAsString(), 'ABC'); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner does not copy app.dill if a dillOutputPath is given', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2428,7 +2542,9 @@ void main() { await residentRunner.run(); expect(globals.fs.file(globals.fs.path.join('build', 'cache.dill')), isNot(exists)); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner copies compiled app.dill to cache during startup with --track-widget-creation', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2461,7 +2577,9 @@ void main() { await residentRunner.run(); expect(await globals.fs.file(globals.fs.path.join('build', 'cache.dill.track.dill')).readAsString(), 'ABC'); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner unforwards device ports', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2494,7 +2612,9 @@ void main() { await residentRunner.run(); verify(mockPortForwarder.dispose()).called(1); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('HotRunner handles failure to write vmservice file', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ @@ -2523,7 +2643,9 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => ThrowingForwardingFileSystem(MemoryFileSystem.test()), - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('ColdRunner writes vm service file when providing debugging option', () => testbed.run(() async { @@ -2550,7 +2672,9 @@ void main() { expect(await globals.fs.file('foo').readAsString(), testUri.toString()); expect(fakeVmServiceHost.hasRemainingExpectations, false); - })); + }), overrides: { + DevtoolsLauncher: () => mockDevtoolsLauncher, + }); testUsingContext('FlutterDevice uses dartdevc configuration when targeting web', () async { fakeVmServiceHost = FakeVmServiceHost(requests: []); diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart index e5f46f70e43..9033b580f6d 100644 --- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart @@ -290,6 +290,48 @@ void main() { expect(FlutterCommandResult.warning().exitStatus, ExitStatus.warning); }); + testUsingContext('devToolsServerAddress returns parsed uri', () async { + final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(); + await createTestCommandRunner(command).run([ + 'dummy', + '--${FlutterCommand.kDevToolsServerAddress}', + 'http://127.0.0.1:9105', + ]); + expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105')); + }); + + testUsingContext('devToolsServerAddress returns null for bad input', () async { + final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run([ + 'dummy', + '--${FlutterCommand.kDevToolsServerAddress}', + 'hello-world', + ]); + expect(command.devToolsServerAddress, isNull); + + await runner.run([ + 'dummy', + '--${FlutterCommand.kDevToolsServerAddress}', + '', + ]); + expect(command.devToolsServerAddress, isNull); + + await runner.run([ + 'dummy', + '--${FlutterCommand.kDevToolsServerAddress}', + '9101', + ]); + expect(command.devToolsServerAddress, isNull); + + await runner.run([ + 'dummy', + '--${FlutterCommand.kDevToolsServerAddress}', + '127.0.0.1:9101', + ]); + expect(command.devToolsServerAddress, isNull); + }); + group('signals tests', () { MockIoProcessSignal mockSignal; ProcessSignal signalUnderTest; diff --git a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart index 7ec82d1011a..5c0eea2ae90 100644 --- a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart +++ b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart @@ -285,13 +285,6 @@ void main() { verify(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1); }); - testWithoutContext('v - launchDevTools', () async { - when(mockResidentRunner.supportsServiceProtocol).thenReturn(true); - await terminalHandler.processTerminalInput('v'); - - verify(mockResidentRunner.launchDevTools(openInBrowser: true)).called(1); - }); - testWithoutContext('w,W - debugDumpApp with service protocol', () async { await terminalHandler.processTerminalInput('w'); await terminalHandler.processTerminalInput('W');