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 @@
import 'dart:async';
import 'package:args/args.dart';
import 'package:process/process.dart';
import '../lib/src/base/common.dart';
import '../lib/src/base/config.dart';
......@@ -13,7 +14,6 @@ import '../lib/src/base/file_system.dart';
import '../lib/src/base/io.dart';
import '../lib/src/base/logger.dart';
import '../lib/src/base/os.dart';
import '../lib/src/base/process_manager.dart';
import '../lib/src/cache.dart';
import '../lib/src/flx.dart';
import '../lib/src/globals.dart';
......@@ -39,7 +39,7 @@ Future<Null> main(List<String> args) async {
executableContext.runInZone(() {
// Initialize the context with some defaults.
context.putIfAbsent(FileSystem, () => new LocalFileSystem());
context.putIfAbsent(ProcessManager, () => new ProcessManager());
context.putIfAbsent(ProcessManager, () => new LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger());
context.putIfAbsent(Cache, () => new Cache());
context.putIfAbsent(Config, () => new Config());
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'package:args/command_runner.dart';
import 'package:process/process.dart';
import 'package:stack_trace/stack_trace.dart';
import 'src/base/common.dart';
......@@ -15,7 +16,6 @@ import 'src/base/io.dart';
import 'src/base/logger.dart';
import 'src/base/os.dart';
import 'src/base/process.dart';
import 'src/base/process_manager.dart';
import 'src/base/utils.dart';
import 'src/cache.dart';
import 'src/commands/analyze.dart';
......@@ -103,7 +103,7 @@ Future<Null> main(List<String> args) async {
// Seed these context entries first since others depend on them
context.putIfAbsent(FileSystem, () => new LocalFileSystem());
context.putIfAbsent(ProcessManager, () => new ProcessManager());
context.putIfAbsent(ProcessManager, () => new LocalProcessManager());
context.putIfAbsent(Logger, () => new StdoutLogger());
// Order-independent context entries
......
......@@ -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)) {
/// [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:
dir = await fs.systemTempDirectory.createTemp('flutter_tools_');
os.unzip(fs.file(location), dir);
addShutdownHook(() async {
await dir.delete(recursive: true);
});
case FileSystemEntityType.LINK:
throwToolExit('record-to location must reference a directory');
break;
case FileSystemEntityType.DIRECTORY:
dir = fs.directory(location);
if (fs.directory(location).listSync(followLinks: false).isNotEmpty)
throwToolExit('record-to directory must be empty');
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');
}
}
@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);
fs.directory(location).createSync(recursive: true);
}
Directory dir = fs.directory(location);
/// 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,
);
if (entry == null)
throw new ProcessException(executable, arguments, 'No matching invocation found');
entry['invoked'] = true;
return entry;
}
ProcessManager delegate = new LocalProcessManager();
RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
addShutdownHook(() async {
await manager.flush(finishRunningProcesses: true);
});
@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.");
}
context.setVariable(ProcessManager, manager);
}
/// 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']);
/// 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');
ProcessManager manager;
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.
//
manager = await ReplayProcessManager.create(dir,
// 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();
});
// 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),
);
} on ArgumentError catch (error) {
throwToolExit('Invalid replay-from: $error');
}
@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);
}
......@@ -100,16 +100,15 @@ class FlutterCommandRunner extends CommandRunner<Null> {
hide: !verboseHelp,
help:
'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'
'file named `recording.zip` will be created in that directory. Otherwise, a ZIP file will be\n'
'created with the path specified in this flag.');
'and serializes that recording to a directory with the path specified in this flag. If the\n'
'directory does not already exist, it will be created.');
argParser.addOption('replay-from',
hide: !verboseHelp,
help:
'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'
'be a ZIP file structured according to the output of --record-to. If the location is a directory,\n'
'it is assumed to be an unzipped version of such a ZIP file.');
'the specified recording (obtained via --record-to). The path specified in this flag must refer\n'
'to a directory that holds serialized process invocations structured according to the output of\n'
'--record-to.');
}
@override
......@@ -164,23 +163,11 @@ class FlutterCommandRunner extends CommandRunner<Null> {
throwToolExit('--record-to and --replay-from cannot be used together.');
if (globalResults['record-to'] != null) {
// Turn on recording.
String recordTo = globalResults['record-to'].trim();
if (recordTo.isEmpty)
recordTo = null;
context.setVariable(ProcessManager,
new RecordingProcessManager(recordTo));
enableRecordingProcessManager(globalResults['record-to'].trim());
}
if (globalResults['replay-from'] != null) {
// Turn on replay-based mocking.
try {
context.setVariable(ProcessManager, await ReplayProcessManager.create(
globalResults['replay-from'].trim(),
));
} on ArgumentError {
throwToolExit('--replay-from must specify a valid file or directory.');
}
await enableReplayProcessManager(globalResults['replay-from'].trim());
}
logger.quiet = globalResults['quiet'];
......
......@@ -22,6 +22,7 @@ dependencies:
mustache: ^0.2.5
package_config: '>=0.1.5 <2.0.0'
path: ^1.4.0
process: ^1.0.0
pub_semver: ^1.0.0
stack_trace: ^1.4.0
usage: ^2.2.1
......
......@@ -37,7 +37,6 @@ import 'install_test.dart' as install_test;
import 'logs_test.dart' as logs_test;
import 'os_utils_test.dart' as os_utils_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 'run_test.dart' as run_test;
import 'stop_test.dart' as stop_test;
......@@ -77,7 +76,6 @@ void main() {
logs_test.main();
os_utils_test.main();
packages_test.main();
process_manager_test.main();
protocol_discovery_test.main();
run_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 @@
// found in the LICENSE file.
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/process_manager.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/runner/flutter_command_runner.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/logger.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/device.dart';
import 'package:flutter_tools/src/devfs.dart';
......@@ -23,6 +22,7 @@ import 'package:flutter_tools/src/usage.dart';
import 'package:mockito/mockito.dart';
import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:test/test.dart';
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
......@@ -43,7 +43,7 @@ void testUsingContext(String description, dynamic testMethod(), {
// Initialize the test context with some default mocks.
// Seed these context entries first since others depend on them
testContext.putIfAbsent(FileSystem, () => new LocalFileSystem());
testContext.putIfAbsent(ProcessManager, () => new ProcessManager());
testContext.putIfAbsent(ProcessManager, () => new LocalProcessManager());
testContext.putIfAbsent(Logger, () => new BufferLogger());
// 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