Unverified Commit a5d23d2a authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

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

parent 9357e70d
...@@ -258,25 +258,30 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -258,25 +258,30 @@ class AndroidLicenseValidator extends DoctorValidator {
return LicensesAccepted.unknown; return LicensesAccepted.unknown;
} }
final Process process = await runCommand( try {
<String>[androidSdk.sdkManagerPath, '--licenses'], final Process process = await runCommand(
environment: androidSdk.sdkManagerEnv, <String>[androidSdk.sdkManagerPath, '--licenses'],
); environment: androidSdk.sdkManagerEnv,
process.stdin.write('n\n'); );
// We expect logcat streams to occasionally contain invalid utf-8, process.stdin.write('n\n');
// see: https://github.com/flutter/flutter/pull/8864. // We expect logcat streams to occasionally contain invalid utf-8,
final Future<void> output = process.stdout // see: https://github.com/flutter/flutter/pull/8864.
.transform<String>(const Utf8Decoder(reportErrors: false)) final Future<void> output = process.stdout
.transform<String>(const LineSplitter()) .transform<String>(const Utf8Decoder(reportErrors: false))
.listen(_handleLine) .transform<String>(const LineSplitter())
.asFuture<void>(null); .listen(_handleLine)
final Future<void> errors = process.stderr .asFuture<void>(null);
.transform<String>(const Utf8Decoder(reportErrors: false)) final Future<void> errors = process.stderr
.transform<String>(const LineSplitter()) .transform<String>(const Utf8Decoder(reportErrors: false))
.listen(_handleLine) .transform<String>(const LineSplitter())
.asFuture<void>(null); .listen(_handleLine)
await Future.wait<void>(<Future<void>>[output, errors]); .asFuture<void>(null);
return status ?? LicensesAccepted.unknown; 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. /// Run the Android SDK manager tool in order to accept SDK licenses.
...@@ -296,23 +301,29 @@ class AndroidLicenseValidator extends DoctorValidator { ...@@ -296,23 +301,29 @@ class AndroidLicenseValidator extends DoctorValidator {
throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath)); throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
} }
final Process process = await runCommand( try {
<String>[androidSdk.sdkManagerPath, '--licenses'], final Process process = await runCommand(
environment: androidSdk.sdkManagerEnv, <String>[androidSdk.sdkManagerPath, '--licenses'],
); environment: androidSdk.sdkManagerEnv,
);
// The real stdin will never finish streaming. Pipe until the child process
// finishes. // The real stdin will never finish streaming. Pipe until the child process
unawaited(process.stdin.addStream(stdin)); // finishes.
// Wait for stdout and stderr to be fully processed, because process.exitCode unawaited(process.stdin.addStream(stdin));
// may complete first. // Wait for stdout and stderr to be fully processed, because process.exitCode
await waitGroup<void>(<Future<void>>[ // may complete first.
stdout.addStream(process.stdout), await waitGroup<void>(<Future<void>>[
stderr.addStream(process.stderr), 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() { static bool _canRunSdkManager() {
......
...@@ -102,6 +102,10 @@ class UserMessages { ...@@ -102,6 +102,10 @@ class UserMessages {
'Android sdkmanager tool not found ($sdkManagerPath).\n' 'Android sdkmanager tool not found ($sdkManagerPath).\n'
'Try re-installing or updating your Android SDK,\n' 'Try re-installing or updating your Android SDK,\n'
'visit https://flutter.dev/setup/#android-setup for detailed instructions.'; '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) => String androidSdkBuildToolsOutdated(String managerPath, int sdkMinVersion, String buildToolsMinVersion) =>
'Flutter requires Android SDK $sdkMinVersion and the Android BuildTools $buildToolsMinVersion\n' 'Flutter requires Android SDK $sdkMinVersion and the Android BuildTools $buildToolsMinVersion\n'
'To update using sdkmanager, run:\n' 'To update using sdkmanager, run:\n'
......
...@@ -42,8 +42,22 @@ void main() { ...@@ -42,8 +42,22 @@ void main() {
return (List<String> command) => MockProcess(stdout: stdoutStream); 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 { testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
processManager.succeed = false; processManager.runSucceeds = false;
when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted; final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
...@@ -168,7 +182,20 @@ void main() { ...@@ -168,7 +182,20 @@ void main() {
testUsingContext('runLicenseManager errors when sdkmanager is not found', () async { testUsingContext('runLicenseManager errors when sdkmanager is not found', () async {
when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); 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()); expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
......
...@@ -152,11 +152,12 @@ typedef ProcessFactory = Process Function(List<String> command); ...@@ -152,11 +152,12 @@ typedef ProcessFactory = Process Function(List<String> command);
/// A ProcessManager that starts Processes by delegating to a ProcessFactory. /// A ProcessManager that starts Processes by delegating to a ProcessFactory.
class MockProcessManager implements ProcessManager { class MockProcessManager implements ProcessManager {
ProcessFactory processFactory = (List<String> commands) => MockProcess(); ProcessFactory processFactory = (List<String> commands) => MockProcess();
bool succeed = true; bool canRunSucceeds = true;
bool runSucceeds = true;
List<String> commands; List<String> commands;
@override @override
bool canRun(dynamic command, { String workingDirectory }) => succeed; bool canRun(dynamic command, { String workingDirectory }) => canRunSucceeds;
@override @override
Future<Process> start( Future<Process> start(
...@@ -167,7 +168,7 @@ class MockProcessManager implements ProcessManager { ...@@ -167,7 +168,7 @@ class MockProcessManager implements ProcessManager {
bool runInShell = false, bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal, ProcessStartMode mode = ProcessStartMode.normal,
}) { }) {
if (!succeed) { if (!runSucceeds) {
final String executable = command[0]; final String executable = command[0];
final List<String> arguments = command.length > 1 ? command.sublist(1) : <String>[]; final List<String> arguments = command.length > 1 ? command.sublist(1) : <String>[];
throw ProcessException(executable, arguments); throw ProcessException(executable, arguments);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment