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>[]); } }