Remove record/replay/bug report functionality from the tool (#45999)

This commit is contained in:
Jonah Williams 2019-12-03 13:24:45 -08:00 committed by GitHub
parent a484db665a
commit b96d818c19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 0 additions and 584 deletions

View file

@ -5,18 +5,15 @@
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:file/record_replay.dart';
import 'package:meta/meta.dart';
import 'common.dart' show throwToolExit;
import 'context.dart';
import 'platform.dart';
import 'process.dart';
export 'package:file/file.dart';
export 'package:file/local.dart';
const String _kRecordingType = 'file';
const FileSystem _kLocalFs = LocalFileSystem();
/// Currently active implementation of the file system.
@ -25,33 +22,6 @@ const FileSystem _kLocalFs = LocalFileSystem();
/// with [MemoryFileSystem].
FileSystem get fs => context.get<FileSystem>() ?? _kLocalFs;
/// Gets a [FileSystem] that will record file system activity to the specified
/// base recording [location].
///
/// 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.
RecordingFileSystem getRecordingFileSystem(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
final RecordingFileSystem fileSystem = RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
addShutdownHook(() async {
await fileSystem.recording.flush();
}, ShutdownStage.SERIALIZE_RECORDING);
return fileSystem;
}
/// Gets a [FileSystem] 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
/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
ReplayFileSystem getReplayFileSystem(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
return ReplayFileSystem(recording: dir);
}
/// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) {
final String dirPath = fs.path.dirname(filePath);
@ -105,49 +75,6 @@ void copyDirectorySync(
}
}
/// 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) {
final 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.notFound:
_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) {
final Directory dir = _kLocalFs.directory(_kLocalFs.path.join(dirname, basename));
if (!dir.existsSync()) {
throwToolExit('Invalid replay-from location: $dirname ("$basename" does not exist)');
}
return dir;
}
/// Canonicalizes [path].
///
/// This function implements the behavior of `canonicalize` from

View file

@ -5,39 +5,9 @@
import 'package:platform/platform.dart';
import 'context.dart';
import 'file_system.dart';
export 'package:platform/platform.dart';
const Platform _kLocalPlatform = LocalPlatform();
const String _kRecordingType = 'platform';
Platform get platform => context.get<Platform>() ?? _kLocalPlatform;
/// Serializes 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.
///
/// Returns the existing platform.
Platform getRecordingPlatform(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
final File file = _getPlatformManifest(dir);
file.writeAsStringSync(platform.toJson(), flush: true);
return platform;
}
FakePlatform getReplayPlatform(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
final File file = _getPlatformManifest(dir);
final String json = file.readAsStringSync();
return FakePlatform.fromJson(json);
}
File _getPlatformManifest(Directory dir) {
final String path = dir.fileSystem.path.join(dir.path, 'MANIFEST.txt');
return dir.fileSystem.file(path);
}

View file

@ -2,54 +2,11 @@
// 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:process/process.dart';
import 'package:process/record_replay.dart';
import 'common.dart';
import 'context.dart';
import 'file_system.dart';
import 'process.dart';
const String _kRecordingType = 'process';
const ProcessManager _kLocalProcessManager = LocalProcessManager();
/// The active process manager.
ProcessManager get processManager => context.get<ProcessManager>() ?? _kLocalProcessManager;
/// Gets a [ProcessManager] that will record process invocation activity to the
/// specified base recording [location].
///
/// 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.
RecordingProcessManager getRecordingProcessManager(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
const ProcessManager delegate = LocalProcessManager();
final RecordingProcessManager manager = RecordingProcessManager(delegate, dir);
addShutdownHook(() async {
await manager.flush(finishRunningProcesses: true);
}, ShutdownStage.SERIALIZE_RECORDING);
return manager;
}
/// Gets a [ProcessManager] that replays process activity from a previously
/// recorded set of invocations.
///
/// [location] must represent a directory to which process activity has been
/// recorded (i.e. the result of having been previously passed to
/// [getRecordingProcessManager]), or a [ToolExit] will be thrown.
Future<ReplayProcessManager> getReplayProcessManager(String location) async {
final Directory dir = getReplaySource(location, _kRecordingType);
ProcessManager manager;
try {
manager = await ReplayProcessManager.create(dir);
} on ArgumentError catch (error) {
throwToolExit('Invalid replay-from: $error');
}
return manager as ReplayProcessManager;
}

View file

