Merge pull request #4982 from johnmccutchan/add_hot_mode
Add --hot mode for flutter run with Android devices and iOS simulators
This commit is contained in:
commit
3dd963b088
@ -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
|
||||
bool get supportsRestart => true;
|
||||
|
||||
|
@ -17,7 +17,6 @@ import '../ios/devices.dart';
|
||||
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 @@ class AppDomain extends Domain {
|
||||
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 @@ class AppDomain extends Domain {
|
||||
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 @@ class AppDomain extends Domain {
|
||||
device,
|
||||
target: target,
|
||||
debuggingOptions: options,
|
||||
usesTerminalUI: false
|
||||
usesTerminalUI: false,
|
||||
hotMode: hotMode
|
||||
);
|
||||
|
||||
AppInstance app = new AppInstance(_getNextAppId(), runner);
|
||||
|
@ -19,9 +19,6 @@ import 'build_apk.dart';
|
||||
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 @@ class RunCommand extends RunCommandBase {
|
||||
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 @@ class RunCommand extends RunCommandBase {
|
||||
|
||||
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(
|
||||
|
237
packages/flutter_tools/lib/src/devfs.dart
Normal file
237
packages/flutter_tools/lib/src/devfs.dart
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -189,6 +189,19 @@ abstract class Device {
|
||||
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;
|
||||
|
@ -13,11 +13,9 @@ import '../application_package.dart';
|
||||
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 @@ class IOSSimulator extends Device {
|
||||
@override
|
||||
bool get isLocalEmulator => true;
|
||||
|
||||
@override
|
||||
bool get supportsHotMode => true;
|
||||
|
||||
@override
|
||||
bool get needsDevFS => false;
|
||||
|
||||
_IOSSimulatorLogReader _logReader;
|
||||
_IOSSimulatorDevicePortForwarder _portForwarder;
|
||||
|
||||
@ -575,32 +579,6 @@ class IOSSimulator extends Device {
|
||||
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
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
|
@ -9,6 +9,7 @@ import 'dart:io';
|
||||
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 @@ class Observatory {
|
||||
});
|
||||
}
|
||||
|
||||
// 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 @@ 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 {
|
||||
Response(this.response);
|
||||
|
||||
|
@ -14,10 +14,10 @@ import 'build_info.dart';
|
||||
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 @@ class RunAndStayResident {
|
||||
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 @@ class RunAndStayResident {
|
||||
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 @@ class RunAndStayResident {
|
||||
// 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 @@ class RunAndStayResident {
|
||||
|
||||
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 @@ class RunAndStayResident {
|
||||
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 @@ void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) {
|
||||
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();
|
||||
}
|
||||
|
59
packages/flutter_tools/test/devfs_test.dart
Normal file
59
packages/flutter_tools/test/devfs_test.dart
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
@ -7,6 +7,7 @@ import 'dart:async';
|
||||
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 @@ void applyMocksToCommand(FlutterCommand command) {
|
||||
..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');
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user