[flutter_tool] Hide unsafe std{out,err} operations (#49561)

diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index 55a785c..e9d6996 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -111,12 +111,12 @@
     }
   } else {
     // We've crashed; emit a log report.
-    safeStdioWrite(stderr, '\n');
+    globals.stdio.stderrWrite('\n');
 
     if (!reportCrashes) {
       // Print the stack trace on the bots - don't write a crash report.
-      safeStdioWrite(stderr, '$error\n');
-      safeStdioWrite(stderr, '$stackTrace\n');
+      globals.stdio.stderrWrite('$error\n');
+      globals.stdio.stderrWrite('$stackTrace\n');
       return _exit(1);
     }
 
@@ -137,8 +137,7 @@
 
       return _exit(1);
     } catch (error) {
-      safeStdioWrite(
-        stderr,
+      globals.stdio.stderrWrite(
         'Unable to generate crash report due to secondary error: $error\n'
         'please let us know at https://github.com/flutter/flutter/issues.\n',
       );
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index f8e437a..6045fa4 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -323,19 +323,34 @@
 
       // The real stdin will never finish streaming. Pipe until the child process
       // finishes.
-      unawaited(process.stdin.addStream(stdin));
+      unawaited(process.stdin.addStream(globals.stdio.stdin)
+        // If the process exits unexpectedly with an error, that will be
+        // handled by the caller.
+        .catchError((dynamic err, StackTrace stack) {
+          globals.printTrace('Echoing stdin to the licenses subprocess failed:');
+          globals.printTrace('$err\n$stack');
+        }
+      ));
+
       // Wait for stdout and stderr to be fully processed, because process.exitCode
       // may complete first.
-      await waitGroup<void>(<Future<void>>[
-        stdout.addStream(process.stdout),
-        stderr.addStream(process.stderr),
-      ]);
+      try {
+        await waitGroup<void>(<Future<void>>[
+          globals.stdio.addStdoutStream(process.stdout),
+          globals.stdio.addStderrStream(process.stderr),
+        ]);
+      } catch (err, stack) {
+        globals.printTrace('Echoing stdout or stderr from the license subprocess failed:');
+        globals.printTrace('$err\n$stack');
+      }
 
       final int exitCode = await process.exitCode;
       return exitCode == 0;
     } on ProcessException catch (e) {
       throwToolExit(userMessages.androidCannotRunSdkManager(
-          androidSdk.sdkManagerPath, e.toString()));
+        androidSdk.sdkManagerPath,
+        e.toString(),
+      ));
       return false;
     }
   }
diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index 7dc0194..b915d52 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -205,11 +205,17 @@
   }
 }
 
+/// A class that wraps stdout, stderr, and stdin, and exposes the allowed
+/// operations.
 class Stdio {
   const Stdio();
 
   Stream<List<int>> get stdin => io.stdin;
+
+  @visibleForTesting
   io.Stdout get stdout => io.stdout;
+
+  @visibleForTesting
   io.IOSink get stderr => io.stderr;
 
   bool get hasTerminal => io.stdout.hasTerminal;
@@ -246,27 +252,41 @@
   int get terminalColumns => hasTerminal ? io.stdout.terminalColumns : null;
   int get terminalLines => hasTerminal ? io.stdout.terminalLines : null;
   bool get supportsAnsiEscapes => hasTerminal && io.stdout.supportsAnsiEscapes;
-}
 
-io.Stdout get stdout => globals.stdio.stdout;
-Stream<List<int>> get stdin => globals.stdio.stdin;
-io.IOSink get stderr => globals.stdio.stderr;
-bool get stdinHasTerminal => globals.stdio.stdinHasTerminal;
+  /// 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);
 
-/// 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);
+  /// 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);
+
+  // Helper for safeStderrWrite and safeStdoutWrite.
+  void _stdioWrite(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);
+      }
     }
   }
