[flutter_tools] Handle errors on the std{out,err}.done future (#51660)
diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index f704f9f..ddd3864 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -42,6 +42,7 @@
Stdin,
StdinException,
Stdout,
+ StdoutException,
stdout;
import 'package:meta/meta.dart';
@@ -208,16 +209,64 @@
/// A class that wraps stdout, stderr, and stdin, and exposes the allowed
/// operations.
+///
+/// In particular, there are three ways that writing to stdout and stderr
+/// can fail. A call to stdout.write() can fail:
+/// * by throwing a regular synchronous exception,
+/// * by throwing an exception asynchronously, and
+/// * by completing the Future stdout.done with an error.
+///
+/// This class enapsulates all three so that we don't have to worry about it
+/// anywhere else.
class Stdio {
- const Stdio();
+ Stdio();
+
+ /// Tests can provide overrides to use instead of the stdout and stderr from
+ /// dart:io.
+ @visibleForTesting
+ Stdio.test({
+ @required io.Stdout stdout,
+ @required io.IOSink stderr,
+ }) : _stdoutOverride = stdout, _stderrOverride = stderr;
+
+ io.Stdout _stdoutOverride;
+ io.IOSink _stderrOverride;
+
+ // These flags exist to remember when the done Futures on stdout and stderr
+ // complete to avoid trying to write to a closed stream sink, which would
+ // generate a [StateError].
+ bool _stdoutDone = false;
+ bool _stderrDone = false;
Stream<List<int>> get stdin => io.stdin;
@visibleForTesting
- io.Stdout get stdout => io.stdout;
+ io.Stdout get stdout {
+ if (_stdout != null) {
+ return _stdout;
+ }
+ _stdout = _stdoutOverride ?? io.stdout;
+ _stdout.done.then(
+ (void _) { _stdoutDone = true; },
+ onError: (Object err, StackTrace st) { _stdoutDone = true; },
+ );
+ return _stdout;
+ }
+ io.Stdout _stdout;
@visibleForTesting
- io.IOSink get stderr => io.stderr;
+ io.IOSink get stderr {
+ if (_stderr != null) {
+ return _stderr;
+ }
+ _stderr = _stderrOverride ?? io.stderr;
+ _stderr.done.then(
+ (void _) { _stderrDone = true; },
+ onError: (Object err, StackTrace st) { _stderrDone = true; },
+ );
+ return _stderr;
+ }
+ io.IOSink _stderr;
bool get hasTerminal => io.stdout.hasTerminal;
@@ -250,25 +299,45 @@
return _stdinHasTerminal = true;
}
- int get terminalColumns => hasTerminal ? io.stdout.terminalColumns : null;
- int get terminalLines => hasTerminal ? io.stdout.terminalLines : null;
- bool get supportsAnsiEscapes => hasTerminal && io.stdout.supportsAnsiEscapes;
+ int get terminalColumns => hasTerminal ? stdout.terminalColumns : null;
+ int get terminalLines => hasTerminal ? stdout.terminalLines : null;
+ bool get supportsAnsiEscapes => hasTerminal && stdout.supportsAnsiEscapes;
/// Writes [message] to [stderr], falling back on [fallback] if the write
/// throws any exception. The default fallback calls [print] on [message].
void stderrWrite(
String message, {
void Function(String, dynamic, StackTrace) fallback,
- }) => _stdioWrite(stderr, message, fallback: fallback);
+ }) {
+ if (!_stderrDone) {
+ _stdioWrite(stderr, message, fallback: fallback);
+ return;
+ }
+ fallback == null ? print(message) : fallback(
+ message,
+ const io.StdoutException('stderr is done'),
+ StackTrace.current,
+ );
+ }
/// Writes [message] to [stdout], falling back on [fallback] if the write
/// throws any exception. The default fallback calls [print] on [message].
void stdoutWrite(
String message, {
void Function(String, dynamic, StackTrace) fallback,
- }) => _stdioWrite(stdout, message, fallback: fallback);
+ }) {
+ if (!_stdoutDone) {
+ _stdioWrite(stdout, message, fallback: fallback);
+ return;
+ }
+ fallback == null ? print(message) : fallback(
+ message,
+ const io.StdoutException('stdout is done'),
+ StackTrace.current,
+ );
+ }
- // Helper for safeStderrWrite and safeStdoutWrite.
+ // Helper for [stderrWrite] and [stdoutWrite].
void _stdioWrite(io.IOSink sink, String message, {
void Function(String, dynamic, StackTrace) fallback,
}) {
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 39043f9..960cbbe 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -175,7 +175,7 @@
ShutdownHooks: () => ShutdownHooks(logger: globals.logger),
Signals: () => Signals(),
SimControl: () => SimControl(),
- Stdio: () => const Stdio(),
+ Stdio: () => Stdio(),
SystemClock: () => const SystemClock(),
TimeoutConfiguration: () => const TimeoutConfiguration(),
Usage: () => Usage(
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index 07d8146..4fb7de7 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -150,14 +150,16 @@
);
/// The global Stdio wrapper.
-Stdio get stdio => context.get<Stdio>() ?? const Stdio();
+Stdio get stdio => context.get<Stdio>() ?? (_stdioInstance ??= Stdio());
+Stdio _stdioInstance;
-PlistParser get plistParser => context.get<PlistParser>() ?? (_defaultInstance ??= PlistParser(
- fileSystem: fs,
- processManager: processManager,
- logger: logger,
+PlistParser get plistParser => context.get<PlistParser>() ?? (
+ _plistInstance ??= PlistParser(
+ fileSystem: fs,
+ processManager: processManager,
+ logger: logger,
));
-PlistParser _defaultInstance;
+PlistParser _plistInstance;
/// The [ChromeLauncher] instance.
ChromeLauncher get chromeLauncher => context.get<ChromeLauncher>();
diff --git a/packages/flutter_tools/test/general.shard/base/logger_test.dart b/packages/flutter_tools/test/general.shard/base/logger_test.dart
index 66ea475..36de0f4 100644
--- a/packages/flutter_tools/test/general.shard/base/logger_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/logger_test.dart
@@ -23,16 +23,6 @@
class MockStdout extends Mock implements Stdout {}
-class ThrowingStdio extends Stdio {
- ThrowingStdio(this.stdout, this.stderr);
-
- @override
- final Stdout stdout;
-
- @override
- final IOSink stderr;
-}
-
void main() {
group('AppContext', () {
FakeStopwatch fakeStopWatch;
@@ -94,9 +84,11 @@
testWithoutContext('Logger does not throw when stdio write throws synchronously', () async {
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
- final ThrowingStdio stdio = ThrowingStdio(stdout, stderr);
+ final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
bool stdoutThrew = false;
bool stderrThrew = false;
+ final Completer<void> stdoutError = Completer<void>();
+ final Completer<void> stderrError = Completer<void>();
when(stdout.write(any)).thenAnswer((_) {
stdoutThrew = true;
throw 'Error';
@@ -105,6 +97,8 @@
stderrThrew = true;
throw 'Error';
});
+ when(stdout.done).thenAnswer((_) => stdoutError.future);
+ when(stderr.done).thenAnswer((_) => stderrError.future);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
@@ -123,7 +117,9 @@
testWithoutContext('Logger does not throw when stdio write throws asynchronously', () async {
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
- final ThrowingStdio stdio = ThrowingStdio(stdout, stderr);
+ final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
+ final Completer<void> stdoutError = Completer<void>();
+ final Completer<void> stderrError = Completer<void>();
bool stdoutThrew = false;
bool stderrThrew = false;
final Completer<void> stdoutCompleter = Completer<void>();
@@ -142,6 +138,8 @@
throw 'Error';
}, null);
});
+ when(stdout.done).thenAnswer((_) => stdoutError.future);
+ when(stderr.done).thenAnswer((_) => stderrError.future);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
@@ -159,6 +157,43 @@
expect(stderrThrew, true);
});
+ testWithoutContext('Logger does not throw when stdio completes done with an error', () async {
+ final MockStdout stdout = MockStdout();
+ final MockStdout stderr = MockStdout();
+ final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
+ final Completer<void> stdoutError = Completer<void>();
+ final Completer<void> stderrError = Completer<void>();
+ final Completer<void> stdoutCompleter = Completer<void>();
+ final Completer<void> stderrCompleter = Completer<void>();
+ when(stdout.write(any)).thenAnswer((_) {
+ Zone.current.runUnaryGuarded<void>((_) {
+ stdoutError.completeError(Exception('Some pipe error'));
+ stdoutCompleter.complete();
+ }, null);
+ });
+ when(stderr.write(any)).thenAnswer((_) {
+ Zone.current.runUnaryGuarded<void>((_) {
+ stderrError.completeError(Exception('Some pipe error'));
+ stderrCompleter.complete();
+ }, null);
+ });
+ when(stdout.done).thenAnswer((_) => stdoutError.future);
+ when(stderr.done).thenAnswer((_) => stderrError.future);
+ final Logger logger = StdoutLogger(
+ terminal: AnsiTerminal(
+ stdio: stdio,
+ platform: _kNoAnsiPlatform,
+ ),
+ stdio: stdio,
+ outputPreferences: OutputPreferences.test(),
+ timeoutConfiguration: const TimeoutConfiguration(),
+ );
+ logger.printStatus('message');
+ logger.printError('error message');
+ await stdoutCompleter.future;
+ await stderrCompleter.future;
+ });
+
group('Spinners', () {
mocks.MockStdio mockStdio;
FakeStopwatch mockStopwatch;
diff --git a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
index 75d44f0..fe401d7 100644
--- a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
+++ b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
@@ -56,7 +56,7 @@
await verifyCrashReportSent(requestInfo);
}, overrides: <Type, Generator>{
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('should print an explanatory message when there is a SocketException', () async {
@@ -77,7 +77,7 @@
expect(await exitCodeCompleter.future, 1);
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
}, overrides: <Type, Generator>{
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('should print an explanatory message when there is an HttpException', () async {
@@ -98,7 +98,7 @@
expect(await exitCodeCompleter.future, 1);
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
}, overrides: <Type, Generator>{
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('should send crash reports when async throws', () async {
@@ -120,7 +120,7 @@
expect(await exitCodeCompleter.future, 1);
await verifyCrashReportSent(requestInfo);
}, overrides: <Type, Generator>{
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('should send only one crash report when async throws many', () async {
@@ -151,7 +151,7 @@
await verifyCrashReportSent(requestInfo, crashes: 4);
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('should not send a crash report if on a user-branch', () async {
@@ -183,7 +183,7 @@
expect(testLogger.traceText, isNot(contains('Crash report sent')));
}, overrides: <Type, Generator>{
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
testUsingContext('can override base URL', () async {
@@ -223,7 +223,7 @@
},
script: Uri(scheme: 'data'),
),
- Stdio: () => const _NoStderr(),
+ Stdio: () => _NoStderr(),
});
});
}
@@ -400,7 +400,7 @@
}
class _NoStderr extends Stdio {
- const _NoStderr();
+ _NoStderr();
@override
IOSink get stderr => const _NoopIOSink();
diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
index 6c1bc2c..353cfb8 100644
--- a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
+++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
@@ -16,12 +16,13 @@
const String _kBranch = 'stable';
const FileSystem fileSystem = LocalFileSystem();
const ProcessManager processManager = LocalProcessManager();
+final Stdio stdio = Stdio();
final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
terminal: AnsiTerminal(
platform: const LocalPlatform(),
- stdio: const Stdio(),
+ stdio: stdio,
),
- stdio: const Stdio(),
+ stdio: stdio,
outputPreferences: OutputPreferences.test(wrapText: true),
timeoutConfiguration: const TimeoutConfiguration(),
));