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 37bb5f1300e67fe590c44bb9ecda653b2967e347 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.
This commit is contained in:
parent
0793add818
commit
eba6ceb85d
@ -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', () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user