+
+  /// Adds [stream] to [stdout].
+  Future<void> addStdoutStream(Stream<List<int>> stream) => stdout.addStream(stream);
+
+  /// Adds [srtream] to [stderr].
+  Future<void> addStderrStream(Stream<List<int>> stream) => stderr.addStream(stream);
 }
 
 // TODO(zra): Move pid and writePidFile into `ProcessInfo`.
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index e407dfd..11060db 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -9,7 +9,7 @@
 
 import '../base/context.dart';
 import '../globals.dart' as globals;
-import 'io.dart' hide stderr, stdin, stdout;
+import 'io.dart';
 import 'terminal.dart' show AnsiTerminal, TerminalColor, OutputPreferences;
 import 'utils.dart';
 
@@ -268,14 +268,10 @@
   }
 
   @protected
-  void writeToStdOut(String message) {
-    safeStdioWrite(_stdio.stdout, message);
-  }
+  void writeToStdOut(String message) => _stdio.stdoutWrite(message);
 
   @protected
-  void writeToStdErr(String message) {
-    safeStdioWrite(_stdio.stderr, message);
-  }
+  void writeToStdErr(String message) => _stdio.stderrWrite(message);
 
   @override
   void printTrace(String message) { }
@@ -363,7 +359,7 @@
     final String windowsMessage = message
       .replaceAll('✗', 'X')
       .replaceAll('✓', '√');
-    safeStdioWrite(_stdio.stdout, windowsMessage);
+    _stdio.stdoutWrite(windowsMessage);
   }
 }
 
@@ -794,9 +790,7 @@
     super.start();
   }
 
-  void _writeToStdOut(String message) {
-    safeStdioWrite(_stdio.stdout, message);
-  }
+  void _writeToStdOut(String message) => _stdio.stdoutWrite(message);
 
   void _printMessage() {
     assert(!_messageShowingOnCurrentLine);
@@ -903,9 +897,7 @@
     _startSpinner();
   }
 
-  void _writeToStdOut(String message) {
-    safeStdioWrite(_stdio.stdout, message);
-  }
+  void _writeToStdOut(String message) => _stdio.stdoutWrite(message);
 
   void _startSpinner() {
     _writeToStdOut(_clear); // for _callback to backspace over
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index cd6b7c3..a9e43eb 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -143,7 +143,7 @@
     final dynamic id = request['id'];
 
     if (id == null) {
-      safeStdioWrite(stderr, 'no id for request: $request\n');
+      globals.stdio.stderrWrite('no id for request: $request\n');
       return;
     }
 
@@ -323,9 +323,11 @@
           // capture the print output for testing.
           print(message.message);
         } else if (message.level == 'error') {
-          safeStdioWrite(stderr, '${message.message}\n');
+          globals.stdio.stderrWrite('${message.message}\n');
           if (message.stackTrace != null) {
-            safeStdioWrite(stderr, '${message.stackTrace.toString().trimRight()}\n');
+            globals.stdio.stderrWrite(
+              '${message.stackTrace.toString().trimRight()}\n',
+            );
           }
         }
       } else {
@@ -862,7 +864,7 @@
   }
 }
 
-Stream<Map<String, dynamic>> get stdinCommandStream => stdin
+Stream<Map<String, dynamic>> get stdinCommandStream => globals.stdio.stdin
   .transform<String>(utf8.decoder)
   .transform<String>(const LineSplitter())
   .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
