Update tools to use package:process (#7590)

diff --git a/packages/flutter_tools/lib/src/base/process_manager.dart b/packages/flutter_tools/lib/src/base/process_manager.dart
index c416fe8..e5cab3d 100644
--- a/packages/flutter_tools/lib/src/base/process_manager.dart
+++ b/packages/flutter_tools/lib/src/base/process_manager.dart
@@ -3,849 +3,82 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert';
 
-import 'package:archive/archive.dart';
-import 'package:intl/intl.dart';
-import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+import 'package:process/record_replay.dart';
 
+import 'common.dart';
 import 'context.dart';
-import 'file_system.dart' hide IOSink;
-import 'io.dart';
-import 'os.dart';
+import 'file_system.dart';
 import 'process.dart';
 
+/// The active process manager.
 ProcessManager get processManager => context[ProcessManager];
 
-const String _kManifestName = 'MANIFEST.txt';
-
-bool _areListsEqual<T>(List<T> list1, List<T> list2) {
-  int i = 0;
-  return list1 != null
-      && list2 != null
-      && list1.length == list2.length
-      && list1.every((dynamic element) => element == list2[i++]);
-}
-
-/// A class that manages the creation of operating system processes. This
-/// provides a lightweight wrapper around the underlying [Process] static
-/// methods to allow the implementation of these methods to be mocked out or
-/// decorated for testing or debugging purposes.
-class ProcessManager {
-  Future<Process> start(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    ProcessStartMode mode: ProcessStartMode.NORMAL,
-  }) {
-    return Process.start(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      mode: mode,
-    );
-  }
-
-  Future<ProcessResult> run(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) {
-    return Process.run(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-    );
-  }
-
-  ProcessResult runSync(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) {
-    return Process.runSync(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-    );
-  }
-
-  bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
-    return Process.killPid(pid, signal);
-  }
-}
-
-/// A [ProcessManager] implementation that decorates the standard behavior by
-/// recording all process invocation activity (including the stdout and stderr
-/// of the associated processes) and serializing that recording to a ZIP file
-/// when the Flutter tools process exits.
-class RecordingProcessManager implements ProcessManager {
-  static const String kDefaultRecordTo = 'recording.zip';
-  static const List<String> _kSkippableExecutables = const <String>[
-    'env',
-    'xcrun',
-  ];
-
-  final String _recordTo;
-  final ProcessManager _delegate = new ProcessManager();
-  final Directory _tmpDir = fs.systemTempDirectory.createTempSync('flutter_tools_');
-  final List<Map<String, dynamic>> _manifest = <Map<String, dynamic>>[];
-  final Map<int, Future<int>> _runningProcesses = <int, Future<int>>{};
-
-  /// Constructs a new `RecordingProcessManager` that will record all process
-  /// invocations and serialize them to the a ZIP file at the specified
-  /// [recordTo] location.
-  ///
-  /// If [recordTo] is a directory, a ZIP file named
-  /// [kDefaultRecordTo](`recording.zip`) will be created in the specified
-  /// directory.
-  ///
-  /// If [recordTo] is a file (or doesn't exist), it is taken to be the name
-  /// of the ZIP file that will be created, and the containing folder will be
-  /// created as needed.
-  RecordingProcessManager(this._recordTo) {
-    addShutdownHook(_onShutdown);
-  }
-
-  @override
-  Future<Process> start(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    ProcessStartMode mode: ProcessStartMode.NORMAL,
-  }) async {
-    Process process = await _delegate.start(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      mode: mode,
-    );
-
-    String basename = _getBasename(process.pid, executable, arguments);
-    Map<String, dynamic> manifestEntry = _createManifestEntry(
-      pid: process.pid,
-      basename: basename,
-      executable: executable,
-      arguments: arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      mode: mode,
-    );
-    _manifest.add(manifestEntry);
-
-    _RecordingProcess result = new _RecordingProcess(
-      manager: this,
-      basename: basename,
-      delegate: process,
-    );
-    await result.startRecording();
-    _runningProcesses[process.pid] = result.exitCode.then((int exitCode) {
-      _runningProcesses.remove(process.pid);
-      manifestEntry['exitCode'] = exitCode;
-    });
-
-    return result;
-  }
-
-  @override
-  Future<ProcessResult> run(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) async {
-    ProcessResult result = await _delegate.run(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-    );
-
-    String basename = _getBasename(result.pid, executable, arguments);
-    _manifest.add(_createManifestEntry(
-      pid: result.pid,
-      basename: basename,
-      executable: executable,
-      arguments: arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-      exitCode: result.exitCode,
-    ));
-
-    await _recordData(result.stdout, stdoutEncoding, '$basename.stdout');
-    await _recordData(result.stderr, stderrEncoding, '$basename.stderr');
-
-    return result;
-  }
-
-  Future<Null> _recordData(dynamic data, Encoding encoding, String basename) async {
-    String path = '${_tmpDir.path}/$basename';
-    File file = await fs.file(path).create();
-    RandomAccessFile recording = await file.open(mode: FileMode.WRITE);
-    try {
-      if (encoding == null)
-        await recording.writeFrom(data);
-      else
-        await recording.writeString(data, encoding: encoding);
-      await recording.flush();
-    } finally {
-      await recording.close();
-    }
-  }
-
-  @override
-  ProcessResult runSync(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) {
-    ProcessResult result = _delegate.runSync(
-      executable,
-      arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-    );
-
-    String basename = _getBasename(result.pid, executable, arguments);
-    _manifest.add(_createManifestEntry(
-      pid: result.pid,
-      basename: basename,
-      executable: executable,
-      arguments: arguments,
-      workingDirectory: workingDirectory,
-      environment: environment,
-      stdoutEncoding: stdoutEncoding,
-      stderrEncoding: stderrEncoding,
-      exitCode: result.exitCode,
-    ));
-
-    _recordDataSync(result.stdout, stdoutEncoding, '$basename.stdout');
-    _recordDataSync(result.stderr, stderrEncoding, '$basename.stderr');
-
-    return result;
-  }
-
-  void _recordDataSync(dynamic data, Encoding encoding, String basename) {
-    String path = '${_tmpDir.path}/$basename';
-    File file = fs.file(path)..createSync();
-    RandomAccessFile recording = file.openSync(mode: FileMode.WRITE);
-    try {
-      if (encoding == null)
-        recording.writeFromSync(data);
-      else
-        recording.writeStringSync(data, encoding: encoding);
-      recording.flushSync();
-    } finally {
-      recording.closeSync();
-    }
-  }
-
-  @override
-  bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
-    return _delegate.killPid(pid, signal);
-  }
-
-  /// Creates a JSON-encodable manifest entry representing the specified
-  /// process invocation.
-  Map<String, dynamic> _createManifestEntry({
-    int pid,
-    String basename,
-    String executable,
-    List<String> arguments,
-    String workingDirectory,
-    Map<String, String> environment,
-    ProcessStartMode mode,
-    Encoding stdoutEncoding,
-    Encoding stderrEncoding,
-    int exitCode,
-  }) {
-    return new _ManifestEntryBuilder()
-      .add('pid', pid)
-      .add('basename', basename)
-      .add('executable', executable)
-      .add('arguments', arguments)
-      .add('workingDirectory', workingDirectory)
-      .add('environment', environment)
-      .add('mode', mode, () => mode.toString())
-      .add('stdoutEncoding', stdoutEncoding, () => stdoutEncoding.name)
-      .add('stderrEncoding', stderrEncoding, () => stderrEncoding.name)
-      .add('exitCode', exitCode)
-      .entry;
-  }
-
-  /// Returns a human-readable identifier for the specified executable.
-  String _getBasename(int pid, String executable, List<String> arguments) {
-    String index = new NumberFormat('000').format(_manifest.length);
-    String identifier = path.basename(executable);
-    if (_kSkippableExecutables.contains(identifier)
-        && arguments != null
-        && arguments.isNotEmpty) {
-      identifier = path.basename(arguments.first);
-    }
-    return '$index.$identifier.$pid';
-  }
-
-  /// Invoked when the outermost executable process is about to shutdown
-  /// safely. This saves our recording to a ZIP file at the location specified
-  /// in the [new RecordingProcessManager] constructor.
-  Future<Null> _onShutdown() async {
-    await _waitForRunningProcessesToExit();
-    await _writeManifestToDisk();
-    await _saveRecording();
-    await _tmpDir.delete(recursive: true);
-  }
-
-  /// Waits for all running processes to exit, and records their exit codes in
-  /// the process manifest. Any process that doesn't exit in a timely fashion
-  /// will have a `"daemon"` marker added to its manifest and be signalled with
-  /// `SIGTERM`. If such processes *still* don't exit in a timely fashion after
-  /// being signalled, they'll have a `"notResponding"` marker added to their
-  /// manifest.
-  Future<Null> _waitForRunningProcessesToExit() async {
-    await _waitForRunningProcessesToExitWithTimeout(
-      onTimeout: (int pid, Map<String, dynamic> manifestEntry) {
-        manifestEntry['daemon'] = true;
-        Process.killPid(pid);
-      });
-    // Now that we explicitly signalled the processes that timed out asking
-    // them to shutdown, wait one more time for those processes to exit.
-    await _waitForRunningProcessesToExitWithTimeout(
-      onTimeout: (int pid, Map<String, dynamic> manifestEntry) {
-        manifestEntry['notResponding'] = true;
-      });
-  }
-
-  Future<Null> _waitForRunningProcessesToExitWithTimeout({
-    void onTimeout(int pid, Map<String, dynamic> manifestEntry),
-  }) async {
-    await Future.wait(new List<Future<int>>.from(_runningProcesses.values))
-        .timeout(const Duration(milliseconds: 20), onTimeout: () {
-          _runningProcesses.forEach((int pid, Future<int> future) {
-            Map<String, dynamic> manifestEntry = _manifest
-                .firstWhere((Map<String, dynamic> entry) => entry['pid'] == pid);
-            onTimeout(pid, manifestEntry);
-          });
-        });
-  }
-
-  /// Writes our process invocation manifest to disk in our temp folder.
-  Future<Null> _writeManifestToDisk() async {
-    JsonEncoder encoder = new JsonEncoder.withIndent('  ');
-    String encodedManifest = encoder.convert(_manifest);
-    File manifestFile = await fs.file('${_tmpDir.path}/$_kManifestName').create();
-    await manifestFile.writeAsString(encodedManifest, flush: true);
-  }
-
-  /// Saves our recording to a ZIP file at the specified location.
-  Future<Null> _saveRecording() async {
-    File zipFile = await _createZipFile();
-    List<int> zipData = await _getRecordingZipBytes();
-    await zipFile.writeAsBytes(zipData);
-  }
-
-  /// Creates our recording ZIP file at the location specified
-  /// in the [new RecordingProcessManager] constructor.
-  Future<File> _createZipFile() async {
-    File zipFile;
-    String recordTo = _recordTo ?? fs.currentDirectory.path;
-    if (fs.isDirectorySync(recordTo)) {
-      zipFile = fs.file('$recordTo/$kDefaultRecordTo');
-    } else {
-      zipFile = fs.file(recordTo);
-      await fs.directory(path.dirname(zipFile.path)).create(recursive: true);
-    }
-
-    // Resolve collisions.
-    String basename = path.basename(zipFile.path);
-    for (int i = 1; zipFile.existsSync(); i++) {
-      assert(fs.isFileSync(zipFile.path));
-      String disambiguator = new NumberFormat('00').format(i);
-      String newBasename = basename;
-      if (basename.contains('.')) {
-        List<String> parts = basename.split('.');
-        parts[parts.length - 2] += '-$disambiguator';
-        newBasename = parts.join('.');
-      } else {
-        newBasename += '-$disambiguator';
-      }
-      zipFile = fs.file(path.join(path.dirname(zipFile.path), newBasename));
-    }
-
-    return await zipFile.create();
-  }
-
-  /// Gets the bytes of our ZIP file recording.
-  Future<List<int>> _getRecordingZipBytes() async {
-    Archive archive = new Archive();
-    Stream<FileSystemEntity> files = _tmpDir.list(recursive: true)
-        .where((FileSystemEntity entity) => fs.isFileSync(entity.path));
-    List<Future<dynamic>> addAllFilesToArchive = <Future<dynamic>>[];
-    await files.forEach((FileSystemEntity entity) {
-      File file = entity;
-      Future<dynamic> readAsBytes = file.readAsBytes();
-      addAllFilesToArchive.add(readAsBytes.then<Null>((List<int> data) {
-        archive.addFile(new ArchiveFile.noCompress(
-          path.basename(file.path), data.length, data)
-        );
-      }));
-    });
-
-    await Future.wait<dynamic>(addAllFilesToArchive);
-    return new ZipEncoder().encode(archive);
-  }
-}
-
-/// A lightweight class that provides a builder pattern for building a
-/// manifest entry.
-class _ManifestEntryBuilder {
-  Map<String, dynamic> entry = <String, dynamic>{};
-
-  /// Adds the specified key/value pair to the manifest entry iff the value
-  /// is non-null. If [jsonValue] is specified, its value will be used instead
-  /// of the raw value.
-  _ManifestEntryBuilder add(String name, dynamic value, [dynamic jsonValue()]) {
-    if (value != null)
-      entry[name] = jsonValue == null ? value : jsonValue();
-    return this;
-  }
-}
-
-/// A [Process] implementation that records `stdout` and `stderr` stream events
-/// to disk before forwarding them on to the underlying stream listener.
-class _RecordingProcess implements Process {
-  final Process delegate;
-  final String basename;
-  final RecordingProcessManager manager;
-
-  bool _started = false;
-
-  StreamController<List<int>> _stdoutController = new StreamController<List<int>>();
-  StreamController<List<int>> _stderrController = new StreamController<List<int>>();
-
-  _RecordingProcess({this.manager, this.basename, this.delegate});
-
-  Future<Null> startRecording() async {
-    assert(!_started);
-    _started = true;
-    await Future.wait(<Future<Null>>[
-      _recordStream(delegate.stdout, _stdoutController, 'stdout'),
-      _recordStream(delegate.stderr, _stderrController, 'stderr'),
-    ]);
-  }
-
-  Future<Null> _recordStream(
-    Stream<List<int>> stream,
-    StreamController<List<int>> controller,
-    String suffix,
-  ) async {
-    String path = '${manager._tmpDir.path}/$basename.$suffix';
-    File file = await fs.file(path).create();
-    RandomAccessFile recording = await file.open(mode: FileMode.WRITE);
-    stream.listen(
-      (List<int> data) {
-        // Write synchronously to guarantee that the order of data
-        // within our recording is preserved across stream notifications.
-        recording.writeFromSync(data);
-        // Flush immediately so that if the program crashes, forensic
-        // data from the recording won't be lost.
-        recording.flushSync();
-        controller.add(data);
-      },
-      onError: (dynamic error, StackTrace stackTrace) {
-        recording.closeSync();
-        controller.addError(error, stackTrace);
-      },
-      onDone: () {
-        recording.closeSync();
-        controller.close();
-      },
-    );
-  }
-
-  @override
-  Future<int> get exitCode => delegate.exitCode;
-
-  // TODO(tvolkert): Remove this once the dart sdk in both the target and
-  // the host have picked up dart-lang/sdk@e5a16b1
-  @override // ignore: OVERRIDE_ON_NON_OVERRIDING_SETTER
-  set exitCode(Future<int> exitCode) => throw new UnsupportedError('set exitCode');
-
-  @override
-  Stream<List<int>> get stdout {
-    assert(_started);
-    return _stdoutController.stream;
-  }
-
-  @override
-  Stream<List<int>> get stderr {
-    assert(_started);
-    return _stderrController.stream;
-  }
-
-  @override
-  IOSink get stdin {
-    // We don't currently support recording `stdin`.
-    return delegate.stdin;
-  }
-
-  @override
-  int get pid => delegate.pid;
-
-  @override
-  bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => delegate.kill(signal);
-}
-
-/// A [ProcessManager] implementation that mocks out all process invocations
-/// by replaying a previously-recorded series of invocations, throwing an
-/// exception if the requested invocations substantively differ in any way
-/// from those in the recording.
+/// Enables recording of process invocation activity to the specified location.
 ///
