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 {
@override
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 {
if (!xcode.isInstalled)
if (!iMobileDevice.isInstalled)
return <IOSDevice>[];
final List<IOSDevice> devices = <IOSDevice>[];
final Iterable<String> deviceLines = (await xcode.getAvailableDevices())
.split('\n')
.map((String line) => line.trim());
for (String line in deviceLines) {
final Match match = _deviceRegex.firstMatch(line);
if (match != null) {
final String deviceName = match.group(1);
final String sdkVersion = match.group(2);
final String deviceID = match.group(3);
devices.add(new IOSDevice(deviceID, name: deviceName, sdkVersion: sdkVersion));
}
for (String id in (await iMobileDevice.getAvailableDeviceIDs()).split('\n')) {
id = id.trim();
if (id.isEmpty)
continue;
final String deviceName = await iMobileDevice.getInfoForDevice(id, 'DeviceName');
final String sdkVersion = await iMobileDevice.getInfoForDevice(id, 'ProductVersion');
devices.add(new IOSDevice(id, name: deviceName, sdkVersion: sdkVersion));
}
return devices;
}
......
......@@ -70,6 +70,28 @@ class IMobileDevice {
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.
Future<Process> startLogger() => runCommand(<String>['idevicesyslog']);
......@@ -164,48 +186,6 @@ class Xcode {
return false;
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) {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
......@@ -40,6 +41,19 @@ void main() {
}
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
ProcessResult runSync(
List<dynamic> command, {
......
......@@ -29,40 +29,38 @@ void main() {
osx.operatingSystem = 'macos';
group('getAttachedDevices', () {
MockXcode mockXcode;
MockIMobileDevice mockIMobileDevice;
setUp(() {
mockXcode = new MockXcode();
mockIMobileDevice = new MockIMobileDevice();
});
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);
}, overrides: <Type, Generator>{
Xcode: () => mockXcode,
IMobileDevice: () => mockIMobileDevice,
});
testUsingContext('returns no devices if none are attached', () async {
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.getAvailableDevices()).thenReturn(new Future<String>.value(''));
when(iMobileDevice.isInstalled).thenReturn(true);
when(iMobileDevice.getAvailableDeviceIDs()).thenReturn(new Future<String>.value(''));
final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
expect(devices, isEmpty);
}, overrides: <Type, Generator>{
Xcode: () => mockXcode,
IMobileDevice: () => mockIMobileDevice,
});
testUsingContext('returns attached devices', () async {
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.getAvailableDevices()).thenReturn(new Future<String>.value('''
Known Devices:
je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77]
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.isInstalled).thenReturn(true);
when(iMobileDevice.getAvailableDeviceIDs()).thenReturn(new Future<String>.value('''
98206e7a4afd4aedaff06e687594e089dede3c44
f577a7903cc54959be2e34bc4f7f80b7009efcf4
'''));
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();
expect(devices, hasLength(2));
expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
......@@ -70,7 +68,7 @@ iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator)
expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[1].name, 'Puits sans fond');
}, overrides: <Type, Generator>{
Xcode: () => mockXcode,
IMobileDevice: () => mockIMobileDevice,
});
});
......
......@@ -22,8 +22,37 @@ class MockFile extends Mock implements File {}
void main() {
group('IMobileDevice', () {
final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform());
osx.operatingSystem = 'macos';
final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform())
..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', () {
final String outputPath = fs.path.join('some', 'test', 'path', 'image.png');
......@@ -68,7 +97,6 @@ void main() {
group('Xcode', () {
MockProcessManager mockProcessManager;
final FakePlatform fakePlatform = new FakePlatform(environment: <String, String>{'USER': 'rwaters'});
Xcode xcode;
setUp(() {
......@@ -212,69 +240,6 @@ void main() {
}, overrides: <Type, Generator>{
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', () {
......
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