Commit 3dd963b0 authored by John McCutchan's avatar John McCutchan Committed by GitHub

Merge pull request #4982 from johnmccutchan/add_hot_mode

Add --hot mode for flutter run with Android devices and iOS simulators
parents 5352e89b 0de69162
......@@ -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(
......
// 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);
......
This diff is collapsed.
// 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');
}
}
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