Write log trace to $PUB_CACHE/log/pub_log.txt when crashing (#3240)

And also when given --verbose.
diff --git a/lib/src/command.dart b/lib/src/command.dart
index 3f06db1..556c04f 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -10,6 +10,7 @@
 import 'package:collection/collection.dart' show IterableExtension;
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
 
 import 'authentication/token_store.dart';
 import 'command_runner.dart';
@@ -55,7 +56,11 @@
     return a;
   }
 
-  String get directory => argResults['directory'] ?? _pubTopLevel.directory;
+  String get directory =>
+      (argResults.options.contains('directory')
+          ? argResults['directory']
+          : null) ??
+      _pubTopLevel.directory;
 
   late final SystemCache cache = SystemCache(isOffline: isOffline);
 
@@ -170,12 +175,11 @@
   @nonVirtual
   FutureOr<int> run() async {
     computeCommand(_pubTopLevel.argResults);
-    if (_pubTopLevel.trace) {
-      log.recordTranscript();
-    }
+
     log.verbosity = _pubTopLevel.verbosity;
     log.fine('Pub ${sdk.version}');
 
+    var crashed = false;
     try {
       await captureErrors<void>(() async => runProtected(),
           captureStackChains: _pubTopLevel.captureStackChains);
@@ -187,8 +191,24 @@
       log.exception(error, chain);
 
       if (_pubTopLevel.trace) {
-        log.dumpTranscript();
+        log.dumpTranscriptToStdErr();
       } else if (!isUserFacingException(error)) {
+        log.error('''
+This is an unexpected error. The full log and other details are collected in:
+
+    $transcriptPath
+
+Consider creating an issue on https://github.com/dart-lang/pub/issues/new
+and attaching the relevant parts of that log file.
+''');
+        crashed = true;
+      }
+      return _chooseExitCode(error);
+    } finally {
+      final verbose = _pubTopLevel.verbosity == log.Verbosity.all;
+
+      // Write the whole log transcript to file.
+      if (verbose || crashed) {
         // Escape the argument for users to copy-paste in bash.
         // Wrap with single quotation, and use '\'' to insert single quote, as
         // long as we have no spaces this doesn't create a new argument.
@@ -196,16 +216,23 @@
             RegExp(r'^[a-zA-Z0-9-_]+$').stringMatch(x) == null
                 ? "'${x.replaceAll("'", r"'\''")}'"
                 : x;
-        log.error("""
-This is an unexpected error. Please run
 
-    dart pub --trace ${_topCommand.name} ${_topCommand.argResults!.arguments.map(protectArgument).join(' ')}
+        late final Entrypoint? e;
+        try {
+          e = entrypoint;
+        } on ApplicationException {
+          e = null;
+        }
+        log.dumpTranscriptToFile(
+          transcriptPath,
+          'dart pub ${_topCommand.argResults!.arguments.map(protectArgument).join(' ')}',
+          e,
+        );
 
-and include the logs in an issue on https://github.com/dart-lang/pub/issues/new
-""");
+        if (!crashed) {
+          log.message('Logs written to $transcriptPath.');
+        }
       }
-      return _chooseExitCode(error);
-    } finally {
       httpClient.close();
     }
   }
@@ -288,6 +315,10 @@
     }
     _command = list.join(' ');
   }
+
+  String get transcriptPath {
+    return p.join(cache.rootDir, 'log', 'pub_log.txt');
+  }
 }
 
 abstract class PubTopLevel {
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 02faaf9..21f568b 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -72,6 +72,7 @@
       default:
         // No specific verbosity given, so check for the shortcut.
         if (argResults['verbose']) return log.Verbosity.all;
+        if (runningFromTest) return log.Verbosity.testing;
         return log.Verbosity.normal;
     }
   }
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 930e0fb..db24d7a 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -12,9 +12,11 @@
 import 'package:source_span/source_span.dart';
 import 'package:stack_trace/stack_trace.dart';
 
+import 'entrypoint.dart';
 import 'exceptions.dart';
 import 'io.dart';
 import 'progress.dart';
+import 'sdk.dart';
 import 'transcript.dart';
 import 'utils.dart';
 
