Commit 9c05c345 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Enable record/replay of file system activity and platform metadata (#8104)

parent ea74e076
......@@ -5,14 +5,17 @@
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:file/record_replay.dart';
import 'package:path/path.dart' as path;
import 'common.dart' show throwToolExit;
import 'context.dart';
import 'process.dart';
export 'package:file/file.dart';
export 'package:file/local.dart';
const String _kRecordingType = 'file';
const FileSystem _kLocalFs = const LocalFileSystem();
/// Currently active implementation of the file system.
......@@ -21,6 +24,36 @@ const FileSystem _kLocalFs = const LocalFileSystem();
/// with [MemoryFileSystem].
FileSystem get fs => context == null ? _kLocalFs : context[FileSystem];
/// Enables recording of file system activity to the specified base recording
/// [location].
///
/// This sets the [active file system](fs) to one that records all invocation
/// activity before delegating to a [LocalFileSystem].
///
/// Activity will be recorded in a subdirectory of [location] named `"file"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"file"` subdirectory.
void enableRecordingFileSystem(String location) {
Directory dir = getRecordingSink(location, _kRecordingType);
RecordingFileSystem fileSystem = new RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
addShutdownHook(() => fileSystem.recording.flush());
context.setVariable(FileSystem, fileSystem);
}
/// Enables file system replay mode.
///
/// This sets the [active file system](fs) to one that replays invocation
/// activity from a previously recorded set of invocations.
///
/// [location] must represent a directory to which file system activity has
/// been recorded (i.e. the result of having been previously passed to
/// [enableRecordingFileSystem]), or a [ToolExit] will be thrown.
void enableReplayFileSystem(String location) {
Directory dir = getReplaySource(location, _kRecordingType);
context.setVariable(FileSystem, new ReplayFileSystem(recording: dir));
}
/// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) {
String dirPath = path.dirname(filePath);
......@@ -56,3 +89,44 @@ void copyDirectorySync(Directory srcDir, Directory destDir) {
}
});
}
/// Gets a directory to act as a recording destination, creating the directory
/// as necessary.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory already exists as a directory, the existing
/// directory must be empty, or a [ToolExit] will be thrown. If the target
/// directory exists as an entity other than a directory, a [ToolExit] will
/// also be thrown.
Directory getRecordingSink(String dirname, String basename) {
String location = _kLocalFs.path.join(dirname, basename);
switch (_kLocalFs.typeSync(location, followLinks: false)) {
case FileSystemEntityType.FILE:
case FileSystemEntityType.LINK:
throwToolExit('Invalid record-to location: $dirname ("$basename" exists as non-directory)');
break;
case FileSystemEntityType.DIRECTORY:
if (_kLocalFs.directory(location).listSync(followLinks: false).isNotEmpty)
throwToolExit('Invalid record-to location: $dirname ("$basename" is not empty)');
break;
case FileSystemEntityType.NOT_FOUND:
_kLocalFs.directory(location).createSync(recursive: true);
}
return _kLocalFs.directory(location);
}
/// Gets a directory that holds a saved recording to be used for the purpose of
/// replay.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory does not exist, a [ToolExit] will be thrown.
Directory getReplaySource(String dirname, String basename) {
Directory dir = _kLocalFs.directory(_kLocalFs.path.join(dirname, basename));
if (!dir.existsSync())
throwToolExit('Invalid replay-from location: $dirname ("$basename" does not exist)');
return dir;
}
......@@ -2,12 +2,41 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:platform/platform.dart';
import 'context.dart';
import 'file_system.dart';
export 'package:platform/platform.dart';
const Platform _kLocalPlatform = const LocalPlatform();
const String _kRecordingType = 'platform';
Platform get platform => context == null ? _kLocalPlatform : context[Platform];
/// Enables serialization of the current [platform] to the specified base
/// recording [location].
///
/// Platform metadata will be recorded in a subdirectory of [location] named
/// `"platform"`. It is permissible for [location] to represent an existing
/// non-empty directory as long as there is no collision with the `"platform"`
/// subdirectory.
Future<Null> enableRecordingPlatform(String location) async {
Directory dir = getRecordingSink(location, _kRecordingType);
File file = _getPlatformManifest(dir);
await file.writeAsString(platform.toJson(), flush: true);
}
Future<Null> enableReplayPlatform(String location) async {
Directory dir = getReplaySource(location, _kRecordingType);
File file = _getPlatformManifest(dir);
String json = await file.readAsString();
context.setVariable(Platform, new FakePlatform.fromJson(json));
}
File _getPlatformManifest(Directory dir) {
String path = dir.fileSystem.path.join(dir.path, 'MANIFEST.txt');
return dir.fileSystem.file(path);
}
......@@ -12,42 +12,26 @@ import 'context.dart';
import 'file_system.dart';
import 'process.dart';
const String _kRecordingType = 'process';
/// The active process manager.
ProcessManager get processManager => context[ProcessManager];
/// Enables recording of process invocation activity to the specified location.
/// Enables recording of process invocation activity to the specified base
/// recording [location].
///
/// This sets the [active process manager](processManager) to one that records
/// all process activity before delegating to a [LocalProcessManager].
///
/// [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].
/// Activity will be recorded in a subdirectory of [location] named `"process"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"process"`
/// subdirectory.
void enableRecordingProcessManager(String location) {
if (location.isEmpty)
throwToolExit('record-to location not specified');
switch (fs.typeSync(location, followLinks: false)) {
case FileSystemEntityType.FILE:
case FileSystemEntityType.LINK:
throwToolExit('record-to location must reference a directory');
break;
case FileSystemEntityType.DIRECTORY:
if (fs.directory(location).listSync(followLinks: false).isNotEmpty)
throwToolExit('record-to directory must be empty');
break;
case FileSystemEntityType.NOT_FOUND:
fs.directory(location).createSync(recursive: true);
}
Directory dir = fs.directory(location);
Directory dir = getRecordingSink(location, _kRecordingType);
ProcessManager delegate = new LocalProcessManager();
RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
addShutdownHook(() async {
await manager.flush(finishRunningProcesses: true);
});
addShutdownHook(() => manager.flush(finishRunningProcesses: true));
context.setVariable(ProcessManager, manager);
}
......@@ -58,13 +42,9 @@ void enableRecordingProcessManager(String location) {
///
/// [location] must represent a directory to which process activity has been
/// recorded (i.e. the result of having been previously passed to
/// [enableRecordingProcessManager]).
/// [enableRecordingProcessManager]), or a [ToolExit] will be thrown.
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');
Directory dir = getReplaySource(location, _kRecordingType);
ProcessManager manager;
try {
......
......@@ -100,7 +100,8 @@ 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 a directory with the path specified in this flag. If the\n'
'and file system access (reads and writes).\n'
'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,
......@@ -163,11 +164,21 @@ class FlutterCommandRunner extends CommandRunner<Null> {
throwToolExit('--record-to and --replay-from cannot be used together.');
if (globalResults['record-to'] != null) {
enableRecordingProcessManager(globalResults['record-to'].trim());
String recordTo = globalResults['record-to'].trim();
if (recordTo.isEmpty)
throwToolExit('record-to location not specified');
enableRecordingProcessManager(recordTo);
enableRecordingFileSystem(recordTo);
await enableRecordingPlatform(recordTo);
}
if (globalResults['replay-from'] != null) {
await enableReplayProcessManager(globalResults['replay-from'].trim());
String replayFrom = globalResults['replay-from'].trim();
if (replayFrom.isEmpty)
throwToolExit('replay-from location not specified');
await enableReplayProcessManager(replayFrom);
enableReplayFileSystem(replayFrom);
await enableReplayPlatform(replayFrom);
}
logger.quiet = globalResults['quiet'];
......
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