@ -8,8 +8,6 @@ import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:completion/completion.dart';
import 'package:file/file.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
@ -17,10 +15,7 @@ import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart' as io;
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/user_messages.dart';
import '../base/utils.dart';
@ -32,7 +27,6 @@ import '../globals.dart';
import '../reporting/reporting.dart';
import '../tester/flutter_tester.dart';
import '../version.dart';
import '../vmservice.dart';
const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
@ -96,10 +90,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
argParser.addFlag('suppress-analytics',
negatable: false,
help: 'Suppress analytics reporting when this command runs.');
argParser.addFlag('bug-report',
negatable: false,
help: 'Captures a bug report file to submit to the Flutter team.\n'
'Contains local paths, device identifiers, and log snippets.');
String packagesHelp;
bool showPackagesCommand;
@ -141,19 +131,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
if (verboseHelp) {
argParser.addSeparator('Options for testing the "flutter" tool itself:');
}
argParser.addOption('record-to',
hide: !verboseHelp,
help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), '
'and file system access (reads and writes).\n'
'Serializes that recording to a directory with the path specified in this flag. If the '
'directory does not already exist, it will be created.');
argParser.addOption('replay-from',
hide: !verboseHelp,
help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from '
'the specified recording (obtained via --record-to). The path specified in this flag must refer '
'to a directory that holds serialized process invocations structured according to the output of '
'--record-to.');
argParser.addFlag('show-test-device',
negatable: false,
hide: !verboseHelp,
@ -291,63 +268,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
FlutterTesterDevices.showFlutterTesterDevice = true;
}
String recordTo = topLevelResults['record-to'] as String;
String replayFrom = topLevelResults['replay-from'] as String;
if (topLevelResults['bug-report'] as bool) {
// --bug-report implies --record-to=<tmp_path>
final Directory tempDir = const LocalFileSystem()
.systemTempDirectory
.createTempSync('flutter_tools_bug_report.');
recordTo = tempDir.path;
// Record the arguments that were used to invoke this runner.
final File manifest = tempDir.childFile('MANIFEST.txt');
final StringBuffer buffer = StringBuffer()
..writeln('# arguments')
..writeln(topLevelResults.arguments)
..writeln()
..writeln('# rest')
..writeln(topLevelResults.rest);
manifest.writeAsStringSync(buffer.toString(), flush: true);
// ZIP the recording up once the recording has been serialized.
addShutdownHook(() {
final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip');
os.zip(tempDir, zipFile);
printStatus(userMessages.runnerBugReportFinished(zipFile.basename));
}, ShutdownStage.POST_PROCESS_RECORDING);
addShutdownHook(() => tempDir.deleteSync(recursive: true), ShutdownStage.CLEANUP);
}
assert(recordTo == null || replayFrom == null);
if (recordTo != null) {
recordTo = recordTo.trim();
if (recordTo.isEmpty) {
throwToolExit(userMessages.runnerNoRecordTo);
}
contextOverrides.addAll(<Type, dynamic>{
ProcessManager: getRecordingProcessManager(recordTo),
FileSystem: getRecordingFileSystem(recordTo),
Platform: getRecordingPlatform(recordTo),
});
VMService.enableRecordingConnection(recordTo);
}
if (replayFrom != null) {
replayFrom = replayFrom.trim();
if (replayFrom.isEmpty) {
throwToolExit(userMessages.runnerNoReplayFrom);
}
contextOverrides.addAll(<Type, dynamic>{
ProcessManager: await getReplayProcessManager(replayFrom),
FileSystem: getReplayFileSystem(replayFrom),
Platform: getReplayPlatform(replayFrom),
});
VMService.enableReplayConnection(replayFrom);
}
// We must set Cache.flutterRoot early because other features use it (e.g.
// enginePath's initializer uses it).
final String flutterRoot = topLevelResults['flutter-root'] as String ?? defaultFlutterRoot;

View file

