Unverified Commit 34d2c8d0 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Better support for wireless devices in IDEs (#123716)

Better support for wireless devices in IDEs
parent e3bc8efd
......@@ -59,7 +59,31 @@ class IOSDevices extends PollingDeviceDiscovery {
@override
bool get requiresExtendedWirelessDeviceDiscovery => true;
StreamSubscription<Map<XCDeviceEvent, String>>? _observedDeviceEventsSubscription;
StreamSubscription<XCDeviceEventNotification>? _observedDeviceEventsSubscription;
/// Cache for all devices found by `xcdevice list`, including not connected
/// devices. Used to minimize the need to call `xcdevice list`.
///
/// Separate from `deviceNotifier` since `deviceNotifier` should only contain
/// connected devices.
final Map<String, IOSDevice> _cachedPolledDevices = <String, IOSDevice>{};
/// Maps device id to a map of the device's observed connections. When the
/// mapped connection is `true`, that means that observed events indicated
/// the device is connected via that particular interface.
///
/// The device id must be missing from the map or both interfaces must be
/// false for the device to be considered disconnected.
///
/// Example:
/// {
/// device-id: {
/// usb: false,
/// wifi: false,
/// },
/// }
final Map<String, Map<XCDeviceEventInterface, bool>> _observedConnectionsByDeviceId =
<String, Map<XCDeviceEventInterface, bool>>{};
@override
Future<void> startPolling() async {
......@@ -75,16 +99,13 @@ class IOSDevices extends PollingDeviceDiscovery {
deviceNotifier ??= ItemListNotifier<Device>();
// Start by populating all currently attached devices.
final List<Device> devices = await pollingGetDevices();
// Only show connected devices.
final List<Device> filteredDevices = devices.where((Device device) => device.isConnected == true).toList();
deviceNotifier!.updateWithNewList(filteredDevices);
_updateCachedDevices(await pollingGetDevices());
_updateNotifierFromCache();
// cancel any outstanding subscriptions.
await _observedDeviceEventsSubscription?.cancel();
_observedDeviceEventsSubscription = xcdevice.observedDeviceEvents()?.listen(
_onDeviceEvent,
onDeviceEvent,
onError: (Object error, StackTrace stack) {
_logger.printTrace('Process exception running xcdevice observe:\n$error\n$stack');
}, onDone: () {
......@@ -98,32 +119,89 @@ class IOSDevices extends PollingDeviceDiscovery {
);
}
Future<void> _onDeviceEvent(Map<XCDeviceEvent, String> event) async {
final XCDeviceEvent eventType = event.containsKey(XCDeviceEvent.attach) ? XCDeviceEvent.attach : XCDeviceEvent.detach;
final String? deviceIdentifier = event[eventType];
@visibleForTesting
Future<void> onDeviceEvent(XCDeviceEventNotification event) async {
final ItemListNotifier<Device>? notifier = deviceNotifier;
if (notifier == null) {
return;
}
Device? knownDevice;
for (final Device device in notifier.items) {
if (device.id == deviceIdentifier) {
if (device.id == event.deviceIdentifier) {
knownDevice = device;
}
}
// Ignore already discovered devices (maybe populated at the beginning).
if (eventType == XCDeviceEvent.attach && knownDevice == null) {
// There's no way to get details for an individual attached device,
// so repopulate them all.
final List<Device> devices = await pollingGetDevices();
final Map<XCDeviceEventInterface, bool> deviceObservedConnections =
_observedConnectionsByDeviceId[event.deviceIdentifier] ??
<XCDeviceEventInterface, bool>{
XCDeviceEventInterface.usb: false,
XCDeviceEventInterface.wifi: false,
};
if (event.eventType == XCDeviceEvent.attach) {
// Update device's observed connections.
deviceObservedConnections[event.eventInterface] = true;
_observedConnectionsByDeviceId[event.deviceIdentifier] = deviceObservedConnections;
// If device was not already in notifier, add it.
if (knownDevice == null) {
if (_cachedPolledDevices[event.deviceIdentifier] == null) {
// If device is not found in cache, there's no way to get details
// for an individual attached device, so repopulate them all.
_updateCachedDevices(await pollingGetDevices());
}
_updateNotifierFromCache();
}
} else {
// Update device's observed connections.
deviceObservedConnections[event.eventInterface] = false;
_observedConnectionsByDeviceId[event.deviceIdentifier] = deviceObservedConnections;
// If device is in the notifier and does not have other observed
// connections, remove it.
if (knownDevice != null &&
!_deviceHasObservedConnection(deviceObservedConnections)) {
notifier.removeItem(knownDevice);
}
}
}
/// Adds or updates devices in cache. Does not remove devices from cache.
void _updateCachedDevices(List<Device> devices) {
for (final Device device in devices) {
if (device is! IOSDevice) {
continue;
}
_cachedPolledDevices[device.id] = device;
}
}
// Only show connected devices.
final List<Device> filteredDevices = devices.where((Device device) => device.isConnected == true).toList();
notifier.updateWithNewList(filteredDevices);
} else if (eventType == XCDeviceEvent.detach && knownDevice != null) {
notifier.removeItem(knownDevice);
/// Updates notifier with devices found in the cache that are determined
/// to be connected.
void _updateNotifierFromCache() {
final ItemListNotifier<Device>? notifier = deviceNotifier;
if (notifier == null) {
return;
}
// Device is connected if it has either an observed usb or wifi connection
// or it has not been observed but was found as connected in the cache.
final List<Device> connectedDevices = _cachedPolledDevices.values.where((Device device) {
final Map<XCDeviceEventInterface, bool>? deviceObservedConnections =
_observedConnectionsByDeviceId[device.id];
return (deviceObservedConnections != null &&
_deviceHasObservedConnection(deviceObservedConnections)) ||
(deviceObservedConnections == null && device.isConnected);
}).toList();
notifier.updateWithNewList(connectedDevices);
}
bool _deviceHasObservedConnection(
Map<XCDeviceEventInterface, bool> deviceObservedConnections,
) {
return (deviceObservedConnections[XCDeviceEventInterface.usb] ?? false) ||
(deviceObservedConnections[XCDeviceEventInterface.wifi] ?? false);
}
@override
......
......@@ -413,7 +413,7 @@ void main() {
});
testWithoutContext('start polling', () async {
final IOSDevices iosDevices = IOSDevices(
final TestIOSDevices iosDevices = TestIOSDevices(
platform: macPlatform,
xcdevice: xcdevice,
iosWorkflow: iosWorkflow,
......@@ -447,25 +447,68 @@ void main() {
expect(iosDevices.deviceNotifier!.items, isEmpty);
expect(xcdevice.deviceEventController.hasListener, isTrue);
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
});
xcdevice.deviceEventController.add(
XCDeviceEventNotification(
XCDeviceEvent.attach,
XCDeviceEventInterface.usb,
'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
),
);
await added.future;
expect(iosDevices.deviceNotifier!.items.length, 2);
expect(iosDevices.deviceNotifier!.items, contains(device1));
expect(iosDevices.deviceNotifier!.items, contains(device2));
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
});
expect(iosDevices.eventsReceived, 1);
iosDevices.resetEventCompleter();
xcdevice.deviceEventController.add(
XCDeviceEventNotification(
XCDeviceEvent.attach,
XCDeviceEventInterface.wifi,
'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
),
);
await iosDevices.receivedEvent.future;
expect(iosDevices.deviceNotifier!.items.length, 2);
expect(iosDevices.deviceNotifier!.items, contains(device1));
expect(iosDevices.deviceNotifier!.items, contains(device2));
expect(iosDevices.eventsReceived, 2);
iosDevices.resetEventCompleter();
xcdevice.deviceEventController.add(
XCDeviceEventNotification(
XCDeviceEvent.detach,
XCDeviceEventInterface.usb,
'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
),
);
await iosDevices.receivedEvent.future;
expect(iosDevices.deviceNotifier!.items.length, 2);
expect(iosDevices.deviceNotifier!.items, contains(device1));
expect(iosDevices.deviceNotifier!.items, contains(device2));
expect(iosDevices.eventsReceived, 3);
xcdevice.deviceEventController.add(
XCDeviceEventNotification(
XCDeviceEvent.detach,
XCDeviceEventInterface.wifi,
'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
),
);
await removed.future;
expect(iosDevices.deviceNotifier!.items, <Device>[device2]);
// Remove stream will throw over-completion if called more than once
// which proves this is ignored.
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'bogus',
});
expect(iosDevices.eventsReceived, 4);
iosDevices.resetEventCompleter();
xcdevice.deviceEventController.add(
XCDeviceEventNotification(
XCDeviceEvent.detach,
XCDeviceEventInterface.usb,
'bogus'
),
);
await iosDevices.receivedEvent.future;
expect(iosDevices.eventsReceived, 5);
expect(addedCount, 2);
......@@ -485,7 +528,7 @@ void main() {
xcdevice.devices.add(<IOSDevice>[]);
xcdevice.devices.add(<IOSDevice>[]);
final StreamController<Map<XCDeviceEvent, String>> rescheduledStream = StreamController<Map<XCDeviceEvent, String>>();
final StreamController<XCDeviceEventNotification> rescheduledStream = StreamController<XCDeviceEventNotification>();
unawaited(xcdevice.deviceEventController.done.whenComplete(() {
xcdevice.deviceEventController = rescheduledStream;
......@@ -723,13 +766,35 @@ class FakeIOSApp extends Fake implements IOSApp {
final String name;
}
class TestIOSDevices extends IOSDevices {
TestIOSDevices({required super.platform, required super.xcdevice, required super.iosWorkflow, required super.logger,});
Completer<void> receivedEvent = Completer<void>();
int eventsReceived = 0;
void resetEventCompleter() {
receivedEvent = Completer<void>();
}
@override
Future<void> onDeviceEvent(XCDeviceEventNotification event) async {
await super.onDeviceEvent(event);
if (!receivedEvent.isCompleted) {
receivedEvent.complete();
}
eventsReceived++;
return;
}
}
class FakeIOSWorkflow extends Fake implements IOSWorkflow { }
class FakeXcdevice extends Fake implements XCDevice {
int getAvailableIOSDevicesCount = 0;
final List<List<IOSDevice>> devices = <List<IOSDevice>>[];
final List<String> diagnostics = <String>[];
StreamController<Map<XCDeviceEvent, String>> deviceEventController = StreamController<Map<XCDeviceEvent, String>>();
StreamController<XCDeviceEventNotification> deviceEventController = StreamController<XCDeviceEventNotification>();
XCDeviceEventNotification? waitForDeviceEvent;
@override
......@@ -741,7 +806,7 @@ class FakeXcdevice extends Fake implements XCDevice {
}
@override
Stream<Map<XCDeviceEvent, String>> observedDeviceEvents() {
Stream<XCDeviceEventNotification> observedDeviceEvents() {
return deviceEventController.stream;
}
......
......@@ -342,32 +342,57 @@ void main() {
'xcrun',
'xcdevice',
'observe',
'--both',
], stdout: 'Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418\n'
'--usb',
], stdout: 'Listening for all devices, on USB.\n'
'Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418\n'
'Attach: 00008027-00192736010F802E\n'
'Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418',
stderr: 'Some error',
stderr: 'Some usb error',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--wifi',
], stdout: 'Listening for all devices, on WiFi.\n'
'Attach: 00000001-0000000000000000\n'
'Detach: 00000001-0000000000000000',
stderr: 'Some wifi error',
));
final Completer<void> attach1 = Completer<void>();
final Completer<void> attach2 = Completer<void>();
final Completer<void> detach1 = Completer<void>();
final Completer<void> attach3 = Completer<void>();
final Completer<void> detach2 = Completer<void>();
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: 00008027-00192736010F802E
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
xcdevice.observedDeviceEvents()!.listen((Map<XCDeviceEvent, String> event) {
expect(event.length, 1);
if (event.containsKey(XCDeviceEvent.attach)) {
if (event[XCDeviceEvent.attach] == 'd83d5bc53967baa0ee18626ba87b6254b2ab5418') {
xcdevice.observedDeviceEvents()!.listen((XCDeviceEventNotification event) {
if (event.eventType == XCDeviceEvent.attach) {
if (event.deviceIdentifier == 'd83d5bc53967baa0ee18626ba87b6254b2ab5418') {
attach1.complete();
} else
if (event[XCDeviceEvent.attach] == '00008027-00192736010F802E') {
if (event.deviceIdentifier == '00008027-00192736010F802E') {
attach2.complete();
}
} else if (event.containsKey(XCDeviceEvent.detach)) {
expect(event[XCDeviceEvent.detach], 'd83d5bc53967baa0ee18626ba87b6254b2ab5418');
detach1.complete();
if (event.deviceIdentifier == '00000001-0000000000000000') {
attach3.complete();
}
} else if (event.eventType == XCDeviceEvent.detach) {
if (event.deviceIdentifier == 'd83d5bc53967baa0ee18626ba87b6254b2ab5418') {
detach1.complete();
}
if (event.deviceIdentifier == '00000001-0000000000000000') {
detach2.complete();
}
} else {
fail('Unexpected event');
}
......@@ -375,7 +400,10 @@ void main() {
await attach1.future;
await attach2.future;
await detach1.future;
expect(logger.traceText, contains('xcdevice observe error: Some error'));
await attach3.future;
await detach2.future;
expect(logger.errorText, contains('xcdevice observe --usb: Some usb error'));
expect(logger.errorText, contains('xcdevice observe --wifi: Some wifi error'));
});
testUsingContext('handles exit code', () async {
......@@ -388,8 +416,21 @@ void main() {
'xcrun',
'xcdevice',
'observe',
'--both',
'--usb',
],
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--wifi',
],
exitCode: 1,
));
final Completer<void> doneCompleter = Completer<void>();
......@@ -397,7 +438,8 @@ void main() {
doneCompleter.complete();
});
await doneCompleter.future;
expect(logger.traceText, contains('xcdevice exited with code 0'));
expect(logger.traceText, contains('xcdevice observe --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice observe --wifi exited with code 0'));
});
});
......@@ -418,6 +460,7 @@ void main() {
'--usb',
deviceId,
],
stdout: 'Waiting for $deviceId to appear, on USB.\n',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
......@@ -431,7 +474,9 @@ void main() {
'--wifi',
deviceId,
],
stdout: 'Attach: 00000001-0000000000000000\n',
stdout:
'Waiting for $deviceId to appear, on WiFi.\n'
'Attach: 00000001-0000000000000000\n',
));
// Attach: 00000001-0000000000000000
......@@ -459,6 +504,7 @@ void main() {
deviceId,
],
exitCode: 1,
stderr: 'Some error',
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
......@@ -477,6 +523,7 @@ void main() {
final XCDeviceEventNotification? event = await xcdevice.waitForDeviceToConnect(deviceId);
expect(event, isNull);
expect(logger.errorText, contains('xcdevice wait --usb: Some error'));
expect(logger.traceText, contains('xcdevice wait --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice wait --wifi exited with code 0'));
expect(xcdevice.waitStreamController?.isClosed, isTrue);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment