mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add --record-to option to flutter tools (#7136)
* Add --record-to option to flutter tools This option will cause flutter tools to record all process invocations that occur and serialize their stdout and stderr to files that get added to a "recording" ZIP file. This is part of an effort to be able to test flutter tools in a hermetic environment. As a side-benefit, this recording should prove an excellent attachment to any bug report.
This commit is contained in:
parent
2563636d90
commit
c9e7782adb
@ -6,10 +6,19 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import 'context.dart';
|
import 'context.dart';
|
||||||
|
import 'process.dart';
|
||||||
|
|
||||||
ProcessManager get processManager => context[ProcessManager];
|
ProcessManager get processManager => context[ProcessManager];
|
||||||
|
|
||||||
|
/// 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 {
|
class ProcessManager {
|
||||||
Future<Process> start(
|
Future<Process> start(
|
||||||
String executable,
|
String executable,
|
||||||
@ -64,3 +73,409 @@ class ProcessManager {
|
|||||||
return Process.killPid(pid, signal);
|
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 FileSystemEntity _recordTo;
|
||||||
|
final ProcessManager _delegate = new ProcessManager();
|
||||||
|
final Directory _tmpDir = Directory.systemTemp.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({FileSystemEntity recordTo})
|
||||||
|
: _recordTo = recordTo ?? Directory.current {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> manifestEntry = _createManifestEntry(
|
||||||
|
pid: process.pid,
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
mode: mode,
|
||||||
|
);
|
||||||
|
_manifest.add(manifestEntry);
|
||||||
|
|
||||||
|
_RecordingProcess result = new _RecordingProcess(
|
||||||
|
manager: this,
|
||||||
|
basename: _getBasename(process.pid, executable, arguments),
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
_manifest.add(_createManifestEntry(
|
||||||
|
pid: result.pid,
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
));
|
||||||
|
|
||||||
|
String basename = _getBasename(result.pid, executable, arguments);
|
||||||
|
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 new 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
_manifest.add(_createManifestEntry(
|
||||||
|
pid: result.pid,
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
));
|
||||||
|
|
||||||
|
String basename = _getBasename(result.pid, executable, arguments);
|
||||||
|
_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 = new 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 executable,
|
||||||
|
List<String> arguments,
|
||||||
|
String workingDirectory,
|
||||||
|
Map<String, String> environment,
|
||||||
|
ProcessStartMode mode,
|
||||||
|
Encoding stdoutEncoding,
|
||||||
|
Encoding stderrEncoding,
|
||||||
|
int exitCode,
|
||||||
|
}) {
|
||||||
|
Map<String, dynamic> entry = <String, dynamic>{};
|
||||||
|
if (pid != null) entry['pid'] = pid;
|
||||||
|
if (executable != null) entry['executable'] = executable;
|
||||||
|
if (arguments != null) entry['arguments'] = arguments;
|
||||||
|
if (workingDirectory != null) entry['workingDirectory'] = workingDirectory;
|
||||||
|
if (environment != null) entry['environment'] = environment;
|
||||||
|
if (mode != null) entry['mode'] = mode.toString();
|
||||||
|
if (stdoutEncoding != null) entry['stdoutEncoding'] = stdoutEncoding.name;
|
||||||
|
if (stderrEncoding != null) entry['stderrEncoding'] = stderrEncoding.name;
|
||||||
|
if (exitCode != null) entry['exitCode'] = exitCode;
|
||||||
|
return 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 - 1);
|
||||||
|
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(new 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 new File('${_tmpDir.path}/process-manifest.txt').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;
|
||||||
|
if (await FileSystemEntity.type(_recordTo.path) == FileSystemEntityType.DIRECTORY) {
|
||||||
|
zipFile = new File('${_recordTo.path}/$kDefaultRecordTo');
|
||||||
|
} else {
|
||||||
|
zipFile = new File(_recordTo.path);
|
||||||
|
await new Directory(path.dirname(zipFile.path)).create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve collisions.
|
||||||
|
String basename = path.basename(zipFile.path);
|
||||||
|
for (int i = 1; await zipFile.exists(); i++) {
|
||||||
|
assert(await FileSystemEntity.isFile(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 = new 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) => FileSystemEntity.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 [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 new 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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
set exitCode(Future<int> exitCode) => delegate.exitCode = 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);
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import '../android/android_sdk.dart';
|
|||||||
import '../base/context.dart';
|
import '../base/context.dart';
|
||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/process.dart';
|
import '../base/process.dart';
|
||||||
|
import '../base/process_manager.dart';
|
||||||
import '../cache.dart';
|
import '../cache.dart';
|
||||||
import '../dart/package_map.dart';
|
import '../dart/package_map.dart';
|
||||||
import '../device.dart';
|
import '../device.dart';
|
||||||
@ -93,6 +94,14 @@ class FlutterCommandRunner extends CommandRunner<Null> {
|
|||||||
'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
|
'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
|
||||||
'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
|
'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
|
||||||
'This path is relative to --local-engine-src-path/out.');
|
'This path is relative to --local-engine-src-path/out.');
|
||||||
|
argParser.addOption('record-to',
|
||||||
|
help:
|
||||||
|
'Enables recording of process invocations (including stdout and '
|
||||||
|
'stderr of all such invocations), and serializes that recording to '
|
||||||
|
'the specified location.\n'
|
||||||
|
'If the location is a directory, a ZIP file named `recording.zip` '
|
||||||
|
'will be created in that directory. Otherwise, a ZIP file will be '
|
||||||
|
'created with the path specified in this flag.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -142,6 +151,19 @@ class FlutterCommandRunner extends CommandRunner<Null> {
|
|||||||
context.setVariable(Logger, new VerboseLogger());
|
context.setVariable(Logger, new VerboseLogger());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (globalResults['record-to'] != null) {
|
||||||
|
// Turn on recording
|
||||||
|
String recordToPath = globalResults['record-to'].trim();
|
||||||
|
FileSystemEntity recordTo;
|
||||||
|
if (recordToPath.isNotEmpty) {
|
||||||
|
recordTo = await FileSystemEntity.isDirectory(recordToPath)
|
||||||
|
? new Directory(recordToPath)
|
||||||
|
: new File(recordToPath);
|
||||||
|
}
|
||||||
|
context.setVariable(ProcessManager,
|
||||||
|
new RecordingProcessManager(recordTo: recordTo));
|
||||||
|
}
|
||||||
|
|
||||||
logger.quiet = globalResults['quiet'];
|
logger.quiet = globalResults['quiet'];
|
||||||
|
|
||||||
if (globalResults.wasParsed('color'))
|
if (globalResults.wasParsed('color'))
|
||||||
|
@ -14,6 +14,7 @@ dependencies:
|
|||||||
crypto: '>=1.1.1 <3.0.0'
|
crypto: '>=1.1.1 <3.0.0'
|
||||||
file: ^0.1.0
|
file: ^0.1.0
|
||||||
http: ^0.11.3
|
http: ^0.11.3
|
||||||
|
intl: '>=0.14.0 <0.15.0'
|
||||||
json_rpc_2: ^2.0.0
|
json_rpc_2: ^2.0.0
|
||||||
json_schema: 1.0.6
|
json_schema: 1.0.6
|
||||||
linter: ^0.1.21
|
linter: ^0.1.21
|
||||||
|
Loading…
Reference in New Issue
Block a user