Commit 0de69162 authored by John McCutchan's avatar John McCutchan

Add --hot mode for flutter run

parent 8893e328
...@@ -388,6 +388,31 @@ class AndroidDevice extends Device { ...@@ -388,6 +388,31 @@ class AndroidDevice extends Device {
); );
} }
@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 @override
bool get supportsRestart => true; bool get supportsRestart => true;
......
...@@ -17,7 +17,6 @@ import '../ios/devices.dart'; ...@@ -17,7 +17,6 @@ import '../ios/devices.dart';
import '../ios/simulators.dart'; import '../ios/simulators.dart';
import '../run.dart'; import '../run.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'run.dart' as run;
const String protocolVersion = '0.2.0'; const String protocolVersion = '0.2.0';
...@@ -292,7 +291,7 @@ class AppDomain extends Domain { ...@@ -292,7 +291,7 @@ class AppDomain extends Domain {
String route = _getStringArg(args, 'route'); String route = _getStringArg(args, 'route');
String mode = _getStringArg(args, 'mode'); String mode = _getStringArg(args, 'mode');
String target = _getStringArg(args, 'target'); String target = _getStringArg(args, 'target');
bool reloadSources = _getBoolArg(args, 'reload-sources'); bool hotMode = _getBoolArg(args, 'hot');
Device device = daemon.deviceDomain._getDevice(deviceId); Device device = daemon.deviceDomain._getDevice(deviceId);
if (device == null) if (device == null)
...@@ -301,9 +300,6 @@ class AppDomain extends Domain { ...@@ -301,9 +300,6 @@ class AppDomain extends Domain {
if (!FileSystemEntity.isDirectorySync(projectDirectory)) if (!FileSystemEntity.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist"; throw "'$projectDirectory' does not exist";
if (reloadSources != null)
run.useReloadSources = reloadSources;
BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug; BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
DebuggingOptions options; DebuggingOptions options;
...@@ -327,7 +323,8 @@ class AppDomain extends Domain { ...@@ -327,7 +323,8 @@ class AppDomain extends Domain {
device, device,
target: target, target: target,
debuggingOptions: options, debuggingOptions: options,
usesTerminalUI: false usesTerminalUI: false,
hotMode: hotMode
); );
AppInstance app = new AppInstance(_getNextAppId(), runner); AppInstance app = new AppInstance(_getNextAppId(), runner);
......
...@@ -19,9 +19,6 @@ import 'build_apk.dart'; ...@@ -19,9 +19,6 @@ import 'build_apk.dart';
import 'install.dart'; import 'install.dart';
import 'trace.dart'; import 'trace.dart';
/// Whether the user has passed the `--reload-sources` command-line option.
bool useReloadSources = false;
abstract class RunCommandBase extends FlutterCommand { abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() { RunCommandBase() {
addBuildModeFlags(defaultToRelease: false); addBuildModeFlags(defaultToRelease: false);
...@@ -58,16 +55,16 @@ class RunCommand extends RunCommandBase { ...@@ -58,16 +55,16 @@ class RunCommand extends RunCommandBase {
argParser.addOption('debug-port', argParser.addOption('debug-port',
help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).'); help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).');
usesPubOption(); usesPubOption();
argParser.addFlag('resident', argParser.addFlag('resident',
defaultsTo: true, defaultsTo: true,
help: 'Don\'t terminate the \'flutter run\' process after starting the application.'); 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 // Option to enable hot reloading.
// embedder via the DevFS observatory API. argParser.addFlag('hot',
argParser.addFlag('devfs', negatable: false, hide: true); negatable: false,
defaultsTo: false,
// Send the _reloadSource command to the VM. help: 'Run with support for hot reloading.');
argParser.addFlag('reload-sources', negatable: true, defaultsTo: false, hide: true);
// Hidden option to enable a benchmarking mode. This will run the given // Hidden option to enable a benchmarking mode. This will run the given
// application, measure the startup time and the app restart time, write the // application, measure the startup time and the app restart time, write the
...@@ -122,14 +119,25 @@ class RunCommand extends RunCommandBase { ...@@ -122,14 +119,25 @@ class RunCommand extends RunCommandBase {
Cache.releaseLockEarly(); 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']) { if (argResults['resident']) {
RunAndStayResident runner = new RunAndStayResident( RunAndStayResident runner = new RunAndStayResident(
deviceForCommand, deviceForCommand,
target: target, target: target,
debuggingOptions: options, debuggingOptions: options,
useDevFS: argResults['devfs'] hotMode: argResults['hot']
); );
return runner.run( return runner.run(
......
// 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;
}
}
...@@ -189,6 +189,19 @@ abstract class Device { ...@@ -189,6 +189,19 @@ abstract class Device {
Map<String, dynamic> platformArgs 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 supportsRestart => false;
bool get restartSendsFrameworkInitEvent => true; bool get restartSendsFrameworkInitEvent => true;
......
...@@ -13,11 +13,9 @@ import '../application_package.dart'; ...@@ -13,11 +13,9 @@ import '../application_package.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../commands/run.dart' as run;
import '../device.dart'; import '../device.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../observatory.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import 'mac.dart'; import 'mac.dart';
...@@ -361,6 +359,12 @@ class IOSSimulator extends Device { ...@@ -361,6 +359,12 @@ class IOSSimulator extends Device {
@override @override
bool get isLocalEmulator => true; bool get isLocalEmulator => true;
@override
bool get supportsHotMode => true;
@override
bool get needsDevFS => false;
_IOSSimulatorLogReader _logReader; _IOSSimulatorLogReader _logReader;
_IOSSimulatorDevicePortForwarder _portForwarder; _IOSSimulatorDevicePortForwarder _portForwarder;
...@@ -575,32 +579,6 @@ class IOSSimulator extends Device { ...@@ -575,32 +579,6 @@ class IOSSimulator extends Device {
return (await flx.build(precompiledSnapshot: true)) == 0; return (await flx.build(precompiledSnapshot: true)) == 0;
} }
@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 @override
Future<bool> stopApp(ApplicationPackage app) async { Future<bool> stopApp(ApplicationPackage app) async {
// Currently we don't have a way to stop an app running on iOS. // Currently we don't have a way to stop an app running on iOS.
......
...@@ -9,6 +9,7 @@ import 'dart:io'; ...@@ -9,6 +9,7 @@ import 'dart:io';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/io.dart';
// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService.
class Observatory { class Observatory {
Observatory._(this.peer, this.port) { Observatory._(this.peer, this.port) {
peer.registerMethod('streamNotify', (rpc.Parameters event) { peer.registerMethod('streamNotify', (rpc.Parameters event) {
...@@ -169,13 +170,13 @@ class Observatory { ...@@ -169,13 +170,13 @@ class Observatory {
}); });
} }
// Write multiple files into a file system. // Read one file from a file system.
Future<Response> writeDevFSFiles(String fsName, { List<DevFSFile> files }) { Future<List<int>> readDevFSFile(String fsName, String path) {
assert(files != null); return sendRequest('_readDevFSFile', <String, dynamic> {
return sendRequest('_writeDevFSFiles', <String, dynamic> {
'fsName': fsName, '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 @@ class Observatory { ...@@ -233,25 +234,6 @@ class Observatory {
} }
} }
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 { class Response {
Response(this.response); Response(this.response);
......
...@@ -14,10 +14,10 @@ import 'build_info.dart'; ...@@ -14,10 +14,10 @@ import 'build_info.dart';
import 'commands/build_apk.dart'; import 'commands/build_apk.dart';
import 'commands/install.dart'; import 'commands/install.dart';
import 'commands/trace.dart'; import 'commands/trace.dart';
import 'dart/package_map.dart';
import 'device.dart'; import 'device.dart';
import 'globals.dart'; import 'globals.dart';
import 'observatory.dart'; import 'observatory.dart';
import 'devfs.dart';
/// Given the value of the --target option, return the path of the Dart file /// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be. /// where the app's main function should be.
...@@ -37,20 +37,20 @@ class RunAndStayResident { ...@@ -37,20 +37,20 @@ class RunAndStayResident {
this.target, this.target,
this.debuggingOptions, this.debuggingOptions,
this.usesTerminalUI: true, this.usesTerminalUI: true,
this.useDevFS: false this.hotMode: false
}); });
final Device device; final Device device;
final String target; final String target;
final DebuggingOptions debuggingOptions; final DebuggingOptions debuggingOptions;
final bool usesTerminalUI; final bool usesTerminalUI;
final bool useDevFS; final bool hotMode;
ApplicationPackage _package; ApplicationPackage _package;
String _mainPath; String _mainPath;
LaunchResult _result; LaunchResult _result;
Completer<int> _exitCompleter = new Completer<int>(); final Completer<int> _exitCompleter = new Completer<int>();
StreamSubscription<String> _loggingSubscription; StreamSubscription<String> _loggingSubscription;
Observatory observatory; Observatory observatory;
...@@ -207,7 +207,15 @@ class RunAndStayResident { ...@@ -207,7 +207,15 @@ class RunAndStayResident {
if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.debuggingEnabled) {
observatory = await Observatory.connect(_result.observatoryPort); observatory = await Observatory.connect(_result.observatoryPort);
printTrace('Connected to observatory port: ${_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.populateIsolateInfo();
observatory.onExtensionEvent.listen((Event event) { observatory.onExtensionEvent.listen((Event event) {
printTrace(event.toString()); printTrace(event.toString());
...@@ -250,15 +258,17 @@ class RunAndStayResident { ...@@ -250,15 +258,17 @@ class RunAndStayResident {
// F1, help // F1, help
_printHelp(); _printHelp();
} else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
if (hotMode) {
_reloadSources();
} else {
if (device.supportsRestart) { if (device.supportsRestart) {
// F5, restart // F5, restart
restart(); restart();
} }
}
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
// F10, exit // F10, exit
_stopApp(); _stopApp();
} else if (useDevFS && lower == 'd') {
_updateDevFS();
} else if (lower == 'w') { } else if (lower == 'w') {
_debugDumpApp(); _debugDumpApp();
} else if (lower == 't') { } else if (lower == 't') {
...@@ -269,12 +279,14 @@ class RunAndStayResident { ...@@ -269,12 +279,14 @@ class RunAndStayResident {
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async { ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async {
_resetTerminal(); _resetTerminal();
await _cleanupDevFS();
await _stopLogger(); await _stopLogger();
await _stopApp(); await _stopApp();
exit(0); exit(0);
}); });
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async { ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async {
_resetTerminal(); _resetTerminal();
await _cleanupDevFS();
await _stopLogger(); await _stopLogger();
await _stopApp(); await _stopApp();
exit(0); exit(0);
...@@ -311,78 +323,84 @@ class RunAndStayResident { ...@@ -311,78 +323,84 @@ class RunAndStayResident {
observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId); observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId);
} }
DevFS devFS; DevFS _devFS;
String _devFSProjectRootPath;
Future<Null> _updateDevFS() async { Future<bool> _updateDevFS() async {
if (devFS == null) { if (_devFS == null) {
devFS = new DevFS(Directory.current, observatory); Directory directory = Directory.current;
_devFSProjectRootPath = directory.path;
String fsName = path.basename(directory.path);
_devFS = new DevFS(observatory, fsName, directory);
try { try {
await devFS.init(); await _devFS.create();
} catch (error) { } catch (error) {
devFS = null; _devFS = null;
printError('Error initializing development client: $error'); printError('Error initializing DevFS: $error');
return null; return false;
}
} }
// Send the root and lib directories. _exitCompleter.future.then((_) async {
Directory directory = Directory.current; await _cleanupDevFS();
_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))
);
}
} }
printStatus('DevFS: Updating files on device...');
await _devFS.update();
printStatus('DevFS: Finished updating files on device...');
return true;
} }
try { Future<Null> _cleanupDevFS() async {
await devFS.flush(); if (_devFS != null) {
} catch (error) { // Cleanup the devFS.
printError('Error sending sources to the client device: $error'); await _devFS.destroy();
} }
_devFS = null;
} }
void _sendFiles(Directory base, String prefix, List<File> files) { Future<Null> _launchFromDevFS(ApplicationPackage package,
String basePath = base.path; 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);
}
for (File file in files) { Future<bool> _reloadSources() async {
String devPath = file.path.substring(basePath.length); if (observatory.firstIsolateId == null)
if (devPath.startsWith('/')) throw 'Application isolate not found';
devPath = devPath.substring(1); if (_devFS != null) {
devFS.stageFile(prefix.isEmpty ? devPath : '$prefix/$devPath', file); 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 =
List<File> _dartFiles(List<FileSystemEntity> entities) { logger.startProgress('Reassembling application');
return new List<File>.from(entities await observatory.flutterReassemble(observatory.firstIsolateId);
.where((FileSystemEntity entity) => entity is File && entity.path.endsWith('.dart'))); reassembleStatus.stop(showElapsedTime: true);
return true;
} }
void _printHelp() { 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 "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.'); 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() { Future<dynamic> _stopLogger() {
...@@ -433,88 +451,3 @@ void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) { ...@@ -433,88 +451,3 @@ void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) {
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
printStatus('Run benchmark written to $benchmarkOut ($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();
}
// 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();
});
});
}
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/build_info.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/device.dart';
import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/simulators.dart';
...@@ -69,3 +70,36 @@ void applyMocksToCommand(FlutterCommand command) { ...@@ -69,3 +70,36 @@ void applyMocksToCommand(FlutterCommand command) {
..applicationPackages = new MockApplicationPackageStore() ..applicationPackages = new MockApplicationPackageStore()
..commandValidator = () => true; ..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');
}
}
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