-/// Recordings are expected to be of the form produced by
-/// [RecordingProcessManager]. Namely, this includes:
+/// This sets the [active process manager](processManager) to one that records
+/// all process activity before delegating to a [LocalProcessManager].
 ///
-/// - a [_kManifestName](manifest file) encoded as UTF-8 JSON that lists all
-///   invocations in order, along with the following metadata for each
-///   invocation:
-///   - `pid` (required): The process id integer.
-///   - `basename` (required): A string specifying the base filename from which
-///     the incovation's `stdout` and `stderr` files can be located.
-///   - `executable` (required): A string specifying the path to the executable
-///     command that kicked off the process.
-///   - `arguments` (required): A list of strings that were passed as arguments
-///     to the executable.
-///   - `workingDirectory` (required): The current working directory from which
-///     the process was spawned.
-///   - `environment` (required): A map from string environment variable keys
-///     to their corresponding string values.
-///   - `mode` (optional): A string specifying the [ProcessStartMode].
-///   - `stdoutEncoding` (optional): The name of the encoding scheme that was
-///     used in the `stdout` file. If unspecified, then the file was written
-///     as binary data.
-///   - `stderrEncoding` (optional): The name of the encoding scheme that was
-///     used in the `stderr` file. If unspecified, then the file was written
-///     as binary data.
-///   - `exitCode` (required): The exit code of the process, or null if the
-///     process was not responding.
-///   - `daemon` (optional): A boolean indicating that the process is to stay
-///     resident during the entire lifetime of the master Flutter tools process.
-/// - a `stdout` file for each process invocation. The location of this file
-///   can be derived from the `basename` manifest property like so:
-///   `'$basename.stdout'`.
-/// - a `stderr` file for each process invocation. The location of this file
-///   can be derived from the `basename` manifest property like so:
-///   `'$basename.stderr'`.
-class ReplayProcessManager implements ProcessManager {
-  final List<Map<String, dynamic>> _manifest;
-  final Directory _dir;
-
-  ReplayProcessManager._(this._manifest, this._dir);
-
-  /// Creates a new `ReplayProcessManager` capable of replaying a recording at
-  /// the specified location.
-  ///
-  /// If [location] represents a file, it will be treated like a recording
-  /// ZIP file. If it points to a directory, it will be treated like an
-  /// unzipped recording. If [location] points to a non-existent file or
-  /// directory, an [ArgumentError] will be thrown.
-  static Future<ReplayProcessManager> create(String location) async {
-    Directory dir;
-    switch (fs.typeSync(location)) {
-      case FileSystemEntityType.FILE:
-        dir = await fs.systemTempDirectory.createTemp('flutter_tools_');
-        os.unzip(fs.file(location), dir);
-        addShutdownHook(() async {
-          await dir.delete(recursive: true);
-        });
-        break;
-      case FileSystemEntityType.DIRECTORY:
-        dir = fs.directory(location);
-        break;
-      case FileSystemEntityType.NOT_FOUND:
-        throw new ArgumentError.value(location, 'location', 'Does not exist');
-    }
-
-    File manifestFile = fs.file(path.join(dir.path, _kManifestName));
-    if (!manifestFile.existsSync()) {
-      // We use the existence of the manifest as a proxy for this being a
-      // valid replay directory. Namely, we don't validate the structure of the
-      // JSON within the manifest, and we don't validate the existence of
-      // all stdout and stderr files referenced in the manifest.
-      throw new ArgumentError.value(location, 'location',
-          'Does not represent a valid recording (it does not '
-          'contain $_kManifestName).');
-    }
-
-    String content = await manifestFile.readAsString();
-    try {
-      List<Map<String, dynamic>> manifest = new JsonDecoder().convert(content);
-      return new ReplayProcessManager._(manifest, dir);
-    } on FormatException catch (e) {
-      throw new ArgumentError('$_kManifestName is not a valid JSON file: $e');
-    }
+/// [location] must either represent a valid, empty directory or a non-existent
+/// file system entity, in which case a directory will be created at that path.
+/// Process invocation activity will be serialized to opaque files in that
+/// directory. The resulting (populated) directory will be suitable for use
+/// with [enableReplayProcessManager].
+void enableRecordingProcessManager(String location) {
+  if (location.isEmpty)
+    throwToolExit('record-to location not specified');
+  switch (fs.typeSync(location, followLinks: false)) {
+    case FileSystemEntityType.FILE:
+    case FileSystemEntityType.LINK:
+      throwToolExit('record-to location must reference a directory');
+      break;
+    case FileSystemEntityType.DIRECTORY:
+      if (fs.directory(location).listSync(followLinks: false).isNotEmpty)
+        throwToolExit('record-to directory must be empty');
+      break;
+    case FileSystemEntityType.NOT_FOUND:
+      fs.directory(location).createSync(recursive: true);
   }
+  Directory dir = fs.directory(location);
 
-  @override
-  Future<Process> start(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    ProcessStartMode mode: ProcessStartMode.NORMAL,
-  }) async {
-    Map<String, dynamic> entry = _popEntry(executable, arguments, mode: mode);
-    _ReplayProcessResult result = await _ReplayProcessResult.create(
-        executable, arguments, _dir, entry);
-    return result.asProcess(entry['daemon'] ?? false);
-  }
+  ProcessManager delegate = new LocalProcessManager();
+  RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
+  addShutdownHook(() async {
+    await manager.flush(finishRunningProcesses: true);
+  });
 
-  @override
-  Future<ProcessResult> run(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) async {
-    Map<String, dynamic> entry = _popEntry(executable, arguments,
-        stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
-    return await _ReplayProcessResult.create(
-        executable, arguments, _dir, entry);
-  }
+  context.setVariable(ProcessManager, manager);
+}
 
-  @override
-  ProcessResult runSync(
-    String executable,
-    List<String> arguments, {
-    String workingDirectory,
-    Map<String, String> environment,
-    Encoding stdoutEncoding: SYSTEM_ENCODING,
-    Encoding stderrEncoding: SYSTEM_ENCODING,
-  }) {
-    Map<String, dynamic> entry = _popEntry(executable, arguments,
-        stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
-    return _ReplayProcessResult.createSync(
-        executable, arguments, _dir, entry);
-  }
+/// Enables process invocation replay mode.
+///
+/// This sets the [active process manager](processManager) to one that replays
+/// process activity from a previously recorded set of invocations.
+///
+/// [location] must represent a directory to which process activity has been
+/// recorded (i.e. the result of having been previously passed to
+/// [enableRecordingProcessManager]).
+Future<Null> enableReplayProcessManager(String location) async {
+  if (location.isEmpty)
+    throwToolExit('replay-from location not specified');
+  Directory dir = fs.directory(location);
+  if (!dir.existsSync())
+    throwToolExit('replay-from location must reference a directory');
 
-  /// Finds and returns the next entry in the process manifest that matches
-  /// the specified process arguments. Once found, it marks the manifest entry
-  /// as having been invoked and thus not eligible for invocation again.
-  Map<String, dynamic> _popEntry(String executable, List<String> arguments, {
-    ProcessStartMode mode,
-    Encoding stdoutEncoding,
-    Encoding stderrEncoding,
-  }) {
-    Map<String, dynamic> entry = _manifest.firstWhere(
-      (Map<String, dynamic> entry) {
-        // Ignore workingDirectory & environment, as they could
-        // yield false negatives.
-        return entry['executable'] == executable
-            && _areListsEqual(entry['arguments'], arguments)
-            && entry['mode'] == mode?.toString()
-            && entry['stdoutEncoding'] == stdoutEncoding?.name
-            && entry['stderrEncoding'] == stderrEncoding?.name
-            && !(entry['invoked'] ?? false);
-      },
-      orElse: () => null,
+  ProcessManager manager;
+  try {
+    manager = await ReplayProcessManager.create(dir,
+      // TODO(tvolkert): Once https://github.com/flutter/flutter/issues/7166 is
+      //     resolved, we can use the default `streamDelay`. In the
+      //     meantime, native file I/O operations cause our `tail` process
+      //     streams to flush before our protocol discovery is listening on
+      //     them, causing us to timeout waiting for the observatory port.
+      streamDelay: const Duration(milliseconds: 50),
     );
-
-    if (entry == null)
-      throw new ProcessException(executable, arguments, 'No matching invocation found');
-
-    entry['invoked'] = true;
-    return entry;
+  } on ArgumentError catch (error) {
+    throwToolExit('Invalid replay-from: $error');
   }
 
-  @override
-  bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
-    throw new UnsupportedError(
-        "$runtimeType.killPid() has not been implemented because at the time "
-        "of its writing, it wasn't needed. If you're hitting this error, you "
-        "should implement it.");
-  }
-}
-
-/// A [ProcessResult] implementation that derives its data from a recording
-/// fragment.
-class _ReplayProcessResult implements ProcessResult {
-  @override
-  final int pid;
-
-  @override
-  final int exitCode;
-
-  @override
-  final dynamic stdout;
-
-  @override
-  final dynamic stderr;
-
-  _ReplayProcessResult._({this.pid, this.exitCode, this.stdout, this.stderr});
-
-  static Future<_ReplayProcessResult> create(
-    String executable,
-    List<String> arguments,
-    Directory dir,
-    Map<String, dynamic> entry,
-  ) async {
-    String basePath = path.join(dir.path, entry['basename']);
-    try {
-      return new _ReplayProcessResult._(
-        pid: entry['pid'],
-        exitCode: entry['exitCode'],
-        stdout: await _getData('$basePath.stdout', entry['stdoutEncoding']),
-        stderr: await _getData('$basePath.stderr', entry['stderrEncoding']),
-      );
-    } catch (e) {
-      throw new ProcessException(executable, arguments, e.toString());
-    }
-  }
-
-  static Future<dynamic> _getData(String path, String encoding) async {
-    File file = fs.file(path);
-    return encoding == null
-        ? await file.readAsBytes()
-        : await file.readAsString(encoding: _getEncodingByName(encoding));
-  }
-
-  static _ReplayProcessResult createSync(
-    String executable,
-    List<String> arguments,
-    Directory dir,
-    Map<String, dynamic> entry,
-  ) {
-    String basePath = path.join(dir.path, entry['basename']);
-    try {
-      return new _ReplayProcessResult._(
-        pid: entry['pid'],
-        exitCode: entry['exitCode'],
-        stdout: _getDataSync('$basePath.stdout', entry['stdoutEncoding']),
-        stderr: _getDataSync('$basePath.stderr', entry['stderrEncoding']),
-      );
-    } catch (e) {
-      throw new ProcessException(executable, arguments, e.toString());
-    }
-  }
-
-  static dynamic _getDataSync(String path, String encoding) {
-    File file = fs.file(path);
-    return encoding == null
-        ? file.readAsBytesSync()
-        : file.readAsStringSync(encoding: _getEncodingByName(encoding));
-  }
-
-  static Encoding _getEncodingByName(String encoding) {
-    if (encoding == 'system')
-      return SYSTEM_ENCODING;
-    else if (encoding != null)
-      return Encoding.getByName(encoding);
-    return null;
-  }
-
-  Process asProcess(bool daemon) {
-    assert(stdout is List<int>);
-    assert(stderr is List<int>);
-    return new _ReplayProcess(this, daemon);
-  }
-}
-
-/// A [Process] implementation derives its data from a recording fragment.
-class _ReplayProcess implements Process {
-  @override
-  final int pid;
-
-  final List<int> _stdout;
-  final List<int> _stderr;
-  final StreamController<List<int>> _stdoutController;
-  final StreamController<List<int>> _stderrController;
-  final int _exitCode;
-  final Completer<int> _exitCodeCompleter;
-
-  _ReplayProcess(_ReplayProcessResult result, bool daemon)
-      : pid = result.pid,
-        _stdout = result.stdout,
-        _stderr = result.stderr,
-        _stdoutController = new StreamController<List<int>>(),
-        _stderrController = new StreamController<List<int>>(),
-        _exitCode = result.exitCode,
-        _exitCodeCompleter = new Completer<int>() {
-    // Don't flush our stdio streams until we reach the outer event loop. This
-    // is necessary because some of our process invocations transform the stdio
-    // streams into broadcast streams (e.g. DeviceLogReader implementations),
-    // and delaying our stdio stream production until we reach the outer event
-    // loop allows all code running in the microtask loop to register as
-    // listeners on these streams before we flush them.
-    //
-    // TODO(tvolkert): Once https://github.com/flutter/flutter/issues/7166 is
-    //                 resolved, running on the outer event loop should be
-    //                 sufficient (as described above), and we should switch to
-    //                 Duration.ZERO. In the meantime, native file I/O
-    //                 operations are causing a Duration.ZERO callback here to
-    //                 run before our ProtocolDiscovery instantiation, and thus,
-    //                 we flush our stdio streams before our protocol discovery
-    //                 is listening on them (causing us to timeout waiting for
-    //                 the observatory port discovery).
-    new Timer(const Duration(milliseconds: 50), () {
-      _stdoutController.add(_stdout);
-      _stderrController.add(_stderr);
-      if (!daemon)
-        kill();
-    });
-  }
-
-  @override
-  Stream<List<int>> get stdout => _stdoutController.stream;
-
-  @override
-  Stream<List<int>> get stderr => _stderrController.stream;
-
-  @override
-  Future<int> get exitCode => _exitCodeCompleter.future;
-
-  // TODO(tvolkert): Remove this once the dart sdk in both the target and
-  // the host have picked up dart-lang/sdk@e5a16b1
-  @override // ignore: OVERRIDE_ON_NON_OVERRIDING_SETTER
-  set exitCode(Future<int> exitCode) => throw new UnsupportedError('set exitCode');
-
-  @override
-  IOSink get stdin => throw new UnimplementedError();
-
-  @override
-  bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) {
-    if (!_exitCodeCompleter.isCompleted) {
-      _stdoutController.close();
-      _stderrController.close();
-      _exitCodeCompleter.complete(_exitCode);
-      return true;
-    }
-    return false;
-  }
+  context.setVariable(ProcessManager, manager);
 }