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