Commit 93a5b7d4 authored by Zachary Anderson's avatar Zachary Anderson Committed by Flutter GitHub Bot

[flutter_tool] Don't crash on a failure to write to std{out,err} (#49380)

parent bcfc293c
...@@ -112,12 +112,12 @@ Future<int> _handleToolError( ...@@ -112,12 +112,12 @@ Future<int> _handleToolError(
} }
} else { } else {
// We've crashed; emit a log report. // We've crashed; emit a log report.
stderr.writeln(); safeStdioWrite(stderr, '\n');
if (!reportCrashes) { if (!reportCrashes) {
// Print the stack trace on the bots - don't write a crash report. // Print the stack trace on the bots - don't write a crash report.
stderr.writeln('$error'); safeStdioWrite(stderr, '$error\n');
stderr.writeln(stackTrace.toString()); safeStdioWrite(stderr, '$stackTrace\n');
return _exit(1); return _exit(1);
} }
...@@ -138,9 +138,10 @@ Future<int> _handleToolError( ...@@ -138,9 +138,10 @@ Future<int> _handleToolError(
return _exit(1); return _exit(1);
} catch (error) { } catch (error) {
stderr.writeln( safeStdioWrite(
stderr,
'Unable to generate crash report due to secondary error: $error\n' 'Unable to generate crash report due to secondary error: $error\n'
'please let us know at https://github.com/flutter/flutter/issues.', 'please let us know at https://github.com/flutter/flutter/issues.\n',
); );
// Any exception throw here (including one thrown by `_exit()`) will // Any exception throw here (including one thrown by `_exit()`) will
// get caught by our zone's `onError` handler. In order to avoid an // get caught by our zone's `onError` handler. In order to avoid an
......
...@@ -253,6 +253,22 @@ Stream<List<int>> get stdin => globals.stdio.stdin; ...@@ -253,6 +253,22 @@ Stream<List<int>> get stdin => globals.stdio.stdin;
io.IOSink get stderr => globals.stdio.stderr; io.IOSink get stderr => globals.stdio.stderr;
bool get stdinHasTerminal => globals.stdio.stdinHasTerminal; bool get stdinHasTerminal => globals.stdio.stdinHasTerminal;
/// Writes [message] to [sink], falling back on [fallback] if the write
/// throws any exception. The default fallback calls [print] on [message].
void safeStdioWrite(io.IOSink sink, String message, {
void Function(String, dynamic, StackTrace) fallback,
}) {
try {
sink.write(message);
} catch (err, stack) {
if (fallback == null) {
print(message);
} else {
fallback(message, err, stack);
}
}
}
// TODO(zra): Move pid and writePidFile into `ProcessInfo`. // TODO(zra): Move pid and writePidFile into `ProcessInfo`.
void writePidFile(String pidFile) { void writePidFile(String pidFile) {
if (pidFile != null) { if (pidFile != null) {
......
...@@ -229,9 +229,9 @@ class StdoutLogger extends Logger { ...@@ -229,9 +229,9 @@ class StdoutLogger extends Logger {
message = _terminal.bolden(message); message = _terminal.bolden(message);
} }
message = _terminal.color(message, color ?? TerminalColor.red); message = _terminal.color(message, color ?? TerminalColor.red);
_stdio.stderr.writeln(message); writeToStdErr('$message\n');
if (stackTrace != null) { if (stackTrace != null) {
_stdio.stderr.writeln(stackTrace.toString()); writeToStdErr('$stackTrace\n');
} }
_status?.resume(); _status?.resume();
} }
...@@ -269,7 +269,12 @@ class StdoutLogger extends Logger { ...@@ -269,7 +269,12 @@ class StdoutLogger extends Logger {
@protected @protected
void writeToStdOut(String message) { void writeToStdOut(String message) {
_stdio.stdout.write(message); safeStdioWrite(_stdio.stdout, message);
}
@protected
void writeToStdErr(String message) {
safeStdioWrite(_stdio.stderr, message);
} }
@override @override
...@@ -355,10 +360,10 @@ class WindowsStdoutLogger extends StdoutLogger { ...@@ -355,10 +360,10 @@ class WindowsStdoutLogger extends StdoutLogger {
@override @override
void writeToStdOut(String message) { void writeToStdOut(String message) {
// TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio]. // TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
_stdio.stdout.write(message final String windowsMessage = message
.replaceAll('✗', 'X') .replaceAll('✗', 'X')
.replaceAll('✓', '√') .replaceAll('✓', '√');
); safeStdioWrite(_stdio.stdout, windowsMessage);
} }
} }
...@@ -789,9 +794,13 @@ class SummaryStatus extends Status { ...@@ -789,9 +794,13 @@ class SummaryStatus extends Status {
super.start(); super.start();
} }
void _writeToStdOut(String message) {
safeStdioWrite(_stdio.stdout, message);
}
void _printMessage() { void _printMessage() {
assert(!_messageShowingOnCurrentLine); assert(!_messageShowingOnCurrentLine);
_stdio.stdout.write('${message.padRight(padding)} '); _writeToStdOut('${message.padRight(padding)} ');
_messageShowingOnCurrentLine = true; _messageShowingOnCurrentLine = true;
} }
...@@ -802,14 +811,14 @@ class SummaryStatus extends Status { ...@@ -802,14 +811,14 @@ class SummaryStatus extends Status {
} }
super.stop(); super.stop();
writeSummaryInformation(); writeSummaryInformation();
_stdio.stdout.write('\n'); _writeToStdOut('\n');
} }
@override @override
void cancel() { void cancel() {
super.cancel(); super.cancel();
if (_messageShowingOnCurrentLine) { if (_messageShowingOnCurrentLine) {
_stdio.stdout.write('\n'); _writeToStdOut('\n');
} }
} }
...@@ -822,16 +831,16 @@ class SummaryStatus extends Status { ...@@ -822,16 +831,16 @@ class SummaryStatus extends Status {
/// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)` /// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)`
void writeSummaryInformation() { void writeSummaryInformation() {
assert(_messageShowingOnCurrentLine); assert(_messageShowingOnCurrentLine);
_stdio.stdout.write(elapsedTime.padLeft(_kTimePadding)); _writeToStdOut(elapsedTime.padLeft(_kTimePadding));
if (seemsSlow) { if (seemsSlow) {
_stdio.stdout.write(' (!)'); _writeToStdOut(' (!)');
} }
} }
@override @override
void pause() { void pause() {
super.pause(); super.pause();
_stdio.stdout.write('\n'); _writeToStdOut('\n');
_messageShowingOnCurrentLine = false; _messageShowingOnCurrentLine = false;
} }
} }
...@@ -894,8 +903,12 @@ class AnsiSpinner extends Status { ...@@ -894,8 +903,12 @@ class AnsiSpinner extends Status {
_startSpinner(); _startSpinner();
} }
void _writeToStdOut(String message) {
safeStdioWrite(_stdio.stdout, message);
}
void _startSpinner() { void _startSpinner() {
_stdio.stdout.write(_clear); // for _callback to backspace over _writeToStdOut(_clear); // for _callback to backspace over
timer = Timer.periodic(const Duration(milliseconds: 100), _callback); timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
_callback(timer); _callback(timer);
} }
...@@ -904,21 +917,21 @@ class AnsiSpinner extends Status { ...@@ -904,21 +917,21 @@ class AnsiSpinner extends Status {
assert(this.timer == timer); assert(this.timer == timer);
assert(timer != null); assert(timer != null);
assert(timer.isActive); assert(timer.isActive);
_stdio.stdout.write(_backspace); _writeToStdOut(_backspace);
ticks += 1; ticks += 1;
if (seemsSlow) { if (seemsSlow) {
if (!timedOut) { if (!timedOut) {
timedOut = true; timedOut = true;
_stdio.stdout.write('$_clear\n'); _writeToStdOut('$_clear\n');
} }
if (slowWarningCallback != null) { if (slowWarningCallback != null) {
_slowWarning = slowWarningCallback(); _slowWarning = slowWarningCallback();
} else { } else {
_slowWarning = _defaultSlowWarning; _slowWarning = _defaultSlowWarning;
} }
_stdio.stdout.write(_slowWarning); _writeToStdOut(_slowWarning);
} }
_stdio.stdout.write('${_clearChar * spinnerIndent}$_currentAnimationFrame'); _writeToStdOut('${_clearChar * spinnerIndent}$_currentAnimationFrame');
} }
@override @override
...@@ -932,7 +945,7 @@ class AnsiSpinner extends Status { ...@@ -932,7 +945,7 @@ class AnsiSpinner extends Status {
} }
void _clearSpinner() { void _clearSpinner() {
_stdio.stdout.write('$_backspace$_clear$_backspace'); _writeToStdOut('$_backspace$_clear$_backspace');
} }
@override @override
...@@ -1002,20 +1015,20 @@ class AnsiStatus extends AnsiSpinner { ...@@ -1002,20 +1015,20 @@ class AnsiStatus extends AnsiSpinner {
void _startStatus() { void _startStatus() {
final String line = '${message.padRight(padding)}$_margin'; final String line = '${message.padRight(padding)}$_margin';
_totalMessageLength = line.length; _totalMessageLength = line.length;
_stdio.stdout.write(line); _writeToStdOut(line);
} }
@override @override
void stop() { void stop() {
super.stop(); super.stop();
writeSummaryInformation(); writeSummaryInformation();
_stdio.stdout.write('\n'); _writeToStdOut('\n');
} }
@override @override
void cancel() { void cancel() {
super.cancel(); super.cancel();
_stdio.stdout.write('\n'); _writeToStdOut('\n');
} }
/// Print summary information when a task is done. /// Print summary information when a task is done.
...@@ -1026,16 +1039,20 @@ class AnsiStatus extends AnsiSpinner { ...@@ -1026,16 +1039,20 @@ class AnsiStatus extends AnsiSpinner {
/// line before writing the elapsed time. /// line before writing the elapsed time.
void writeSummaryInformation() { void writeSummaryInformation() {
if (multilineOutput) { if (multilineOutput) {
_stdio.stdout.write('\n${'$message Done'.padRight(padding)}$_margin'); _writeToStdOut('\n${'$message Done'.padRight(padding)}$_margin');
} }
_stdio.stdout.write(elapsedTime.padLeft(_kTimePadding)); _writeToStdOut(elapsedTime.padLeft(_kTimePadding));
if (seemsSlow) { if (seemsSlow) {
_stdio.stdout.write(' (!)'); _writeToStdOut(' (!)');
} }
} }
void _clearStatus() { void _clearStatus() {
_stdio.stdout.write('${_backspaceChar * _totalMessageLength}${_clearChar * _totalMessageLength}${_backspaceChar * _totalMessageLength}'); _writeToStdOut(
'${_backspaceChar * _totalMessageLength}'
'${_clearChar * _totalMessageLength}'
'${_backspaceChar * _totalMessageLength}',
);
} }
@override @override
......
...@@ -143,7 +143,7 @@ class Daemon { ...@@ -143,7 +143,7 @@ class Daemon {
final dynamic id = request['id']; final dynamic id = request['id'];
if (id == null) { if (id == null) {
stderr.writeln('no id for request: $request'); safeStdioWrite(stderr, 'no id for request: $request\n');
return; return;
} }
...@@ -323,9 +323,9 @@ class DaemonDomain extends Domain { ...@@ -323,9 +323,9 @@ class DaemonDomain extends Domain {
// capture the print output for testing. // capture the print output for testing.
print(message.message); print(message.message);
} else if (message.level == 'error') { } else if (message.level == 'error') {
stderr.writeln(message.message); safeStdioWrite(stderr, '${message.message}\n');
if (message.stackTrace != null) { if (message.stackTrace != null) {
stderr.writeln(message.stackTrace.toString().trimRight()); safeStdioWrite(stderr, '${message.stackTrace.toString().trimRight()}\n');
} }
} }
} else { } else {
...@@ -872,7 +872,13 @@ Stream<Map<String, dynamic>> get stdinCommandStream => stdin ...@@ -872,7 +872,13 @@ Stream<Map<String, dynamic>> get stdinCommandStream => stdin
}); });
void stdoutCommandResponse(Map<String, dynamic> command) { void stdoutCommandResponse(Map<String, dynamic> command) {
stdout.writeln('[${jsonEncodeObject(command)}]'); safeStdioWrite(
stdout,
'[${jsonEncodeObject(command)}]\n',
fallback: (String message, dynamic error, StackTrace stack) {
throwToolExit('Failed to write daemon command response to stdout: $error');
},
);
} }
String jsonEncodeObject(dynamic object) { String jsonEncodeObject(dynamic object) {
......
...@@ -49,7 +49,8 @@ class ShellCompletionCommand extends FlutterCommand { ...@@ -49,7 +49,8 @@ class ShellCompletionCommand extends FlutterCommand {
} }
if (argResults.rest.isEmpty || argResults.rest.first == '-') { if (argResults.rest.isEmpty || argResults.rest.first == '-') {
stdout.write(generateCompletionScript(<String>['flutter'])); final String script = generateCompletionScript(<String>['flutter']);
safeStdioWrite(stdout, script);
return FlutterCommandResult.warning(); return FlutterCommandResult.warning();
} }
......
...@@ -5,12 +5,14 @@ ...@@ -5,12 +5,14 @@
import 'dart:convert' show jsonEncode; import 'dart:convert' show jsonEncode;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/terminal.dart';
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/mocks.dart'; import '../../src/mocks.dart' as mocks;
final Platform _kNoAnsiPlatform = FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; final Platform _kNoAnsiPlatform = FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
final String red = RegExp.escape(AnsiTerminal.red); final String red = RegExp.escape(AnsiTerminal.red);
...@@ -18,6 +20,9 @@ final String bold = RegExp.escape(AnsiTerminal.bold); ...@@ -18,6 +20,9 @@ final String bold = RegExp.escape(AnsiTerminal.bold);
final String resetBold = RegExp.escape(AnsiTerminal.resetBold); final String resetBold = RegExp.escape(AnsiTerminal.resetBold);
final String resetColor = RegExp.escape(AnsiTerminal.resetColor); final String resetColor = RegExp.escape(AnsiTerminal.resetColor);
class MockStdio extends Mock implements Stdio {}
class MockStdout extends Mock implements Stdout {}
void main() { void main() {
group('AppContext', () { group('AppContext', () {
FakeStopwatch fakeStopWatch; FakeStopwatch fakeStopWatch;
...@@ -25,10 +30,11 @@ void main() { ...@@ -25,10 +30,11 @@ void main() {
setUp(() { setUp(() {
fakeStopWatch = FakeStopwatch(); fakeStopWatch = FakeStopwatch();
}); });
testWithoutContext('error', () async { testWithoutContext('error', () async {
final BufferLogger mockLogger = BufferLogger( final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal( terminal: AnsiTerminal(
stdio: MockStdio(), stdio: mocks.MockStdio(),
platform: _kNoAnsiPlatform, platform: _kNoAnsiPlatform,
), ),
outputPreferences: OutputPreferences.test(showColor: false), outputPreferences: OutputPreferences.test(showColor: false),
...@@ -51,7 +57,7 @@ void main() { ...@@ -51,7 +57,7 @@ void main() {
testWithoutContext('ANSI colored errors', () async { testWithoutContext('ANSI colored errors', () async {
final BufferLogger mockLogger = BufferLogger( final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal( terminal: AnsiTerminal(
stdio: MockStdio(), stdio: mocks.MockStdio(),
platform: FakePlatform()..stdoutSupportsAnsi = true, platform: FakePlatform()..stdoutSupportsAnsi = true,
), ),
outputPreferences: OutputPreferences.test(showColor: true), outputPreferences: OutputPreferences.test(showColor: true),
...@@ -75,8 +81,40 @@ void main() { ...@@ -75,8 +81,40 @@ void main() {
}); });
}); });
testWithoutContext('Logger does not throw when stdio write throws', () async {
final MockStdio stdio = MockStdio();
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
bool stdoutThrew = false;
bool stderrThrew = false;
when(stdio.stdout).thenReturn(stdout);
when(stdio.stderr).thenReturn(stderr);
when(stdout.write(any)).thenAnswer((_) {
stdoutThrew = true;
throw 'Error';
});
when(stderr.write(any)).thenAnswer((_) {
stderrThrew = true;
throw 'Error';
});
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
platform: _kNoAnsiPlatform,
),
stdio: stdio,
outputPreferences: OutputPreferences.test(),
timeoutConfiguration: const TimeoutConfiguration(),
platform: FakePlatform(),
);
logger.printStatus('message');
logger.printError('error message');
expect(stdoutThrew, true);
expect(stderrThrew, true);
});
group('Spinners', () { group('Spinners', () {
MockStdio mockStdio; mocks.MockStdio mockStdio;
FakeStopwatch mockStopwatch; FakeStopwatch mockStopwatch;
FakeStopwatchFactory stopwatchFactory; FakeStopwatchFactory stopwatchFactory;
int called; int called;
...@@ -85,7 +123,7 @@ void main() { ...@@ -85,7 +123,7 @@ void main() {
setUp(() { setUp(() {
mockStopwatch = FakeStopwatch(); mockStopwatch = FakeStopwatch();
mockStdio = MockStdio(); mockStdio = mocks.MockStdio();
called = 0; called = 0;
stopwatchFactory = FakeStopwatchFactory(mockStopwatch); stopwatchFactory = FakeStopwatchFactory(mockStopwatch);
}); });
...@@ -375,14 +413,15 @@ void main() { ...@@ -375,14 +413,15 @@ void main() {
}); });
} }
}); });
group('Output format', () { group('Output format', () {
MockStdio mockStdio; mocks.MockStdio mockStdio;
SummaryStatus summaryStatus; SummaryStatus summaryStatus;
int called; int called;
final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)'); final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
setUp(() { setUp(() {
mockStdio = MockStdio(); mockStdio = mocks.MockStdio();
called = 0; called = 0;
summaryStatus = SummaryStatus( summaryStatus = SummaryStatus(
message: 'Hello world', message: 'Hello world',
......
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