Better support for wireless devices in IDEs (#123716)

Better support for wireless devices in IDEs
This commit is contained in:
Victoria Ashworth 2023-04-04 15:39:38 -05:00 committed by GitHub
parent e3bc8efd39
commit 34d2c8d030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 392 additions and 174 deletions

View file

@ -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,34 +119,91 @@ 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,
};
// 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);
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;
}
}
/// 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
Future<void> stopPolling() async {
await _observedDeviceEventsSubscription?.cancel();

View file

@ -86,7 +86,8 @@ class XCDevice {
}
void dispose() {
_deviceObservationProcess?.kill();
_usbDeviceObserveProcess?.kill();
_wifiDeviceObserveProcess?.kill();
_usbDeviceWaitProcess?.kill();
_wifiDeviceWaitProcess?.kill();
}
@ -99,8 +100,10 @@ class XCDevice {
final IProxy _iProxy;
List<Object>? _cachedListResults;
Process? _deviceObservationProcess;
StreamController<Map<XCDeviceEvent, String>>? _deviceIdentifierByEvent;
Process? _usbDeviceObserveProcess;
Process? _wifiDeviceObserveProcess;
StreamController<XCDeviceEventNotification>? _observeStreamController;
@visibleForTesting
StreamController<XCDeviceEventNotification>? waitStreamController;
@ -109,9 +112,9 @@ class XCDevice {
Process? _wifiDeviceWaitProcess;
void _setupDeviceIdentifierByEventStream() {
// _deviceIdentifierByEvent Should always be available for listeners
// _observeStreamController Should always be available for listeners
// in case polling needs to be stopped and restarted.
_deviceIdentifierByEvent = StreamController<Map<XCDeviceEvent, String>>.broadcast(
_observeStreamController = StreamController<XCDeviceEventNotification>.broadcast(
onListen: _startObservingTetheredIOSDevices,
onCancel: _stopObservingTetheredIOSDevices,
);
@ -121,7 +124,7 @@ class XCDevice {
Future<List<Object>?> _getAllDevices({
bool useCache = false,
required Duration timeout
required Duration timeout,
}) async {
if (!isInstalled) {
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
@ -166,14 +169,14 @@ class XCDevice {
/// Observe identifiers (UDIDs) of devices as they attach and detach.
///
/// Each attach and detach event is a tuple of one event type
/// and identifier.
Stream<Map<XCDeviceEvent, String>>? observedDeviceEvents() {
/// Each attach and detach event contains information on the event type,
/// the event interface, and the device identifer.
Stream<XCDeviceEventNotification>? observedDeviceEvents() {
if (!isInstalled) {
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
return null;
}
return _deviceIdentifierByEvent?.stream;
return _observeStreamController?.stream;
}
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
@ -183,66 +186,130 @@ class XCDevice {
Future<void> _startObservingTetheredIOSDevices() async {
try {
if (_deviceObservationProcess != null) {
if (_usbDeviceObserveProcess != null || _wifiDeviceObserveProcess != null) {
throw Exception('xcdevice observe restart failed');
}
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
_deviceObservationProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'observe',
'--both',
],
_usbDeviceObserveProcess = await _startObserveProcess(
XCDeviceEventInterface.usb,
);
final StreamSubscription<String> stdoutSubscription = _deviceObservationProcess!.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
XCDeviceEventInterface.usb,
);
if (event != null) {
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
event.eventType: event.deviceIdentifier,
});
}
_wifiDeviceObserveProcess = await _startObserveProcess(
XCDeviceEventInterface.wifi,
);
final Future<void> usbProcessExited = _usbDeviceObserveProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice observe --usb exited with code $exitCode');
// Kill other process in case only one was killed.
_wifiDeviceObserveProcess?.kill();
});
final StreamSubscription<String> stderrSubscription = _deviceObservationProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice observe error: $line');
final Future<void> wifiProcessExited = _wifiDeviceObserveProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice observe --wifi exited with code $exitCode');
// Kill other process in case only one was killed.
_usbDeviceObserveProcess?.kill();
});
unawaited(_deviceObservationProcess?.exitCode.then((int status) {
_logger.printTrace('xcdevice exited with code $exitCode');
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
}).whenComplete(() async {
if (_deviceIdentifierByEvent?.hasListener ?? false) {
unawaited(Future.wait(<Future<void>>[
usbProcessExited,
wifiProcessExited,
]).whenComplete(() async {
if (_observeStreamController?.hasListener ?? false) {
// Tell listeners the process died.
await _deviceIdentifierByEvent?.close();
await _observeStreamController?.close();
}
_deviceObservationProcess = null;
_usbDeviceObserveProcess = null;
_wifiDeviceObserveProcess = null;
// Reopen it so new listeners can resume polling.
_setupDeviceIdentifierByEventStream();
}));
} on ProcessException catch (exception, stackTrace) {
_deviceIdentifierByEvent?.addError(exception, stackTrace);
_observeStreamController?.addError(exception, stackTrace);
} on ArgumentError catch (exception, stackTrace) {
_deviceIdentifierByEvent?.addError(exception, stackTrace);
_observeStreamController?.addError(exception, stackTrace);
}
}
Future<Process> _startObserveProcess(XCDeviceEventInterface eventInterface) {
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
return _streamXCDeviceEventCommand(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'observe',
'--${eventInterface.name}',
],
prefix: 'xcdevice observe --${eventInterface.name}: ',
mapFunction: (String line) {
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
eventInterface,
);
if (event != null) {
_observeStreamController?.add(event);
}
return line;
},
);
}
/// Starts the command and streams stdout/stderr from the child process to
/// this process' stdout/stderr.
///
/// If [mapFunction] is present, all lines are forwarded to [mapFunction] for
/// further processing.
Future<Process> _streamXCDeviceEventCommand(
List<String> cmd, {
String prefix = '',
StringConverter? mapFunction,
}) async {
final Process process = await _processUtils.start(cmd);
final StreamSubscription<String> stdoutSubscription = process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
String? mappedLine = line;
if (mapFunction != null) {
mappedLine = mapFunction(line);
}
if (mappedLine != null) {
final String message = '$prefix$mappedLine';
_logger.printTrace(message);
}
});
final StreamSubscription<String> stderrSubscription = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
String? mappedLine = line;
if (mapFunction != null) {
mappedLine = mapFunction(line);
}
if (mappedLine != null) {
_logger.printError('$prefix$mappedLine', wrap: false);
}
});
unawaited(process.exitCode.whenComplete(() {
stdoutSubscription.cancel();
stderrSubscription.cancel();
}));
return process;
}
void _stopObservingTetheredIOSDevices() {
_usbDeviceObserveProcess?.kill();
_wifiDeviceObserveProcess?.kill();
}
XCDeviceEventNotification? _processXCDeviceStdOut(
String line,
XCDeviceEventInterface eventInterface,
@ -275,10 +342,6 @@ class XCDevice {
return null;
}
void _stopObservingTetheredIOSDevices() {
_deviceObservationProcess?.kill();
}
/// Wait for a connect event for a specific device. Must use device's exact UDID.
///
/// To cancel this process, call [cancelWaitForDeviceToConnect].
@ -292,72 +355,26 @@ class XCDevice {
waitStreamController = StreamController<XCDeviceEventNotification>();
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
_usbDeviceWaitProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'wait',
'--${XCDeviceEventInterface.usb.name}',
deviceId,
],
);
_wifiDeviceWaitProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'wait',
'--${XCDeviceEventInterface.wifi.name}',
deviceId,
],
);
final StreamSubscription<String> usbStdoutSubscription = _processWaitStdOut(
_usbDeviceWaitProcess!,
_usbDeviceWaitProcess = await _startWaitProcess(
deviceId,
XCDeviceEventInterface.usb,
);
final StreamSubscription<String> wifiStdoutSubscription = _processWaitStdOut(
_wifiDeviceWaitProcess!,
_wifiDeviceWaitProcess = await _startWaitProcess(
deviceId,
XCDeviceEventInterface.wifi,
);
final StreamSubscription<String> usbStderrSubscription = _usbDeviceWaitProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice wait --usb error: $line');
});
final StreamSubscription<String> wifiStderrSubscription = _wifiDeviceWaitProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice wait --wifi error: $line');
});
final Future<void> usbProcessExited = _usbDeviceWaitProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice wait --usb exited with code $exitCode');
// Kill other process in case only one was killed.
_wifiDeviceWaitProcess?.kill();
unawaited(usbStdoutSubscription.cancel());
unawaited(usbStderrSubscription.cancel());
});
final Future<void> wifiProcessExited = _wifiDeviceWaitProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice wait --wifi exited with code $exitCode');
// Kill other process in case only one was killed.
_usbDeviceWaitProcess?.kill();
unawaited(wifiStdoutSubscription.cancel());
unawaited(wifiStderrSubscription.cancel());
});
final Future<void> allProcessesExited = Future.wait(
@ -389,22 +406,33 @@ class XCDevice {
return null;
}
StreamSubscription<String> _processWaitStdOut(
Process process,
XCDeviceEventInterface eventInterface,
) {
return process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
eventInterface,
);
if (event != null && event.eventType == XCDeviceEvent.attach) {
waitStreamController?.add(event);
}
});
Future<Process> _startWaitProcess(String deviceId, XCDeviceEventInterface eventInterface) {
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
return _streamXCDeviceEventCommand(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'wait',
'--${eventInterface.name}',
deviceId,
],
prefix: 'xcdevice wait --${eventInterface.name}: ',
mapFunction: (String line) {
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
eventInterface,
);
if (event != null && event.eventType == XCDeviceEvent.attach) {
waitStreamController?.add(event);
}
return line;
},
);
}
void cancelWaitForDeviceToConnect() {

View file

@ -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));
expect(iosDevices.eventsReceived, 1);
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
});
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]);
expect(iosDevices.eventsReceived, 4);
// Remove stream will throw over-completion if called more than once
// which proves this is ignored.
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'bogus',
});
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;
}

View file

@ -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,16 +416,30 @@ 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>();
xcdevice.observedDeviceEvents()!.listen(null, onDone: () {
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);