mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
851 lines
29 KiB
Dart
851 lines
29 KiB
Dart
// 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:intl/intl.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'context.dart';
|
|
import 'file_system.dart' hide IOSink;
|
|
import 'io.dart';
|
|
import 'os.dart';
|
|
import 'process.dart';
|
|
|
|
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((List<int> data) {
|
|
archive.addFile(new ArchiveFile.noCompress(
|
|
path.basename(file.path), data.length, data));
|
|
}));
|
|
});
|
|
|
|
await Future.wait(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
|
|
/// [RecordingProcessManager]. Namely, this includes:
|
|
///
|
|
/// - 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');
|
|
}
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
}
|