Enable record/replay of file system activity and platform metadata (#8104)
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 5ec714b..e95eefe 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -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 @@ /// 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 @@ } }); } + +/// 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; +}
diff --git a/packages/flutter_tools/lib/src/base/platform.dart b/packages/flutter_tools/lib/src/base/platform.dart index 6de07dd..2645a94 100644 --- a/packages/flutter_tools/lib/src/base/platform.dart +++ b/packages/flutter_tools/lib/src/base/platform.dart
@@ -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); +}
diff --git a/packages/flutter_tools/lib/src/base/process_manager.dart b/packages/flutter_tools/lib/src/base/process_manager.dart index e5cab3d..382ef86 100644 --- a/packages/flutter_tools/lib/src/base/process_manager.dart +++ b/packages/flutter_tools/lib/src/base/process_manager.dart
@@ -12,42 +12,26 @@ 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 @@ /// /// [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 {
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 9a57f51..78f2efb 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -100,7 +100,8 @@ 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 @@ 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'];