diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index d3f9a5ea2e..ff0364f43f 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -59,7 +59,31 @@ class IOSDevices extends PollingDeviceDiscovery { @override bool get requiresExtendedWirelessDeviceDiscovery => true; - StreamSubscription>? _observedDeviceEventsSubscription; + StreamSubscription? _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 _cachedPolledDevices = {}; + + /// 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> _observedConnectionsByDeviceId = + >{}; @override Future startPolling() async { @@ -75,16 +99,13 @@ class IOSDevices extends PollingDeviceDiscovery { deviceNotifier ??= ItemListNotifier(); // Start by populating all currently attached devices. - final List devices = await pollingGetDevices(); - - // Only show connected devices. - final List 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 _onDeviceEvent(Map event) async { - final XCDeviceEvent eventType = event.containsKey(XCDeviceEvent.attach) ? XCDeviceEvent.attach : XCDeviceEvent.detach; - final String? deviceIdentifier = event[eventType]; + @visibleForTesting + Future onDeviceEvent(XCDeviceEventNotification event) async { final ItemListNotifier? 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 devices = await pollingGetDevices(); + final Map deviceObservedConnections = + _observedConnectionsByDeviceId[event.deviceIdentifier] ?? + { + XCDeviceEventInterface.usb: false, + XCDeviceEventInterface.wifi: false, + }; - // Only show connected devices. - final List 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 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? 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 connectedDevices = _cachedPolledDevices.values.where((Device device) { + final Map? deviceObservedConnections = + _observedConnectionsByDeviceId[device.id]; + return (deviceObservedConnections != null && + _deviceHasObservedConnection(deviceObservedConnections)) || + (deviceObservedConnections == null && device.isConnected); + }).toList(); + + notifier.updateWithNewList(connectedDevices); + } + + bool _deviceHasObservedConnection( + Map deviceObservedConnections, + ) { + return (deviceObservedConnections[XCDeviceEventInterface.usb] ?? false) || + (deviceObservedConnections[XCDeviceEventInterface.wifi] ?? false); + } + @override Future stopPolling() async { await _observedDeviceEventsSubscription?.cancel(); diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index eff1bf8cb4..61d004b7fa 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -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? _cachedListResults; - Process? _deviceObservationProcess; - StreamController>? _deviceIdentifierByEvent; + + Process? _usbDeviceObserveProcess; + Process? _wifiDeviceObserveProcess; + StreamController? _observeStreamController; @visibleForTesting StreamController? 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>.broadcast( + _observeStreamController = StreamController.broadcast( onListen: _startObservingTetheredIOSDevices, onCancel: _stopObservingTetheredIOSDevices, ); @@ -121,7 +124,7 @@ class XCDevice { Future?> _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>? observedDeviceEvents() { + /// Each attach and detach event contains information on the event type, + /// the event interface, and the device identifer. + Stream? 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 _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( - [ - 'script', - '-t', - '0', - '/dev/null', - ..._xcode.xcrunCommand(), - 'xcdevice', - 'observe', - '--both', - ], + _usbDeviceObserveProcess = await _startObserveProcess( + XCDeviceEventInterface.usb, ); - final StreamSubscription stdoutSubscription = _deviceObservationProcess!.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - final XCDeviceEventNotification? event = _processXCDeviceStdOut( - line, - XCDeviceEventInterface.usb, - ); - if (event != null) { - _deviceIdentifierByEvent?.add({ - event.eventType: event.deviceIdentifier, - }); - } + _wifiDeviceObserveProcess = await _startObserveProcess( + XCDeviceEventInterface.wifi, + ); + + final Future 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 stderrSubscription = _deviceObservationProcess!.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - _logger.printTrace('xcdevice observe error: $line'); + + final Future 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(>[ + 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 _startObserveProcess(XCDeviceEventInterface eventInterface) { + // Run in interactive mode (via script) to convince + // xcdevice it has a terminal attached in order to redirect stdout. + return _streamXCDeviceEventCommand( + [ + '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 _streamXCDeviceEventCommand( + List cmd, { + String prefix = '', + StringConverter? mapFunction, + }) async { + final Process process = await _processUtils.start(cmd); + + final StreamSubscription stdoutSubscription = process.stdout + .transform(utf8.decoder) + .transform(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 stderrSubscription = process.stderr + .transform(utf8.decoder) + .transform(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(); - // Run in interactive mode (via script) to convince - // xcdevice it has a terminal attached in order to redirect stdout. - _usbDeviceWaitProcess = await _processUtils.start( - [ - 'script', - '-t', - '0', - '/dev/null', - ..._xcode.xcrunCommand(), - 'xcdevice', - 'wait', - '--${XCDeviceEventInterface.usb.name}', - deviceId, - ], - ); - - _wifiDeviceWaitProcess = await _processUtils.start( - [ - 'script', - '-t', - '0', - '/dev/null', - ..._xcode.xcrunCommand(), - 'xcdevice', - 'wait', - '--${XCDeviceEventInterface.wifi.name}', - deviceId, - ], - ); - - final StreamSubscription usbStdoutSubscription = _processWaitStdOut( - _usbDeviceWaitProcess!, + _usbDeviceWaitProcess = await _startWaitProcess( + deviceId, XCDeviceEventInterface.usb, ); - final StreamSubscription wifiStdoutSubscription = _processWaitStdOut( - _wifiDeviceWaitProcess!, + + _wifiDeviceWaitProcess = await _startWaitProcess( + deviceId, XCDeviceEventInterface.wifi, ); - final StreamSubscription usbStderrSubscription = _usbDeviceWaitProcess!.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - _logger.printTrace('xcdevice wait --usb error: $line'); - }); - final StreamSubscription wifiStderrSubscription = _wifiDeviceWaitProcess!.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - _logger.printTrace('xcdevice wait --wifi error: $line'); - }); - final Future 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 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 allProcessesExited = Future.wait( @@ -389,22 +406,33 @@ class XCDevice { return null; } - StreamSubscription _processWaitStdOut( - Process process, - XCDeviceEventInterface eventInterface, - ) { - return process.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - final XCDeviceEventNotification? event = _processXCDeviceStdOut( - line, - eventInterface, - ); - if (event != null && event.eventType == XCDeviceEvent.attach) { - waitStreamController?.add(event); - } - }); + Future _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( + [ + '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() { diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 15d81b92e0..efac31c1c4 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -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.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.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, [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.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([]); xcdevice.devices.add([]); - final StreamController> rescheduledStream = StreamController>(); + final StreamController rescheduledStream = StreamController(); 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 receivedEvent = Completer(); + int eventsReceived = 0; + + void resetEventCompleter() { + receivedEvent = Completer(); + } + + @override + Future 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> devices = >[]; final List diagnostics = []; - StreamController> deviceEventController = StreamController>(); + StreamController deviceEventController = StreamController(); + XCDeviceEventNotification? waitForDeviceEvent; @override @@ -741,7 +806,7 @@ class FakeXcdevice extends Fake implements XCDevice { } @override - Stream> observedDeviceEvents() { + Stream observedDeviceEvents() { return deviceEventController.stream; } diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index d5baff4356..e7a9ee8f45 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -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: [ + '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 attach1 = Completer(); final Completer attach2 = Completer(); final Completer detach1 = Completer(); + final Completer attach3 = Completer(); + final Completer detach2 = Completer(); // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418 // Attach: 00008027-00192736010F802E // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418 - xcdevice.observedDeviceEvents()!.listen((Map 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: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'observe', + '--wifi', + ], + exitCode: 1, + )); final Completer doneCompleter = Completer(); 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: [ @@ -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: [ @@ -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);