@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:file/file.dart';
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart' show required;
@ -22,7 +21,6 @@ import 'convert.dart' show base64, utf8;
import 'device.dart';
import 'globals.dart';
import 'version.dart';
import 'vmservice_record_replay.dart';
/// Override `WebSocketConnector` in [context] to use a different constructor
/// for [WebSocket]s (used by tests).
@ -62,8 +60,6 @@ typedef CompileExpression = Future<String> Function(
bool isStatic,
);
const String _kRecordingType = 'vmservice';
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async {
Duration delay = const Duration(milliseconds: 100);
int attempts = 0;
@ -284,31 +280,6 @@ class VMService {
}
}
/// Enables recording of VMService JSON-rpc activity to the specified base
/// recording [location].
///
/// Activity will be recorded in a subdirectory of [location] named
/// `"vmservice"`. It is permissible for [location] to represent an existing
/// non-empty directory as long as there is no collision with the
/// `"vmservice"` subdirectory.
static void enableRecordingConnection(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
_openChannel = (Uri uri, {io.CompressionOptions compression}) async {
final StreamChannel<String> delegate = await _defaultOpenChannel(uri);
return RecordingVMServiceChannel(delegate, dir);
};
}
/// Enables VMService JSON-rpc replay mode.
///
/// [location] must represent a directory to which VMService JSON-rpc
/// activity has been recorded (i.e. the result of having been previously
/// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown.
static void enableReplayConnection(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
_openChannel = (Uri uri, {io.CompressionOptions compression}) async => ReplayVMServiceChannel(dir);
}
static void _unhandledError(dynamic error, dynamic stack) {
logger.printTrace('Error in internal implementation of JSON RPC.\n$error\n$stack');
assert(false);

View file

@ -1,294 +0,0 @@
// Copyright 2014 The Flutter 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 'package:file/file.dart';
import 'package:stream_channel/stream_channel.dart';
import 'base/io.dart';
import 'base/process.dart';
import 'base/utils.dart';
import 'convert.dart';
import 'globals.dart';
const String _kManifest = 'MANIFEST.txt';
const String _kRequest = 'request';
const String _kResponse = 'response';
const String _kId = 'id';
const String _kType = 'type';
const String _kData = 'data';
/// A [StreamChannel] that expects VM service (JSON-rpc) protocol messages and
/// serializes all such messages to the file system for later playback.
class RecordingVMServiceChannel extends DelegatingStreamChannel<String> {
RecordingVMServiceChannel(StreamChannel<String> delegate, Directory location)
: super(delegate) {
addShutdownHook(() {
// Sort the messages such that they are ordered
// `[request1, response1, request2, response2, ...]`. This serves no
// purpose other than to make the serialized format more human-readable.
_messages.sort();
final File file = _getManifest(location);
final String json = const JsonEncoder.withIndent(' ').convert(_messages);
file.writeAsStringSync(json, flush: true);
}, ShutdownStage.SERIALIZE_RECORDING);
}
final List<_Message> _messages = <_Message>[];
_RecordingStream _streamRecorder;
_RecordingSink _sinkRecorder;
@override
Stream<String> get stream {
_streamRecorder ??= _RecordingStream(super.stream, _messages);
return _streamRecorder.stream;
}
@override
StreamSink<String> get sink => _sinkRecorder ??= _RecordingSink(super.sink, _messages);
}
/// Base class for request and response JSON-rpc messages.
abstract class _Message implements Comparable<_Message> {
_Message(this.type, this.data);
factory _Message.fromRecording(Map<String, dynamic> recordingData) {
return recordingData[_kType] == _kRequest
? _Request(castStringKeyedMap(recordingData[_kData]))
: _Response(castStringKeyedMap(recordingData[_kData]));
}
final String type;
final Map<String, dynamic> data;
int get id => data[_kId] as int;
/// Allows [JsonEncoder] to properly encode objects of this type.
Map<String, dynamic> toJson() {
return <String, dynamic>{
_kType: type,
_kData: data,
};
}
@override
int compareTo(_Message other) {
if (id == null) {
printError('Invalid VMService message data detected: $data');
return -1;
}
final int result = id.compareTo(other.id);
if (result != 0) {
return result;
} else if (type == _kRequest) {
return -1;
} else {
return 1;
}
}
}
/// A VM service JSON-rpc request (sent to the VM).
class _Request extends _Message {
_Request(Map<String, dynamic> data) : super(_kRequest, data);
_Request.fromString(String data) : this(castStringKeyedMap(json.decode(data)));
}
/// A VM service JSON-rpc response (from the VM).
class _Response extends _Message {
_Response(Map<String, dynamic> data) : super(_kResponse, data);
_Response.fromString(String data) : this(castStringKeyedMap(json.decode(data)));
}
/// A matching request/response pair.
///
/// A request and response match by virtue of having matching
/// [IDs](_Message.id).
class _Transaction {
_Request request;
_Response response;
}
/// A helper class that monitors a [Stream] of VM service JSON-rpc responses
/// and saves the responses to a recording.
class _RecordingStream {
_RecordingStream(Stream<String> stream, this._recording)
: _delegate = stream,
_controller = stream.isBroadcast
? StreamController<String>.broadcast()
: StreamController<String>() {
_controller.onListen = () {
assert(_subscription == null);
_subscription = _listenToStream();
};
_controller.onCancel = () async {
assert(_subscription != null);
await _subscription.cancel();
_subscription = null;
};
_controller.onPause = () {
assert(_subscription != null && !_subscription.isPaused);
_subscription.pause();
};
_controller.onResume = () {
assert(_subscription != null && _subscription.isPaused);
_subscription.resume();
};
}
final Stream<String> _delegate;
final StreamController<String> _controller;
final List<_Message> _recording;
StreamSubscription<String> _subscription;
StreamSubscription<String> _listenToStream() {
return _delegate.listen(
(String element) {
_recording.add(_Response.fromString(element));
_controller.add(element);
},
onError: _controller.addError, // We currently don't support recording of errors.
onDone: _controller.close,
);
}
/// The wrapped [Stream] to expose to callers.
Stream<String> get stream => _controller.stream;
}
/// A [StreamSink] that monitors VM service JSON-rpc requests and saves the
/// requests to a recording.
class _RecordingSink implements StreamSink<String> {
_RecordingSink(this._delegate, this._recording);
final StreamSink<String> _delegate;
final List<_Message> _recording;
@override
Future<dynamic> close() => _delegate.close();
@override
Future<dynamic> get done => _delegate.done;
@override
void add(String data) {
_delegate.add(data);
_recording.add(_Request.fromString(data));
}
@override
void addError(dynamic errorEvent, [ StackTrace stackTrace ]) {
throw UnimplementedError('Add support for this if the need ever arises');
}
@override
Future<dynamic> addStream(Stream<String> stream) {
throw UnimplementedError('Add support for this if the need ever arises');
}
}
/// A [StreamChannel] that expects VM service (JSON-rpc) requests to be written
/// to its [StreamChannel.sink], looks up those requests in a recording, and
/// replays the corresponding responses back from the recording.
class ReplayVMServiceChannel extends StreamChannelMixin<String> {
ReplayVMServiceChannel(Directory location)
: _transactions = _loadTransactions(location);
final Map<int, _Transaction> _transactions;
final StreamController<String> _controller = StreamController<String>();
_ReplaySink _replaySink;
static Map<int, _Transaction> _loadTransactions(Directory location) {
final File file = _getManifest(location);
final String jsonData = file.readAsStringSync();
final Iterable<_Message> messages = (json.decode(jsonData) as List<dynamic>)
.cast<Map<String, dynamic>>()
.map<_Message>(_toMessage);
final Map<int, _Transaction> transactions = <int, _Transaction>{};
for (_Message message in messages) {
final _Transaction transaction =
transactions.putIfAbsent(message.id, () => _Transaction());
if (message.type == _kRequest) {
assert(transaction.request == null);
transaction.request = message as _Request;
} else {
assert(transaction.response == null);
transaction.response = message as _Response;
}
}
return transactions;
}
static _Message _toMessage(Map<String, dynamic> jsonData) {
return _Message.fromRecording(jsonData);
}
void send(_Request request) {
if (!_transactions.containsKey(request.id)) {
throw ArgumentError('No matching invocation found');
}
final _Transaction transaction = _transactions.remove(request.id);
// TODO(tvolkert): validate that `transaction.request` matches `request`
if (transaction.response == null) {
// This signals that when we were recording, the VM shut down before
// we received the response. This is typically due to the user quitting
// the app runner. We follow suit here and exit.
printStatus('Exiting due to dangling request');
exit(0);
} else {
_controller.add(json.encoder.convert(transaction.response.data));
if (_transactions.isEmpty) {
_controller.close();
}
}
}
@override
StreamSink<String> get sink => _replaySink ??= _ReplaySink(this);
@override
Stream<String> get stream => _controller.stream;
}
class _ReplaySink implements StreamSink<String> {
_ReplaySink(this.channel);
final ReplayVMServiceChannel channel;
final Completer<void> _completer = Completer<void>();
@override
Future<dynamic> close() {
_completer.complete();
return _completer.future;
}
@override
Future<dynamic> get done => _completer.future;
@override
void add(String data) {
if (_completer.isCompleted) {
throw StateError('Sink already closed');
}
channel.send(_Request.fromString(data));
}
@override
void addError(dynamic errorEvent, [ StackTrace stackTrace ]) {
throw UnimplementedError('Add support for this if the need ever arises');
}
@override
Future<dynamic> addStream(Stream<String> stream) {
throw UnimplementedError('Add support for this if the need ever arises');
}
}
File _getManifest(Directory location) {
final String path = location.fileSystem.path.join(location.path, _kManifest);
return location.fileSystem.file(path);
}

View file

@ -1,35 +0,0 @@
// Copyright 2014 The Flutter 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 'package:file_testing/file_testing.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_tools/executable.dart' as tools;
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/os.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
Cache.disableLocking();
int exitCode;
setExitFunctionForTests((int code) {
exitCode = code;
});
group('--bug-report', () {
testUsingContext('generates valid zip file', () async {
await tools.main(<String>['devices', '--bug-report']);
expect(exitCode, 0);
verify(os.zip(any, argThat(hasPath(matches(r'bugreport_01\.zip')))));
}, overrides: <Type, Generator>{
OperatingSystemUtils: () => MockOperatingSystemUtils(),
});
});
}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { }