Clean up orphaned Instruments processes (#11189)

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.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 8fb6ef1..87c9f15 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -165,7 +165,37 @@
     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']);
diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart
index 01d891e..229504b 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -21,10 +21,10 @@
 class MockFile extends Mock implements File {}
 
 void main() {
-  final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform());
-  osx.operatingSystem = 'macos';
-
   group('IMobileDevice', () {
+    final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform());
+    osx.operatingSystem = 'macos';
+
     group('screenshot', () {
       final String outputPath = fs.path.join('some', 'test', 'path', 'image.png');
       MockProcessManager mockProcessManager;
@@ -68,6 +68,7 @@
 
   group('Xcode', () {
     MockProcessManager mockProcessManager;
+    final FakePlatform fakePlatform = new FakePlatform(environment: <String, String>{'USER': 'rwaters'});
     Xcode xcode;
 
     setUp(() {
@@ -213,6 +214,8 @@
     });
 
     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());
@@ -221,6 +224,8 @@
     });
 
     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());
@@ -229,12 +234,47 @@
     });
 
     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', () {