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(
}
} else {
// We've crashed; emit a log report.
stderr.writeln();
safeStdioWrite(stderr, '\n');
if (!reportCrashes) {
// Print the stack trace on the bots - don't write a crash report.
stderr.writeln('$error');
stderr.writeln(stackTrace.toString());
safeStdioWrite(stderr, '$error\n');
safeStdioWrite(stderr, '$stackTrace\n');
return _exit(1);
}
......@@ -138,9 +138,10 @@ Future<int> _handleToolError(
return _exit(1);
} catch (error) {
stderr.writeln(
safeStdioWrite(
stderr,
'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
// 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;
io.IOSink get stderr => globals.stdio.stderr;
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`.
void writePidFile(String pidFile) {
if (pidFile != null) {
......
......@@ -229,9 +229,9 @@ class StdoutLogger extends Logger {
message = _terminal.bolden(message);
}
message = _terminal.color(message, color ?? TerminalColor.red);
_stdio.stderr.writeln(message);
writeToStdErr('$message\n');
if (stackTrace != null) {
_stdio.stderr.writeln(stackTrace.toString());
writeToStdErr('$stackTrace\n');
}
_status?.resume();
}
......@@ -269,7 +269,12 @@ class StdoutLogger extends Logger {
@protected
void writeToStdOut(String message) {
_stdio.stdout.write(message);
safeStdioWrite(_stdio.stdout, message);
}
@protected
void writeToStdErr(String message) {
safeStdioWrite(_stdio.stderr, message);
}
@override
......@@ -355,10 +360,10 @@ class WindowsStdoutLogger extends StdoutLogger {
@override
void writeToStdOut(String message) {
// TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
_stdio.stdout.write(message
.replaceAll('✗', 'X')
.replaceAll('✓', '√')
);
final String windowsMessage = message
.replaceAll('✗', 'X')
.replaceAll('✓', '√');
safeStdioWrite(_stdio.stdout, windowsMessage);
}
}
......@@ -789,9 +794,13 @@ class SummaryStatus extends Status {
super.start();
}
void _writeToStdOut(String message) {
safeStdioWrite(_stdio.stdout, message);
}
void _printMessage() {
assert(!_messageShowingOnCurrentLine);
_stdio.stdout.write('${message.padRight(padding)} ');
_writeToStdOut('${message.padRight(padding)} ');
_messageShowingOnCurrentLine = true;
}
......@@ -802,14 +811,14 @@ class SummaryStatus extends Status {
}
super.stop();
writeSummaryInformation();
_stdio.stdout.write('\n');
_writeToStdOut('\n');
}
@override
void cancel() {
super.cancel();
if (_messageShowingOnCurrentLine) {
_stdio.stdout.write('\n');
_writeToStdOut('\n');
}
}
......@@ -822,16 +831,16 @@ class SummaryStatus extends Status {
/// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)`
void writeSummaryInformation() {
assert(_messageShowingOnCurrentLine);
_stdio.stdout.write(elapsedTime.padLeft(_kTimePadding));
_writeToStdOut(elapsedTime.padLeft(_kTimePadding));
if (seemsSlow) {
_stdio.stdout.write(' (!)');
_writeToStdOut(' (!)');
}
}
@override
void pause() {
super.pause();
_stdio.stdout.write('\n');
_writeToStdOut('\n');
_messageShowingOnCurrentLine = false;
}
}
......@@ -894,8 +903,12 @@ class AnsiSpinner extends Status {
_startSpinner();
}
void _writeToStdOut(String message) {
safeStdioWrite(_stdio.stdout, message);
}
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);
_callback(timer);
}
......@@ -904,21 +917,21 @@ class AnsiSpinner extends Status {
assert(this.timer == timer);
assert(timer != null);
assert(timer.isActive);
_stdio.stdout.write(_backspace);
_writeToStdOut(_backspace);
ticks += 1;
if (seemsSlow) {
if (!timedOut) {
timedOut = true;
_stdio.stdout.write('$_clear\n');
_writeToStdOut('$_clear\n');
}
if (slowWarningCallback != null) {
_slowWarning = slowWarningCallback();
} else {
_slowWarning = _defaultSlowWarning;
}
_stdio.stdout.write(_slowWarning);
_writeToStdOut(_slowWarning);
}
_stdio.stdout.write('${_clearChar * spinnerIndent}$_currentAnimationFrame');
_writeToStdOut('${_clearChar * spinnerIndent}$_currentAnimationFrame');
}
@override
......@@ -932,7 +945,7 @@ class AnsiSpinner extends Status {
}
void _clearSpinner() {
_stdio.stdout.write('$_backspace$_clear$_backspace');
_writeToStdOut('$_backspace$_clear$_backspace');
}
@override
......@@ -1002,20 +1015,20 @@ class AnsiStatus extends AnsiSpinner {
void _startStatus() {
final String line = '${message.padRight(padding)}$_margin';
_totalMessageLength = line.length;
_stdio.stdout.write(line);
_writeToStdOut(line);
}
@override
void stop() {
super.stop();
writeSummaryInformation();
_stdio.stdout.write('\n');
_writeToStdOut('\n');
}
@override
void cancel() {
super.cancel();
_stdio.stdout.write('\n');
_writeToStdOut('\n');
}
/// Print summary information when a task is done.
......@@ -1026,16 +1039,20 @@ class AnsiStatus extends AnsiSpinner {
/// line before writing the elapsed time.
void writeSummaryInformation() {
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) {
_stdio.stdout.write(' (!)');
_writeToStdOut(' (!)');
}
}
void _clearStatus() {
_stdio.stdout.write('${_backspaceChar * _totalMessageLength}${_clearChar * _totalMessageLength}${_backspaceChar * _totalMessageLength}');
_writeToStdOut(
'${_backspaceChar * _totalMessageLength}'
'${_clearChar * _totalMessageLength}'
'${_backspaceChar * _totalMessageLength}',
);
}
@override
......
......@@ -143,7 +143,7 @@ class Daemon {
final dynamic id = request['id'];
if (id == null) {
stderr.writeln('no id for request: $request');
safeStdioWrite(stderr, 'no id for request: $request\n');
return;
}
......@@ -323,9 +323,9 @@ class DaemonDomain extends Domain {
// capture the print output for testing.
print(message.message);
} else if (message.level == 'error') {
stderr.writeln(message.message);
safeStdioWrite(stderr, '${message.message}\n');
if (message.stackTrace != null) {
stderr.writeln(message.stackTrace.toString().trimRight());
safeStdioWrite(stderr, '${message.stackTrace.toString().trimRight()}\n');
}
}
} else {
......@@ -872,7 +872,13 @@ Stream<Map<String, dynamic>> get stdinCommandStream => stdin
});
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) {
......
......@@ -49,7 +49,8 @@ class ShellCompletionCommand extends FlutterCommand {
}
if (argResults.rest.isEmpty || argResults.rest.first == '-') {
stdout.write(generateCompletionScript(<String>['flutter']));
final String script = generateCompletionScript(<String>['flutter']);
safeStdioWrite(stdout, script);
return FlutterCommandResult.warning();
}
......
......@@ -5,12 +5,14 @@
import 'dart:convert' show jsonEncode;
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/terminal.dart';
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.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 String red = RegExp.escape(AnsiTerminal.red);
......@@ -18,6 +20,9 @@ final String bold = RegExp.escape(AnsiTerminal.bold);
final String resetBold = RegExp.escape(AnsiTerminal.resetBold);
final String resetColor = RegExp.escape(AnsiTerminal.resetColor);
class MockStdio extends Mock implements Stdio {}
class MockStdout extends Mock implements Stdout {}
void main() {
group('AppContext', () {
FakeStopwatch fakeStopWatch;
......@@ -25,10 +30,11 @@ void main() {
setUp(() {
fakeStopWatch = FakeStopwatch();
});
testWithoutContext('error', () async {
final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal(
stdio: MockStdio(),
stdio: mocks.MockStdio(),
platform: _kNoAnsiPlatform,
),
outputPreferences: OutputPreferences.test(showColor: false),
......@@ -51,7 +57,7 @@ void main() {
testWithoutContext('ANSI colored errors', () async {
final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal(
stdio: MockStdio(),
stdio: mocks.MockStdio(),
platform: FakePlatform()..stdoutSupportsAnsi = true,
),
outputPreferences: OutputPreferences.test(showColor: true),
......@@ -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', () {
MockStdio mockStdio;
mocks.MockStdio mockStdio;
FakeStopwatch mockStopwatch;
FakeStopwatchFactory stopwatchFactory;
int called;
......@@ -85,7 +123,7 @@ void main() {
setUp(() {
mockStopwatch = FakeStopwatch();
mockStdio = MockStdio();
mockStdio = mocks.MockStdio();
called = 0;
stopwatchFactory = FakeStopwatchFactory(mockStopwatch);
});
......@@ -375,14 +413,15 @@ void main() {
});
}
});
group('Output format', () {
MockStdio mockStdio;
mocks.MockStdio mockStdio;
SummaryStatus summaryStatus;
int called;
final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
setUp(() {
mockStdio = MockStdio();
mockStdio = mocks.MockStdio();
called = 0;
summaryStatus = SummaryStatus(
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