Commit b6ba37d7 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

Adds initial support for hot reload for Fuchsia to flutter_tool. (#8764)

parent 1b4f817b
......@@ -32,6 +32,7 @@ import 'src/commands/devices.dart';
import 'src/commands/doctor.dart';
import 'src/commands/drive.dart';
import 'src/commands/format.dart';
import 'src/commands/fuchsia_reload.dart';
import 'src/commands/install.dart';
import 'src/commands/logs.dart';
import 'src/commands/packages.dart';
......@@ -74,6 +75,7 @@ Future<Null> main(List<String> args) async {
new DoctorCommand(),
new DriveCommand(),
new FormatCommand(),
new FuchsiaReloadCommand(),
new InstallCommand(),
new LogsCommand(),
new PackagesCommand(),
......
......@@ -261,6 +261,7 @@ ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, {
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
return null;
}
assert(platform != null);
......@@ -286,6 +287,7 @@ class ApplicationPackageStore {
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
return null;
}
return null;
......
......@@ -89,6 +89,7 @@ class CachedArtifacts extends Artifacts {
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
return _getHostArtifactPath(artifact, platform);
}
assert(false, 'Invalid platform $platform.');
......@@ -170,6 +171,7 @@ class CachedArtifacts extends Artifacts {
case TargetPlatform.linux_x64:
case TargetPlatform.darwin_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
assert(mode == null, 'Platform $platform does not support different build modes.');
return fs.path.join(engineDir, platformName);
case TargetPlatform.ios:
......
......@@ -67,7 +67,8 @@ enum TargetPlatform {
ios,
darwin_x64,
linux_x64,
windows_x64
windows_x64,
fuchsia,
}
String getNameForTargetPlatform(TargetPlatform platform) {
......@@ -86,6 +87,8 @@ String getNameForTargetPlatform(TargetPlatform platform) {
return 'linux-x64';
case TargetPlatform.windows_x64:
return 'windows-x64';
case TargetPlatform.fuchsia:
return 'fuchsia';
}
assert(false);
return null;
......
......@@ -175,6 +175,7 @@ Future<String> _buildAotSnapshot(
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
assert(false);
}
......@@ -232,6 +233,7 @@ Future<String> _buildAotSnapshot(
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
assert(false);
}
......
// Copyright 2017 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:math';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../fuchsia/fuchsia_device.dart';
import '../globals.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
// Usage:
// With e.g. flutter_gallery already running, a HotRunner can be attached to it
// with:
// $ flutter fuchsia_reload -f ~/fuchsia -a 192.168.1.39 \
// -g //lib/flutter/examples/flutter_gallery:flutter_gallery
class FuchsiaReloadCommand extends FlutterCommand {
String _fuchsiaRoot;
String _projectRoot;
String _projectName;
String _fuchsiaProjectPath;
String _target;
String _address;
String _dotPackagesPath;
@override
final String name = 'fuchsia_reload';
@override
final String description = 'Hot reload on Fuchsia.';
FuchsiaReloadCommand() {
addBuildModeFlags(defaultToRelease: false);
argParser.addOption('address',
abbr: 'a',
help: 'Fuchsia device network name or address.');
argParser.addOption('build-type',
abbr: 'b',
defaultsTo: 'release-x86-64',
help: 'Fuchsia build type, e.g. release-x86-64.');
argParser.addOption('fuchsia-root',
abbr: 'f',
defaultsTo: platform.environment['FUCHSIA_ROOT'],
help: 'Path to Fuchsia source tree.');
argParser.addOption('gn-target',
abbr: 'g',
help: 'GN target of the application, e.g //path/to/app:app');
argParser.addOption('target',
abbr: 't',
defaultsTo: flx.defaultMainPath,
help: 'Target app path / main entry-point file. '
'Relative to --gn-target path, e.g. lib/main.dart');
}
@override
Future<Null> runCommand() async {
_validateArguments();
// Find the network ports used on the device by VM service instances.
final List<int> servicePorts = await _getServicePorts();
if (servicePorts.length == 0) {
throwToolExit("Couldn't find any running Observatory instances.");
}
for (int port in servicePorts) {
printStatus("Fuchsia service port: $port");
}
// TODO(zra): Check that there are running VM services on the returned
// ports, and find the Isolates that are running the target app.
// Set up a device and hot runner and attach the hot runner to the first
// vm service we found.
final int firstPort = servicePorts[0];
final FuchsiaDevice device = new FuchsiaDevice("$_address:$firstPort");
final HotRunner hotRunner = new HotRunner(
device,
debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
target: _target,
projectRootPath: _fuchsiaProjectPath,
packagesFilePath: _dotPackagesPath);
final Uri observatoryUri = Uri.parse("http://$_address:$firstPort");
await hotRunner.attach(observatoryUri);
}
void _validateArguments() {
_fuchsiaRoot = argResults['fuchsia-root'];
if (_fuchsiaRoot == null) {
throwToolExit(
"Please give the location of the Fuchsia tree with --fuchsia-root");
}
if (!_directoryExists(_fuchsiaRoot)) {
throwToolExit("Specified --fuchsia-root '$_fuchsiaRoot' does not exist");
}
_address = argResults['address'];
if (_address == null) {
throwToolExit(
"Give the address of the device running Fuchsia with --address");
}
final List<String> gnTarget = _extractPathAndName(argResults['gn-target']);
_projectRoot = gnTarget[0];
_projectName = gnTarget[1];
_fuchsiaProjectPath = "$_fuchsiaRoot/$_projectRoot";
if (!_directoryExists(_fuchsiaProjectPath)) {
throwToolExit(
"Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath");
}
final String relativeTarget = argResults['target'];
if (relativeTarget == null) {
throwToolExit('Give the application entry point with --target');
}
_target = "$_fuchsiaProjectPath/$relativeTarget";
if (!_fileExists(_target)) {
throwToolExit("Couldn't find application entry point at $_target");
}
final String buildType = argResults['build-type'];
if (buildType == null) {
throwToolExit("Give the build type with --build-type");
}
final String packagesFileName = "${_projectName}_dart_package.packages";
_dotPackagesPath =
"$_fuchsiaRoot/out/$buildType/gen/$_projectRoot/$packagesFileName";
if (!_fileExists(_dotPackagesPath)) {
throwToolExit("Couldn't find .packages file at $_dotPackagesPath");
}
}
List<String> _extractPathAndName(String gnTarget) {
final String errorMessage =
"fuchsia_reload --target '$gnTarget' should have the form: "
"'//path/to/app:name'";
// Separate strings like //path/to/target:app into [path/to/target, app]
final int lastColon = gnTarget.lastIndexOf(':');
if (lastColon < 0) {
throwToolExit(errorMessage);
}
final String name = gnTarget.substring(lastColon + 1);
// Skip '//' and chop off after :
if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/')) {
throwToolExit(errorMessage);
}
final String path = gnTarget.substring(2, lastColon);
return <String>[path, name];
}
Future<List<int>> _getServicePorts() async {
final FuchsiaDeviceCommandRunner runner =
new FuchsiaDeviceCommandRunner(_fuchsiaRoot);
final List<String> lsOutput = await runner.run("ls /tmp/dart.services");
final List<int> ports = new List<int>();
for (String s in lsOutput) {
final String trimmed = s.trim();
final int lastSpace = trimmed.lastIndexOf(' ');
final String lastWord = trimmed.substring(lastSpace + 1);
if ((lastWord != '.') && (lastWord != '..')) {
final int value = int.parse(lastWord, onError: (_) => null);
if (value != null) {
ports.add(value);
}
}
}
return ports;
}
bool _directoryExists(String path) {
final Directory d = fs.directory(path);
return d.existsSync();
}
bool _fileExists(String path) {
final File f = fs.file(path);
return f.existsSync();
}
}
// TODO(zra): When Fuchsia has ssh, this should be changed to use that instead.
class FuchsiaDeviceCommandRunner {
final String _fuchsiaRoot;
final Random _rng = new Random(new DateTime.now().millisecondsSinceEpoch);
FuchsiaDeviceCommandRunner(this._fuchsiaRoot);
Future<List<String>> run(String command) async {
final int tag = _rng.nextInt(999999);
const String kNetRunCommand = "out/build-magenta/tools/netruncmd";
final String netruncmd = fs.path.join(_fuchsiaRoot, kNetRunCommand);
const String kNetCP = "out/build-magenta/tools/netcp";
final String netcp = fs.path.join(_fuchsiaRoot, kNetCP);
final String remoteStdout = "/tmp/netruncmd.$tag";
final String localStdout = "${fs.systemTempDirectory.path}/netruncmd.$tag";
final String redirectedCommand = "$command > $remoteStdout";
// Run the command with output directed to a tmp file.
ProcessResult result =
await Process.run(netruncmd, <String>[":", redirectedCommand]);
if (result.exitCode != 0) {
return null;
}
// Copy that file to the local filesystem.
result = await Process.run(netcp, <String>[":$remoteStdout", localStdout]);
// Try to delete the remote file. Don't care about the result;
Process.run(netruncmd, <String>[":", "rm $remoteStdout"]);
if (result.exitCode != 0) {
return null;
}
// Read the local file.
final File f = fs.file(localStdout);
List<String> lines;
try {
lines = await f.readAsLines();
} finally {
f.delete();
}
return lines;
}
}
// Copyright 2017 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 '../application_package.dart';
import '../build_info.dart';
import '../devfs.dart';
import '../device.dart';
/// Read the log for a particular device.
class _FuchsiaLogReader extends DeviceLogReader {
FuchsiaDevice _device;
_FuchsiaLogReader(this._device);
@override String get name => _device.name;
Stream<String> _logLines;
@override
Stream<String> get logLines {
_logLines ??= new Stream<String>.empty();
return _logLines;
}
@override
String toString() => name;
}
class FuchsiaDevice extends Device {
FuchsiaDevice(String id, { this.name }) : super(id);
@override
bool get supportsHotMode => true;
@override
final String name;
@override
bool get isLocalEmulator => false;
@override
bool get supportsStartPaused => false;
@override
bool isAppInstalled(ApplicationPackage app) => false;
@override
bool isLatestBuildInstalled(ApplicationPackage app) => false;
@override
bool installApp(ApplicationPackage app) => false;
@override
bool uninstallApp(ApplicationPackage app) => false;
@override
bool isSupported() => true;
@override
Future<LaunchResult> startApp(
ApplicationPackage app,
BuildMode mode, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication: false,
DevFSContent kernelContent,
bool applicationNeedsRebuild: false,
}) => new Future<Null>.error('unimplemented');
@override
Future<bool> stopApp(ApplicationPackage app) async {
// Currently we don't have a way to stop an app running on Fuchsia.
return false;
}
@override
TargetPlatform get targetPlatform => TargetPlatform.fuchsia;
@override
String get sdkNameAndVersion => 'Fuchsia';
_FuchsiaLogReader _logReader;
@override
DeviceLogReader getLogReader({ApplicationPackage app}) {
_logReader ??= new _FuchsiaLogReader(this);
return _logReader;
}
@override
DevicePortForwarder get portForwarder => null;
@override
void clearLogs() {
}
@override
bool get supportsScreenshot => false;
}
......@@ -89,6 +89,12 @@ abstract class ResidentRunner {
return stopApp();
}
Future<Null> detach() async {
await stopEchoingDeviceLog();
await preStop();
appFinished();
}
Future<Null> _debugDumpApp() async {
if (vmService != null)
await vmService.vm.refreshViews();
......@@ -273,6 +279,9 @@ abstract class ResidentRunner {
// F10, exit
await stop();
return true;
} else if (lower == 'd') {
await detach();
return true;
}
return false;
......
......@@ -87,6 +87,74 @@ class HotRunner extends ResidentRunner {
return true;
}
Future<int> attach(Uri observatoryUri, {
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<Null> appStartedCompleter,
}) async {
_observatoryUri = observatoryUri;
try {
await connectToServiceProtocol(_observatoryUri);
} catch (error) {
printError('Error connecting to the service protocol: $error');
return 2;
}
try {
final Uri baseUri = await _initDevFS();
if (connectionInfoCompleter != null) {
connectionInfoCompleter.complete(
new DebugConnectionInfo(
httpUri: _observatoryUri,
wsUri: vmService.wsAddress,
baseUri: baseUri.toString()
)
);
}
} catch (error) {
printError('Error initializing DevFS: $error');
return 3;
}
final bool devfsResult = await _updateDevFS();
if (!devfsResult) {
printError('Could not perform initial file synchronization.');
return 3;
}
await vmService.vm.refreshViews();
printTrace('Connected to ${vmService.vm.mainView}.');
if (stayResident) {
setupTerminal();
registerSignalHandlers();
}
appStartedCompleter?.complete();
if (benchmarkMode) {
// We are running in benchmark mode.
printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
await vmService.vm.refreshViews();
// TODO(johnmccutchan): Modify script entry point.
printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart(fullRestart: false);
printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await stopApp();
final File benchmarkOutput = fs.file('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
}
if (stayResident)
return waitForAppToFinish();
await cleanupAtFinish();
return 0;
}
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
......@@ -152,68 +220,9 @@ class HotRunner extends ResidentRunner {
return 2;
}
_observatoryUri = result.observatoryUri;
try {
await connectToServiceProtocol(_observatoryUri);
} catch (error) {
printError('Error connecting to the service protocol: $error');
return 2;
}
try {
final Uri baseUri = await _initDevFS();
if (connectionInfoCompleter != null) {
connectionInfoCompleter.complete(
new DebugConnectionInfo(
httpUri: _observatoryUri,
wsUri: vmService.wsAddress,
baseUri: baseUri.toString()
)
);
}
} catch (error) {
printError('Error initializing DevFS: $error');
return 3;
}
final bool devfsResult = await _updateDevFS();
if (!devfsResult) {
printError('Could not perform initial file synchronization.');
return 3;
}
await vmService.vm.refreshViews();
printTrace('Connected to ${vmService.vm.mainView}.');
if (stayResident) {
setupTerminal();
registerSignalHandlers();
}
appStartedCompleter?.complete();
if (benchmarkMode) {
// We are running in benchmark mode.
printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
await vmService.vm.refreshViews();
// TODO(johnmccutchan): Modify script entry point.
printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart(fullRestart: false);
printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await stopApp();
final File benchmarkOutput = fs.file('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
}
if (stayResident)
return waitForAppToFinish();
await cleanupAtFinish();
return 0;
return attach(result.observatoryUri,
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter);
}
@override
......
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