@@ -36,7 +38,7 @@
 
 /// The list of recorded log messages. Will only be recorded if
 /// [recordTranscript()] is called.
-Transcript<_Entry>? _transcript;
+final Transcript<_Entry> _transcript = Transcript(_maxTranscript);
 
 /// The currently-animated progress indicator, if any.
 ///
@@ -168,6 +170,16 @@
     Level.fine: _logToStderrWithLabel
   });
 
+  /// Shows all logs.
+  static const testing = Verbosity._('testing', {
+    Level.error: _logToStderrWithLabel,
+    Level.warning: _logToStderrWithLabel,
+    Level.message: _logToStdoutWithLabel,
+    Level.io: _logToStderrWithLabel,
+    Level.solver: _logToStderrWithLabel,
+    Level.fine: _logToStderrWithLabel
+  });
+
   const Verbosity._(this.name, this._loggers);
 
   final String name;
@@ -233,7 +245,7 @@
   var logFn = verbosity._loggers[level];
   if (logFn != null) logFn(entry);
 
-  if (_transcript != null) _transcript!.add(entry);
+  _transcript.add(entry);
 }
 
 /// Logs the spawning of an [executable] process with [arguments] at [io]
@@ -313,18 +325,10 @@
   }
 }
 
-/// Enables recording of log entries.
-void recordTranscript() {
-  _transcript = Transcript<_Entry>(_maxTranscript);
-}
-
-/// If [recordTranscript()] was called, then prints the previously recorded log
-/// transcript to stderr.
-void dumpTranscript() {
-  if (_transcript == null) return;
-
+/// Prints the recorded log transcript to stderr.
+void dumpTranscriptToStdErr() {
   stderr.writeln('---- Log transcript ----');
-  _transcript!.forEach((entry) {
+  _transcript.forEach((entry) {
     _printToStream(stderr, entry, showLabel: true);
   }, (discarded) {
     stderr.writeln('---- ($discarded discarded) ----');
@@ -332,6 +336,68 @@
   stderr.writeln('---- End log transcript ----');
 }
 
+String _limit(String input, int limit) {
+  const snip = '[...]';
+  if (input.length < limit - snip.length) return input;
+  return '${input.substring(0, limit ~/ 2 - snip.length)}'
+      '$snip'
+      '${input.substring(limit)}';
+}
+
+/// Prints relevant system information and the log transcript to [path].
+void dumpTranscriptToFile(String path, String command, Entrypoint? entrypoint) {
+  final buffer = StringBuffer();
+  buffer.writeln('''
+Information about the latest pub run.
+
+If you believe something is not working right, you can go to 
+https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file.
+
+Before making this file public, make sure to remove any sensitive information!
+
+Pub version: ${sdk.version}
+Created: ${DateTime.now().toIso8601String()}
+FLUTTER_ROOT: ${Platform.environment['FLUTTER_ROOT'] ?? '<not set>'}
+PUB_HOSTED_URL: ${Platform.environment['PUB_HOSTED_URL'] ?? '<not set>'}
+PUB_CACHE: "${Platform.environment['PUB_CACHE'] ?? '<not set>'}"
+Command: $command
+Platform: ${Platform.operatingSystem}
+''');
+
+  if (entrypoint != null) {
+    buffer.writeln('---- ${p.absolute(entrypoint.pubspecPath)} ----');
+    if (fileExists(entrypoint.pubspecPath)) {
+      buffer.writeln(_limit(readTextFile(entrypoint.pubspecPath), 5000));
+    } else {
+      buffer.writeln('<No pubspec.yaml>');
+    }
+    buffer.writeln('---- End pubspec.yaml ----');
+    buffer.writeln('---- ${p.absolute(entrypoint.lockFilePath)} ----');
+    if (fileExists(entrypoint.lockFilePath)) {
+      buffer.writeln(_limit(readTextFile(entrypoint.lockFilePath), 5000));
+    } else {
+      buffer.writeln('<No pubspec.lock>');
+    }
+    buffer.writeln('---- End pubspec.lock ----');
+  }
+
+  buffer.writeln('---- Log transcript ----');
+
+  _transcript.forEach((entry) {
+    _printToStream(buffer, entry, showLabel: true);
+  }, (discarded) {
+    buffer.writeln('---- ($discarded entries discarded) ----');
+  });
+  buffer.writeln('---- End log transcript ----');
+  ensureDir(p.dirname(path));
+  try {
+    writeTextFile(path, buffer.toString(), dontLogContents: true);
+  } on IOException catch (e) {
+    stderr.writeln('Failed writing log to `$path` ($e), writing it to stderr:');
+    dumpTranscriptToStdErr();
+  }
+}
+
 /// Filter out normal pub output when not attached to a terminal
 ///
 /// Unless the user has overriden the verbosity,
@@ -492,7 +558,7 @@
   _printToStream(sink, entry, showLabel: showLabel);
 }
 
-void _printToStream(IOSink sink, _Entry entry, {required bool showLabel}) {
+void _printToStream(StringSink sink, _Entry entry, {required bool showLabel}) {
   _stopProgress();
 
   var firstLine = true;
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index ccb16f6..a24d5d5 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -6,8 +6,10 @@
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
+import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
 import 'package:test_process/test_process.dart';
+
 import '../descriptor.dart' as d;
 import '../golden_file.dart';
 import '../test_pub.dart';
@@ -16,6 +18,8 @@
 
 late String snapshot;
 
+final logFile = p.join(d.sandbox, cachePath, 'log', 'pub_log.txt');
+
 /// Runs `dart tool/test-bin/pub_command_runner.dart [args]` and appends the output to [buffer].
 Future<void> runEmbeddingToBuffer(
   List<String> args,
@@ -37,12 +41,9 @@
 
   buffer.writeln([
     '\$ $_commandRunner ${args.join(' ')}',
-    ...await process.stdout.rest.toList(),
+    ...await process.stdout.rest.map(_filter).toList(),
+    ...await process.stderr.rest.map((e) => '[E] ${_filter(e)}').toList(),
   ].join('\n'));
-  final stdErr = await process.stderr.rest.toList();
-  if (stdErr.isNotEmpty) {
-    buffer.writeln(stdErr.map((e) => '[E] $e').join('\n'));
-  }
   buffer.write('\n');
 }
 
@@ -51,7 +52,7 @@
   /// next section in golden file.
   Future<void> runEmbedding(
     List<String> args, {
-    String? workingDirextory,
+    String? workingDirectory,
     Map<String, String>? environment,
     dynamic exitCode = 0,
   }) async {
@@ -59,7 +60,7 @@
     await runEmbeddingToBuffer(
       args,
       buffer,
-      workingDirectory: workingDirextory,
+      workingDirectory: workingDirectory,
       environment: environment,
       exitCode: exitCode,
     );
@@ -82,6 +83,7 @@
   });
 
   testWithGolden('run works, though hidden', (ctx) async {
+    await servePackages();
     await d.dir(appPath, [
       d.pubspec({
         'name': 'myapp',
@@ -101,12 +103,53 @@
     ]).create();
     await ctx.runEmbedding(
       ['pub', 'get'],
-      workingDirextory: d.path(appPath),
+      workingDirectory: d.path(appPath),
     );
     await ctx.runEmbedding(
       ['pub', 'run', 'bin/main.dart'],
       exitCode: 123,
-      workingDirextory: d.path(appPath),
+      workingDirectory: d.path(appPath),
+    );
+  });
+
+  testWithGolden(
+      'logfile is written with --verbose and on unexpected exceptions',
+      (context) async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    await d.appDir({'foo': 'any'}).create();
+
+    // TODO(sigurdm) This logs the entire verbose trace to a golden file.
+    //
+    // This is fragile, and can break for all sorts of small reasons. We think
+    // this might be worth while having to have at least minimal testing of the
+    // verbose stack trace.
+    //
+    // But if you, future contributor, think this test is annoying: feel free to
+    // remove it, or rewrite it to filter out the stack-trace itself, only
+    // testing for creation of the file.
+    //
+    //  It is a fragile test, and we acknowledge that it's usefulness can be
+    //  debated...
+    await context.runEmbedding(
+      ['pub', '--verbose', 'get'],
+      workingDirectory: d.path(appPath),
+    );
+    context.expectNextSection(
+      _filter(
+        File(logFile).readAsStringSync(),
+      ),
+    );
+    await d.dir('empty').create();
+    await context.runEmbedding(
+      ['pub', 'fail'],
+      workingDirectory: d.path('empty'),
+      exitCode: 1,
+    );
+    context.expectNextSection(
+      _filter(
+        File(logFile).readAsStringSync(),
+      ),
     );
   });
 
@@ -175,5 +218,106 @@
         }
       },
     });
+    // Don't write the logs to file on a normal run.
+    expect(File(logFile).existsSync(), isFalse);
   });
 }
