Add --hot mode for flutter run
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 3197416..0b71755 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -389,6 +389,31 @@
}
@override
+ bool get supportsHotMode => true;
+
+ @override
+ Future<bool> runFromFile(ApplicationPackage package,
+ String scriptUri,
+ String packagesUri) async {
+ AndroidApk apk = package;
+ List<String> cmd = adbCommandForDevice(<String>[
+ 'shell', 'am', 'start',
+ '-a', 'android.intent.action.RUN',
+ '-d', _deviceBundlePath,
+ '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
+ ]);
+ cmd.addAll(<String>['--es', 'file', scriptUri]);
+ cmd.addAll(<String>['--es', 'packages', packagesUri]);
+ cmd.add(apk.launchActivity);
+ String result = runCheckedSync(cmd);
+ if (result.contains('Error: ')) {
+ printError(result.trim());
+ return false;
+ }
+ return true;
+ }
+
+ @override
bool get supportsRestart => true;
@override
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index a328b5b..0486ec3 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -17,7 +17,6 @@
import '../ios/simulators.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
-import 'run.dart' as run;
const String protocolVersion = '0.2.0';
@@ -292,7 +291,7 @@
String route = _getStringArg(args, 'route');
String mode = _getStringArg(args, 'mode');
String target = _getStringArg(args, 'target');
- bool reloadSources = _getBoolArg(args, 'reload-sources');
+ bool hotMode = _getBoolArg(args, 'hot');
Device device = daemon.deviceDomain._getDevice(deviceId);
if (device == null)
@@ -301,9 +300,6 @@
if (!FileSystemEntity.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist";
- if (reloadSources != null)
- run.useReloadSources = reloadSources;
-
BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
DebuggingOptions options;
@@ -327,7 +323,8 @@
device,
target: target,
debuggingOptions: options,
- usesTerminalUI: false
+ usesTerminalUI: false,
+ hotMode: hotMode
);
AppInstance app = new AppInstance(_getNextAppId(), runner);
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 87ad569..7ff0d62 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -19,9 +19,6 @@
import 'install.dart';
import 'trace.dart';
-/// Whether the user has passed the `--reload-sources` command-line option.
-bool useReloadSources = false;
-
abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() {
addBuildModeFlags(defaultToRelease: false);
@@ -58,16 +55,16 @@
argParser.addOption('debug-port',
help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).');
usesPubOption();
+
argParser.addFlag('resident',
defaultsTo: true,
help: 'Don\'t terminate the \'flutter run\' process after starting the application.');
- // Hidden option to ship all the sources of the current project over to the
- // embedder via the DevFS observatory API.
- argParser.addFlag('devfs', negatable: false, hide: true);
-
- // Send the _reloadSource command to the VM.
- argParser.addFlag('reload-sources', negatable: true, defaultsTo: false, hide: true);
+ // Option to enable hot reloading.
+ argParser.addFlag('hot',
+ negatable: false,
+ defaultsTo: false,
+ help: 'Run with support for hot reloading.');
// Hidden option to enable a benchmarking mode. This will run the given
// application, measure the startup time and the app restart time, write the
@@ -122,14 +119,25 @@
Cache.releaseLockEarly();
- useReloadSources = argResults['reload-sources'];
+ // Do some early error checks for hot mode.
+ bool hotMode = argResults['hot'];
+ if (hotMode) {
+ if (getBuildMode() != BuildMode.debug) {
+ printError('Hot mode only works with debug builds.');
+ return 1;
+ }
+ if (!deviceForCommand.supportsHotMode) {
+ printError('Hot mode is not supported by this device.');
+ return 1;
+ }
+ }
if (argResults['resident']) {
RunAndStayResident runner = new RunAndStayResident(
deviceForCommand,
target: target,
debuggingOptions: options,
- useDevFS: argResults['devfs']
+ hotMode: argResults['hot']
);
return runner.run(
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
new file mode 100644
index 0000000..09b7f8e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -0,0 +1,237 @@
+// Copyright 2016 The Chromium 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 'dart:convert' show BASE64, UTF8;
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import 'dart/package_map.dart';
+import 'globals.dart';
+import 'observatory.dart';
+
+// A file that has been added to a DevFS.
+class DevFSEntry {
+ DevFSEntry(this.devicePath, this.file);
+
+ final String devicePath;
+ final File file;
+ FileStat _fileStat;
+
+ DateTime get lastModified => _fileStat?.modified;
+ bool get stillExists {
+ _stat();
+ return _fileStat.type != FileSystemEntityType.NOT_FOUND;
+ }
+ bool get isModified {
+ if (_fileStat == null) {
+ _stat();
+ return true;
+ }
+ FileStat _oldFileStat = _fileStat;
+ _stat();
+ return _fileStat.modified.isAfter(_oldFileStat.modified);
+ }
+
+ void _stat() {
+ _fileStat = file.statSync();
+ }
+}
+
+
+/// Abstract DevFS operations interface.
+abstract class DevFSOperations {
+ Future<Uri> create(String fsName);
+ Future<dynamic> destroy(String fsName);
+ Future<dynamic> writeFile(String fsName, DevFSEntry entry);
+ Future<dynamic> writeSource(String fsName,
+ String devicePath,
+ String contents);
+}
+
+/// An implementation of [DevFSOperations] that speaks to the
+/// service protocol.
+class ServiceProtocolDevFSOperations implements DevFSOperations {
+ final Observatory serviceProtocol;
+
+ ServiceProtocolDevFSOperations(this.serviceProtocol);
+
+ @override
+ Future<Uri> create(String fsName) async {
+ Response response = await serviceProtocol.createDevFS(fsName);
+ return Uri.parse(response['uri']);
+ }
+
+ @override
+ Future<dynamic> destroy(String fsName) async {
+ await serviceProtocol.sendRequest('_deleteDevFS',
+ <String, dynamic> { 'fsName': fsName });
+ }
+
+ @override
+ Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
+ List<int> bytes;
+ try {
+ bytes = await entry.file.readAsBytes();
+ } catch (e) {
+ return e;
+ }
+ String fileContents = BASE64.encode(bytes);
+ return await serviceProtocol.sendRequest('_writeDevFSFile',
+ <String, dynamic> {
+ 'fsName': fsName,
+ 'path': entry.devicePath,
+ 'fileContents': fileContents
+ });
+ }
+
+ @override
+ Future<dynamic> writeSource(String fsName,
+ String devicePath,
+ String contents) async {
+ String fileContents = BASE64.encode(UTF8.encode(contents));
+ return await serviceProtocol.sendRequest('_writeDevFSFile',
+ <String, dynamic> {
+ 'fsName': fsName,
+ 'path': devicePath,
+ 'fileContents': fileContents
+ });
+ }
+}
+
+class DevFS {
+ /// Create a [DevFS] named [fsName] for the local files in [directory].
+ DevFS(Observatory serviceProtocol,
+ this.fsName,
+ this.rootDirectory)
+ : _operations = new ServiceProtocolDevFSOperations(serviceProtocol);
+
+ DevFS.operations(this._operations,
+ this.fsName,
+ this.rootDirectory);
+
+ final DevFSOperations _operations;
+ final String fsName;
+ final Directory rootDirectory;
+ final Map<String, DevFSEntry> _entries = <String, DevFSEntry>{};
+ final List<Future<Response>> _pendingWrites = new List<Future<Response>>();
+ Uri _baseUri;
+ Uri get baseUri => _baseUri;
+
+ Future<Uri> create() async {
+ _baseUri = await _operations.create(fsName);
+ printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
+ return _baseUri;
+ }
+
+ Future<dynamic> destroy() async {
+ printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
+ return await _operations.destroy(fsName);
+ }
+
+ Future<dynamic> update() async {
+ printTrace('DevFS: Starting sync from $rootDirectory');
+ // Send the root and lib directories.
+ Directory directory = rootDirectory;
+ _syncDirectory(directory, recursive: true);
+ String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName);
+ StringBuffer sb;
+ // Send the packages.
+ if (FileSystemEntity.isFileSync(packagesFilePath)) {
+ PackageMap packageMap = new PackageMap(kPackagesFileName);
+
+ for (String packageName in packageMap.map.keys) {
+ Uri uri = packageMap.map[packageName];
+ // Ignore self-references.
+ if (uri.toString() == 'lib/')
+ continue;
+ Directory directory = new Directory.fromUri(uri);
+ if (_syncDirectory(directory,
+ directoryName: 'packages/$packageName',
+ recursive: true)) {
+ if (sb == null) {
+ sb = new StringBuffer();
+ }
+ sb.writeln('$packageName:packages/$packageName');
+ }
+ }
+ }
+ printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files '
+ 'to finish');
+ await Future.wait(_pendingWrites);
+ _pendingWrites.clear();
+ if (sb != null) {
+ await _operations.writeSource(fsName, '.packages', sb.toString());
+ }
+ printTrace('DevFS: Sync finished');
+ // NB: You must call flush after a printTrace if you want to be printed
+ // immediately.
+ logger.flush();
+ }
+
+ void _syncFile(String devicePath, File file) {
+ DevFSEntry entry = _entries[devicePath];
+ if (entry == null) {
+ // New file.
+ entry = new DevFSEntry(devicePath, file);
+ _entries[devicePath] = entry;
+ }
+ bool needsWrite = entry.isModified;
+ if (needsWrite) {
+ Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry);
+ if (pendingWrite != null) {
+ _pendingWrites.add(pendingWrite);
+ } else {
+ printTrace('DevFS: Failed to sync "$devicePath"');
+ }
+ }
+ }
+
+ bool _shouldIgnore(String path) {
+ List<String> ignoredPrefixes = <String>['android/',
+ 'build/',
+ 'ios/',
+ 'packages/analyzer'];
+ for (String ignoredPrefix in ignoredPrefixes) {
+ if (path.startsWith(ignoredPrefix))
+ return true;
+ }
+ return false;
+ }
+
+ bool _syncDirectory(Directory directory,
+ {String directoryName,
+ bool recursive: false,
+ bool ignoreDotFiles: true}) {
+ String prefix = directoryName;
+ if (prefix == null) {
+ prefix = path.relative(directory.path, from: rootDirectory.path);
+ if (prefix == '.')
+ prefix = '';
+ }
+ try {
+ List<FileSystemEntity> files =
+ directory.listSync(recursive: recursive, followLinks: false);
+ for (FileSystemEntity file in files) {
+ if (file is! File) {
+ // Skip non-files.
+ continue;
+ }
+ if (ignoreDotFiles && path.basename(file.path).startsWith('.')) {
+ // Skip dot files.
+ continue;
+ }
+ final String devicePath =
+ path.join(prefix, path.relative(file.path, from: directory.path));
+ if (!_shouldIgnore(devicePath))
+ _syncFile(devicePath, file);
+ }
+ } catch (e) {
+ // Ignore directory and error.
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 6786712..d343e5f 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -189,6 +189,19 @@
Map<String, dynamic> platformArgs
});
+ /// Does this device implement support for hot reloading / restarting?
+ bool get supportsHotMode => false;
+
+ /// Does this device need a DevFS to support hot mode?
+ bool get needsDevFS => true;
+
+ /// Run from a file. Necessary for hot mode.
+ Future<bool> runFromFile(ApplicationPackage package,
+ String scriptUri,
+ String packagesUri) {
+ throw 'runFromFile unsupported';
+ }
+
bool get supportsRestart => false;
bool get restartSendsFrameworkInitEvent => true;
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 4bc2eb9..78cd7d9 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -13,11 +13,9 @@
import '../base/context.dart';
import '../base/process.dart';
import '../build_info.dart';
-import '../commands/run.dart' as run;
import '../device.dart';
import '../flx.dart' as flx;
import '../globals.dart';
-import '../observatory.dart';
import '../protocol_discovery.dart';
import 'mac.dart';
@@ -361,6 +359,12 @@
@override
bool get isLocalEmulator => true;
+ @override
+ bool get supportsHotMode => true;
+
+ @override
+ bool get needsDevFS => false;
+
_IOSSimulatorLogReader _logReader;
_IOSSimulatorDevicePortForwarder _portForwarder;
@@ -576,32 +580,6 @@
}
@override
- bool get supportsRestart => run.useReloadSources;
-
- @override
- bool get restartSendsFrameworkInitEvent => false;
-
- @override
- Future<bool> restartApp(
- ApplicationPackage package,
- LaunchResult result, {
- String mainPath,
- Observatory observatory
- }) async {
- if (observatory.firstIsolateId == null)
- throw 'Application isolate not found';
- Event result = await observatory.reloadSources(observatory.firstIsolateId);
- dynamic error = result.response['reloadError'];
- if (error != null) {
- printError('Error reloading application sources: $error');
- return false;
- } else {
- await observatory.flutterReassemble(observatory.firstIsolateId);
- return true;
- }
- }
-
- @override
Future<bool> stopApp(ApplicationPackage app) async {
// Currently we don't have a way to stop an app running on iOS.
return false;
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart
index b9d23be..bc6a315 100644
--- a/packages/flutter_tools/lib/src/observatory.dart
+++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -9,6 +9,7 @@
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart';
+// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService.
class Observatory {
Observatory._(this.peer, this.port) {
peer.registerMethod('streamNotify', (rpc.Parameters event) {
@@ -169,13 +170,13 @@
});
}
- // Write multiple files into a file system.
- Future<Response> writeDevFSFiles(String fsName, { List<DevFSFile> files }) {
- assert(files != null);
-
- return sendRequest('_writeDevFSFiles', <String, dynamic> {
+ // Read one file from a file system.
+ Future<List<int>> readDevFSFile(String fsName, String path) {
+ return sendRequest('_readDevFSFile', <String, dynamic> {
'fsName': fsName,
- 'files': files.map((DevFSFile file) => file.toJson()).toList()
+ 'path': path
+ }).then((Response response) {
+ return BASE64.decode(response.response['fileContents']);
});
}
@@ -233,25 +234,6 @@
}
}
-abstract class DevFSFile {
- DevFSFile(this.path);
-
- final String path;
-
- List<int> getContents();
-
- List<String> toJson() => <String>[path, BASE64.encode(getContents())];
-}
-
-class ByteDevFSFile extends DevFSFile {
- ByteDevFSFile(String path, this.contents): super(path);
-
- final List<int> contents;
-
- @override
- List<int> getContents() => contents;
-}
-
class Response {
Response(this.response);
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index c683772..43a357f 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -14,10 +14,10 @@
import 'commands/build_apk.dart';
import 'commands/install.dart';
import 'commands/trace.dart';
-import 'dart/package_map.dart';
import 'device.dart';
import 'globals.dart';
import 'observatory.dart';
+import 'devfs.dart';
/// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be.
@@ -37,20 +37,20 @@
this.target,
this.debuggingOptions,
this.usesTerminalUI: true,
- this.useDevFS: false
+ this.hotMode: false
});
final Device device;
final String target;
final DebuggingOptions debuggingOptions;
final bool usesTerminalUI;
- final bool useDevFS;
+ final bool hotMode;
ApplicationPackage _package;
String _mainPath;
LaunchResult _result;
- Completer<int> _exitCompleter = new Completer<int>();
+ final Completer<int> _exitCompleter = new Completer<int>();
StreamSubscription<String> _loggingSubscription;
Observatory observatory;
@@ -207,7 +207,15 @@
if (debuggingOptions.debuggingEnabled) {
observatory = await Observatory.connect(_result.observatoryPort);
printTrace('Connected to observatory port: ${_result.observatoryPort}.');
-
+ if (hotMode && device.needsDevFS) {
+ bool result = await _updateDevFS();
+ if (!result) {
+ printError('Could not perform initial file synchronization.');
+ return 3;
+ }
+ printStatus('Launching from sources.');
+ await _launchFromDevFS(_package, _mainPath);
+ }
observatory.populateIsolateInfo();
observatory.onExtensionEvent.listen((Event event) {
printTrace(event.toString());
@@ -250,15 +258,17 @@
// F1, help
_printHelp();
} else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
- if (device.supportsRestart) {
- // F5, restart
- restart();
+ if (hotMode) {
+ _reloadSources();
+ } else {
+ if (device.supportsRestart) {
+ // F5, restart
+ restart();
+ }
}
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
// F10, exit
_stopApp();
- } else if (useDevFS && lower == 'd') {
- _updateDevFS();
} else if (lower == 'w') {
_debugDumpApp();
} else if (lower == 't') {
@@ -269,12 +279,14 @@
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async {
_resetTerminal();
+ await _cleanupDevFS();
await _stopLogger();
await _stopApp();
exit(0);
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async {
_resetTerminal();
+ await _cleanupDevFS();
await _stopLogger();
await _stopApp();
exit(0);
@@ -311,78 +323,84 @@
observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId);
}
- DevFS devFS;
-
- Future<Null> _updateDevFS() async {
- if (devFS == null) {
- devFS = new DevFS(Directory.current, observatory);
+ DevFS _devFS;
+ String _devFSProjectRootPath;
+ Future<bool> _updateDevFS() async {
+ if (_devFS == null) {
+ Directory directory = Directory.current;
+ _devFSProjectRootPath = directory.path;
+ String fsName = path.basename(directory.path);
+ _devFS = new DevFS(observatory, fsName, directory);
try {
- await devFS.init();
+ await _devFS.create();
} catch (error) {
- devFS = null;
- printError('Error initializing development client: $error');
- return null;
+ _devFS = null;
+ printError('Error initializing DevFS: $error');
+ return false;
}
+
+ _exitCompleter.future.then((_) async {
+ await _cleanupDevFS();
+ });
}
- // Send the root and lib directories.
- Directory directory = Directory.current;
- _sendFiles(directory, '', _dartFiles(directory.listSync()));
-
- directory = new Directory('lib');
- _sendFiles(directory, 'lib', _dartFiles(directory.listSync(recursive: true)));
-
- // Send the packages.
- if (FileSystemEntity.isFileSync(kPackagesFileName)) {
- PackageMap packageMap = new PackageMap(kPackagesFileName);
-
- for (String packageName in packageMap.map.keys) {
- Uri uri = packageMap.map[packageName];
- // Ignore self-references.
- if (uri.toString() == 'lib/')
- continue;
- Directory directory = new Directory.fromUri(uri);
- if (directory.existsSync()) {
- _sendFiles(
- directory,
- 'packages/$packageName',
- _dartFiles(directory.listSync(recursive: true))
- );
- }
- }
- }
-
- try {
- await devFS.flush();
- } catch (error) {
- printError('Error sending sources to the client device: $error');
- }
+ printStatus('DevFS: Updating files on device...');
+ await _devFS.update();
+ printStatus('DevFS: Finished updating files on device...');
+ return true;
}
- void _sendFiles(Directory base, String prefix, List<File> files) {
- String basePath = base.path;
-
- for (File file in files) {
- String devPath = file.path.substring(basePath.length);
- if (devPath.startsWith('/'))
- devPath = devPath.substring(1);
- devFS.stageFile(prefix.isEmpty ? devPath : '$prefix/$devPath', file);
+ Future<Null> _cleanupDevFS() async {
+ if (_devFS != null) {
+ // Cleanup the devFS.
+ await _devFS.destroy();
}
+ _devFS = null;
}
- List<File> _dartFiles(List<FileSystemEntity> entities) {
- return new List<File>.from(entities
- .where((FileSystemEntity entity) => entity is File && entity.path.endsWith('.dart')));
+ Future<Null> _launchFromDevFS(ApplicationPackage package,
+ String mainScript) async {
+ String entryPath = path.relative(mainScript, from: _devFSProjectRootPath);
+ String deviceEntryPath =
+ _devFS.baseUri.resolve(entryPath).toFilePath();
+ String devicePackagesPath =
+ _devFS.baseUri.resolve('.packages').toFilePath();
+ await device.runFromFile(package,
+ deviceEntryPath,
+ devicePackagesPath);
+ }
+
+ Future<bool> _reloadSources() async {
+ if (observatory.firstIsolateId == null)
+ throw 'Application isolate not found';
+ if (_devFS != null) {
+ await _updateDevFS();
+ }
+ Status reloadStatus = logger.startProgress('Performing hot reload');
+ Event result = await observatory.reloadSources(observatory.firstIsolateId);
+ reloadStatus.stop(showElapsedTime: true);
+ dynamic error = result.response['reloadError'];
+ if (error != null) {
+ printError('Error reloading application sources: $error');
+ return false;
+ }
+ Status reassembleStatus =
+ logger.startProgress('Reassembling application');
+ await observatory.flutterReassemble(observatory.firstIsolateId);
+ reassembleStatus.stop(showElapsedTime: true);
+ return true;
}
void _printHelp() {
- String restartText = device.supportsRestart ? ', "r" or F5 to restart the app,' : '';
+ String restartText = '';
+ if (hotMode) {
+ restartText = ', "r" or F5 to perform a hot reload of the app,';
+ } else if (device.supportsRestart) {
+ restartText = ', "r" or F5 to restart the app,';
+ }
printStatus('Type "h" or F1 for help$restartText and "q", F10, or ctrl-c to quit.');
printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.');
-
- if (useDevFS)
- printStatus('Type "d" to send modified project files to the the client\'s DevFS.');
}
Future<dynamic> _stopLogger() {
@@ -433,88 +451,3 @@
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
printStatus('Run benchmark written to $benchmarkOut ($data).');
}
-
-class DevFS {
- DevFS(this.directory, this.observatory) {
- fsName = path.basename(directory.path);
- }
-
- final Directory directory;
- final Observatory observatory;
-
- String fsName;
- String uri;
- Map<String, _DevFSFileEntry> entries = <String, _DevFSFileEntry>{};
-
- Future<Null> init() async {
- CreateDevFSResponse response = await observatory.createDevFS(fsName);
- uri = response.uri;
- }
-
- void stageFile(String devPath, File file) {
- entries.putIfAbsent(devPath, () => new _DevFSFileEntry(devPath, file));
- }
-
- /// Flush any modified files to the devfs.
- Future<Null> flush() async {
- List<_DevFSFileEntry> toSend = entries.values
- .where((_DevFSFileEntry entry) => entry.isModified)
- .toList();
-
- for (_DevFSFileEntry entry in toSend) {
- printTrace('sending to devfs: ${entry.devPath}');
- entry.updateLastModified();
- }
-
- Status status = logger.startProgress('Sending ${toSend.length} files...');
-
- if (toSend.isEmpty) {
- status.stop(showElapsedTime: true);
- return;
- }
-
- try {
- List<_DevFSFile> files = toSend.map((_DevFSFileEntry entry) {
- return new _DevFSFile('/${entry.devPath}', entry.file);
- }).toList();
-
- // TODO(devoncarew): Batch this up in larger groups using writeDevFSFiles().
- // The current implementation leaves dangling service protocol calls on a timeout.
- await Future.wait(files.map((_DevFSFile file) {
- return observatory.writeDevFSFile(
- fsName,
- path: file.path,
- fileContents: file.getContents()
- );
- })).timeout(new Duration(seconds: 10));
- } finally {
- status.stop(showElapsedTime: true);
- }
- }
-
- Future<List<String>> listDevFSFiles() => observatory.listDevFSFiles(fsName);
-}
-
-class _DevFSFileEntry {
- _DevFSFileEntry(this.devPath, this.file);
-
- final String devPath;
- final File file;
-
- DateTime lastModified;
-
- bool get isModified => lastModified == null || file.lastModifiedSync().isAfter(lastModified);
-
- void updateLastModified() {
- lastModified = file.lastModifiedSync();
- }
-}
-
-class _DevFSFile extends DevFSFile {
- _DevFSFile(String path, this.file) : super(path);
-
- final File file;
-
- @override
- List<int> getContents() => file.readAsBytesSync();
-}
diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart
new file mode 100644
index 0000000..3f8f18b
--- /dev/null
+++ b/packages/flutter_tools/test/devfs_test.dart
@@ -0,0 +1,59 @@
+// Copyright 2016 The Chromium 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:io';
+
+import 'package:flutter_tools/src/devfs.dart';
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+
+import 'src/context.dart';
+import 'src/mocks.dart';
+
+void main() {
+ String filePath = 'bar/foo.txt';
+ String filePath2 = 'foo/bar.txt';
+ Directory tempDir;
+ String basePath;
+ MockDevFSOperations devFSOperations = new MockDevFSOperations();
+ DevFS devFS;
+ group('devfs', () {
+ testUsingContext('create local file system', () async {
+ tempDir = Directory.systemTemp.createTempSync();
+ basePath = tempDir.path;
+ File file = new File(path.join(basePath, filePath));
+ await file.parent.create(recursive: true);
+ file.writeAsBytesSync(<int>[1, 2, 3]);
+ });
+ testUsingContext('create dev file system', () async {
+ devFS = new DevFS.operations(devFSOperations, 'test', tempDir);
+ await devFS.create();
+ expect(devFSOperations.contains('create test'), isTrue);
+ });
+ testUsingContext('populate dev file system', () async {
+ await devFS.update();
+ expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue);
+ });
+ testUsingContext('modify existing file on local file system', () async {
+ File file = new File(path.join(basePath, filePath));
+ file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6]);
+ });
+ testUsingContext('update dev file system', () async {
+ await devFS.update();
+ expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue);
+ });
+ testUsingContext('add new file to local file system', () async {
+ File file = new File(path.join(basePath, filePath2));
+ await file.parent.create(recursive: true);
+ file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]);
+ });
+ testUsingContext('update dev file system', () async {
+ await devFS.update();
+ expect(devFSOperations.contains('writeFile test foo/bar.txt'), isTrue);
+ });
+ testUsingContext('delete dev file system', () async {
+ await devFS.destroy();
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index aeccdb5..49706e7 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -7,6 +7,7 @@
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
@@ -69,3 +70,36 @@
..applicationPackages = new MockApplicationPackageStore()
..commandValidator = () => true;
}
+
+class MockDevFSOperations implements DevFSOperations {
+ final List<String> messages = new List<String>();
+
+ bool contains(String match) {
+ bool result = messages.contains(match);
+ messages.clear();
+ return result;
+ }
+
+ @override
+ Future<Uri> create(String fsName) async {
+ messages.add('create $fsName');
+ return Uri.parse('file:///$fsName');
+ }
+
+ @override
+ Future<dynamic> destroy(String fsName) async {
+ messages.add('destroy $fsName');
+ }
+
+ @override
+ Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
+ messages.add('writeFile $fsName ${entry.devicePath}');
+ }
+
+ @override
+ Future<dynamic> writeSource(String fsName,
+ String devicePath,
+ String contents) async {
+ messages.add('writeSource $fsName $devicePath');
+ }
+}