[flutter_tool] Re-try sending the first crash report (#37198)

diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index f63553c..6999486 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -56,17 +56,25 @@
     );
 
     String getVersion() => flutterVersion ?? FlutterVersion.instance.getVersionString(redactUnknownBranches: true);
+    Object firstError;
+    StackTrace firstStackTrace;
     return await runZoned<Future<int>>(() async {
       try {
         await runner.run(args);
         return await _exit(0);
       } catch (error, stackTrace) {
+        firstError = error;
+        firstStackTrace = stackTrace;
         return await _handleToolError(
             error, stackTrace, verbose, args, reportCrashes, getVersion);
       }
     }, onError: (Object error, StackTrace stackTrace) async {
-      await _handleToolError(
-          error, stackTrace, verbose, args, reportCrashes, getVersion);
+      // If sending a crash report throws an error into the zone, we don't want
+      // to re-try sending the crash report with *that* error. Rather, we want
+      // to send the original error that triggered the crash report.
+      final Object e = firstError ?? error;
+      final StackTrace s = firstStackTrace ?? stackTrace;
+      await _handleToolError(e, s, verbose, args, reportCrashes, getVersion);
     });
   }, overrides: overrides);
 }
diff --git a/packages/flutter_tools/test/general.shard/runner/runner_test.dart b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
new file mode 100644
index 0000000..2889d0a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
@@ -0,0 +1,154 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/runner.dart' as runner;
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart' as io;
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:platform/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('runner', () {
+    setUp(() {
+      runner.crashFileSystem = MemoryFileSystem();
+      // Instead of exiting with dart:io exit(), this causes an exception to
+      // be thrown, which we catch with the onError callback in the zone below.
+      io.setExitFunctionForTests((int _) { throw 'test exit';});
+      Cache.disableLocking();
+    });
+
+    tearDown(() {
+      runner.crashFileSystem = const LocalFileSystem();
+      io.restoreExitFunction();
+      Cache.enableLocking();
+    });
+
+    testUsingContext('error handling', () async {
+      final Completer<void> completer = Completer<void>();
+      // runner.run() asynchronously calls the exit function set above, so we
+      // catch it in a zone.
+      unawaited(runZoned<Future<void>>(() {
+          unawaited(runner.run(
+            <String>['test'],
+            <FlutterCommand>[
+              CrashingFlutterCommand(),
+            ],
+            // This flutterVersion disables crash reporting.
+            flutterVersion: '[user-branch]/',
+            reportCrashes: true,
+          ));
+          return null;
+        },
+        onError: (Object error) {
+          expect(error, 'test exit');
+          completer.complete();
+        }));
+      await completer.future;
+
+      // This is the main check of this test.
+      //
+      // We are checking that, even though crash reporting failed with an
+      // exception on the first attempt, the second attempt tries to report the
+      // *original* crash, and not the crash from the first crash report
+      // attempt.
+      final CrashingUsage crashingUsage = flutterUsage;
+      expect(crashingUsage.sentException, 'runCommand');
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(environment: <String, String>{
+        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
+        'FLUTTER_ROOT': '/',
+      }),
+      FileSystem: () => MemoryFileSystem(),
+      Usage: () => CrashingUsage(),
+    });
+  });
+}
+
+class CrashingFlutterCommand extends FlutterCommand {
+  @override
+  String get description => null;
+
+  @override
+  String get name => 'test';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    throw 'runCommand';
+  }
+}
+
+class CrashingUsage implements Usage {
+  CrashingUsage() : _impl = Usage(versionOverride: '[user-branch]');
+
+  final Usage _impl;
+
+  dynamic get sentException => _sentException;
+  dynamic _sentException;
+
+  bool _firstAttempt = true;
+
+  // Crash while crashing.
+  @override
+  void sendException(dynamic exception) {
+    if (_firstAttempt) {
+      _firstAttempt = false;
+      throw 'sendException';
+    }
+    _sentException = exception;
+  }
+
+  @override
+  bool get isFirstRun => _impl.isFirstRun;
+
+  @override
+  bool get suppressAnalytics => _impl.suppressAnalytics;
+
+  @override
+  set suppressAnalytics(bool value) {
+    _impl.suppressAnalytics = value;
+  }
+
+  @override
+  bool get enabled => _impl.enabled;
+
+  @override
+  set enabled(bool value) {
+    _impl.enabled = value;
+  }
+
+  @override
+  String get clientId => _impl.clientId;
+
+  @override
+  void sendCommand(String command, {Map<String, String> parameters}) =>
+      _impl.sendCommand(command, parameters: parameters);
+
+  @override
+  void sendEvent(String category, String parameter, {
+    Map<String, String> parameters
+  }) => _impl.sendEvent(category, parameter, parameters: parameters);
+
+  @override
+  void sendTiming(String category, String variableName, Duration duration, {
+    String label
+  }) => _impl.sendTiming(category, variableName, duration, label: label);
+
+  @override
+  Stream<Map<String, dynamic>> get onSend => _impl.onSend;
+
+  @override
+  Future<void> ensureAnalyticsSent() => _impl.ensureAnalyticsSent();
+
+  @override
+  void printWelcome() => _impl.printWelcome();
+}