+
+String _filter(String input) {
+  return input
+      .replaceAll(p.toUri(d.sandbox).toString(), r'file://$SANDBOX')
+      .replaceAll(d.sandbox, r'$SANDBOX')
+      .replaceAll(Platform.pathSeparator, '/')
+      .replaceAll(Platform.operatingSystem, r'$OS')
+      .replaceAll(globalServer.port.toString(), r'$PORT')
+      .replaceAll(
+        RegExp(r'^Created:(.*)$', multiLine: true),
+        r'Created: $TIME',
+      )
+      .replaceAll(
+        RegExp(r'Generated by pub on (.*)$', multiLine: true),
+        r'Generated by pub on $TIME',
+      )
+      .replaceAll(
+        RegExp(r'X-Pub-Session-ID(.*)$', multiLine: true),
+        r'X-Pub-Session-ID: $ID',
+      )
+      .replaceAll(
+        RegExp(r'took (.*)$', multiLine: true),
+        r'took: $TIME',
+      )
+      .replaceAll(
+        RegExp(r'date: (.*)$', multiLine: true),
+        r'date: $TIME',
+      )
+      .replaceAll(
+        RegExp(r'Creating (.*) from stream\.$', multiLine: true),
+        r'Creating $FILE from stream',
+      )
+      .replaceAll(
+        RegExp(r'Created (.*) from stream\.$', multiLine: true),
+        r'Created $FILE from stream',
+      )
+      .replaceAll(
+        RegExp(r'Renaming directory $SANDBOX/cache/_temp/(.*?) to',
+            multiLine: true),
+        r'Renaming directory $SANDBOX/cache/_temp/',
+      )
+      .replaceAll(
+        RegExp(r'Extracting .tar.gz stream to (.*?)$', multiLine: true),
+        r'Extracting .tar.gz stream to $DIR',
+      )
+      .replaceAll(
+        RegExp(r'Extracted .tar.gz to (.*?)$', multiLine: true),
+        r'Extracted .tar.gz to $DIR',
+      )
+      .replaceAll(
+        RegExp(r'Reading binary file (.*?)$', multiLine: true),
+        r'Reading binary file $FILE.',
+      )
+      .replaceAll(
+        RegExp(r'Deleting directory (.*)$', multiLine: true),
+        r'Deleting directory $DIR',
+      )
+      .replaceAll(
+        RegExp(r'Deleting directory (.*)$', multiLine: true),
+        r'Deleting directory $DIR',
+      )
+      .replaceAll(
+        RegExp(r'Resolving dependencies finished (.*)$', multiLine: true),
+        r'Resolving dependencies finished ($TIME)',
+      )
+      .replaceAll(
+        RegExp(r'Created temp directory (.*)$', multiLine: true),
+        r'Created temp directory $DIR',
+      )
+      .replaceAll(
+        RegExp(r'Renaming directory (.*)$', multiLine: true),
+        r'Renaming directory $A to $B',
+      )
+      .replaceAll(
+        RegExp(r'"_fetchedAt":"(.*)"}$', multiLine: true),
+        r'"_fetchedAt": "$TIME"}',
+      )
+      .replaceAll(
+        RegExp(r'"generated": "(.*)",$', multiLine: true),
+        r'"generated": "$TIME",',
+      )
+      .replaceAll(
+        RegExp(r'( |^)(/|[A-Z]:)(.*)/tool/test-bin/pub_command_runner.dart',
+            multiLine: true),
+        r' tool/test-bin/pub_command_runner.dart',
+      )
+      .replaceAll(
+        RegExp(r'[ ]{4,}', multiLine: true),
+        r'   ',
+      )
+      .replaceAll(
+        RegExp(r' [\d]+:[\d]+ ', multiLine: true),
+        r' $LINE:$COL ',
+      )
+      .replaceAll(
+        RegExp(r'Writing \d+ characters', multiLine: true),
+        r'Writing $N characters',
+      );
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 025a0a4..fbf63de 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -466,7 +466,7 @@
 
   var dartArgs = ['--packages=$dotPackagesPath', '--enable-asserts'];
   dartArgs
-    ..addAll([pubPath, if (verbose) '--verbose'])
+    ..addAll([pubPath, if (!verbose) '--verbosity=normal'])
     ..addAll(args);
 
   final mergedEnvironment = getPubTestEnvironment(tokenEndpoint);
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
new file mode 100644
index 0000000..1d3a5f5
--- /dev/null
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -0,0 +1,328 @@
+# GENERATED BY: test/embedding/embedding_test.dart
+
+$ tool/test-bin/pub_command_runner.dart pub --verbose get
+MSG : Resolving dependencies...
+MSG : + foo 1.0.0
+MSG : Downloading foo 1.0.0...
+MSG : Changed 1 dependency!
+MSG : Logs written to $SANDBOX/cache/log/pub_log.txt.
+[E] FINE: Pub 0.1.2+3
+[E] SLVR: fact: myapp is 0.0.0
+[E] SLVR: derived: myapp
+[E] SLVR: fact: myapp depends on foo any
+[E] SLVR:   selecting myapp
+[E] SLVR:   derived: foo any
+[E] IO  : Get versions from http://localhost:$PORT/api/packages/foo.
+[E] IO  : HTTP GET http://localhost:$PORT/api/packages/foo
+[E]    | Accept: application/vnd.pub.v2+json
+[E]    | X-Pub-OS: $OS
+[E]    | X-Pub-Command: get
+[E]    | X-Pub-Session-ID: $ID
+[E]    | X-Pub-Environment: test-environment
+[E]    | X-Pub-Reason: direct
+[E]    | user-agent: Dart pub 0.1.2+3
+[E] IO  : HTTP response 200 OK for GET http://localhost:$PORT/api/packages/foo
+[E]    | took: $TIME
+[E]    | date: $TIME
+[E]    | content-length: 197
+[E]    | x-frame-options: SAMEORIGIN
+[E]    | content-type: text/plain; charset=utf-8
+[E]    | x-xss-protection: 1; mode=block
+[E]    | x-content-type-options: nosniff
+[E]    | server: dart:io with Shelf
+[E] IO  : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json.
+[E] FINE: Contents:
+[E]    | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"}
+[E] SLVR:   selecting foo 1.0.0
+[E] SLVR: Version solving took: $TIME
+[E]    | Tried 1 solutions.
+[E] FINE: Resolving dependencies finished ($TIME)
+[E] IO  : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz.
+[E] IO  : Created temp directory $DIR
+[E] IO  : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
+[E]    | X-Pub-OS: $OS
+[E]    | X-Pub-Command: get
+[E]    | X-Pub-Session-ID: $ID
+[E]    | X-Pub-Environment: test-environment
+[E]    | X-Pub-Reason: direct
+[E]    | user-agent: Dart pub 0.1.2+3
+[E] IO  : HTTP response 200 OK for GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
+[E]    | took: $TIME
+[E]    | transfer-encoding: chunked
+[E]    | date: $TIME
+[E]    | x-frame-options: SAMEORIGIN
+[E]    | content-type: text/plain; charset=utf-8
+[E]    | x-xss-protection: 1; mode=block
+[E]    | x-content-type-options: nosniff
+[E]    | server: dart:io with Shelf
+[E] IO  : Creating $FILE from stream
+[E] FINE: Created $FILE from stream
+[E] IO  : Created temp directory $DIR
+[E] IO  : Reading binary file $FILE.
+[E] FINE: Extracting .tar.gz stream to $DIR
+[E] IO  : Creating $FILE from stream
+[E] FINE: Created $FILE from stream
+[E] IO  : Creating $FILE from stream
+[E] FINE: Created $FILE from stream
+[E] FINE: Extracted .tar.gz to $DIR
+[E] IO  : Renaming directory $A to $B
+[E] IO  : Deleting directory $DIR
+[E] IO  : Writing $N characters to text file pubspec.lock.
+[E] FINE: Contents:
+[E]    | # Generated by pub
+[E]    | # See https://dart.dev/tools/pub/glossary#lockfile
+[E]    | packages:
+[E]    |   foo:
+[E]    |   dependency: "direct main"
+[E]    |   description:
+[E]    |   name: foo
+[E]    |   url: "http://localhost:$PORT"
+[E]    |   source: hosted
+[E]    |   version: "1.0.0"
+[E]    | sdks:
+[E]    |   dart: ">=0.1.2 <1.0.0"
+[E] IO  : Writing $N characters to text file .packages.
+[E] FINE: Contents:
+[E]    | # This file is deprecated. Tools should instead consume 
+[E]    | # `.dart_tool/package_config.json`.
+[E]    | # 
+[E]    | # For more info see: https://dart.dev/go/dot-packages-deprecation
+[E]    | # 
+[E]    | # Generated by pub on $TIME
+[E]    | foo:file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0/lib/
+[E]    | myapp:lib/
+[E] IO  : Writing $N characters to text file .dart_tool/package_config.json.
+[E] FINE: Contents:
+[E]    | {
+[E]    |   "configVersion": 2,
+[E]    |   "packages": [
+[E]    |   {
+[E]    |   "name": "foo",
+[E]    |   "rootUri": "file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0",
+[E]    |   "packageUri": "lib/",
+[E]    |   "languageVersion": "2.7"
+[E]    |   },
+[E]    |   {
+[E]    |   "name": "myapp",
+[E]    |   "rootUri": "../",
+[E]    |   "packageUri": "lib/",
+[E]    |   "languageVersion": "0.1"
+[E]    |   }
+[E]    |   ],
+[E]    |   "generated": "$TIME",
+[E]    |   "generator": "pub",
+[E]    |   "generatorVersion": "0.1.2+3"
+[E]    | }
+[E] IO  : Writing $N characters to text file $SANDBOX/cache/log/pub_log.txt.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+Information about the latest pub run.
+
+If you believe something is not working right, you can go to 
+https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file.
+
+Before making this file public, make sure to remove any sensitive information!
+
+Pub version: 0.1.2+3
+Created: $TIME
+FLUTTER_ROOT: <not set>
+PUB_HOSTED_URL: http://localhost:$PORT
+PUB_CACHE: "$SANDBOX/cache"
+Command: dart pub --verbose get
+Platform: $OS
+
+---- $SANDBOX/myapp/pubspec.yaml ----
+{"name":"myapp","environment":{"sdk":">=0.1.2 <1.0.0"},"dependencies":{"foo":"any"}}
+---- End pubspec.yaml ----
+---- $SANDBOX/myapp/pubspec.lock ----
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  foo:
+   dependency: "direct main"
+   description:
+   name: foo
+   url: "http://localhost:$PORT"
+   source: hosted
+   version: "1.0.0"
+sdks:
+  dart: ">=0.1.2 <1.0.0"
+
+---- End pubspec.lock ----
+---- Log transcript ----
+FINE: Pub 0.1.2+3
+MSG : Resolving dependencies...
+SLVR: fact: myapp is 0.0.0
+SLVR: derived: myapp
+SLVR: fact: myapp depends on foo any
+SLVR:   selecting myapp
+SLVR:   derived: foo any
+IO  : Get versions from http://localhost:$PORT/api/packages/foo.
+IO  : HTTP GET http://localhost:$PORT/api/packages/foo
+   | Accept: application/vnd.pub.v2+json
+   | X-Pub-OS: $OS
+   | X-Pub-Command: get
+   | X-Pub-Session-ID: $ID
+   | X-Pub-Environment: test-environment
+   | X-Pub-Reason: direct
+   | user-agent: Dart pub 0.1.2+3
+IO  : HTTP response 200 OK for GET http://localhost:$PORT/api/packages/foo
+   | took: $TIME
+   | date: $TIME
+   | content-length: 197
+   | x-frame-options: SAMEORIGIN
+   | content-type: text/plain; charset=utf-8
+   | x-xss-protection: 1; mode=block
+   | x-content-type-options: nosniff
+   | server: dart:io with Shelf
+IO  : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json.
+FINE: Contents:
+   | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"}
+SLVR:   selecting foo 1.0.0
+SLVR: Version solving took: $TIME
+   | Tried 1 solutions.
+FINE: Resolving dependencies finished ($TIME)
+MSG : + foo 1.0.0
+IO  : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz.
+MSG : Downloading foo 1.0.0...
+IO  : Created temp directory $DIR
+IO  : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
+   | X-Pub-OS: $OS
+   | X-Pub-Command: get
+   | X-Pub-Session-ID: $ID
+   | X-Pub-Environment: test-environment
+   | X-Pub-Reason: direct
+   | user-agent: Dart pub 0.1.2+3
+IO  : HTTP response 200 OK for GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz
+   | took: $TIME
+   | transfer-encoding: chunked
+   | date: $TIME
+   | x-frame-options: SAMEORIGIN
+   | content-type: text/plain; charset=utf-8
+   | x-xss-protection: 1; mode=block
+   | x-content-type-options: nosniff
+   | server: dart:io with Shelf
+IO  : Creating $FILE from stream
+FINE: Created $FILE from stream
+IO  : Created temp directory $DIR
+IO  : Reading binary file $FILE.
+FINE: Extracting .tar.gz stream to $DIR
+IO  : Creating $FILE from stream
+FINE: Created $FILE from stream
+IO  : Creating $FILE from stream
+FINE: Created $FILE from stream
+FINE: Extracted .tar.gz to $DIR
+IO  : Renaming directory $A to $B
+IO  : Deleting directory $DIR
+IO  : Writing $N characters to text file pubspec.lock.
+FINE: Contents:
+   | # Generated by pub
+   | # See https://dart.dev/tools/pub/glossary#lockfile
+   | packages:
+   |   foo:
+   |   dependency: "direct main"
+   |   description:
+   |   name: foo
+   |   url: "http://localhost:$PORT"
+   |   source: hosted
+   |   version: "1.0.0"
+   | sdks:
+   |   dart: ">=0.1.2 <1.0.0"
+MSG : Changed 1 dependency!
+IO  : Writing $N characters to text file .packages.
+FINE: Contents:
+   | # This file is deprecated. Tools should instead consume 
+   | # `.dart_tool/package_config.json`.
+   | # 
+   | # For more info see: https://dart.dev/go/dot-packages-deprecation
+   | # 
+   | # Generated by pub on $TIME
+   | foo:file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0/lib/
+   | myapp:lib/
+IO  : Writing $N characters to text file .dart_tool/package_config.json.
+FINE: Contents:
+   | {
+   |   "configVersion": 2,
+   |   "packages": [
+   |   {
+   |   "name": "foo",
+   |   "rootUri": "file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0",
+   |   "packageUri": "lib/",
+   |   "languageVersion": "2.7"
+   |   },
+   |   {
+   |   "name": "myapp",
+   |   "rootUri": "../",
+   |   "packageUri": "lib/",
+   |   "languageVersion": "0.1"
+   |   }
+   |   ],
+   |   "generated": "$TIME",
+   |   "generator": "pub",
+   |   "generatorVersion": "0.1.2+3"
+   | }
+---- End log transcript ----
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ tool/test-bin/pub_command_runner.dart pub fail
+[E] Bad state: Pub has crashed
+[E]  tool/test-bin/pub_command_runner.dart $LINE:$COL   ThrowingCommand.runProtected
+[E] package:pub/src/command.dart $LINE:$COL   PubCommand.run.<fn>
+[E] package:pub/src/command.dart $LINE:$COL   PubCommand.run.<fn>
+[E] dart:async   new Future.sync
+[E] package:pub/src/utils.dart $LINE:$COL   captureErrors.wrappedCallback
+[E] dart:async   runZonedGuarded
+[E] package:pub/src/utils.dart $LINE:$COL   captureErrors
+[E] package:pub/src/command.dart $LINE:$COL   PubCommand.run
+[E] package:args/command_runner.dart $LINE:$COL   CommandRunner.runCommand
+[E]  tool/test-bin/pub_command_runner.dart $LINE:$COL  Runner.runCommand
+[E]  tool/test-bin/pub_command_runner.dart $LINE:$COL  Runner.run
+[E]  tool/test-bin/pub_command_runner.dart $LINE:$COL  main
+[E] This is an unexpected error. The full log and other details are collected in:
+[E] 
+[E]    $SANDBOX/cache/log/pub_log.txt
+[E] 
+[E] Consider creating an issue on https://github.com/dart-lang/pub/issues/new
+[E] and attaching the relevant parts of that log file.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+Information about the latest pub run.
+
+If you believe something is not working right, you can go to 
+https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file.
+
+Before making this file public, make sure to remove any sensitive information!
+
+Pub version: 0.1.2+3
+Created: $TIME
+FLUTTER_ROOT: <not set>
+PUB_HOSTED_URL: http://localhost:$PORT
+PUB_CACHE: "$SANDBOX/cache"
+Command: dart pub fail
+Platform: $OS
+
+---- Log transcript ----
+FINE: Pub 0.1.2+3
+ERR : Bad state: Pub has crashed
+FINE: Exception type: StateError
+ERR : tool/test-bin/pub_command_runner.dart $LINE:$COL   ThrowingCommand.runProtected
+   | package:pub/src/command.dart $LINE:$COL   PubCommand.run.<fn>
+   | package:pub/src/command.dart $LINE:$COL   PubCommand.run.<fn>
+   | dart:async   new Future.sync
+   | package:pub/src/utils.dart $LINE:$COL   captureErrors.wrappedCallback
+   | dart:async   runZonedGuarded
+   | package:pub/src/utils.dart $LINE:$COL   captureErrors
+   | package:pub/src/command.dart $LINE:$COL   PubCommand.run
+   | package:args/command_runner.dart $LINE:$COL   CommandRunner.runCommand
+   | tool/test-bin/pub_command_runner.dart $LINE:$COL  Runner.runCommand
+   | tool/test-bin/pub_command_runner.dart $LINE:$COL  Runner.run
+   | tool/test-bin/pub_command_runner.dart $LINE:$COL  main
+ERR : This is an unexpected error. The full log and other details are collected in:
+   | 
+   |   $SANDBOX/cache/log/pub_log.txt
+   | 
+   | Consider creating an issue on https://github.com/dart-lang/pub/issues/new
+   | and attaching the relevant parts of that log file.
+---- End log transcript ----
diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart
index 2be51e8..206a95b 100644
--- a/tool/test-bin/pub_command_runner.dart
+++ b/tool/test-bin/pub_command_runner.dart
@@ -9,12 +9,30 @@
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
 import 'package:pub/pub.dart';
+import 'package:pub/src/command.dart';
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:pub/src/log.dart' as log;
 import 'package:usage/usage.dart';
 
 final _LoggingAnalytics loggingAnalytics = _LoggingAnalytics();
 
+// A command for explicitly throwing an exception, to test the handling of
+// unexpected eceptions.
+class ThrowingCommand extends PubCommand {
+  @override
+  String get name => 'fail';
+
+  @override
+  String get description => 'Throws an exception';
+
+  bool get hide => true;
+
+  @override
+  Future<int> runProtected() async {
+    throw StateError('Pub has crashed');
+  }
+}
+
 class Runner extends CommandRunner<int> {
   late ArgResults _options;
 
@@ -23,7 +41,8 @@
         ? PubAnalytics(() => loggingAnalytics,
             dependencyKindCustomDimensionName: 'cd1')
         : null;
-    addCommand(pubCommand(analytics: analytics));
+    addCommand(
+        pubCommand(analytics: analytics)..addSubcommand(ThrowingCommand()));
   }
 
   @override