diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 7272767cb4..0cde25ad10 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -102,6 +102,7 @@ class AttachCommand extends FlutterCommand { ); usesTrackWidgetCreation(verboseHelp: verboseHelp); addDdsOptions(verboseHelp: verboseHelp); + usesDeviceTimeoutOption(); hotRunnerFactory ??= HotRunnerFactory(); } diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart index 53c9eea449..b5135390cf 100644 --- a/packages/flutter_tools/lib/src/commands/devices.dart +++ b/packages/flutter_tools/lib/src/commands/devices.dart @@ -22,8 +22,9 @@ class DevicesCommand extends FlutterCommand { 'timeout', abbr: 't', defaultsTo: null, - help: 'Time in seconds to wait for devices to attach. Longer timeouts may be necessary for networked devices.' + help: '(deprecated) Use --device-timeout instead', ); + usesDeviceTimeoutOption(); } @override @@ -32,20 +33,25 @@ class DevicesCommand extends FlutterCommand { @override final String description = 'List all connected devices.'; - Duration get timeout { - if (argResults['timeout'] == null) { - return null; - } - if (_timeout == null) { + @override + Duration get deviceDiscoveryTimeout { + if (argResults['timeout'] != null) { final int timeoutSeconds = int.tryParse(stringArg('timeout')); if (timeoutSeconds == null) { throwToolExit( 'Could not parse -t/--timeout argument. It must be an integer.'); } - _timeout = Duration(seconds: timeoutSeconds); + return Duration(seconds: timeoutSeconds); } - return _timeout; + return super.deviceDiscoveryTimeout; + } + + @override + Future validateCommand() { + if (argResults['timeout'] != null) { + globals.printError('--timeout has been deprecated, use --${FlutterOptions.kDeviceTimeout} instead'); + } + return super.validateCommand(); } - Duration _timeout; @override Future runCommand() async { @@ -56,7 +62,7 @@ class DevicesCommand extends FlutterCommand { exitCode: 1); } - final List devices = await globals.deviceManager.refreshAllConnectedDevices(timeout: timeout); + final List devices = await globals.deviceManager.refreshAllConnectedDevices(timeout: deviceDiscoveryTimeout); if (boolArg('machine')) { await printDevicesAsJson(devices); @@ -68,8 +74,8 @@ class DevicesCommand extends FlutterCommand { status.writeln('Run "flutter emulators" to list and start any available device emulators.'); status.writeln(); status.write('If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. '); - if (timeout == null) { - status.write('You may also try increasing the time to wait for connected devices with the --timeout flag. '); + if (deviceDiscoveryTimeout == null) { + status.write('You may also try increasing the time to wait for connected devices with the --${FlutterOptions.kDeviceTimeout} flag. '); } status.write('Visit https://flutter.dev/setup/ for troubleshooting tips.'); diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 0980e6899c..1f9772e5e8 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -146,7 +146,7 @@ class DriveCommand extends RunCommandBase { @override Future validateCommand() async { if (userIdentifier != null) { - final Device device = await findTargetDevice(); + final Device device = await findTargetDevice(timeout: deviceDiscoveryTimeout); if (device is! AndroidDevice) { throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android'); } @@ -161,7 +161,7 @@ class DriveCommand extends RunCommandBase { throwToolExit(null); } - _device = await findTargetDevice(); + _device = await findTargetDevice(timeout: deviceDiscoveryTimeout); if (device == null) { throwToolExit(null); } @@ -388,9 +388,9 @@ $ex } } -Future findTargetDevice() async { +Future findTargetDevice({ @required Duration timeout }) async { final DeviceManager deviceManager = globals.deviceManager; - final List devices = await deviceManager.findTargetDevices(FlutterProject.current()); + final List devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: timeout); if (deviceManager.hasSpecifiedDeviceId) { if (devices.isEmpty) { diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart index 8228cbf6c4..62fd7cd589 100644 --- a/packages/flutter_tools/lib/src/commands/install.dart +++ b/packages/flutter_tools/lib/src/commands/install.dart @@ -16,6 +16,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts InstallCommand() { requiresPubspecYaml(); usesDeviceUserOption(); + usesDeviceTimeoutOption(); argParser.addFlag('uninstall-only', negatable: true, defaultsTo: false, diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart index 5b9b9cbb57..5305e14c4d 100644 --- a/packages/flutter_tools/lib/src/commands/logs.dart +++ b/packages/flutter_tools/lib/src/commands/logs.dart @@ -18,6 +18,7 @@ class LogsCommand extends FlutterCommand { abbr: 'c', help: 'Clear log history before reading from logs.', ); + usesDeviceTimeoutOption(); } @override diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index fd5b739068..b70a1a73d9 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -75,6 +75,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment usesTrackWidgetCreation(verboseHelp: verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); usesDeviceUserOption(); + usesDeviceTimeoutOption(); } bool get traceStartup => boolArg('trace-startup'); diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart index 3c32de782b..566affdc8e 100644 --- a/packages/flutter_tools/lib/src/commands/screenshot.dart +++ b/packages/flutter_tools/lib/src/commands/screenshot.dart @@ -51,6 +51,7 @@ class ScreenshotCommand extends FlutterCommand { }, defaultsTo: _kDeviceType, ); + usesDeviceTimeoutOption(); } @override diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 2a16f8ee28..84d0a052fd 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -209,7 +209,12 @@ abstract class DeviceManager { /// * If the user did not specify a device id and there is more than one /// device connected, then filter out unsupported devices and prioritize /// ephemeral devices. - Future> findTargetDevices(FlutterProject flutterProject) async { + Future> findTargetDevices(FlutterProject flutterProject, { Duration timeout }) async { + if (timeout != null) { + // Reset the cache with the specified timeout. + await refreshAllConnectedDevices(timeout: timeout); + } + List devices = await getDevices(); // Always remove web and fuchsia devices from `--all`. This setting diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index e42548b61d..8fa2413b15 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -113,6 +113,7 @@ class FlutterOptions { static const String kPerformanceMeasurementFile = 'performance-measurement-file'; static const String kNullSafety = 'sound-null-safety'; static const String kDeviceUser = 'device-user'; + static const String kDeviceTimeout = 'device-timeout'; static const String kAnalyzeSize = 'analyze-size'; static const String kNullAssertions = 'null-assertions'; } @@ -404,6 +405,28 @@ abstract class FlutterCommand extends Command { valueHelp: '10'); } + void usesDeviceTimeoutOption() { + argParser.addOption( + FlutterOptions.kDeviceTimeout, + help: 'Time in seconds to wait for devices to attach. Longer timeouts may be necessary for networked devices.', + valueHelp: '10' + ); + } + + Duration get deviceDiscoveryTimeout { + if (_deviceDiscoveryTimeout == null + && argResults.options.contains(FlutterOptions.kDeviceTimeout) + && argResults.wasParsed(FlutterOptions.kDeviceTimeout)) { + final int timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout)); + if (timeoutSeconds == null) { + throwToolExit( 'Could not parse --${FlutterOptions.kDeviceTimeout} argument. It must be an integer.'); + } + _deviceDiscoveryTimeout = Duration(seconds: timeoutSeconds); + } + return _deviceDiscoveryTimeout; + } + Duration _deviceDiscoveryTimeout; + void addBuildModeFlags({ bool defaultToRelease = true, bool verboseHelp = false, bool excludeDebug = false }) { // A release build must be the default if a debug build is not possible. assert(defaultToRelease || !excludeDebug); @@ -970,7 +993,7 @@ abstract class FlutterCommand extends Command { return null; } final DeviceManager deviceManager = globals.deviceManager; - List devices = await deviceManager.findTargetDevices(FlutterProject.current()); + List devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: deviceDiscoveryTimeout); if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) { globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId)); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index c2a94e13f6..c820e5af8a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -342,6 +342,8 @@ void main() { '-v', '--device-user', '10', + '--device-timeout', + '15', ]); final VerificationResult verificationResult = verify( mockHotRunnerFactory.build( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index 8a36a9efc2..7f333086e2 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -281,7 +281,7 @@ void main() { when(mockDevice.name).thenReturn('specified-device'); when(mockDevice.id).thenReturn('123'); - final Device device = await findTargetDevice(); + final Device device = await findTargetDevice(timeout: null); expect(device.name, 'specified-device'); }, overrides: { FileSystem: () => fs, @@ -293,7 +293,7 @@ void main() { Platform platform() => FakePlatform(operatingSystem: operatingSystem); testUsingContext('returns null if no devices found', () async { - expect(await findTargetDevice(), isNull); + expect(await findTargetDevice(timeout: null), isNull); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -305,7 +305,7 @@ void main() { when(mockDevice.name).thenReturn('mock-android-device'); testDeviceManager.addDevice(mockDevice); - final Device device = await findTargetDevice(); + final Device device = await findTargetDevice(timeout: null); expect(device.name, 'mock-android-device'); }, overrides: { FileSystem: () => fs, @@ -337,7 +337,7 @@ void main() { testDeviceManager.addDevice(mockDevice); testDeviceManager.addDevice(mockUnsupportedDevice); - final Device device = await findTargetDevice(); + final Device device = await findTargetDevice(timeout: null); expect(device.name, 'mock-android-device'); }, overrides: { FileSystem: () => fs, @@ -366,7 +366,7 @@ void main() { when(mockDevice.isLocalEmulator) .thenAnswer((Invocation invocation) => Future.value(true)); - final Device device = await findTargetDevice(); + final Device device = await findTargetDevice(timeout: null); expect(device.name, 'mock-simulator'); }, overrides: { FileSystem: () => fs, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 52f3324b82..edd606083d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -178,7 +178,7 @@ void main() { when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future>.value(noDevices) ); - when(mockDeviceManager.findTargetDevices(any)).thenAnswer( + when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value(noDevices) ); @@ -214,7 +214,7 @@ void main() { when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) async { return [device]; }); - when(mockDeviceManager.findTargetDevices(any)).thenAnswer((Invocation invocation) async { + when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer((Invocation invocation) async { return [device]; }); when(mockDeviceManager.hasSpecifiedAllDevices).thenReturn(false); @@ -254,7 +254,7 @@ void main() { ]); }); - when(mockDeviceManager.findTargetDevices(any)).thenAnswer( + when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([]), ); @@ -301,7 +301,8 @@ void main() { ); // No devices are attached, we just want to verify update the cache // BEFORE checking for devices - when(mockDeviceManager.findTargetDevices(any)).thenAnswer( + const Duration timeout = Duration(seconds: 10); + when(mockDeviceManager.findTargetDevices(any, timeout: timeout)).thenAnswer( (Invocation invocation) => Future>.value([]) ); @@ -309,6 +310,8 @@ void main() { await createTestCommandRunner(command).run([ 'run', '--no-pub', + '--device-timeout', + '10', ]); fail('Exception expected'); } on ToolExit catch (e) { @@ -324,7 +327,7 @@ void main() { // as part of gathering `requiredArtifacts` mockDeviceManager.getDevices(), // in validateCommand() - mockDeviceManager.findTargetDevices(any), + mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout')), ]); }, overrides: { ApplicationPackageFactory: () => mockApplicationPackageFactory, @@ -365,7 +368,7 @@ void main() { (Invocation invocation) => Future>.value([mockDevice]) ); - when(mockDeviceManager.findTargetDevices(any)).thenAnswer( + when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([mockDevice]) ); @@ -434,7 +437,7 @@ void main() { when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([fakeDevice]); }); - when(mockDeviceManager.findTargetDevices(any)).thenAnswer( + when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([fakeDevice]) ); }); diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index c39d42878d..f2e62eca77 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -58,8 +58,10 @@ void main() { // 3. A device discoverer that succeeds. final DeviceManager deviceManager = TestDeviceManager( devices, - testLongPollingDeviceDiscovery: true, - testThrowingDeviceDiscovery: true, + deviceDiscoveryOverrides: [ + ThrowingPollingDeviceDiscovery(), + LongPollingDeviceDiscovery(), + ], ); Future expectDevice(String id, List expected) async { @@ -89,7 +91,9 @@ void main() { // 2. A device discoverer that succeeds. final DeviceManager deviceManager = TestDeviceManager( devices, - testThrowingDeviceDiscovery: true + deviceDiscoveryOverrides: [ + ThrowingPollingDeviceDiscovery(), + ], ); Future expectDevice(String id, List expected) async { @@ -387,7 +391,62 @@ void main() { Artifacts: () => Artifacts.test(), Cache: () => cache, }); + + testUsingContext('does not refresh device cache without a timeout', () async { + final List devices = [ + ephemeralOne, + ]; + final MockDeviceDiscovery mockDeviceDiscovery = MockDeviceDiscovery(); + when(mockDeviceDiscovery.supportsPlatform).thenReturn(true); + // when(mockDeviceDiscovery.discoverDevices(timeout: timeout)).thenAnswer((_) async => devices); + when(mockDeviceDiscovery.devices).thenAnswer((_) async => devices); + // when(mockDeviceDiscovery.discoverDevices(timeout: timeout)).thenAnswer((_) async => devices); + + final DeviceManager deviceManager = TestDeviceManager([], deviceDiscoveryOverrides: [ + mockDeviceDiscovery + ]); + deviceManager.specifiedDeviceId = ephemeralOne.id; + final List filtered = await deviceManager.findTargetDevices( + FlutterProject.current(), + ); + + expect(filtered.single, ephemeralOne); + verify(mockDeviceDiscovery.devices).called(1); + verifyNever(mockDeviceDiscovery.discoverDevices(timeout: anyNamed('timeout'))); + }, overrides: { + Artifacts: () => Artifacts.test(), + Cache: () => cache, + }); + + testUsingContext('refreshes device cache with a timeout', () async { + final List devices = [ + ephemeralOne, + ]; + const Duration timeout = Duration(seconds: 2); + final MockDeviceDiscovery mockDeviceDiscovery = MockDeviceDiscovery(); + when(mockDeviceDiscovery.supportsPlatform).thenReturn(true); + when(mockDeviceDiscovery.discoverDevices(timeout: timeout)).thenAnswer((_) async => devices); + when(mockDeviceDiscovery.devices).thenAnswer((_) async => devices); + // when(mockDeviceDiscovery.discoverDevices(timeout: timeout)).thenAnswer((_) async => devices); + + final DeviceManager deviceManager = TestDeviceManager([], deviceDiscoveryOverrides: [ + mockDeviceDiscovery + ]); + deviceManager.specifiedDeviceId = ephemeralOne.id; + final List filtered = await deviceManager.findTargetDevices( + FlutterProject.current(), + timeout: timeout, + ); + + expect(filtered.single, ephemeralOne); + verify(mockDeviceDiscovery.devices).called(1); + verify(mockDeviceDiscovery.discoverDevices(timeout: anyNamed('timeout'))).called(1); + }, overrides: { + Artifacts: () => Artifacts.test(), + Cache: () => cache, + }); }); + group('ForwardedPort', () { group('dispose()', () { testUsingContext('does not throw exception if no process is present', () { @@ -427,16 +486,13 @@ void main() { class TestDeviceManager extends DeviceManager { TestDeviceManager(List allDevices, { - bool testLongPollingDeviceDiscovery = false, - bool testThrowingDeviceDiscovery = false, + List deviceDiscoveryOverrides, }) { _fakeDeviceDiscoverer = FakePollingDeviceDiscovery(); _deviceDiscoverers = [ - if (testLongPollingDeviceDiscovery) - LongPollingDeviceDiscovery(), - if (testThrowingDeviceDiscovery) - ThrowingPollingDeviceDiscovery(), _fakeDeviceDiscoverer, + if (deviceDiscoveryOverrides != null) + ...deviceDiscoveryOverrides ]; resetDevices(allDevices); } @@ -464,3 +520,4 @@ class MockProcess extends Mock implements Process {} class MockTerminal extends Mock implements AnsiTerminal {} class MockStdio extends Mock implements Stdio {} class MockCache extends Mock implements Cache {} +class MockDeviceDiscovery extends Mock implements DeviceDiscovery {} diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 559f362034..f24479614b 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -248,7 +248,7 @@ class FakeDeviceManager implements DeviceManager { } @override - Future> findTargetDevices(FlutterProject flutterProject) async { + Future> findTargetDevices(FlutterProject flutterProject, { Duration timeout }) async { return devices; } }