[flutter_tool] More gracefully handle Android sdkmanager failure (#37194)

diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 780e382..8f0a852 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -258,25 +258,30 @@
       return LicensesAccepted.unknown;
     }
 
-    final Process process = await runCommand(
-      <String>[androidSdk.sdkManagerPath, '--licenses'],
-      environment: androidSdk.sdkManagerEnv,
-    );
-    process.stdin.write('n\n');
-    // We expect logcat streams to occasionally contain invalid utf-8,
-    // see: https://github.com/flutter/flutter/pull/8864.
-    final Future<void> output = process.stdout
-      .transform<String>(const Utf8Decoder(reportErrors: false))
-      .transform<String>(const LineSplitter())
-      .listen(_handleLine)
-      .asFuture<void>(null);
-    final Future<void> errors = process.stderr
-      .transform<String>(const Utf8Decoder(reportErrors: false))
-      .transform<String>(const LineSplitter())
-      .listen(_handleLine)
-      .asFuture<void>(null);
-    await Future.wait<void>(<Future<void>>[output, errors]);
-    return status ?? LicensesAccepted.unknown;
+    try {
+      final Process process = await runCommand(
+        <String>[androidSdk.sdkManagerPath, '--licenses'],
+        environment: androidSdk.sdkManagerEnv,
+      );
+      process.stdin.write('n\n');
+      // We expect logcat streams to occasionally contain invalid utf-8,
+      // see: https://github.com/flutter/flutter/pull/8864.
+      final Future<void> output = process.stdout
+        .transform<String>(const Utf8Decoder(reportErrors: false))
+        .transform<String>(const LineSplitter())
+        .listen(_handleLine)
+        .asFuture<void>(null);
+      final Future<void> errors = process.stderr
+        .transform<String>(const Utf8Decoder(reportErrors: false))
+        .transform<String>(const LineSplitter())
+        .listen(_handleLine)
+        .asFuture<void>(null);
+      await Future.wait<void>(<Future<void>>[output, errors]);
+      return status ?? LicensesAccepted.unknown;
+    } on ProcessException catch (e) {
+      printTrace('Failed to run Android sdk manager: $e');
+      return LicensesAccepted.unknown;
+    }
   }
 
   /// Run the Android SDK manager tool in order to accept SDK licenses.
@@ -296,23 +301,29 @@
       throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
     }
 
-    final Process process = await runCommand(
-      <String>[androidSdk.sdkManagerPath, '--licenses'],
-      environment: androidSdk.sdkManagerEnv,
-    );
+    try {
+      final Process process = await runCommand(
+        <String>[androidSdk.sdkManagerPath, '--licenses'],
+        environment: androidSdk.sdkManagerEnv,
+      );
 
-    // The real stdin will never finish streaming. Pipe until the child process
-    // finishes.
-    unawaited(process.stdin.addStream(stdin));
-    // Wait for stdout and stderr to be fully processed, because process.exitCode
-    // may complete first.
-    await waitGroup<void>(<Future<void>>[
-      stdout.addStream(process.stdout),
-      stderr.addStream(process.stderr),
-    ]);
+      // The real stdin will never finish streaming. Pipe until the child process
+      // finishes.
+      unawaited(process.stdin.addStream(stdin));
+      // Wait for stdout and stderr to be fully processed, because process.exitCode
+      // may complete first.
+      await waitGroup<void>(<Future<void>>[
+        stdout.addStream(process.stdout),
+        stderr.addStream(process.stderr),
+      ]);
 
-    final int exitCode = await process.exitCode;
-    return exitCode == 0;
+      final int exitCode = await process.exitCode;
+      return exitCode == 0;
+    } on ProcessException catch (e) {
+      throwToolExit(userMessages.androidCannotRunSdkManager(
+          androidSdk.sdkManagerPath, e.toString()));
+      return false;
+    }
   }
 
   static bool _canRunSdkManager() {
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 0f359b5..1764483 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -102,6 +102,10 @@
       'Android sdkmanager tool not found ($sdkManagerPath).\n'
       'Try re-installing or updating your Android SDK,\n'
       'visit https://flutter.dev/setup/#android-setup for detailed instructions.';
+  String androidCannotRunSdkManager(String sdkManagerPath, String error) =>
+      'Android sdkmanager tool was found, but failed to run ($sdkManagerPath): "$error".\n'
+      'Try re-installing or updating your Android SDK,\n'
+      'visit https://flutter.dev/setup/#android-setup for detailed instructions.';
   String androidSdkBuildToolsOutdated(String managerPath, int sdkMinVersion, String buildToolsMinVersion) =>
       'Flutter requires Android SDK $sdkMinVersion and the Android BuildTools $buildToolsMinVersion\n'
       'To update using sdkmanager, run:\n'
diff --git a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart
index 3ebd229..e2ddec5 100644
--- a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart
@@ -42,8 +42,22 @@
     return (List<String> command) => MockProcess(stdout: stdoutStream);
   }
 
+  testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot find sdkmanager', () async {
+    processManager.canRunSucceeds = false;
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
+    expect(licenseStatus, LicensesAccepted.unknown);
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
   testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
-    processManager.succeed = false;
+    processManager.runSucceeds = false;
     when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
     final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
     final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
@@ -168,7 +182,20 @@
 
   testUsingContext('runLicenseManager errors when sdkmanager is not found', () async {
     when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
-    processManager.succeed = false;
+    processManager.canRunSucceeds = false;
+
+    expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit());
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('runLicenseManager errors when sdkmanager fails to run', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    processManager.runSucceeds = false;
 
     expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit());
   }, overrides: <Type, Generator>{
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index fa3b7a6..d9aed18 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -152,11 +152,12 @@
 /// A ProcessManager that starts Processes by delegating to a ProcessFactory.
 class MockProcessManager implements ProcessManager {
   ProcessFactory processFactory = (List<String> commands) => MockProcess();
-  bool succeed = true;
+  bool canRunSucceeds = true;
+  bool runSucceeds = true;
   List<String> commands;
 
   @override
-  bool canRun(dynamic command, { String workingDirectory }) => succeed;
+  bool canRun(dynamic command, { String workingDirectory }) => canRunSucceeds;
 
   @override
   Future<Process> start(
@@ -167,7 +168,7 @@
     bool runInShell = false,
     ProcessStartMode mode = ProcessStartMode.normal,
   }) {
-    if (!succeed) {
+    if (!runSucceeds) {
       final String executable = command[0];
       final List<String> arguments = command.length > 1 ? command.sublist(1) : <String>[];
       throw ProcessException(executable, arguments);