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.
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 5d311d1..8802c80 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -71,30 +71,19 @@
   @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;
   }
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index e6460ae..c33a696 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -70,6 +70,28 @@
     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 @@
       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) {
diff --git a/packages/flutter_tools/test/commands/devices_test.dart b/packages/flutter_tools/test/commands/devices_test.dart
index 3fe7c57..262c473 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';
 
@@ -41,6 +42,19 @@
 
 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, {
         String workingDirectory,
diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart
index 13a085f..844a24c 100644
--- a/packages/flutter_tools/test/ios/devices_test.dart
+++ b/packages/flutter_tools/test/ios/devices_test.dart
@@ -29,40 +29,38 @@
   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 @@
       expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
       expect(devices[1].name, 'Puits sans fond');
     }, overrides: <Type, Generator>{
-      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 229504b..1681b66 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -22,8 +22,37 @@
 
 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 @@
 
   group('Xcode', () {
     MockProcessManager mockProcessManager;
-    final FakePlatform fakePlatform = new FakePlatform(environment: <String, String>{'USER': 'rwaters'});
     Xcode xcode;
 
     setUp(() {
@@ -212,69 +240,6 @@
     }, 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', () {