diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 046f9339bc..ca81c12882 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -436,11 +436,14 @@ class AndroidLicenseValidator extends DoctorValidator { unawaited(process.stdin.addStream(_stdio.stdin) // If the process exits unexpectedly with an error, that will be // handled by the caller. - .catchError((dynamic err, StackTrace stack) { - _logger.printTrace('Echoing stdin to the licenses subprocess failed:'); - _logger.printTrace('$err\n$stack'); - } - )); + .then( + (Object? socket) => socket, + onError: (dynamic err, StackTrace stack) { + _logger.printError('Echoing stdin to the licenses subprocess failed:'); + _logger.printError('$err\n$stack'); + }, + ), + ); // Wait for stdout and stderr to be fully processed, because process.exitCode // may complete first. @@ -450,8 +453,8 @@ class AndroidLicenseValidator extends DoctorValidator { _stdio.addStderrStream(process.stderr), ]); } on Exception catch (err, stack) { - _logger.printTrace('Echoing stdout or stderr from the license subprocess failed:'); - _logger.printTrace('$err\n$stack'); + _logger.printError('Echoing stdout or stderr from the license subprocess failed:'); + _logger.printError('$err\n$stack'); } final int exitCode = await process.exitCode; 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 7a6977cac9..7c705690e5 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 @@ -7,6 +7,7 @@ import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; import 'package:flutter_tools/src/android/android_workflow.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; @@ -311,6 +312,37 @@ Review licenses that have not been accepted (y/N)? expect(licenseValidator.runLicenseManager(), throwsToolExit()); }); + testWithoutContext('runLicenseManager handles broken pipe without ArgumentError', () async { + sdk.sdkManagerPath = '/foo/bar/sdkmanager'; + const String exceptionMessage = 'Write failed (OS Error: Broken pipe, errno = 32), port = 0'; + const SocketException exception = SocketException(exceptionMessage); + // By using a `Socket` generic parameter, the stdin.addStream will return a `Future` + // We are testing that our error handling properly handles futures of this type + final ThrowingStdin fakeStdin = ThrowingStdin(exception); + final FakeCommand licenseCommand = FakeCommand( + command: [sdk.sdkManagerPath!, '--licenses'], + stdin: fakeStdin, + ); + processManager.addCommand(licenseCommand); + final BufferLogger logger = BufferLogger.test(); + + final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator( + androidSdk: sdk, + fileSystem: fileSystem, + processManager: processManager, + platform: FakePlatform(environment: {'HOME': '/home/me'}), + stdio: stdio, + logger: logger, + userMessages: UserMessages(), + androidStudio: FakeAndroidStudio(), + operatingSystemUtils: FakeOperatingSystemUtils(), + ); + + await licenseValidator.runLicenseManager(); + expect(logger.errorText, contains(exceptionMessage)); + expect(processManager, hasNoRemainingExpectations); + }); + testWithoutContext('runLicenseManager errors when sdkmanager fails to run', () async { sdk.sdkManagerPath = '/foo/bar/sdkmanager'; processManager.excludedExecutables.add('/foo/bar/sdkmanager'); @@ -574,3 +606,14 @@ class FakeAndroidStudio extends Fake implements AndroidStudio { @override String get javaPath => 'java'; } + +class ThrowingStdin extends Fake implements IOSink { + ThrowingStdin(this.exception); + + final Exception exception; + + @override + Future addStream(Stream> stream) { + return Future.error(exception); + } +}