Commit 03d163ce authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Update tools to use package:process (#7590)

parent 24f1b2ee
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:process/process.dart';
import '../lib/src/base/common.dart'; import '../lib/src/base/common.dart';
import '../lib/src/base/config.dart'; import '../lib/src/base/config.dart';
...@@ -13,7 +14,6 @@ import '../lib/src/base/file_system.dart'; ...@@ -13,7 +14,6 @@ import '../lib/src/base/file_system.dart';
import '../lib/src/base/io.dart'; import '../lib/src/base/io.dart';
import '../lib/src/base/logger.dart'; import '../lib/src/base/logger.dart';
import '../lib/src/base/os.dart'; import '../lib/src/base/os.dart';
import '../lib/src/base/process_manager.dart';
import '../lib/src/cache.dart'; import '../lib/src/cache.dart';
import '../lib/src/flx.dart'; import '../lib/src/flx.dart';
import '../lib/src/globals.dart'; import '../lib/src/globals.dart';
...@@ -39,7 +39,7 @@ Future<Null> main(List<String> args) async { ...@@ -39,7 +39,7 @@ Future<Null> main(List<String> args) async {
executableContext.runInZone(() { executableContext.runInZone(() {
// Initialize the context with some defaults. // Initialize the context with some defaults.
context.putIfAbsent(FileSystem, () => new LocalFileSystem()); context.putIfAbsent(FileSystem, () => new LocalFileSystem());
context.putIfAbsent(ProcessManager, () => new ProcessManager()); context.putIfAbsent(ProcessManager, () => new LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger()); context.putIfAbsent(Logger, () => new StdoutLogger());
context.putIfAbsent(Cache, () => new Cache()); context.putIfAbsent(Cache, () => new Cache());
context.putIfAbsent(Config, () => new Config()); context.putIfAbsent(Config, () => new Config());
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:process/process.dart';
import 'package:stack_trace/stack_trace.dart'; import 'package:stack_trace/stack_trace.dart';
import 'src/base/common.dart'; import 'src/base/common.dart';
...@@ -15,7 +16,6 @@ import 'src/base/io.dart'; ...@@ -15,7 +16,6 @@ import 'src/base/io.dart';
import 'src/base/logger.dart'; import 'src/base/logger.dart';
import 'src/base/os.dart'; import 'src/base/os.dart';
import 'src/base/process.dart'; import 'src/base/process.dart';
import 'src/base/process_manager.dart';
import 'src/base/utils.dart'; import 'src/base/utils.dart';
import 'src/cache.dart'; import 'src/cache.dart';
import 'src/commands/analyze.dart'; import 'src/commands/analyze.dart';
...@@ -103,7 +103,7 @@ Future<Null> main(List<String> args) async { ...@@ -103,7 +103,7 @@ Future<Null> main(List<String> args) async {
// Seed these context entries first since others depend on them // Seed these context entries first since others depend on them
context.putIfAbsent(FileSystem, () => new LocalFileSystem()); context.putIfAbsent(FileSystem, () => new LocalFileSystem());
context.putIfAbsent(ProcessManager, () => new ProcessManager()); context.putIfAbsent(ProcessManager, () => new LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger()); context.putIfAbsent(Logger, () => new StdoutLogger());
// Order-independent context entries // Order-independent context entries
......
...@@ -3,849 +3,82 @@ ...@@ -3,849 +3,82 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:archive/archive.dart'; import 'package:process/process.dart';
import 'package:intl/intl.dart'; import 'package:process/record_replay.dart';
import 'package:path/path.dart' as path;
import 'common.dart';
import 'context.dart'; import 'context.dart';
import 'file_system.dart' hide IOSink; import 'file_system.dart';
import 'io.dart';
import 'os.dart';
import 'process.dart'; import 'process.dart';
/// The active process manager.
ProcessManager get processManager => context[ProcessManager]; ProcessManager get processManager => context[ProcessManager];
const String _kManifestName = 'MANIFEST.txt'; /// Enables recording of process invocation activity to the specified location.
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.
/// ///
/// Recordings are expected to be of the form produced by /// This sets the [active process manager](processManager) to one that records
/// [RecordingProcessManager]. Namely, this includes: /// all process activity before delegating to a [LocalProcessManager].
/// ///
/// - a [_kManifestName](manifest file) encoded as UTF-8 JSON that lists all /// [location] must either represent a valid, empty directory or a non-existent
/// invocations in order, along with the following metadata for each /// file system entity, in which case a directory will be created at that path.
/// invocation: /// Process invocation activity will be serialized to opaque files in that
/// - `pid` (required): The process id integer. /// directory. The resulting (populated) directory will be suitable for use
/// - `basename` (required): A string specifying the base filename from which /// with [enableReplayProcessManager].
/// the incovation's `stdout` and `stderr` files can be located. void enableRecordingProcessManager(String location) {
/// - `executable` (required): A string specifying the path to the executable if (location.isEmpty)
/// command that kicked off the process. throwToolExit('record-to location not specified');
/// - `arguments` (required): A list of strings that were passed as arguments switch (fs.typeSync(location, followLinks: false)) {
/// 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: case FileSystemEntityType.FILE:
dir = await fs.systemTempDirectory.createTemp('flutter_tools_'); case FileSystemEntityType.LINK:
os.unzip(fs.file(location), dir); throwToolExit('record-to location must reference a directory');
addShutdownHook(() async {
await dir.delete(recursive: true);
});
break; break;
case FileSystemEntityType.DIRECTORY: case FileSystemEntityType.DIRECTORY:
dir = fs.directory(location); if (fs.directory(location).listSync(followLinks: false).isNotEmpty)
throwToolExit('record-to directory must be empty');
break; break;
case FileSystemEntityType.NOT_FOUND: case FileSystemEntityType.NOT_FOUND:
throw new ArgumentError.value(location, 'location', 'Does not exist'); fs.directory(location).createSync(recursive: true);
}
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');
}
}
@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);
}
@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);
}
@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);
} }
Directory dir = fs.directory(location);
/// Finds and returns the next entry in the process manifest that matches ProcessManager delegate = new LocalProcessManager();
/// the specified process arguments. Once found, it marks the manifest entry RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
/// as having been invoked and thus not eligible for invocation again. addShutdownHook(() async {
Map<String, dynamic> _popEntry(String executable, List<String> arguments, { await manager.flush(finishRunningProcesses: true);
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,
);
if (entry == null)
throw new ProcessException(executable, arguments, 'No matching invocation found');
entry['invoked'] = true;
return entry;
}
@override context.setVariable(ProcessManager, manager);
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 /// Enables process invocation replay mode.
/// fragment. ///
class _ReplayProcessResult implements ProcessResult { /// This sets the [active process manager](processManager) to one that replays
@override /// process activity from a previously recorded set of invocations.
final int pid; ///
/// [location] must represent a directory to which process activity has been
@override /// recorded (i.e. the result of having been previously passed to
final int exitCode; /// [enableRecordingProcessManager]).
Future<Null> enableReplayProcessManager(String location) async {
@override if (location.isEmpty)
final dynamic stdout; throwToolExit('replay-from location not specified');
Directory dir = fs.directory(location);
@override if (!dir.existsSync())
final dynamic stderr; throwToolExit('replay-from location must reference a directory');
_ReplayProcessResult._({this.pid, this.exitCode, this.stdout, this.stderr}); ProcessManager manager;
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 { try {
return new _ReplayProcessResult._( manager = await ReplayProcessManager.create(dir,
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 // TODO(tvolkert): Once https://github.com/flutter/flutter/issues/7166 is
// resolved, running on the outer event loop should be // resolved, we can use the default `streamDelay`. In the
// sufficient (as described above), and we should switch to // meantime, native file I/O operations cause our `tail` process
// Duration.ZERO. In the meantime, native file I/O // streams to flush before our protocol discovery is listening on
// operations are causing a Duration.ZERO callback here to // them, causing us to timeout waiting for the observatory port.
// run before our ProtocolDiscovery instantiation, and thus, streamDelay: const Duration(milliseconds: 50),
// we flush our stdio streams before our protocol discovery );
// is listening on them (causing us to timeout waiting for } on ArgumentError catch (error) {
// the observatory port discovery). throwToolExit('Invalid replay-from: $error');
new Timer(const Duration(milliseconds: 50), () {
_stdoutController.add(_stdout);
_stderrController.add(_stderr);
if (!daemon)
kill();
});
} }
@override context.setVariable(ProcessManager, manager);
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;
}
} }
...@@ -100,16 +100,15 @@ class FlutterCommandRunner extends CommandRunner<Null> { ...@@ -100,16 +100,15 @@ class FlutterCommandRunner extends CommandRunner<Null> {
hide: !verboseHelp, hide: !verboseHelp,
help: help:
'Enables recording of process invocations (including stdout and stderr of all such invocations),\n' 'Enables recording of process invocations (including stdout and stderr of all such invocations),\n'
'and serializes that recording to the specified location. If the location is a directory, a ZIP\n' 'and serializes that recording to a directory with the path specified in this flag. If the\n'
'file named `recording.zip` will be created in that directory. Otherwise, a ZIP file will be\n' 'directory does not already exist, it will be created.');
'created with the path specified in this flag.');
argParser.addOption('replay-from', argParser.addOption('replay-from',
hide: !verboseHelp, hide: !verboseHelp,
help: help:
'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n' 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n'
'the specified recording (obtained via --record-to). If the location is a file, it is assumed to\n' 'the specified recording (obtained via --record-to). The path specified in this flag must refer\n'
'be a ZIP file structured according to the output of --record-to. If the location is a directory,\n' 'to a directory that holds serialized process invocations structured according to the output of\n'
'it is assumed to be an unzipped version of such a ZIP file.'); '--record-to.');
} }
@override @override
...@@ -164,23 +163,11 @@ class FlutterCommandRunner extends CommandRunner<Null> { ...@@ -164,23 +163,11 @@ class FlutterCommandRunner extends CommandRunner<Null> {
throwToolExit('--record-to and --replay-from cannot be used together.'); throwToolExit('--record-to and --replay-from cannot be used together.');
if (globalResults['record-to'] != null) { if (globalResults['record-to'] != null) {
// Turn on recording. enableRecordingProcessManager(globalResults['record-to'].trim());
String recordTo = globalResults['record-to'].trim();
if (recordTo.isEmpty)
recordTo = null;
context.setVariable(ProcessManager,
new RecordingProcessManager(recordTo));
} }
if (globalResults['replay-from'] != null) { if (globalResults['replay-from'] != null) {
// Turn on replay-based mocking. await enableReplayProcessManager(globalResults['replay-from'].trim());
try {
context.setVariable(ProcessManager, await ReplayProcessManager.create(
globalResults['replay-from'].trim(),
));
} on ArgumentError {
throwToolExit('--replay-from must specify a valid file or directory.');
}
} }
logger.quiet = globalResults['quiet']; logger.quiet = globalResults['quiet'];
......
...@@ -22,6 +22,7 @@ dependencies: ...@@ -22,6 +22,7 @@ dependencies:
mustache: ^0.2.5 mustache: ^0.2.5
package_config: '>=0.1.5 <2.0.0' package_config: '>=0.1.5 <2.0.0'
path: ^1.4.0 path: ^1.4.0
process: ^1.0.0
pub_semver: ^1.0.0 pub_semver: ^1.0.0
stack_trace: ^1.4.0 stack_trace: ^1.4.0
usage: ^2.2.1 usage: ^2.2.1
......
...@@ -37,7 +37,6 @@ import 'install_test.dart' as install_test; ...@@ -37,7 +37,6 @@ import 'install_test.dart' as install_test;
import 'logs_test.dart' as logs_test; import 'logs_test.dart' as logs_test;
import 'os_utils_test.dart' as os_utils_test; import 'os_utils_test.dart' as os_utils_test;
import 'packages_test.dart' as packages_test; import 'packages_test.dart' as packages_test;
import 'process_manager_test.dart' as process_manager_test;
import 'protocol_discovery_test.dart' as protocol_discovery_test; import 'protocol_discovery_test.dart' as protocol_discovery_test;
import 'run_test.dart' as run_test; import 'run_test.dart' as run_test;
import 'stop_test.dart' as stop_test; import 'stop_test.dart' as stop_test;
...@@ -77,7 +76,6 @@ void main() { ...@@ -77,7 +76,6 @@ void main() {
logs_test.main(); logs_test.main();
os_utils_test.main(); os_utils_test.main();
packages_test.main(); packages_test.main();
process_manager_test.main();
protocol_discovery_test.main(); protocol_discovery_test.main();
run_test.main(); run_test.main();
stop_test.main(); stop_test.main();
......
[
{
"pid": 100,
"basename": "001.sing.100",
"executable": "sing",
"arguments": [
"ppap"
],
"mode": "ProcessStartMode.NORMAL",
"exitCode": 0
},
{
"pid": 101,
"basename": "002.dance.101",
"executable": "dance",
"arguments": [
"gangnam-style"
],
"stdoutEncoding": "system",
"stderrEncoding": "system",
"exitCode": 2
}
]
// Copyright 2016 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 'dart:convert';
import 'package:archive/archive.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/process_manager.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
typedef bool Predicate<T>(T item);
/// Decodes a UTF8-encoded byte array into a list of Strings, where each list
/// entry represents a line of text.
List<String> _decode(List<int> data) =>
const LineSplitter().convert(UTF8.decode(data));
/// Consumes and returns an entire stream of bytes.
Future<List<int>> _consume(Stream<List<int>> stream) =>
stream.expand((List<int> data) => data).toList();
void main() {
group('RecordingProcessManager', () {
Directory tmp;
ProcessManager manager;
setUp(() {
tmp = fs.systemTempDirectory.createTempSync('flutter_tools_');
manager = new RecordingProcessManager(tmp.path);
});
tearDown(() {
tmp.deleteSync(recursive: true);
});
test('start', () async {
Process process = await manager.start('echo', <String>['foo']);
int pid = process.pid;
int exitCode = await process.exitCode;
List<int> stdout = await _consume(process.stdout);
List<int> stderr = await _consume(process.stderr);
expect(exitCode, 0);
expect(_decode(stdout), <String>['foo']);
expect(stderr, isEmpty);
// Force the recording to be written to disk.
await runShutdownHooks();
_Recording recording = _Recording.readFrom(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
expect(entry['pid'], pid);
expect(entry['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
test('run', () async {
ProcessResult result = await manager.run('echo', <String>['bar']);
int pid = result.pid;
int exitCode = result.exitCode;
String stdout = result.stdout;
String stderr = result.stderr;
expect(exitCode, 0);
expect(stdout, 'bar\n');
expect(stderr, isEmpty);
// Force the recording to be written to disk.
await runShutdownHooks();
_Recording recording = _Recording.readFrom(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
expect(entry['pid'], pid);
expect(entry['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
test('runSync', () async {
ProcessResult result = manager.runSync('echo', <String>['baz']);
int pid = result.pid;
int exitCode = result.exitCode;
String stdout = result.stdout;
String stderr = result.stderr;
expect(exitCode, 0);
expect(stdout, 'baz\n');
expect(stderr, isEmpty);
// Force the recording to be written to disk.
await runShutdownHooks();
_Recording recording = _Recording.readFrom(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
expect(entry['pid'], pid);
expect(entry['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
});
group('ReplayProcessManager', () {
ProcessManager manager;
setUp(() async {
await runInMinimalContext(() async {
Directory dir = fs.directory('test/data/process_manager/replay');
manager = await ReplayProcessManager.create(dir.path);
});
});
tearDown(() async {
// Allow the replay manager to clean up
await runShutdownHooks();
});
test('start', () async {
Process process = await manager.start('sing', <String>['ppap']);
int exitCode = await process.exitCode;
List<int> stdout = await _consume(process.stdout);
List<int> stderr = await _consume(process.stderr);
expect(process.pid, 100);
expect(exitCode, 0);
expect(_decode(stdout), <String>['I have a pen', 'I have a pineapple']);
expect(_decode(stderr), <String>['Uh, pineapple pen']);
});
test('run', () async {
ProcessResult result = await manager.run('dance', <String>['gangnam-style']);
expect(result.pid, 101);
expect(result.exitCode, 2);
expect(result.stdout, '');
expect(result.stderr, 'No one can dance like Psy\n');
});
test('runSync', () {
ProcessResult result = manager.runSync('dance', <String>['gangnam-style']);
expect(result.pid, 101);
expect(result.exitCode, 2);
expect(result.stdout, '');
expect(result.stderr, 'No one can dance like Psy\n');
});
});
}
Future<Null> runInMinimalContext(Future<dynamic> method()) async {
AppContext context = new AppContext();
context.putIfAbsent(FileSystem, () => new LocalFileSystem());
context.putIfAbsent(ProcessManager, () => new ProcessManager());
context.putIfAbsent(Logger, () => new BufferLogger());
context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
await context.runInZone(method);
}
/// A testing utility class that encapsulates a recording.
class _Recording {
final File file;
final Archive _archive;
_Recording(this.file, this._archive);
static _Recording readFrom(Directory dir) {
File file = fs.file(path.join(
dir.path, RecordingProcessManager.kDefaultRecordTo));
Archive archive = new ZipDecoder().decodeBytes(file.readAsBytesSync());
return new _Recording(file, archive);
}
List<Map<String, dynamic>> get manifest {
return JSON.decoder.convert(_getFileContent('MANIFEST.txt', UTF8));
}
dynamic stdoutForEntryAt(int index) =>
_getStdioContent(manifest[index], 'stdout');
dynamic stderrForEntryAt(int index) =>
_getStdioContent(manifest[index], 'stderr');
dynamic _getFileContent(String name, Encoding encoding) {
List<int> bytes = _fileNamed(name).content;
return encoding == null ? bytes : encoding.decode(bytes);
}
dynamic _getStdioContent(Map<String, dynamic> entry, String type) {
String basename = entry['basename'];
String encodingName = entry['${type}Encoding'];
Encoding encoding;
if (encodingName != null)
encoding = encodingName == 'system'
? SYSTEM_ENCODING
: Encoding.getByName(encodingName);
return _getFileContent('$basename.$type', encoding);
}
ArchiveFile _fileNamed(String name) => _archive.firstWhere(_hasName(name));
Predicate<ArchiveFile> _hasName(String name) =>
(ArchiveFile file) => file.name == name;
}
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:process/process.dart';
import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/process_manager.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
......
...@@ -10,7 +10,6 @@ import 'package:flutter_tools/src/base/file_system.dart'; ...@@ -10,7 +10,6 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/process_manager.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
...@@ -23,6 +22,7 @@ import 'package:flutter_tools/src/usage.dart'; ...@@ -23,6 +22,7 @@ import 'package:flutter_tools/src/usage.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
/// Return the test logger. This assumes that the current Logger is a BufferLogger. /// Return the test logger. This assumes that the current Logger is a BufferLogger.
...@@ -43,7 +43,7 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -43,7 +43,7 @@ void testUsingContext(String description, dynamic testMethod(), {
// Initialize the test context with some default mocks. // Initialize the test context with some default mocks.
// Seed these context entries first since others depend on them // Seed these context entries first since others depend on them
testContext.putIfAbsent(FileSystem, () => new LocalFileSystem()); testContext.putIfAbsent(FileSystem, () => new LocalFileSystem());
testContext.putIfAbsent(ProcessManager, () => new ProcessManager()); testContext.putIfAbsent(ProcessManager, () => new LocalProcessManager());
testContext.putIfAbsent(Logger, () => new BufferLogger()); testContext.putIfAbsent(Logger, () => new BufferLogger());
// Order-independent context entries // Order-independent context entries
......
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