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 @@ ...@@ -5,14 +5,17 @@
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file/record_replay.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'common.dart' show throwToolExit; import 'common.dart' show throwToolExit;
import 'context.dart'; import 'context.dart';
import 'process.dart';
export 'package:file/file.dart'; export 'package:file/file.dart';
export 'package:file/local.dart'; export 'package:file/local.dart';
const String _kRecordingType = 'file';
const FileSystem _kLocalFs = const LocalFileSystem(); const FileSystem _kLocalFs = const LocalFileSystem();
/// Currently active implementation of the file system. /// Currently active implementation of the file system.
...@@ -21,6 +24,36 @@ const FileSystem _kLocalFs = const LocalFileSystem(); ...@@ -21,6 +24,36 @@ const FileSystem _kLocalFs = const LocalFileSystem();
/// with [MemoryFileSystem]. /// with [MemoryFileSystem].
FileSystem get fs => context == null ? _kLocalFs : context[FileSystem]; 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. /// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) { void ensureDirectoryExists(String filePath) {
String dirPath = path.dirname(filePath); String dirPath = path.dirname(filePath);
...@@ -56,3 +89,44 @@ void copyDirectorySync(Directory srcDir, Directory destDir) { ...@@ -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 @@ ...@@ -2,12 +2,41 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'context.dart'; import 'context.dart';
import 'file_system.dart';
export 'package:platform/platform.dart'; export 'package:platform/platform.dart';
const Platform _kLocalPlatform = const LocalPlatform(); const Platform _kLocalPlatform = const LocalPlatform();
const String _kRecordingType = 'platform';
Platform get platform => context == null ? _kLocalPlatform : context[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'; ...@@ -12,42 +12,26 @@ import 'context.dart';
import 'file_system.dart'; import 'file_system.dart';
import 'process.dart'; import 'process.dart';
const String _kRecordingType = 'process';
/// The active process manager. /// The active process manager.
ProcessManager get processManager => context[ProcessManager]; 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 /// This sets the [active process manager](processManager) to one that records
/// all process activity before delegating to a [LocalProcessManager]. /// all process activity before delegating to a [LocalProcessManager].
/// ///
/// [location] must either represent a valid, empty directory or a non-existent /// Activity will be recorded in a subdirectory of [location] named `"process"`.
/// file system entity, in which case a directory will be created at that path. /// It is permissible for [location] to represent an existing non-empty
/// Process invocation activity will be serialized to opaque files in that /// directory as long as there is no collision with the `"process"`
/// directory. The resulting (populated) directory will be suitable for use /// subdirectory.
/// with [enableReplayProcessManager].
void enableRecordingProcessManager(String location) { void enableRecordingProcessManager(String location) {
if (location.isEmpty) Directory dir = getRecordingSink(location, _kRecordingType);
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);
ProcessManager delegate = new LocalProcessManager(); ProcessManager delegate = new LocalProcessManager();
RecordingProcessManager manager = new RecordingProcessManager(delegate, dir); RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
addShutdownHook(() async { addShutdownHook(() => manager.flush(finishRunningProcesses: true));
await manager.flush(finishRunningProcesses: true);
});
context.setVariable(ProcessManager, manager); context.setVariable(ProcessManager, manager);
} }
...@@ -58,13 +42,9 @@ void enableRecordingProcessManager(String location) { ...@@ -58,13 +42,9 @@ void enableRecordingProcessManager(String location) {
/// ///
/// [location] must represent a directory to which process activity has been /// [location] must represent a directory to which process activity has been
/// recorded (i.e. the result of having been previously passed to /// 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 { Future<Null> enableReplayProcessManager(String location) async {
if (location.isEmpty) Directory dir = getReplaySource(location, _kRecordingType);
throwToolExit('replay-from location not specified');
Directory dir = fs.directory(location);
if (!dir.existsSync())
throwToolExit('replay-from location must reference a directory');
ProcessManager manager; ProcessManager manager;
try { try {
......
...@@ -100,7 +100,8 @@ class FlutterCommandRunner extends CommandRunner<Null> { ...@@ -100,7 +100,8 @@ class FlutterCommandRunner extends CommandRunner<Null> {
hide: !verboseHelp, hide: !verboseHelp,
help: help:
'Enables recording of process invocations (including stdout and stderr of all such invocations),\n' 'Enables recording of process invocations (including stdout and stderr of all such invocations),\n'
'and serializes that recording to 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.'); 'directory does not already exist, it will be created.');
argParser.addOption('replay-from', argParser.addOption('replay-from',
hide: !verboseHelp, hide: !verboseHelp,
...@@ -163,11 +164,21 @@ class FlutterCommandRunner extends CommandRunner<Null> { ...@@ -163,11 +164,21 @@ class FlutterCommandRunner extends CommandRunner<Null> {
throwToolExit('--record-to and --replay-from cannot be used together.'); throwToolExit('--record-to and --replay-from cannot be used together.');
if (globalResults['record-to'] != null) { if (globalResults['record-to'] != null) {
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) { 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']; 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