@@ -872,8 +874,7 @@
   });
 
 void stdoutCommandResponse(Map<String, dynamic> command) {
-  safeStdioWrite(
-    stdout,
+  globals.stdio.stdoutWrite(
     '[${jsonEncodeObject(command)}]\n',
     fallback: (String message, dynamic error, StackTrace stack) {
       throwToolExit('Failed to write daemon command response to stdout: $error');
diff --git a/packages/flutter_tools/lib/src/commands/shell_completion.dart b/packages/flutter_tools/lib/src/commands/shell_completion.dart
index 1892058..b85bd30 100644
--- a/packages/flutter_tools/lib/src/commands/shell_completion.dart
+++ b/packages/flutter_tools/lib/src/commands/shell_completion.dart
@@ -8,7 +8,6 @@
 
 import '../base/common.dart';
 import '../base/file_system.dart';
-import '../base/io.dart';
 import '../globals.dart' as globals;
 import '../runner/flutter_command.dart';
 
@@ -50,7 +49,7 @@
 
     if (argResults.rest.isEmpty || argResults.rest.first == '-') {
       final String script = generateCompletionScript(<String>['flutter']);
-      safeStdioWrite(stdout, script);
+      globals.stdio.stdoutWrite(script);
       return FlutterCommandResult.warning();
     }
 
diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart
index ff4b18b..b4d45db 100644
--- a/packages/flutter_tools/lib/src/dart/pub.dart
+++ b/packages/flutter_tools/lib/src/dart/pub.dart
@@ -308,7 +308,7 @@
     );
 
     // Pipe the Flutter tool stdin to the pub stdin.
-    unawaited(process.stdin.addStream(io.stdin)
+    unawaited(process.stdin.addStream(globals.stdio.stdin)
       // If pub exits unexpectedly with an error, that will be reported below
       // by the tool exit after the exit code check.
       .catchError((dynamic err, StackTrace stack) {
@@ -320,8 +320,8 @@
     // Pipe the pub stdout and stderr to the tool stdout and stderr.
     try {
       await Future.wait<dynamic>(<Future<dynamic>>[
-        io.stdout.addStream(process.stdout),
-        io.stderr.addStream(process.stderr),
+        globals.stdio.addStdoutStream(process.stdout),
+        globals.stdio.addStderrStream(process.stderr),
       ]);
     } catch (err, stack) {
       globals.printTrace('Echoing stdout or stderr from the pub subprocess failed:');
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 312845d..92e03e4 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -630,7 +630,8 @@
       final Map<CustomDimensions, String> additionalUsageValues =
         <CustomDimensions, String>{
           ...?await usageValues,
-          CustomDimensions.commandHasTerminal: io.stdout.hasTerminal ? 'true' : 'false',
+          CustomDimensions.commandHasTerminal:
+            globals.stdio.hasTerminal ? 'true' : 'false',
         };
       Usage.command(commandPath, parameters: additionalUsageValues);
     }
diff --git a/packages/flutter_tools/lib/src/test/event_printer.dart b/packages/flutter_tools/lib/src/test/event_printer.dart
index 91ef721..9d753d6 100644
--- a/packages/flutter_tools/lib/src/test/event_printer.dart
+++ b/packages/flutter_tools/lib/src/test/event_printer.dart
@@ -2,13 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import '../base/io.dart' show stdout;
 import '../convert.dart';
+import '../globals.dart' as globals;
 import 'watcher.dart';
 
 /// Prints JSON events when running a test in --machine mode.
 class EventPrinter extends TestWatcher {
-  EventPrinter({StringSink out}) : _out = out ?? stdout;
+  EventPrinter({StringSink out}) : _out = out ?? globals.stdio.stdout;
 
   final StringSink _out;
 
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 e1c3374..0b6ff80 100644
--- a/packages/flutter_tools/test/general.shard/base/logger_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/logger_test.dart
@@ -20,9 +20,18 @@
 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 {}
 
+class ThrowingStdio extends Stdio {
+  ThrowingStdio(this.stdout, this.stderr);
+
+  @override
+  final Stdout stdout;
+
+  @override
+  final IOSink stderr;
+}
+
 void main() {
   group('AppContext', () {
     FakeStopwatch fakeStopWatch;
@@ -82,13 +91,11 @@
   });
 
   testWithoutContext('Logger does not throw when stdio write throws', () async {
-    final MockStdio stdio = MockStdio();
     final MockStdout stdout = MockStdout();
     final MockStdout stderr = MockStdout();
+    final ThrowingStdio stdio = ThrowingStdio(stdout, stderr);
     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';