Unverified Commit b0c98b66 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Detect USB/network interface from iOS devices (#58257)

parent 7d17c539
...@@ -58,7 +58,7 @@ class IOSDevices extends PollingDeviceDiscovery { ...@@ -58,7 +58,7 @@ class IOSDevices extends PollingDeviceDiscovery {
); );
} }
return await _xcdevice.getAvailableTetheredIOSDevices(timeout: timeout); return await _xcdevice.getAvailableIOSDevices(timeout: timeout);
} }
@override @override
...@@ -73,11 +73,18 @@ class IOSDevices extends PollingDeviceDiscovery { ...@@ -73,11 +73,18 @@ class IOSDevices extends PollingDeviceDiscovery {
} }
} }
enum IOSDeviceInterface {
none,
usb,
network,
}
class IOSDevice extends Device { class IOSDevice extends Device {
IOSDevice(String id, { IOSDevice(String id, {
@required FileSystem fileSystem, @required FileSystem fileSystem,
@required this.name, @required this.name,
@required this.cpuArchitecture, @required this.cpuArchitecture,
@required this.interfaceType,
@required String sdkVersion, @required String sdkVersion,
@required Platform platform, @required Platform platform,
@required Artifacts artifacts, @required Artifacts artifacts,
...@@ -123,16 +130,21 @@ class IOSDevice extends Device { ...@@ -123,16 +130,21 @@ class IOSDevice extends Device {
} }
@override @override
bool get supportsHotReload => true; bool get supportsHotReload => interfaceType == IOSDeviceInterface.usb;
@override @override
bool get supportsHotRestart => true; bool get supportsHotRestart => interfaceType == IOSDeviceInterface.usb;
@override
bool get supportsFlutterExit => interfaceType == IOSDeviceInterface.usb;
@override @override
final String name; final String name;
final DarwinArch cpuArchitecture; final DarwinArch cpuArchitecture;
final IOSDeviceInterface interfaceType;
Map<IOSApp, DeviceLogReader> _logReaders; Map<IOSApp, DeviceLogReader> _logReaders;
DevicePortForwarder _portForwarder; DevicePortForwarder _portForwarder;
...@@ -178,6 +190,7 @@ class IOSDevice extends Device { ...@@ -178,6 +190,7 @@ class IOSDevice extends Device {
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
launchArguments: <String>[], launchArguments: <String>[],
interfaceType: interfaceType,
); );
} on ProcessException catch (e) { } on ProcessException catch (e) {
_logger.printError(e.message); _logger.printError(e.message);
...@@ -319,6 +332,7 @@ class IOSDevice extends Device { ...@@ -319,6 +332,7 @@ class IOSDevice extends Device {
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
launchArguments: launchArguments, launchArguments: launchArguments,
interfaceType: interfaceType,
); );
if (installationResult != 0) { if (installationResult != 0) {
_logger.printError('Could not run ${bundle.path} on $id.'); _logger.printError('Could not run ${bundle.path} on $id.');
...@@ -410,7 +424,7 @@ class IOSDevice extends Device { ...@@ -410,7 +424,7 @@ class IOSDevice extends Device {
void clearLogs() { } void clearLogs() { }
@override @override
bool get supportsScreenshot => _iMobileDevice.isInstalled; bool get supportsScreenshot => _iMobileDevice.isInstalled && interfaceType == IOSDeviceInterface.usb;
@override @override
Future<void> takeScreenshot(File outputFile) async { Future<void> takeScreenshot(File outputFile) async {
......
...@@ -14,6 +14,7 @@ import '../base/process.dart'; ...@@ -14,6 +14,7 @@ import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import 'code_signing.dart'; import 'code_signing.dart';
import 'devices.dart';
// Error message patterns from ios-deploy output // Error message patterns from ios-deploy output
const String noProvisioningProfileErrorOne = 'Error 0xe8008015'; const String noProvisioningProfileErrorOne = 'Error 0xe8008015';
...@@ -84,6 +85,7 @@ class IOSDeploy { ...@@ -84,6 +85,7 @@ class IOSDeploy {
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required List<String>launchArguments, @required List<String>launchArguments,
@required IOSDeviceInterface interfaceType,
}) async { }) async {
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
_binaryPath, _binaryPath,
...@@ -91,7 +93,8 @@ class IOSDeploy { ...@@ -91,7 +93,8 @@ class IOSDeploy {
deviceId, deviceId,
'--bundle', '--bundle',
bundlePath, bundlePath,
'--no-wifi', if (interfaceType != IOSDeviceInterface.network)
'--no-wifi',
if (launchArguments.isNotEmpty) ...<String>[ if (launchArguments.isNotEmpty) ...<String>[
'--args', '--args',
launchArguments.join(' '), launchArguments.join(' '),
...@@ -113,6 +116,7 @@ class IOSDeploy { ...@@ -113,6 +116,7 @@ class IOSDeploy {
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required List<String> launchArguments, @required List<String> launchArguments,
@required IOSDeviceInterface interfaceType,
}) async { }) async {
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
_binaryPath, _binaryPath,
...@@ -120,7 +124,8 @@ class IOSDeploy { ...@@ -120,7 +124,8 @@ class IOSDeploy {
deviceId, deviceId,
'--bundle', '--bundle',
bundlePath, bundlePath,
'--no-wifi', if (interfaceType != IOSDeviceInterface.network)
'--no-wifi',
'--justlaunch', '--justlaunch',
if (launchArguments.isNotEmpty) ...<String>[ if (launchArguments.isNotEmpty) ...<String>[
'--args', '--args',
......
...@@ -290,7 +290,7 @@ class XCDevice { ...@@ -290,7 +290,7 @@ class XCDevice {
List<dynamic> _cachedListResults; List<dynamic> _cachedListResults;
/// [timeout] defaults to 2 seconds. /// [timeout] defaults to 2 seconds.
Future<List<IOSDevice>> getAvailableTetheredIOSDevices({ Duration timeout }) async { Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async {
final List<dynamic> allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2)); final List<dynamic> allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
if (allAvailableDevices == null) { if (allAvailableDevices == null) {
...@@ -364,8 +364,11 @@ class XCDevice { ...@@ -364,8 +364,11 @@ class XCDevice {
} }
} }
final IOSDeviceInterface interface = _interfaceType(deviceProperties);
// Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network). // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network).
if (!_isUSBTethered(deviceProperties)) { // TODO(jmagman): Remove this check once wirelessly detected devices can be observed and attached, https://github.com/flutter/flutter/issues/15072.
if (interface != IOSDeviceInterface.usb) {
continue; continue;
} }
...@@ -373,6 +376,7 @@ class XCDevice { ...@@ -373,6 +376,7 @@ class XCDevice {
device['identifier'] as String, device['identifier'] as String,
name: device['name'] as String, name: device['name'] as String,
cpuArchitecture: _cpuArchitecture(deviceProperties), cpuArchitecture: _cpuArchitecture(deviceProperties),
interfaceType: interface,
sdkVersion: _sdkVersion(deviceProperties), sdkVersion: _sdkVersion(deviceProperties),
artifacts: globals.artifacts, artifacts: globals.artifacts,
fileSystem: globals.fs, fileSystem: globals.fs,
...@@ -409,10 +413,18 @@ class XCDevice { ...@@ -409,10 +413,18 @@ class XCDevice {
return null; return null;
} }
static bool _isUSBTethered(Map<String, dynamic> deviceProperties) { static IOSDeviceInterface _interfaceType(Map<String, dynamic> deviceProperties) {
// Interface can be "usb", "network", or not present for simulators. // Interface can be "usb", "network", or "none" for simulators
return deviceProperties.containsKey('interface') && // and unknown future interfaces.
(deviceProperties['interface'] as String).toLowerCase() == 'usb'; if (deviceProperties.containsKey('interface')) {
if ((deviceProperties['interface'] as String).toLowerCase() == 'network') {
return IOSDeviceInterface.network;
} else {
return IOSDeviceInterface.usb;
}
}
return IOSDeviceInterface.none;
} }
static String _sdkVersion(Map<String, dynamic> deviceProperties) { static String _sdkVersion(Map<String, dynamic> deviceProperties) {
......
...@@ -72,7 +72,8 @@ void main() { ...@@ -72,7 +72,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
sdkVersion: '13.3', sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64 cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceInterface.usb,
); );
}); });
...@@ -87,7 +88,8 @@ void main() { ...@@ -87,7 +88,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
sdkVersion: '1.0.0' sdkVersion: '1.0.0',
interfaceType: IOSDeviceInterface.usb,
).majorSdkVersion, 1); ).majorSdkVersion, 1);
expect(IOSDevice( expect(IOSDevice(
'device-123', 'device-123',
...@@ -99,7 +101,8 @@ void main() { ...@@ -99,7 +101,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
sdkVersion: '13.1.1' sdkVersion: '13.1.1',
interfaceType: IOSDeviceInterface.usb,
).majorSdkVersion, 13); ).majorSdkVersion, 13);
expect(IOSDevice( expect(IOSDevice(
'device-123', 'device-123',
...@@ -111,7 +114,8 @@ void main() { ...@@ -111,7 +114,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
sdkVersion: '10' sdkVersion: '10',
interfaceType: IOSDeviceInterface.usb,
).majorSdkVersion, 10); ).majorSdkVersion, 10);
expect(IOSDevice( expect(IOSDevice(
'device-123', 'device-123',
...@@ -123,7 +127,8 @@ void main() { ...@@ -123,7 +127,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
sdkVersion: '0' sdkVersion: '0',
interfaceType: IOSDeviceInterface.usb,
).majorSdkVersion, 0); ).majorSdkVersion, 0);
expect(IOSDevice( expect(IOSDevice(
'device-123', 'device-123',
...@@ -135,7 +140,8 @@ void main() { ...@@ -135,7 +140,8 @@ void main() {
iMobileDevice: iMobileDevice, iMobileDevice: iMobileDevice,
name: 'iPhone 1', name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
sdkVersion: 'bogus' sdkVersion: 'bogus',
interfaceType: IOSDeviceInterface.usb,
).majorSdkVersion, 0); ).majorSdkVersion, 0);
}); });
...@@ -154,6 +160,7 @@ void main() { ...@@ -154,6 +160,7 @@ void main() {
name: 'iPhone 1', name: 'iPhone 1',
sdkVersion: '13.3', sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceInterface.usb,
); );
}, },
throwsAssertionError, throwsAssertionError,
...@@ -237,6 +244,7 @@ void main() { ...@@ -237,6 +244,7 @@ void main() {
name: 'iPhone 1', name: 'iPhone 1',
sdkVersion: '13.3', sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceInterface.usb,
); );
logReader1 = createLogReader(device, appPackage1, mockProcess1); logReader1 = createLogReader(device, appPackage1, mockProcess1);
logReader2 = createLogReader(device, appPackage2, mockProcess2); logReader2 = createLogReader(device, appPackage2, mockProcess2);
...@@ -321,8 +329,9 @@ void main() { ...@@ -321,8 +329,9 @@ void main() {
logger: logger, logger: logger,
platform: macPlatform, platform: macPlatform,
fileSystem: MemoryFileSystem.test(), fileSystem: MemoryFileSystem.test(),
interfaceType: IOSDeviceInterface.usb,
); );
when(mockXcdevice.getAvailableTetheredIOSDevices()) when(mockXcdevice.getAvailableIOSDevices())
.thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device])); .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device]));
final List<Device> devices = await iosDevices.pollingGetDevices(); final List<Device> devices = await iosDevices.pollingGetDevices();
......
...@@ -25,7 +25,7 @@ const Map<String, String> kDyLdLibEntry = <String, String>{ ...@@ -25,7 +25,7 @@ const Map<String, String> kDyLdLibEntry = <String, String>{
}; };
void main() { void main() {
testWithoutContext('IOSDevice.installApp calls ios-deploy correctly', () async { testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with USB', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
final IOSApp iosApp = PrebuiltIOSApp( final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app', projectBundleId: 'app',
...@@ -47,6 +47,36 @@ void main() { ...@@ -47,6 +47,36 @@ void main() {
final IOSDevice device = setUpIOSDevice( final IOSDevice device = setUpIOSDevice(
processManager: processManager, processManager: processManager,
fileSystem: fileSystem, fileSystem: fileSystem,
interfaceType: IOSDeviceInterface.usb,
);
final bool wasInstalled = await device.installApp(iosApp);
expect(wasInstalled, true);
expect(processManager.hasRemainingExpectations, false);
});
testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with network', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleDir: fileSystem.currentDirectory,
);
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>[
'ios-deploy',
'--id',
'1234',
'--bundle',
'/',
], environment: <String, String>{
'PATH': '/usr/bin:null',
...kDyLdLibEntry,
})
]);
final IOSDevice device = setUpIOSDevice(
processManager: processManager,
fileSystem: fileSystem,
interfaceType: IOSDeviceInterface.network,
); );
final bool wasInstalled = await device.installApp(iosApp); final bool wasInstalled = await device.installApp(iosApp);
...@@ -237,6 +267,7 @@ IOSDevice setUpIOSDevice({ ...@@ -237,6 +267,7 @@ IOSDevice setUpIOSDevice({
@required ProcessManager processManager, @required ProcessManager processManager,
FileSystem fileSystem, FileSystem fileSystem,
Logger logger, Logger logger,
IOSDeviceInterface interfaceType,
}) { }) {
logger ??= BufferLogger.test(); logger ??= BufferLogger.test();
final FakePlatform platform = FakePlatform( final FakePlatform platform = FakePlatform(
...@@ -270,6 +301,7 @@ IOSDevice setUpIOSDevice({ ...@@ -270,6 +301,7 @@ IOSDevice setUpIOSDevice({
cache: cache, cache: cache,
), ),
artifacts: artifacts, artifacts: artifacts,
interfaceType: interfaceType,
); );
} }
......
...@@ -89,6 +89,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { ...@@ -89,6 +89,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
sdkVersion: '13.3', sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
artifacts: artifacts, artifacts: artifacts,
interfaceType: IOSDeviceInterface.usb,
); );
} }
......
...@@ -329,6 +329,7 @@ IOSDevice setUpIOSDevice({ ...@@ -329,6 +329,7 @@ IOSDevice setUpIOSDevice({
cache: cache, cache: cache,
), ),
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceInterface.usb,
); );
} }
......
...@@ -398,6 +398,7 @@ IOSDevice setUpIOSDevice({ ...@@ -398,6 +398,7 @@ IOSDevice setUpIOSDevice({
cache: cache, cache: cache,
), ),
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceInterface.usb,
); );
} }
......
...@@ -99,7 +99,7 @@ void main() { ...@@ -99,7 +99,7 @@ void main() {
when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2'])) when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2']))
.thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2'])); .thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']));
expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
}); });
testWithoutContext('diagnostics xcdevice fails', () async { testWithoutContext('diagnostics xcdevice fails', () async {
...@@ -359,7 +359,7 @@ void main() { ...@@ -359,7 +359,7 @@ void main() {
testWithoutContext('Xcode not installed', () async { testWithoutContext('Xcode not installed', () async {
when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false); when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
}); });
testUsingContext('returns devices', () async { testUsingContext('returns devices', () async {
...@@ -466,7 +466,7 @@ void main() { ...@@ -466,7 +466,7 @@ void main() {
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'], command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput, stdout: devicesOutput,
)); ));
final List<IOSDevice> devices = await xcdevice.getAvailableTetheredIOSDevices(); final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(3)); expect(devices, hasLength(3));
expect(devices[0].id, 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'); expect(devices[0].id, 'd83d5bc53967baa0ee18626ba87b6254b2ab5418');
expect(devices[0].name, 'An iPhone (Space Gray)'); expect(devices[0].name, 'An iPhone (Space Gray)');
...@@ -496,7 +496,7 @@ void main() { ...@@ -496,7 +496,7 @@ void main() {
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '20'], command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '20'],
stdout: '[]', stdout: '[]',
)); ));
await xcdevice.getAvailableTetheredIOSDevices(timeout: const Duration(seconds: 20)); await xcdevice.getAvailableIOSDevices(timeout: const Duration(seconds: 20));
expect(fakeProcessManager.hasRemainingExpectations, isFalse); expect(fakeProcessManager.hasRemainingExpectations, isFalse);
}); });
...@@ -535,7 +535,7 @@ void main() { ...@@ -535,7 +535,7 @@ void main() {
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'], command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput, stdout: devicesOutput,
)); ));
final List<IOSDevice> devices = await xcdevice.getAvailableTetheredIOSDevices(); final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1)); expect(devices, hasLength(1));
expect(devices[0].id, '43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7'); expect(devices[0].id, '43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7');
expect(fakeProcessManager.hasRemainingExpectations, isFalse); expect(fakeProcessManager.hasRemainingExpectations, isFalse);
...@@ -583,7 +583,7 @@ void main() { ...@@ -583,7 +583,7 @@ void main() {
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'], command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput, stdout: devicesOutput,
)); ));
final List<IOSDevice> devices = await xcdevice.getAvailableTetheredIOSDevices(); final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices[0].cpuArchitecture, DarwinArch.armv7); expect(devices[0].cpuArchitecture, DarwinArch.armv7);
expect(devices[1].cpuArchitecture, DarwinArch.arm64); expect(devices[1].cpuArchitecture, DarwinArch.arm64);
expect(fakeProcessManager.hasRemainingExpectations, isFalse); expect(fakeProcessManager.hasRemainingExpectations, isFalse);
...@@ -634,7 +634,7 @@ void main() { ...@@ -634,7 +634,7 @@ void main() {
stdout: devicesOutput, stdout: devicesOutput,
)); ));
await xcdevice.getAvailableTetheredIOSDevices(); await xcdevice.getAvailableIOSDevices();
final List<String> errors = await xcdevice.getDiagnostics(); final List<String> errors = await xcdevice.getDiagnostics();
expect(errors, hasLength(1)); expect(errors, hasLength(1));
expect(fakeProcessManager.hasRemainingExpectations, isFalse); expect(fakeProcessManager.hasRemainingExpectations, isFalse);
......
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