-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Update target device selection to better handle wireless devices #121262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Test failures with the Cirrus CI tests related to #121242, which is being worked on. |
|
|
||
| @override | ||
| DeviceConnectionInterface get connectionInterface => | ||
| id.contains('_adb-tls-connect') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any documentation to this magic string we can link to from a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discovered it by trying it out, but I just found this: https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/adb_mdns.h#30
so maybe I should take out the leading _. Should I add a comment linked to the android source code?
| bool get canListAnything => _androidWorkflow.canListDevices; | ||
|
|
||
| @override | ||
| bool get supportsWirelessDevices => true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this PR introducing wireless support for android devices?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So Android wireless devices already worked with Flutter. You just had to pair the device through Android Studio or the command line and then it worked. When you did flutter devices, flutter run, etc the wireless device would just be grouped with all other devices found.
| static const String cursorBeginningOfLine = '\u001b[1G'; | ||
|
|
||
| // Clear the entire line, cursor position does not change. | ||
| static const String clearEntireLineCode = '\u001b[2K'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh cool, TIL about this code.
| Future.wait<List<Device>?>(futureDevices), | ||
| ]); | ||
|
|
||
| if (waitForDeviceToConnect == true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (waitForDeviceToConnect == true) { | |
| if (waitForDeviceToConnect) { |
| Duration? timeout, | ||
| DeviceDiscoveryFilter? filter, | ||
| }) async { | ||
| // return refreshAllDevices(timeout: timeout, filter: filter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm guessing this can be deleted
| devices = ephemeralDevices; | ||
| } | ||
| } | ||
| final FlutterProject? flutterProject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this really be null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, when includeUnsupportedDevices is true, the flutterProject will be null. This is true in flutter logs and can be true in flutter drive.
| flutterProject: includeUnsupportedDevices ? null : FlutterProject.current(), |
| device = await findTargetDevice(includeUnsupportedDevices: true); |
| return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null); |
| Future<bool> isDeviceSupportedForAll(Device device) async { | ||
| // User has specified `--device all`. | ||
| // | ||
| // Always remove web and fuchsia devices from `--all`. This setting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this context to a dartdoc for this function?
| if ((mustBeConnected && !device.isConnected) || | ||
| (localSupportFilter != null && | ||
| !await localSupportFilter.matchesRequirements(device)) || | ||
| !matchesDeviceConnectionInterface(device, deviceConnectionFilter)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. I think pulling some of these sub-expressions into named variables would make this easier to read:
| if ((mustBeConnected && !device.isConnected) || | |
| (localSupportFilter != null && | |
| !await localSupportFilter.matchesRequirements(device)) || | |
| !matchesDeviceConnectionInterface(device, deviceConnectionFilter)) { | |
| final bool deviceIsNotConnectedButMustBe = mustBeConnected && !device.isConnected; | |
| final bool deviceFailsFilter = localSupportFilter != null && !(await localSupportFilter.matchesRequirements(device)); | |
| final bool doesNotMatchConnectionInterface = !matchesDeviceConnectionInterface(device, deviceConnectionFilter); | |
| if (deviceIsNotConnectedButMustBe || deviceFailsFilter || doesNotMatchConnectionInterface) { |
| devices = <Device>[ | ||
| for (final Device device in devices) | ||
| if (await matchesRequirements(device)) device, | ||
| ]; | ||
| return devices; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit don't re-assign an argument:
| devices = <Device>[ | |
| for (final Device device in devices) | |
| if (await matchesRequirements(device)) device, | |
| ]; | |
| return devices; | |
| return <Device>[ | |
| for (final Device device in devices) | |
| if (await matchesRequirements(device)) device, | |
| ]; |
| /// For example, 'windows' or 'linux'. | ||
| List<String> get wellKnownIds; | ||
|
|
||
| bool exactlyMatchesDeviceId(String deviceId, Device device) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are device IDs not case sensitive? and is this true across platforms?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i suppose if this is the comparison we were making before then we should keep doing it. it's not obvious from the diff here if we were doing a similar comparison previously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sorry about the diff. It is the same as before.
| ).onError((dynamic error, StackTrace stackTrace) { | ||
| // Fail quietly so other discovers can find matches, even if this one fails. | ||
| logger.printTrace('Ignored error discovering $deviceId: $error'); | ||
| return <Device>[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is susceptible to a very obscure gotcha I reported in dart-lang/sdk#51248
| ).onError((dynamic error, StackTrace stackTrace) { | |
| // Fail quietly so other discovers can find matches, even if this one fails. | |
| logger.printTrace('Ignored error discovering $deviceId: $error'); | |
| return <Device>[]; | |
| ).then( | |
| (List<Device> devices) => devices, | |
| onError: (Object error, StackTrace stackTrace) { | |
| // Fail quietly so other discoverers can find matches, even if this one fails. | |
| logger.printTrace('Ignored error discovering $deviceId: $error'); | |
| return <Device>[]; | |
| } |
The seemingly silly anonymous closure we pass to .then() of (List<Device> devices) => devices ensures that the Future we call the error handling callback on is of type List<Device>, rather than that of a covariant type, such as List<IosDevice>.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI you were not expected to know this, I'm working on landing a lint to catch this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, since you're awaiting this, I think you can just use a try catch:
List<Device>? foundDevices;
try {
foundDevices = await devices(
filter: filter,
);
} catch (error) {
// Fail quietly so other discovers can find matches, even if this one fails.
logger.printTrace('Ignored error discovering $deviceId: $error');
foundDevices = const <Device>[];
});| }) async { | ||
| if (deviceNotifier == null || resetCache) { | ||
| final List<Device> devices = await pollingGetDevices(timeout: timeout); | ||
| // If the cache was populated while the polling was ongoing, do not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is huge and therefore hard to review thoroughly. Is there any way to break it into incremental features?
| _slowWarning = slowWarningCallback!(); | ||
| final SlowWarningCallback? callback = slowWarningCallback; | ||
| final TerminalColor? color = warningColor; | ||
| if (_slowWarning == '' && callback != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (_slowWarning == '' && callback != null) { | |
| if (_slowWarning.isEmpty && callback != null) { |
| String flutterNoMatchingDevice(String deviceId) => 'No supported devices found with name or id ' | ||
| "matching '$deviceId'."; | ||
| String get flutterNoDevicesFound => 'No devices found'; | ||
| String get flutterNoDevicesFound => 'No devices found.'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@christopherfujino how do we decide what strings to put in user_messages? I think it's primarily the few strings that need to be overridden by g3?
(see google3_user_messages.dart in cs).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that was the intent of the UserMessages class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, perhaps I should move not-overridden ones into the file they are used and make them consts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM
| class DeviceDiscoverySupportFilter { | ||
| DeviceDiscoverySupportFilter({ | ||
| required this.flutterProject, | ||
| this.mustBeSupportedByFlutter = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about includeDevicesSupportedByFlutter, includeDevicesSupportedByProject, includeUnsupportedDevices or similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I had a hard time picking variable names for this. So by default (if you don't set anything) it will include all devices and none will be filtered out. So I thought that using the word include was a bit confusing. For example, if everything is already included, what does includeDevicesSupportedByFlutter really mean? It really means exclude everything that's not supported by flutter. What do you think of excludeDevicesNotSupportedByFlutter, excludeDevicesNotSupportedByProject, excludeDevicesNotSupportedByAll?
| if (device.isSupported() && | ||
| devicePlatform != TargetPlatform.fuchsia_arm64 && | ||
| devicePlatform != TargetPlatform.fuchsia_x64 && | ||
| devicePlatform != TargetPlatform.web_javascript && | ||
| isDeviceSupportedForProject(device)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or you could just return this conditional.
| notifier.updateWithNewList(devices); | ||
| } else if (eventType == XCDeviceEvent.detach && knownDevice != null) { | ||
| notifier.removeItem(knownDevice); | ||
| final Map<XCDeviceEventInterface, bool> deviceObservedConnections = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
observedConnectionTypeByDevice?
You're tracking whether usb and wifi were both connected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it tracks both so that if one disconnects, we can check if it is still connected a different way
| wifi, | ||
| } | ||
|
|
||
| String getXCDeviceEventInterface(XCDeviceEventInterface eventInterface) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out enhanced enums to hang strings and other enums off of specific enum values instead of this pattern: https://dart.dev/guides/language/language-tour#enumerated-types
| final Completer<XCDeviceEventNotification?> cancelledCompleter = | ||
| Completer<XCDeviceEventNotification?>(); | ||
|
|
||
| unawaited(_usbDeviceWaitProcess!.exitCode.then((int status) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these need to be marked unawaited?
|
Rebase on #121251 to get the Cirrus fix. |
| Future<ValidationResult> validate() async { | ||
| final List<Device> devices = await _deviceManager.getAllConnectedDevices(); | ||
| final List<Device> devices = await _deviceManager.refreshAllDevices( | ||
| timeout: DeviceManager.minimumWirelessTimeout, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this behavior different from the flutter devices command?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in flutter devices we load the attached devices and display them immediately, show a message that we're checking for wireless devices. Then once the wireless devices are done loading, we print them.
In flutter doctor we just don't display any devices until they are all done loading.
| @visibleForTesting | ||
| final XCDevice xcdevice; | ||
|
|
||
| final Map<String, IOSDevice> _cachedObserveredDevices = <String, IOSDevice>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spelling nit
| final Map<String, IOSDevice> _cachedObserveredDevices = <String, IOSDevice>{}; | |
| final Map<String, IOSDevice> _cachedObservedDevices = <String, IOSDevice>{}; |
| deviceNotifier!.updateWithNewList(connectedDevices); | ||
| } | ||
|
|
||
| bool _deviceHasObserveredConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spelling nit
| bool _deviceHasObserveredConnection( | |
| bool _deviceHasObservedConnection( |
| ).onError((dynamic error, StackTrace stackTrace) { | ||
| logger.printTrace('Ignored error discovering $deviceId: $error'); | ||
| return <Device>[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ).onError((dynamic error, StackTrace stackTrace) { | |
| logger.printTrace('Ignored error discovering $deviceId: $error'); | |
| return <Device>[]; | |
| ).then( | |
| (List<Device> devices) => devices, | |
| onError: (Object error, StackTrace stackTrace) { | |
| logger.printTrace('Ignored error discovering $deviceId: $error'); | |
| return <Device>[]; |
| final DarwinArch cpuArchitecture; | ||
|
|
||
| final IOSDeviceConnectionInterface interfaceType; | ||
| IOSDeviceConnectionInterface interfaceType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is both known at the time we construct the class (that is, non-null) but also mutable (non-final)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, so when we get the initial IOSDeviceConnectionInterface, it might be incorrect but we still get one. For example, xcdevice list will say a wireless device has an interface of usb (it will also have an error) if it didn't have long enough to check for the network connection.
Here's an example of what it will return for a wireless device it didn't have enough time to properly check:
{
"modelCode" : "iPad14,3",
"simulator" : false,
"modelName" : "iPad Pro (11-inch) (4th generation)\"",
"error" : {
"code" : -13,
"failureReason" : "",
"underlyingErrors" : [
{
"code" : 4,
"failureReason" : "",
"description" : "iPad is locked.",
"recoverySuggestion" : "To use iPad with Xcode, unlock it.",
"domain" : "DVTDeviceIneligibilityErrorDomain"
}
],
"description" : "iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPad is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
},
"operatingSystemVersion" : "16.1.1 (20B101)",
"identifier" : "xxx",
"platform" : "com.apple.platform.iphoneos",
"architecture" : "arm64e",
"interface" : "usb",
"available" : false,
"name" : "iPad",
"modelUTI" : "com.apple.ipad-pro-11-4th-2"
},There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, that makes sense. Could you add a dartdoc to this field, noting its initial value from xcdevice list could be wrong?
| _logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.'); | ||
|
|
||
| final int defaultTimeout = interfaceType == IOSDeviceConnectionInterface.network ? 45 : 30; | ||
| final int defaultTimeout = isWirelesslyConnected ? 45 : 30; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the 30 here equivalent to DeviceManager.minimumWirelessTimeout or is that just coincidental?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wait, no, here the 30 is if we're wired, whereas if we're wireless it's 45. I'm a little worried we're spreading out the timeout values across these files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmagman thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so DeviceManager.minimumWirelessTimeout is supposed to be the time we take to discover wireless devices for device selection. The 30/45 second timeout here is how long we wait to display error message when waiting for the dart vm service to be discovered. Maybe I should rename minimumWirelessTimeout to minimumWirelessDiscoveryForSelectionTimeout or minimumTargetDeviceWirelessDiscoveryTimeout?
| Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async { | ||
| globals.preRunValidator.validate(); | ||
|
|
||
| startFindingWirelessDevices(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if, instead of always calling this method and requiring methods that don't need it to override it, we add a boolean field to FlutterCommand: final bool usesWirelessDevices = false;
Then commands that require it must explicitly opt in. This way someone adding a new command does not have to consciously opt out.
This line would then become:
if (usesWirelessDevice) {
startFindingWirelessDevices();
}|
@christopherfujino @jmagman Thanks for reviewing so far! As Jenn recommended, I'm going to make this into multiple PRs so it's a little easier to review. I'll incorporate the comments you've left |
|
Closing and making smaller PRs. |
Rename variables, update comments, etc from `network` to `wireless` to keep it more uniform. Also, move non-overriden messages related to device selection into the file they're used. Part 7 in breakdown of #121262.
Updates device discovery process and user experience to better handle wireless devices. Also, renames usage of
networkdevices towirelessdevices.There are many different outputs that can happen and it would be overkill to list them all here. I would recommend looking at this test file https://github.com/flutter/flutter/blob/ae8dad4e5fe396cd9e68bf3f95ceab742a0f982e/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart to get a look at all the potential outputs.
Here are some of the biggest changes:
2.1. If the terminal has stdin and supports ANSI, a message is displayed while wireless devices are loading and other devices can be selected. After they are done loading, it will clear out the message and update with the device list
After done loading:
2.2. If the terminal does not have stdin, a message is displayed while wireless devices are loading and after all devices are loaded, they are listed with instructions to use a device selection flag.
-dflag is found, either allow the user to select from them (if stdin) or display the list of them (if no stdin).Fixes #118109.
Fixes #118113.
Fixes #119271.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.