Catch FormatException caused by bad simctl output (#37958)

diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index ecf9bf5..2d5db2b 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -46,11 +46,12 @@
   /// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
   static IOSSimulatorUtils get instance => context.get<IOSSimulatorUtils>();
 
-  List<IOSSimulator> getAttachedDevices() {
+  Future<List<IOSSimulator>> getAttachedDevices() async {
     if (!xcode.isInstalledAndMeetsVersionCheck)
       return <IOSSimulator>[];
 
-    return SimControl.instance.getConnectedDevices().map<IOSSimulator>((SimDevice device) {
+    final List<SimDevice> connected = await SimControl.instance.getConnectedDevices();
+    return connected.map<IOSSimulator>((SimDevice device) {
       return IOSSimulator(device.udid, name: device.name, simulatorCategory: device.category);
     }).toList();
   }
@@ -63,7 +64,7 @@
 
   /// Runs `simctl list --json` and returns the JSON of the corresponding
   /// [section].
-  Map<String, dynamic> _list(SimControlListSection section) {
+  Future<Map<String, dynamic>> _list(SimControlListSection section) async {
     // Sample output from `simctl list --json`:
     //
     // {
@@ -83,20 +84,27 @@
 
     final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
     printTrace(command.join(' '));
-    final ProcessResult results = processManager.runSync(command);
+    final ProcessResult results = await processManager.run(command);
     if (results.exitCode != 0) {
       printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
       return <String, Map<String, dynamic>>{};
     }
-
-    return json.decode(results.stdout)[section.name];
+    try {
+      return json.decode(results.stdout)[section.name];
+    } on FormatException {
+      // We failed to parse the simctl output, or it returned junk.
+      // One known message is "Install Started" isn't valid JSON but is
+      // returned sometimes.
+      printError('simctl returned non-JSON response: ${results.stdout}');
+      return <String, dynamic>{};
+    }
   }
 
   /// Returns a list of all available devices, both potential and connected.
-  List<SimDevice> getDevices() {
+  Future<List<SimDevice>> getDevices() async {
     final List<SimDevice> devices = <SimDevice>[];
 
-    final Map<String, dynamic> devicesSection = _list(SimControlListSection.devices);
+    final Map<String, dynamic> devicesSection = await _list(SimControlListSection.devices);
 
     for (String deviceCategory in devicesSection.keys) {
       final List<dynamic> devicesData = devicesSection[deviceCategory];
@@ -109,8 +117,9 @@
   }
 
   /// Returns all the connected simulator devices.
-  List<SimDevice> getConnectedDevices() {
-    return getDevices().where((SimDevice device) => device.isBooted).toList();
+  Future<List<SimDevice>> getConnectedDevices() async {
+    final List<SimDevice> simDevices = await getDevices();
+    return simDevices.where((SimDevice device) => device.isBooted).toList();
   }
 
   Future<bool> isInstalled(String deviceId, String appId) {
diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
index 59aca13..bafc54c 100644
--- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
@@ -390,14 +390,15 @@
 
     setUp(() {
       mockProcessManager = MockProcessManager();
-      when(mockProcessManager.runSync(any))
-          .thenReturn(ProcessResult(mockPid, 0, validSimControlOutput, ''));
+      when(mockProcessManager.run(any)).thenAnswer((Invocation _) async {
+        return ProcessResult(mockPid, 0, validSimControlOutput, '');
+      });
 
       simControl = SimControl();
     });
 
-    testUsingContext('getDevices succeeds', () {
-      final List<SimDevice> devices = simControl.getDevices();
+    testUsingContext('getDevices succeeds', () async {
+      final List<SimDevice> devices = await simControl.getDevices();
 
       final SimDevice watch = devices[0];
       expect(watch.category, 'watchOS 4.3');
@@ -426,6 +427,17 @@
       ProcessManager: () => mockProcessManager,
       SimControl: () => simControl,
     });
+
+    testUsingContext('getDevices handles bad simctl output', () async {
+      when(mockProcessManager.run(any))
+          .thenAnswer((Invocation _) async => ProcessResult(mockPid, 0, 'Install Started', ''));
+      final List<SimDevice> devices = await simControl.getDevices();
+
+      expect(devices, isEmpty);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      SimControl: () => simControl,
+    });
   });
 
   group('startApp', () {
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 2cafbcd..996c210 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -77,7 +77,7 @@
           HttpClient: () => MockHttpClient(),
           IOSSimulatorUtils: () {
             final MockIOSSimulatorUtils mock = MockIOSSimulatorUtils();
-            when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
+            when(mock.getAttachedDevices()).thenAnswer((Invocation _) async => <IOSSimulator>[]);
             return mock;
           },
           OutputPreferences: () => OutputPreferences(showColor: false),
@@ -227,7 +227,7 @@
 
 class MockSimControl extends Mock implements SimControl {
   MockSimControl() {
-    when(getConnectedDevices()).thenReturn(<SimDevice>[]);
+    when(getConnectedDevices()).thenAnswer((Invocation _) async => <SimDevice>[]);
   }
 }