Commit eba6ceb8 authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Use idevice_id, ideviceinfo for iOS device listing (#11883)

This patch migrates iOS device listing from using Xcode instruments to
using the libimobiledevice tools idevice_id and ideviceinfo.

ideviceinfo was previously incompatible with iOS 11 physical devices;
this has now been fixed.

In 37bb5f13 flutter_tools migrated from
libimobiledevice-based device listing on iOS to using Xcode instruments
to work around the lack of support for iOS 11. Using instruments entails
several downsides, including a significantly higher performance hit, and
leaking hung DTServiceHub processes in certain cases when a simulator is
running, necessitating workarounds in which we watched for, and cleaned
up leaked DTServiceHub processes. This patch returns reverts the move to
instruments now that it's no longer necessary.
parent 0793add8
...@@ -71,30 +71,19 @@ class IOSDevice extends Device { ...@@ -71,30 +71,19 @@ class IOSDevice extends Device {
@override @override
bool get supportsStartPaused => false; bool get supportsStartPaused => false;
// Physical device line format to be matched:
// My iPhone (10.3.2) [75b90e947c5f429fa67f3e9169fda0d89f0492f1]
//
// Other formats in output (desktop, simulator) to be ignored:
// my-mac-pro [2C10513E-4dA5-405C-8EF5-C44353DB3ADD]
// iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator)
static final RegExp _deviceRegex = new RegExp(r'^(.*) +\((.*)\) +\[(.*)\]$');
static Future<List<IOSDevice>> getAttachedDevices() async { static Future<List<IOSDevice>> getAttachedDevices() async {
if (!xcode.isInstalled) if (!iMobileDevice.isInstalled)
return <IOSDevice>[]; return <IOSDevice>[];
final List<IOSDevice> devices = <IOSDevice>[]; final List<IOSDevice> devices = <IOSDevice>[];
final Iterable<String> deviceLines = (await xcode.getAvailableDevices()) for (String id in (await iMobileDevice.getAvailableDeviceIDs()).split('\n')) {
.split('\n') id = id.trim();
.map((String line) => line.trim()); if (id.isEmpty)
for (String line in deviceLines) { continue;
final Match match = _deviceRegex.firstMatch(line);
if (match != null) { final String deviceName = await iMobileDevice.getInfoForDevice(id, 'DeviceName');
final String deviceName = match.group(1); final String sdkVersion = await iMobileDevice.getInfoForDevice(id, 'ProductVersion');
final String sdkVersion = match.group(2); devices.add(new IOSDevice(id, name: deviceName, sdkVersion: sdkVersion));
final String deviceID = match.group(3);
devices.add(new IOSDevice(deviceID, name: deviceName, sdkVersion: sdkVersion));
}
} }
return devices; return devices;
} }
......
...@@ -70,6 +70,28 @@ class IMobileDevice { ...@@ -70,6 +70,28 @@ class IMobileDevice {
return await exitsHappyAsync(<String>['idevicename']); return await exitsHappyAsync(<String>['idevicename']);
} }
Future<String> getAvailableDeviceIDs() async {
try {
final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
if (result.exitCode != 0)
throw new ToolExit('idevice_id returned an error:\n${result.stderr}');
return result.stdout;
} on ProcessException {
throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
}
}
Future<String> getInfoForDevice(String deviceID, String key) async {
try {
final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key,]);
if (result.exitCode != 0)
throw new ToolExit('idevice_id returned an error:\n${result.stderr}');
return result.stdout.trim();
} on ProcessException {
throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
}
}
/// Starts `idevicesyslog` and returns the running process. /// Starts `idevicesyslog` and returns the running process.
Future<Process> startLogger() => runCommand(<String>['idevicesyslog']); Future<Process> startLogger() => runCommand(<String>['idevicesyslog']);
...@@ -164,48 +186,6 @@ class Xcode { ...@@ -164,48 +186,6 @@ class Xcode {
return false; return false;
return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion); return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion);
} }
final RegExp _processRegExp = new RegExp(r'^(\S+)\s+1\s+(\d+)\s+(.+)$');
/// Kills any orphaned Instruments processes belonging to the user.
///
/// In some cases, we've seen interactions between Instruments and the iOS
/// simulator that cause hung instruments and DTServiceHub processes. If
/// enough instances pile up, the host machine eventually becomes
/// unresponsive. Until the underlying issue is resolved, manually kill any
/// orphaned instances (where the parent process has died and PPID is 1)
/// before launching another instruments run.
Future<Null> _killOrphanedInstrumentsProcesses() async {
final ProcessResult result = await processManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']);
if (result.exitCode != 0)
return;
for (String line in result.stdout.split('\n')) {
final Match match = _processRegExp.firstMatch(line.trim());
if (match == null || match[1] != platform.environment['USER'])
continue;
if (<String>['/instruments', '/DTServiceHub'].any(match[3].endsWith)) {
try {
printTrace('Killing orphaned Instruments process: ${match[2]}');
processManager.killPid(int.parse(match[2]));
} catch (_) {
printTrace('Failed to kill orphaned Instruments process:\n$line');
}
}
}
}
Future<String> getAvailableDevices() async {
await _killOrphanedInstrumentsProcesses();
try {
final ProcessResult result = await processManager.run(
<String>['/usr/bin/instruments', '-s', 'devices']);
if (result.exitCode != 0)
throw new ToolExit('/usr/bin/instruments returned an error:\n${result.stderr}');
return result.stdout;
} on ProcessException {
throw new ToolExit('Failed to invoke /usr/bin/instruments. Is Xcode installed?');
}
}
} }
bool _xcodeVersionCheckValid(int major, int minor) { bool _xcodeVersionCheckValid(int major, int minor) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
...@@ -40,6 +41,19 @@ void main() { ...@@ -40,6 +41,19 @@ void main() {
} }
class MockProcessManager extends Mock implements ProcessManager { class MockProcessManager extends Mock implements ProcessManager {
@override
Future<ProcessResult> run(
List<dynamic> command, {
String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment: true,
bool runInShell: false,
Encoding stdoutEncoding: SYSTEM_ENCODING,
Encoding stderrEncoding: SYSTEM_ENCODING,
}) async {
return new ProcessResult(0, 0, '', '');
}
@override @override
ProcessResult runSync( ProcessResult runSync(
List<dynamic> command, { List<dynamic> command, {
......
...@@ -29,40 +29,38 @@ void main() { ...@@ -29,40 +29,38 @@ void main() {
osx.operatingSystem = 'macos'; osx.operatingSystem = 'macos';
group('getAttachedDevices', () { group('getAttachedDevices', () {
MockXcode mockXcode; MockIMobileDevice mockIMobileDevice;
setUp(() { setUp(() {
mockXcode = new MockXcode(); mockIMobileDevice = new MockIMobileDevice();
}); });
testUsingContext('return no devices if Xcode is not installed', () async { testUsingContext('return no devices if Xcode is not installed', () async {
when(mockXcode.isInstalled).thenReturn(false); when(mockIMobileDevice.isInstalled).thenReturn(false);
expect(await IOSDevice.getAttachedDevices(), isEmpty); expect(await IOSDevice.getAttachedDevices(), isEmpty);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Xcode: () => mockXcode, IMobileDevice: () => mockIMobileDevice,
}); });
testUsingContext('returns no devices if none are attached', () async { testUsingContext('returns no devices if none are attached', () async {
when(mockXcode.isInstalled).thenReturn(true); when(iMobileDevice.isInstalled).thenReturn(true);
when(mockXcode.getAvailableDevices()).thenReturn(new Future<String>.value('')); when(iMobileDevice.getAvailableDeviceIDs()).thenReturn(new Future<String>.value(''));
final List<IOSDevice> devices = await IOSDevice.getAttachedDevices(); final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
expect(devices, isEmpty); expect(devices, isEmpty);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Xcode: () => mockXcode, IMobileDevice: () => mockIMobileDevice,
}); });
testUsingContext('returns attached devices', () async { testUsingContext('returns attached devices', () async {
when(mockXcode.isInstalled).thenReturn(true); when(iMobileDevice.isInstalled).thenReturn(true);
when(mockXcode.getAvailableDevices()).thenReturn(new Future<String>.value(''' when(iMobileDevice.getAvailableDeviceIDs()).thenReturn(new Future<String>.value('''
Known Devices: 98206e7a4afd4aedaff06e687594e089dede3c44
je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77] f577a7903cc54959be2e34bc4f7f80b7009efcf4
La tele me regarde (10.3.2) [98206e7a4afd4aedaff06e687594e089dede3c44]
Puits sans fond (10.3.2) [f577a7903cc54959be2e34bc4f7f80b7009efcf4]
iPhone 6 Plus (9.3) [FBA880E6-4020-49A5-8083-DCD50CA5FA09] (Simulator)
iPhone 6s (11.0) [E805F496-FC6A-4EA4-92FF-B7901FF4E7CC] (Simulator)
iPhone 7 (11.0) + Apple Watch Series 2 - 38mm (4.0) [60027FDD-4A7A-42BF-978F-C2209D27AD61] (Simulator)
iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator)
''')); '''));
when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')).thenReturn('La tele me regarde');
when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'ProductVersion')).thenReturn('10.3.2');
when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')).thenReturn('Puits sans fond');
when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'ProductVersion')).thenReturn('11.0');
final List<IOSDevice> devices = await IOSDevice.getAttachedDevices(); final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
expect(devices, hasLength(2)); expect(devices, hasLength(2));
expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
...@@ -70,7 +68,7 @@ iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator) ...@@ -70,7 +68,7 @@ iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator)
expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[1].name, 'Puits sans fond'); expect(devices[1].name, 'Puits sans fond');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Xcode: () => mockXcode, IMobileDevice: () => mockIMobileDevice,
}); });
}); });
......
...@@ -22,8 +22,37 @@ class MockFile extends Mock implements File {} ...@@ -22,8 +22,37 @@ class MockFile extends Mock implements File {}
void main() { void main() {
group('IMobileDevice', () { group('IMobileDevice', () {
final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform()); final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform())
osx.operatingSystem = 'macos'; ..operatingSystem = 'macos';
MockProcessManager mockProcessManager;
setUp(() {
mockProcessManager = new MockProcessManager();
});
testUsingContext('getAvailableDeviceIDs throws ToolExit when libimobiledevice is not installed', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenThrow(const ProcessException('idevice_id', const <String>['-l']));
expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenReturn(new ProcessResult(1, 1, '', 'Sad today'));
expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenReturn(new ProcessResult(1, 0, 'foo', ''));
expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo');
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
group('screenshot', () { group('screenshot', () {
final String outputPath = fs.path.join('some', 'test', 'path', 'image.png'); final String outputPath = fs.path.join('some', 'test', 'path', 'image.png');
...@@ -68,7 +97,6 @@ void main() { ...@@ -68,7 +97,6 @@ void main() {
group('Xcode', () { group('Xcode', () {
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
final FakePlatform fakePlatform = new FakePlatform(environment: <String, String>{'USER': 'rwaters'});
Xcode xcode; Xcode xcode;
setUp(() { setUp(() {
...@@ -212,69 +240,6 @@ void main() { ...@@ -212,69 +240,6 @@ void main() {
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,
}); });
testUsingContext('getAvailableDevices throws ToolExit when instruments is not installed', () async {
when(mockProcessManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']))
.thenReturn(new ProcessResult(1, 0, '', ''));
when(mockProcessManager.run(<String>['/usr/bin/instruments', '-s', 'devices']))
.thenThrow(const ProcessException('/usr/bin/instruments', const <String>['-s', 'devices']));
expect(() async => await xcode.getAvailableDevices(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDevices throws ToolExit when instruments returns non-zero', () async {
when(mockProcessManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']))
.thenReturn(new ProcessResult(1, 0, '', ''));
when(mockProcessManager.run(<String>['/usr/bin/instruments', '-s', 'devices']))
.thenReturn(new ProcessResult(1, 1, '', 'Sad today'));
expect(() async => await xcode.getAvailableDevices(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDevices returns instruments output when installed', () async {
when(mockProcessManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']))
.thenReturn(new ProcessResult(1, 0, '', ''));
when(mockProcessManager.run(<String>['/usr/bin/instruments', '-s', 'devices']))
.thenReturn(new ProcessResult(1, 0, 'Known Devices:\niPhone 6s (10.3.3) [foo]', ''));
expect(await xcode.getAvailableDevices(), 'Known Devices:\niPhone 6s (10.3.3) [foo]');
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDevices works even if orphan listing fails', () async {
when(mockProcessManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']))
.thenReturn(new ProcessResult(1, 1, '', ''));
when(mockProcessManager.run(<String>['/usr/bin/instruments', '-s', 'devices']))
.thenReturn(new ProcessResult(1, 0, 'Known Devices:\niPhone 6s (10.3.3) [foo]', ''));
expect(await xcode.getAvailableDevices(), 'Known Devices:\niPhone 6s (10.3.3) [foo]');
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('getAvailableDevices cleans up orphaned intstruments processes', () async {
when(mockProcessManager.run(<String>['ps', '-e', '-o', 'user,ppid,pid,comm']))
.thenReturn(new ProcessResult(1, 0, '''
USER PPID PID COMM
rwaters 1 36580 /Applications/Xcode.app/Contents/Developer/usr/bin/make
rwaters 36579 36581 /Applications/Xcode.app/Contents/Developer/usr/bin/instruments
rwaters 1 36582 /Applications/Xcode.app/Contents/Developer/usr/bin/instruments
rwaters 1 36583 /Applications/Xcode.app/Contents/SharedFrameworks/DVTInstrumentsFoundation.framework/Resources/DTServiceHub
rwaters 36581 36584 /Applications/Xcode.app/Contents/SharedFrameworks/DVTInstrumentsFoundation.framework/Resources/DTServiceHub
''', ''));
when(mockProcessManager.run(<String>['/usr/bin/instruments', '-s', 'devices']))
.thenReturn(new ProcessResult(1, 0, 'Known Devices:\niPhone 6s (10.3.3) [foo]', ''));
await xcode.getAvailableDevices();
verify(mockProcessManager.killPid(36582));
verify(mockProcessManager.killPid(36583));
verifyNever(mockProcessManager.killPid(36580));
verifyNever(mockProcessManager.killPid(36581));
verifyNever(mockProcessManager.killPid(36584));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => fakePlatform,
});
}); });
group('Diagnose Xcode build failure', () { group('Diagnose Xcode build failure', () {
......
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