diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 5d311d1c76..8802c800a9 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -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> getAttachedDevices() async { - if (!xcode.isInstalled) + if (!iMobileDevice.isInstalled) return []; final List devices = []; - final Iterable 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; } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index e6460aeaa9..c33a69641a 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -70,6 +70,28 @@ class IMobileDevice { return await exitsHappyAsync(['idevicename']); } + Future getAvailableDeviceIDs() async { + try { + final ProcessResult result = await processManager.run(['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 getInfoForDevice(String deviceID, String key) async { + try { + final ProcessResult result = await processManager.run(['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 startLogger() => runCommand(['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 _killOrphanedInstrumentsProcesses() async { - final ProcessResult result = await processManager.run(['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 (['/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 getAvailableDevices() async { - await _killOrphanedInstrumentsProcesses(); - try { - final ProcessResult result = await processManager.run( - ['/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) { diff --git a/packages/flutter_tools/test/commands/devices_test.dart b/packages/flutter_tools/test/commands/devices_test.dart index 3fe7c571fe..262c4730b5 100644 --- a/packages/flutter_tools/test/commands/devices_test.dart +++ b/packages/flutter_tools/test/commands/devices_test.dart @@ -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 run( + List command, { + String workingDirectory, + Map 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 command, { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 13a085f624..844a24c5cb 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -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: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); testUsingContext('returns no devices if none are attached', () async { - when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(new Future.value('')); + when(iMobileDevice.isInstalled).thenReturn(true); + when(iMobileDevice.getAvailableDeviceIDs()).thenReturn(new Future.value('')); final List devices = await IOSDevice.getAttachedDevices(); expect(devices, isEmpty); }, overrides: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); testUsingContext('returns attached devices', () async { - when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(new Future.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.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 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: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); }); diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart index 229504b8ef..1681b66d3a 100644 --- a/packages/flutter_tools/test/ios/mac_test.dart +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -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(['idevice_id', '-l'])) + .thenThrow(const ProcessException('idevice_id', const ['-l'])); + expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit()); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async { + when(mockProcessManager.run(['idevice_id', '-l'])) + .thenReturn(new ProcessResult(1, 1, '', 'Sad today')); + expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit()); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async { + when(mockProcessManager.run(['idevice_id', '-l'])) + .thenReturn(new ProcessResult(1, 0, 'foo', '')); + expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo'); + }, overrides: { + 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: {'USER': 'rwaters'}); Xcode xcode; setUp(() { @@ -212,69 +240,6 @@ void main() { }, overrides: { ProcessManager: () => mockProcessManager, }); - - testUsingContext('getAvailableDevices throws ToolExit when instruments is not installed', () async { - when(mockProcessManager.run(['ps', '-e', '-o', 'user,ppid,pid,comm'])) - .thenReturn(new ProcessResult(1, 0, '', '')); - when(mockProcessManager.run(['/usr/bin/instruments', '-s', 'devices'])) - .thenThrow(const ProcessException('/usr/bin/instruments', const ['-s', 'devices'])); - expect(() async => await xcode.getAvailableDevices(), throwsToolExit()); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('getAvailableDevices throws ToolExit when instruments returns non-zero', () async { - when(mockProcessManager.run(['ps', '-e', '-o', 'user,ppid,pid,comm'])) - .thenReturn(new ProcessResult(1, 0, '', '')); - when(mockProcessManager.run(['/usr/bin/instruments', '-s', 'devices'])) - .thenReturn(new ProcessResult(1, 1, '', 'Sad today')); - expect(() async => await xcode.getAvailableDevices(), throwsToolExit()); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('getAvailableDevices returns instruments output when installed', () async { - when(mockProcessManager.run(['ps', '-e', '-o', 'user,ppid,pid,comm'])) - .thenReturn(new ProcessResult(1, 0, '', '')); - when(mockProcessManager.run(['/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: { - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('getAvailableDevices works even if orphan listing fails', () async { - when(mockProcessManager.run(['ps', '-e', '-o', 'user,ppid,pid,comm'])) - .thenReturn(new ProcessResult(1, 1, '', '')); - when(mockProcessManager.run(['/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: { - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('getAvailableDevices cleans up orphaned intstruments processes', () async { - when(mockProcessManager.run(['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(['/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: { - ProcessManager: () => mockProcessManager, - Platform: () => fakePlatform, - }); }); group('Diagnose Xcode build failure', () {