mirror of
https://github.com/flutter/flutter
synced 2024-10-06 00:09:53 +00:00
Better support for wireless devices in IDEs (#123716)
Better support for wireless devices in IDEs
This commit is contained in:
parent
e3bc8efd39
commit
34d2c8d030
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue