Adds initial support for hot reload for Fuchsia to flutter_tool. (#8764)
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index b9bde1e..0f6f5f1 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -32,6 +32,7 @@
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 @@
new DoctorCommand(),
new DriveCommand(),
new FormatCommand(),
+ new FuchsiaReloadCommand(),
new InstallCommand(),
new LogsCommand(),
new PackagesCommand(),
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 33918cc..11c5347 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -261,6 +261,7 @@
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
+ case TargetPlatform.fuchsia:
return null;
}
assert(platform != null);
@@ -286,6 +287,7 @@
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
+ case TargetPlatform.fuchsia:
return null;
}
return null;
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index d0dd1fd..c3b3e79 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -89,6 +89,7 @@
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 @@
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:
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 8c0bcc5..52dc5cf 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -67,7 +67,8 @@
ios,
darwin_x64,
linux_x64,
- windows_x64
+ windows_x64,
+ fuchsia,
}
String getNameForTargetPlatform(TargetPlatform platform) {
@@ -86,6 +87,8 @@
return 'linux-x64';
case TargetPlatform.windows_x64:
return 'windows-x64';
+ case TargetPlatform.fuchsia:
+ return 'fuchsia';
}
assert(false);
return null;
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index e2bbeb0..778a323 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -175,6 +175,7 @@
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
+ case TargetPlatform.fuchsia:
assert(false);
}
@@ -232,6 +233,7 @@
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
+ case TargetPlatform.fuchsia:
assert(false);
}
diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
new file mode 100644
index 0000000..31a8268
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -0,0 +1,227 @@
+// 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;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
new file mode 100644
index 0000000..10fff76
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -0,0 +1,102 @@
+// 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;
+}
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 78e109d..6224f56 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -89,6 +89,12 @@
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 @@
// F10, exit
await stop();
return true;
+ } else if (lower == 'd') {
+ await detach();
+ return true;
}
return false;
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 4245355..99a1bc0 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -87,6 +87,74 @@
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 @@
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