Skip to content

Conversation

@vashworth
Copy link
Contributor

@vashworth vashworth commented Feb 22, 2023

Updates device discovery process and user experience to better handle wireless devices. Also, renames usage of network devices to wireless devices.

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:

  1. Wireless devices are now separated into their own section (this includes Android and iOS wireless devices).
Multiple devices found:
target-device-1 (mobile) • xxx • android • Android 10

Wirelessly connected devices:
target-device-4 (mobile) • xxx • android • Android 10

[1]: target-device-1 (xxx)
[2]: target-device-4 (xxx)
  1. When on MacOS, wireless devices take longer to find

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

Multiple devices found:
target-device-1 (mobile) • xxx • ios • iOS 16
target-device-2 (mobile) • xxx • ios • iOS 16

Checking for wireless devices...

[1]: target-device-1 (xxx)
[2]: target-device-2 (xxx)

After done loading:

Multiple devices found:
target-device-1 (mobile) • xxx • ios • iOS 16
target-device-2 (mobile) • xxx • ios • iOS 16

Wirelessly connected devices:
target-device-5 (mobile) • xxx • ios • iOS 16

[1]: target-device-1 (xxx)
[2]: target-device-2 (xxx)
[3]: target-device-5 (xxx)

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.

Checking for wireless devices...

More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

target-device-1 (mobile) • xxx • ios • iOS 16
target-device-2 (mobile) • xxx • ios • iOS 16
target-device-4 (mobile) • xxx • ios • iOS 16

Wirelessly connected devices:
target-device-5 (mobile) • xxx • ios • iOS 16
target-device-7 (mobile) • xxx • ios • iOS 16
  1. When an exact iOS match is found but the device is not connected, wait for it to connect.
Waiting for device-name to connect...
  1. When more than 1 match for the -d flag is found, either allow the user to select from them (if stdin) or display the list of them (if no stdin).
Found 2 devices with name or id matching target-device:
target-device-1 (mobile) • xxx • android • Android 10

Wirelessly connected devices:
target-device-4 (mobile) • xxx • android • Android 10

[1]: target-device-1 (xxx)
[3]: target-device-4 (xxx)

Fixes #118109.
Fixes #118113.
Fixes #119271.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically tool Affects the "flutter" command-line tool. See also t: labels. labels Feb 22, 2023
@vashworth vashworth marked this pull request as ready for review February 22, 2023 22:54
@vashworth
Copy link
Contributor Author

Test failures with the Cirrus CI tests related to #121242, which is being worked on.


@override
DeviceConnectionInterface get connectionInterface =>
id.contains('_adb-tls-connect')
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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';
Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (waitForDeviceToConnect == true) {
if (waitForDeviceToConnect) {

Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async {
// return refreshAllDevices(timeout: timeout, filter: filter);
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Comment on lines +369 to +372
if ((mustBeConnected && !device.isConnected) ||
(localSupportFilter != null &&
!await localSupportFilter.matchesRequirements(device)) ||
!matchesDeviceConnectionInterface(device, deviceConnectionFilter)) {
Copy link
Contributor

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:

Suggested change
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) {

Comment on lines +380 to +384
devices = <Device>[
for (final Device device in devices)
if (await matchesRequirements(device)) device,
];
return devices;
Copy link
Contributor

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:

Suggested change
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) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +464 to +467
).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>[];
Copy link
Contributor

@christopherfujino christopherfujino Feb 23, 2023

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

Suggested change
).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>.

Copy link
Contributor

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.

Copy link
Contributor

@christopherfujino christopherfujino Feb 23, 2023

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Member

@jmagman jmagman left a 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.';
Copy link
Member

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).

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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,
Copy link
Member

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.

Copy link
Contributor Author

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?

Comment on lines +321 to +325
if (device.isSupported() &&
devicePlatform != TargetPlatform.fuchsia_arm64 &&
devicePlatform != TargetPlatform.fuchsia_x64 &&
devicePlatform != TargetPlatform.web_javascript &&
isDeviceSupportedForProject(device)) {
Copy link
Member

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 =
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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) {
Copy link
Member

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?

@jmagman
Copy link
Member

jmagman commented Feb 23, 2023

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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>{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling nit

Suggested change
final Map<String, IOSDevice> _cachedObserveredDevices = <String, IOSDevice>{};
final Map<String, IOSDevice> _cachedObservedDevices = <String, IOSDevice>{};

deviceNotifier!.updateWithNewList(connectedDevices);
}

bool _deviceHasObserveredConnection(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling nit

Suggested change
bool _deviceHasObserveredConnection(
bool _deviceHasObservedConnection(

Comment on lines +229 to +231
).onError((dynamic error, StackTrace stackTrace) {
logger.printTrace('Ignored error discovering $deviceId: $error');
return <Device>[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
).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;
Copy link
Contributor

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)?

Copy link
Contributor Author

@vashworth vashworth Feb 23, 2023

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"
  },

Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmagman thoughts?

Copy link
Contributor Author

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();
Copy link
Contributor

@christopherfujino christopherfujino Feb 23, 2023

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();
    }

@vashworth
Copy link
Contributor Author

@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

@vashworth
Copy link
Contributor Author

Closing and making smaller PRs.

@vashworth vashworth closed this Feb 23, 2023
auto-submit bot pushed a commit that referenced this pull request Apr 19, 2023
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.
@vashworth vashworth deleted the wireless_device_discovery branch September 27, 2023 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

3 participants