From 94ce956f0a53c4d22f4f4645297cb90d6da9898c Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Tue, 21 May 2019 08:49:43 -0700 Subject: [PATCH] [flutter_tool] Adds support for 'run' for Fuchsia devices (#32849) --- .../lib/src/application_package.dart | 5 +- packages/flutter_tools/lib/src/base/os.dart | 33 ++ .../flutter_tools/lib/src/context_runner.dart | 10 +- .../lib/src/fuchsia/amber_ctl.dart | 76 ++++ .../lib/src/fuchsia/application_package.dart | 71 ++++ .../lib/src/fuchsia/fuchsia_build.dart | 5 +- .../lib/src/fuchsia/fuchsia_dev_finder.dart | 56 +++ .../lib/src/fuchsia/fuchsia_device.dart | 202 +++++++--- .../src/fuchsia/fuchsia_kernel_compiler.dart | 15 +- .../lib/src/fuchsia/fuchsia_pm.dart | 221 ++++++++--- .../lib/src/fuchsia/fuchsia_sdk.dart | 56 ++- .../lib/src/fuchsia/tiles_ctl.dart | 113 ++++++ .../lib/src/resident_runner.dart | 10 +- .../test/commands/build_fuchsia_test.dart | 141 ++++--- .../test/fuchsia/fuchsa_device_test.dart | 348 +++++++++++++++++- 15 files changed, 1166 insertions(+), 196 deletions(-) create mode 100644 packages/flutter_tools/lib/src/fuchsia/amber_ctl.dart create mode 100644 packages/flutter_tools/lib/src/fuchsia/application_package.dart create mode 100644 packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart create mode 100644 packages/flutter_tools/lib/src/fuchsia/tiles_ctl.dart diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index 9d4e9d2e370..1301d687de2 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -17,6 +17,7 @@ import 'base/os.dart' show os; import 'base/process.dart'; import 'base/user_messages.dart'; import 'build_info.dart'; +import 'fuchsia/application_package.dart'; import 'globals.dart'; import 'ios/ios_workflow.dart'; import 'ios/plist_utils.dart' as plist; @@ -66,7 +67,9 @@ class ApplicationPackageFactory { ? WindowsApp.fromWindowsProject(FlutterProject.current().windows) : WindowsApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.fuchsia: - return null; + return applicationBinary == null + ? FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia) + : FuchsiaApp.fromPrebuiltApp(applicationBinary); } assert(platform != null); return null; diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index 30c832e469d..1cfa6ffed90 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. import 'package:archive/archive.dart'; + +import '../globals.dart'; import 'context.dart'; import 'file_system.dart'; import 'io.dart'; @@ -72,6 +74,37 @@ abstract class OperatingSystemUtils { /// Returns the separator between items in the PATH environment variable. String get pathVarSeparator; + + /// Returns an unused network port. + /// + /// Returns 0 if an unused port cannot be found. + /// + /// The port returned by this function may become used before it is bound by + /// its intended user. + Future findFreePort({bool ipv6 = false}) async { + int port = 0; + ServerSocket serverSocket; + final InternetAddress loopback = + ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4; + try { + serverSocket = await ServerSocket.bind(loopback, 0); + port = serverSocket.port; + } on SocketException catch (e) { + // If ipv4 loopback bind fails, try ipv6. + if (!ipv6) { + return findFreePort(ipv6: true); + } + printTrace('findFreePort failed: $e'); + } catch (e) { + // Failures are signaled by a return value of 0 from this function. + printTrace('findFreePort failed: $e'); + } finally { + if (serverSocket != null) { + await serverSocket.close(); + } + } + return port; + } } class _PosixUtils extends OperatingSystemUtils { diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index f69b8721625..7b6bbaf988a 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -27,10 +27,9 @@ import 'devfs.dart'; import 'device.dart'; import 'doctor.dart'; import 'emulator.dart'; -import 'fuchsia/fuchsia_kernel_compiler.dart'; -import 'fuchsia/fuchsia_pm.dart'; -import 'fuchsia/fuchsia_sdk.dart'; -import 'fuchsia/fuchsia_workflow.dart'; +import 'fuchsia/fuchsia_device.dart' show FuchsiaDeviceTools; +import 'fuchsia/fuchsia_sdk.dart' show FuchsiaSdk, FuchsiaArtifacts; +import 'fuchsia/fuchsia_workflow.dart' show FuchsiaWorkflow; import 'ios/cocoapods.dart'; import 'ios/ios_workflow.dart'; import 'ios/mac.dart'; @@ -76,8 +75,7 @@ Future runInContext( Flags: () => const EmptyFlags(), FlutterVersion: () => FlutterVersion(const SystemClock()), FuchsiaArtifacts: () => FuchsiaArtifacts.find(), - FuchsiaKernelCompiler: () => FuchsiaKernelCompiler(), - FuchsiaPM: () => FuchsiaPM(), + FuchsiaDeviceTools: () => FuchsiaDeviceTools(), FuchsiaSdk: () => FuchsiaSdk(), FuchsiaWorkflow: () => FuchsiaWorkflow(), GenSnapshot: () => const GenSnapshot(), diff --git a/packages/flutter_tools/lib/src/fuchsia/amber_ctl.dart b/packages/flutter_tools/lib/src/fuchsia/amber_ctl.dart new file mode 100644 index 00000000000..8389eb75173 --- /dev/null +++ b/packages/flutter_tools/lib/src/fuchsia/amber_ctl.dart @@ -0,0 +1,76 @@ +// Copyright 2019 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 '../base/process.dart'; + +import 'fuchsia_device.dart'; +import 'fuchsia_pm.dart'; + +// usage: amber_ctl [opts] +// Commands +// get_up - get an update for a package +// Options +// -n: name of the package +// -v: version of the package to retrieve, if none is supplied any +// package instance could match +// -m: merkle root of the package to retrieve, if none is supplied +// any package instance could match +// +// get_blob - get the specified content blob +// -i: content ID of the blob +// +// add_src - add a source to the list we can use +// -n: name of the update source (optional, with URL) +// -f: file path or url to a source config file +// -h: SHA256 hash of source config file (optional, with URL) +// -x: do not disable other active sources (if the provided source is +// enabled) +// +// rm_src - remove a source, if it exists +// -n: name of the update source +// +// list_srcs - list the set of sources we can use +// +// enable_src +// -n: name of the update source +// -x: do not disable other active sources +// +// disable_src +// -n: name of the update source +// +// system_update - check for, download, and apply any available system +// update +// +// gc - trigger a garbage collection +// +// print_state - print go routine state of amber process + +/// Simple wrapper for interacting with the 'amber_ctl' tool running on the +/// Fuchsia device. +class FuchsiaAmberCtl { + /// Teaches the amber instance running on [device] about the Fuchsia package + /// server accessible via [configUrl]. + Future addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async { + final String configUrl = '${server.url}/config.json'; + final RunResult result = + await device.shell('amber_ctl add_src -x -f $configUrl'); + return result.exitCode == 0; + } + + /// Instructs the amber instance running on [device] to forget about the + /// Fuchsia package server that it was accessing via [serverUrl]. + Future rmSrc(FuchsiaDevice device, FuchsiaPackageServer server) async { + final RunResult result = + await device.shell('amber_ctl rm_src -n ${server.url}'); + return result.exitCode == 0; + } + + /// Instructs the amber instance running on [device] to prefetch the package + /// [packageName]. + Future getUp(FuchsiaDevice device, String packageName) async { + final RunResult result = + await device.shell('amber_ctl get_up -n $packageName'); + return result.exitCode == 0; + } +} diff --git a/packages/flutter_tools/lib/src/fuchsia/application_package.dart b/packages/flutter_tools/lib/src/fuchsia/application_package.dart new file mode 100644 index 00000000000..7ddd330e84a --- /dev/null +++ b/packages/flutter_tools/lib/src/fuchsia/application_package.dart @@ -0,0 +1,71 @@ +// Copyright 2019 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 'package:meta/meta.dart'; + +import '../application_package.dart'; +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../project.dart'; + +abstract class FuchsiaApp extends ApplicationPackage { + FuchsiaApp({@required String projectBundleId}) : super(id: projectBundleId); + + /// Creates a new [FuchsiaApp] from a fuchsia sub project. + factory FuchsiaApp.fromFuchsiaProject(FuchsiaProject project) { + return BuildableFuchsiaApp( + project: project, + ); + } + + /// Creates a new [FuchsiaApp] from an existing .far archive. + /// + /// [applicationBinary] is the path to the .far archive. + factory FuchsiaApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { + return PrebuiltFuchsiaApp( + farArchive: applicationBinary.path, + ); + } + + @override + String get displayName => id; + + /// The location of the 'far' archive containing the built app. + File farArchive(BuildMode buildMode); +} + +class PrebuiltFuchsiaApp extends FuchsiaApp { + PrebuiltFuchsiaApp({ + @required String farArchive, + }) : _farArchive = farArchive, + // TODO(zra): Extract the archive and extract the id from meta/package. + super(projectBundleId: farArchive); + + final String _farArchive; + + @override + File farArchive(BuildMode buildMode) => fs.file(_farArchive); + + @override + String get name => _farArchive; +} + +class BuildableFuchsiaApp extends FuchsiaApp { + BuildableFuchsiaApp({this.project}) : + super(projectBundleId: project.project.manifest.appName); + + final FuchsiaProject project; + + @override + File farArchive(BuildMode buildMode) { + // TODO(zra): Distinguish among build modes. + final String outDir = getFuchsiaBuildDirectory(); + final String pkgDir = fs.path.join(outDir, 'pkg'); + final String appName = project.project.manifest.appName; + return fs.file(fs.path.join(pkgDir, '$appName-0.far')); + } + + @override + String get name => project.project.manifest.appName; +} diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart index c39b83b5e2c..d9752bd7bdc 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart @@ -12,8 +12,8 @@ import '../bundle.dart'; import '../devfs.dart'; import '../project.dart'; -import 'fuchsia_kernel_compiler.dart'; import 'fuchsia_pm.dart'; +import 'fuchsia_sdk.dart'; // Building a Fuchsia package has a few steps: // 1. Do the custom kernel compile using the kernel compiler from the Fuchsia @@ -30,7 +30,7 @@ Future buildFuchsia( outDir.createSync(recursive: true); } - await fuchsiaKernelCompiler.build( + await fuchsiaSdk.fuchsiaKernelCompiler.build( fuchsiaProject: fuchsiaProject, target: target, buildInfo: buildInfo); await _buildAssets(fuchsiaProject, target, buildInfo); await _buildPackage(fuchsiaProject, target, buildInfo); @@ -96,6 +96,7 @@ Future _buildPackage( manifestFile.writeAsStringSync('meta/package=$pkgDir/meta/package\n', mode: FileMode.append); + final FuchsiaPM fuchsiaPM = fuchsiaSdk.fuchsiaPM; if (!await fuchsiaPM.init(pkgDir, appName)) { return; } diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart new file mode 100644 index 00000000000..a110ac8cf07 --- /dev/null +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart @@ -0,0 +1,56 @@ +// Copyright 2019 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 '../base/common.dart'; +import '../base/process.dart'; +import 'fuchsia_sdk.dart'; + +// Usage: dev_finder +// +// Subcommands: +// commands list all command names +// flags describe all known top-level flags +// help describe subcommands and their syntax +// list lists all Fuchsia devices on the network +// resolve attempts to resolve all passed Fuchsia domain names on the +// network + +/// A simple wrapper for the Fuchsia SDK's 'dev_finder' tool. +class FuchsiaDevFinder { + /// Returns a list of attached devices as a list of strings with entries + /// formatted as follows: + /// 192.168.42.172 scare-cable-skip-joy + Future> list() async { + if (fuchsiaArtifacts.devFinder == null) { + throwToolExit('Fuchsia dev_finder tool not found.'); + } + final List command = [ + fuchsiaArtifacts.devFinder.path, + 'list', + '-full' + ]; + final RunResult result = await runAsync(command); + return (result.exitCode == 0) ? result.stdout.split('\n') : null; + } + + /// Returns the host address by which the device [deviceName] should use for + /// the host. + /// + /// The string [deviceName] should be the name of the device from the + /// 'list' command, e.g. 'scare-cable-skip-joy'. + Future resolve(String deviceName) async { + if (fuchsiaArtifacts.devFinder == null) { + throwToolExit('Fuchsia dev_finder tool not found.'); + } + final List command = [ + fuchsiaArtifacts.devFinder.path, + 'resolve', + '-local', + '-device-limit', '1', + deviceName + ]; + final RunResult result = await runAsync(command); + return (result.exitCode == 0) ? result.stdout.trim() : null; + } +} diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart index f9fa27b195d..b48aac83a11 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart @@ -9,8 +9,11 @@ import 'package:meta/meta.dart'; import '../application_package.dart'; import '../artifacts.dart'; import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; +import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; @@ -21,8 +24,28 @@ import '../globals.dart'; import '../project.dart'; import '../vmservice.dart'; +import 'amber_ctl.dart'; +import 'application_package.dart'; +import 'fuchsia_build.dart'; +import 'fuchsia_pm.dart'; import 'fuchsia_sdk.dart'; import 'fuchsia_workflow.dart'; +import 'tiles_ctl.dart'; + +/// The [FuchsiaDeviceTools] instance. +FuchsiaDeviceTools get fuchsiaDeviceTools => context.get(); + +/// Fuchsia device-side tools. +class FuchsiaDeviceTools { + FuchsiaAmberCtl _amberCtl; + FuchsiaAmberCtl get amberCtl => _amberCtl ??= FuchsiaAmberCtl(); + + FuchsiaTilesCtl _tilesCtl; + FuchsiaTilesCtl get tilesCtl => _tilesCtl ??= FuchsiaTilesCtl(); +} + +final FuchsiaAmberCtl _amberCtl = fuchsiaDeviceTools.amberCtl; +final FuchsiaTilesCtl _tilesCtl = fuchsiaDeviceTools.tilesCtl; final String _ipv4Loopback = InternetAddress.loopbackIPv4.address; final String _ipv6Loopback = InternetAddress.loopbackIPv6.address; @@ -48,11 +71,15 @@ class _FuchsiaLogReader extends DeviceLogReader { Stream _logLines; @override Stream get logLines { - _logLines ??= _processLogs(fuchsiaSdk.syslogs(_device.id)); + final Stream logStream = fuchsiaSdk.syslogs(_device.id); + _logLines ??= _processLogs(logStream); return _logLines; } Stream _processLogs(Stream lines) { + if (lines == null) { + return null; + } // Get the starting time of the log processor to filter logs from before // the process attached. final DateTime startTime = systemClock.now(); @@ -60,7 +87,7 @@ class _FuchsiaLogReader extends DeviceLogReader { // the correct fuchsia module. final RegExp matchRegExp = _app == null ? _flutterLogOutput - : RegExp('INFO: ${_app.name}\\(flutter\\): '); + : RegExp('INFO: ${_app.name}(\.cmx)?\\(flutter\\): '); return Stream.eventTransformed( lines, (Sink outout) => _FuchsiaLogSink(outout, matchRegExp, startTime), @@ -188,7 +215,7 @@ class FuchsiaDevice extends Device { @override Future startApp( - ApplicationPackage package, { + covariant FuchsiaApp package, { String mainPath, String route, DebuggingOptions debuggingOptions, @@ -196,13 +223,110 @@ class FuchsiaDevice extends Device { bool prebuiltApplication = false, bool usesTerminalUi = true, bool ipv6 = false, - }) => - Future.error('unimplemented'); + }) async { + if (!prebuiltApplication) { + await buildFuchsia(fuchsiaProject: FlutterProject.current().fuchsia, + target: mainPath, + buildInfo: debuggingOptions.buildInfo); + } + // Stop the app if it's currently running. + await stopApp(package); + // Find out who the device thinks we are. + final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(name); + final int port = await os.findFreePort(); + if (port == 0) { + printError('Failed to find a free port'); + return LaunchResult.failed(); + } + final Directory packageRepo = + fs.directory(fs.path.join(getFuchsiaBuildDirectory(), '.pkg-repo')); + packageRepo.createSync(recursive: true); + + final String appName = FlutterProject.current().manifest.appName; + + final Status status = logger.startProgress( + 'Starting Fuchsia application...', + timeout: null, + ); + FuchsiaPackageServer fuchsiaPackageServer; + bool serverRegistered = false; + try { + // Start up a package server. + fuchsiaPackageServer = FuchsiaPackageServer(packageRepo.path, host, port); + if (!await fuchsiaPackageServer.start()) { + printError('Failed to start the Fuchsia package server'); + return LaunchResult.failed(); + } + final File farArchive = package.farArchive( + debuggingOptions.buildInfo.mode); + if (!await fuchsiaPackageServer.addPackage(farArchive)) { + printError('Failed to add package to the package server'); + return LaunchResult.failed(); + } + + // Teach amber about the package server. + if (!await _amberCtl.addSrc(this, fuchsiaPackageServer)) { + printError('Failed to teach amber about the package server'); + return LaunchResult.failed(); + } + serverRegistered = true; + + // Tell amber to prefetch the app. + if (!await _amberCtl.getUp(this, appName)) { + printError('Failed to get amber to prefetch the package'); + return LaunchResult.failed(); + } + + // Ensure tiles_ctl is started, and start the app. + if (!await FuchsiaTilesCtl.ensureStarted(this)) { + printError('Failed to ensure that tiles is started on the device'); + return LaunchResult.failed(); + } + + // Instruct tiles_ctl to start the app. + final String fuchsiaUrl = + 'fuchsia-pkg://fuchsia.com/$appName#meta/$appName.cmx'; + if (!await _tilesCtl.add(this, fuchsiaUrl, [])) { + printError('Failed to add the app to tiles'); + return LaunchResult.failed(); + } + } finally { + // Try to un-teach amber about the package server if needed. + if (serverRegistered) { + await _amberCtl.rmSrc(this, fuchsiaPackageServer); + } + // Shutdown the package server and delete the package repo; + fuchsiaPackageServer.stop(); + packageRepo.deleteSync(recursive: true); + status.cancel(); + } + + if (!debuggingOptions.buildInfo.isDebug && + !debuggingOptions.buildInfo.isProfile) { + return LaunchResult.succeeded(); + } + + // In a debug or profile build, try to find the observatory uri. + final FuchsiaIsolateDiscoveryProtocol discovery = + FuchsiaIsolateDiscoveryProtocol(this, appName); + try { + final Uri observatoryUri = await discovery.uri; + return LaunchResult.succeeded(observatoryUri: observatoryUri); + } finally { + discovery.dispose(); + } + } @override - Future stopApp(ApplicationPackage app) async { - // Currently we don't have a way to stop an app running on Fuchsia. - return false; + Future stopApp(covariant FuchsiaApp app) async { + final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id); + if (appKey != -1) { + if (!await _tilesCtl.remove(this, appKey)) { + printError('tiles_ctl remove on ${app.id} failed.'); + return false; + } + } + return true; } @override @@ -250,7 +374,13 @@ class FuchsiaDevice extends Device { /// List the ports currently running a dart observatory. Future> servicePorts() async { - final String findOutput = await shell('find /hub -name vmservice-port'); + const String findCommand = 'find /hub -name vmservice-port'; + final RunResult findResult = await shell(findCommand); + if (findResult.exitCode != 0) { + throwToolExit("'$findCommand' on device $id failed"); + return null; + } + final String findOutput = findResult.stdout; if (findOutput.trim() == '') { throwToolExit( 'No Dart Observatories found. Are you running a debug build?'); @@ -261,7 +391,13 @@ class FuchsiaDevice extends Device { if (path == '') { continue; } - final String lsOutput = await shell('ls $path'); + final String lsCommand = 'ls $path'; + final RunResult lsResult = await shell(lsCommand); + if (lsResult.exitCode != 0) { + throwToolExit("'$lsCommand' on device $id failed"); + return null; + } + final String lsOutput = lsResult.stdout; for (String line in lsOutput.split('\n')) { if (line == '') { continue; @@ -276,20 +412,18 @@ class FuchsiaDevice extends Device { } /// Run `command` on the Fuchsia device shell. - Future shell(String command) async { - final RunResult result = await runAsync([ + Future shell(String command) async { + if (fuchsiaArtifacts.sshConfig == null) { + throwToolExit('Cannot interact with device. No ssh config.\n' + 'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.'); + } + return await runAsync([ 'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, id, command ]); - if (result.exitCode != 0) { - throwToolExit( - 'Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}'); - return null; - } - return result.stdout; } /// Finds the first port running a VM matching `isolateName` from the @@ -332,7 +466,9 @@ class FuchsiaDevice extends Device { FuchsiaIsolateDiscoveryProtocol(this, isolateName); @override - bool isSupportedForProject(FlutterProject flutterProject) => true; + bool isSupportedForProject(FlutterProject flutterProject) { + return flutterProject.fuchsia.existsSync(); + } } class FuchsiaIsolateDiscoveryProtocol { @@ -431,7 +567,10 @@ class _FuchsiaPortForwarder extends DevicePortForwarder { @override Future forward(int devicePort, {int hostPort}) async { - hostPort ??= await _findPort(); + hostPort ??= await os.findFreePort(); + if (hostPort == 0) { + throwToolExit('Failed to forward port $devicePort. No free host-side ports'); + } // Note: the provided command works around a bug in -N, see US-515 // for more explanation. final List command = [ @@ -483,27 +622,4 @@ class _FuchsiaPortForwarder extends DevicePortForwarder { throwToolExit(result.stderr); } } - - static Future _findPort() async { - int port = 0; - ServerSocket serverSocket; - try { - serverSocket = await ServerSocket.bind(_ipv4Loopback, 0); - port = serverSocket.port; - } catch (e) { - // Failures are signaled by a return value of 0 from this function. - printTrace('_findPort failed: $e'); - } - if (serverSocket != null) { - await serverSocket.close(); - } - return port; - } -} - -class FuchsiaModulePackage extends ApplicationPackage { - FuchsiaModulePackage({@required this.name}) : super(id: name); - - @override - final String name; } diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart index e31a5bf2dc5..3377b575614 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_kernel_compiler.dart @@ -6,11 +6,10 @@ import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../base/common.dart'; -import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; -import '../base/process_manager.dart'; +import '../base/process.dart'; import '../build_info.dart'; import '../convert.dart'; import '../globals.dart'; @@ -18,10 +17,8 @@ import '../project.dart'; import 'fuchsia_sdk.dart'; -/// The [FuchsiaKernelCompiler] instance. -FuchsiaKernelCompiler get fuchsiaKernelCompiler => - context.get(); - +/// This is a simple wrapper around the custom kernel compiler from the Fuchsia +/// SDK. class FuchsiaKernelCompiler { /// Compiles the [fuchsiaProject] with entrypoint [target] to a collection of /// .dilp files (consisting of the app split along package: boundaries, but @@ -33,6 +30,9 @@ class FuchsiaKernelCompiler { BuildInfo buildInfo = BuildInfo.debug, }) async { // TODO(zra): Use filesystem root and scheme information from buildInfo. + if (fuchsiaArtifacts.kernelCompiler == null) { + throwToolExit('Fuchisa kernel compiler not found'); + } const String multiRootScheme = 'main-root'; final String packagesFile = fuchsiaProject.project.packagesFile.path; final String outDir = getFuchsiaBuildDirectory(); @@ -87,8 +87,7 @@ class FuchsiaKernelCompiler { artifacts.getArtifactPath(Artifact.engineDartBinary), fuchsiaArtifacts.kernelCompiler.path, ]..addAll(flags); - printTrace("Running: '${command.join(' ')}'"); - final Process process = await processManager.start(command); + final Process process = await runCommand(command); final Status status = logger.startProgress( 'Building Fuchsia application...', timeout: null, diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart index 755e08df518..eb612e583b8 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_pm.dart @@ -2,16 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../base/context.dart'; +import '../base/common.dart'; +import '../base/file_system.dart'; import '../base/io.dart'; -import '../base/process_manager.dart'; +import '../base/process.dart'; +import '../convert.dart'; import '../globals.dart'; import 'fuchsia_sdk.dart'; -/// The [FuchsiaPM] instance. -FuchsiaPM get fuchsiaPM => context.get(); - /// This is a basic wrapper class for the Fuchsia SDK's `pm` tool. class FuchsiaPM { /// Initializes the staging area at [buildPath] for creating the Fuchsia @@ -21,47 +20,27 @@ class FuchsiaPM { /// /// NB: The [buildPath] should probably be e.g. `build/fuchsia/pkg`, and the /// [appName] should probably be the name of the app from the pubspec file. - Future init(String buildPath, String appName) async { - final List command = [ - fuchsiaArtifacts.pm.path, + Future init(String buildPath, String appName) { + return _runPMCommand([ '-o', buildPath, '-n', appName, 'init', - ]; - printTrace("Running: '${command.join(' ')}'"); - final ProcessResult result = await processManager.run(command); - if (result.exitCode != 0) { - printError('Error initializing Fuchsia package for $appName: '); - printError(result.stdout); - printError(result.stderr); - return false; - } - return true; + ]); } /// Generates a new private key to be used to sign a Fuchsia package. /// /// [buildPath] should be the same [buildPath] passed to [init]. - Future genkey(String buildPath, String outKeyPath) async { - final List command = [ - fuchsiaArtifacts.pm.path, + Future genkey(String buildPath, String outKeyPath) { + return _runPMCommand([ '-o', buildPath, '-k', outKeyPath, 'genkey', - ]; - printTrace("Running: '${command.join(' ')}'"); - final ProcessResult result = await processManager.run(command); - if (result.exitCode != 0) { - printError('Error generating key for Fuchsia package: '); - printError(result.stdout); - printError(result.stderr); - return false; - } - return true; + ]); } /// Updates, signs, and seals a Fuchsia package. @@ -80,9 +59,8 @@ class FuchsiaPM { /// where $APPNAME is the same [appName] passed to [init], and meta/package /// is set up to be the file `meta/package` created by [init]. Future build( - String buildPath, String keyPath, String manifestPath) async { - final List command = [ - fuchsiaArtifacts.pm.path, + String buildPath, String keyPath, String manifestPath) { + return _runPMCommand([ '-o', buildPath, '-k', @@ -90,16 +68,7 @@ class FuchsiaPM { '-m', manifestPath, 'build', - ]; - printTrace("Running: '${command.join(' ')}'"); - final ProcessResult result = await processManager.run(command); - if (result.exitCode != 0) { - printError('Error building Fuchsia package: '); - printError(result.stdout); - printError(result.stderr); - return false; - } - return true; + ]); } /// Constructs a .far representation of the Fuchsia package. @@ -109,10 +78,8 @@ class FuchsiaPM { /// /// [buildPath] should be the same path passed to [init], and [manfiestPath] /// should be the same manifest passed to [build]. - Future archive( - String buildPath, String keyPath, String manifestPath) async { - final List command = [ - fuchsiaArtifacts.pm.path, + Future archive(String buildPath, String keyPath, String manifestPath) { + return _runPMCommand([ '-o', buildPath, '-k', @@ -120,15 +87,155 @@ class FuchsiaPM { '-m', manifestPath, 'archive', - ]; - printTrace("Running: '${command.join(' ')}'"); - final ProcessResult result = await processManager.run(command); - if (result.exitCode != 0) { - printError('Error archiving Fuchsia package: '); - printError(result.stdout); - printError(result.stderr); - return false; + ]); + } + + /// Initializes a new package repository at [repoPath] to be later served by + /// the 'serve' command. + Future newrepo(String repoPath) { + return _runPMCommand([ + 'newrepo', + '-repo', + repoPath, + ]); + } + + /// Spawns an http server in a new process for serving Fuchisa packages. + /// + /// The arguemnt [repoPath] should have previously been an arguemnt to + /// [newrepo]. The [host] should be the host reported by + /// [FuchsiaDevFinder.resolve], and [port] should be an unused port for the + /// http server to bind. + Future serve(String repoPath, String host, int port) async { + if (fuchsiaArtifacts.pm == null) { + throwToolExit('Fuchsia pm tool not found'); } - return true; + final List command = [ + fuchsiaArtifacts.pm.path, + 'serve', + '-repo', + repoPath, + '-l', + '$host:$port', + ]; + final Process process = await runCommand(command); + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(printTrace); + process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(printError); + return process; + } + + /// Publishes a Fuchsia package to a served package repository. + /// + /// For a package repo initialized with [newrepo] at [repoPath] and served + /// by [serve], this call publishes the `far` package at [packagePath] to + /// the repo such that it will be visible to devices connecting to the + /// package server. + Future publish(String repoPath, String packagePath) { + return _runPMCommand([ + 'publish', + '-a', + '-r', + repoPath, + '-f', + packagePath, + ]); + } + + Future _runPMCommand(List args) async { + if (fuchsiaArtifacts.pm == null) { + throwToolExit('Fuchsia pm tool not found'); + } + final List command = [fuchsiaArtifacts.pm.path] + args; + final RunResult result = await runAsync(command); + return result.exitCode == 0; + } +} + +/// A class for running and retaining state for a Fuchsia package server. +/// +/// [FuchsiaPackageServer] takes care of initializing the package repository, +/// spinning up the package server, publishing packages, and shutting down the +/// the server. +/// +/// Example usage: +/// var server = FuchsiaPackageServer( +/// '/path/to/repo', +/// await FuchsiaDevFinder.resolve(deviceName), +/// await freshPort()); +/// try { +/// await server.start(); +/// await server.addPackage(farArchivePath); +/// ... +/// } finally { +/// server.stop(); +/// } +class FuchsiaPackageServer { + FuchsiaPackageServer(this._repo, this._host, this._port); + + final String _repo; + final String _host; + final int _port; + + Process _process; + + /// The url that can be used by the device to access this package server. + String get url => 'http://$_host:$_port'; + + /// Usees [FuchiaPM.newrepo] and [FuchsiaPM.serve] to spin up a new Fuchsia + /// package server. + /// + /// Returns false if ther repo could not be created or the server could not + /// be spawned, and true otherwise. + Future start() async { + if (_process != null) { + printError('$this already started!'); + return false; + } + // initialize a new repo. + if (!await fuchsiaSdk.fuchsiaPM.newrepo(_repo)) { + printError('Failed to create a new package server repo'); + return false; + } + _process = await fuchsiaSdk.fuchsiaPM.serve(_repo, _host, _port); + // Put a completer on _process.exitCode to watch for error. + unawaited(_process.exitCode.whenComplete(() { + // If _process is null, then the server was stopped deliberately. + if (_process != null) { + printError('Error running Fuchsia pm tool "serve" command'); + } + })); + return true; + } + + /// Forcefully stops the package server process by sending it SIGTERM. + void stop() { + if (_process != null) { + _process.kill(); + _process = null; + } + } + + /// Uses [FuchsiaPM.publish] to add the Fuchsia 'far' package at + /// [packagePath] to the package server. + /// + /// Returns true on success and false if the server wasn't started or the + /// publish command failed. + Future addPackage(File package) async { + if (_process == null) { + return false; + } + return await fuchsiaSdk.fuchsiaPM.publish(_repo, package.path); + } + + @override + String toString() { + final String p = (_process == null) ? 'stopped' : 'running ${_process.pid}'; + return 'FuchsiaPackageServer at $_host:$_port ($p)'; } } diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart index 256eae2bdda..898c90f6bba 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart @@ -8,12 +8,15 @@ import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; -import '../base/process.dart'; import '../base/process_manager.dart'; import '../cache.dart'; import '../convert.dart'; import '../globals.dart'; +import 'fuchsia_dev_finder.dart'; +import 'fuchsia_kernel_compiler.dart'; +import 'fuchsia_pm.dart'; + /// The [FuchsiaSdk] instance. FuchsiaSdk get fuchsiaSdk => context.get(); @@ -25,23 +28,32 @@ FuchsiaArtifacts get fuchsiaArtifacts => context.get(); /// This workflow assumes development within the fuchsia source tree, /// including a working fx command-line tool in the user's PATH. class FuchsiaSdk { + /// Interface to the 'pm' tool. + FuchsiaPM get fuchsiaPM => _fuchsiaPM ??= FuchsiaPM(); + FuchsiaPM _fuchsiaPM; + + /// Interface to the 'dev_finder' tool. + FuchsiaDevFinder _fuchsiaDevFinder; + FuchsiaDevFinder get fuchsiaDevFinder => + _fuchsiaDevFinder ??= FuchsiaDevFinder(); + + /// Interface to the 'kernel_compiler' tool. + FuchsiaKernelCompiler _fuchsiaKernelCompiler; + FuchsiaKernelCompiler get fuchsiaKernelCompiler => + _fuchsiaKernelCompiler ??= FuchsiaKernelCompiler(); + /// Example output: /// $ dev_finder list -full /// > 192.168.42.56 paper-pulp-bush-angel Future listDevices() async { - try { - final String path = fuchsiaArtifacts.devFinder.absolute.path; - final RunResult process = await runAsync([path, 'list', '-full']); - return process.stdout.trim(); - } catch (exception) { - printTrace('$exception'); + if (fuchsiaArtifacts.devFinder == null) { + return null; } - return null; + final List devices = await fuchsiaDevFinder.list(); + return devices.isNotEmpty ? devices[0] : null; } /// Returns the fuchsia system logs for an attached device. - /// - /// Does not currently support multiple attached devices. Stream syslogs(String id) { Process process; try { @@ -50,6 +62,8 @@ class FuchsiaSdk { process.kill(); }); if (fuchsiaArtifacts.sshConfig == null) { + printError('Cannot read device logs: No ssh config.'); + printError('Have you set FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR?'); return null; } const String remoteCommand = 'log_listener --clock Local'; @@ -101,6 +115,15 @@ class FuchsiaArtifacts { final String tools = fs.path.join(fuchsia, 'tools'); final String dartPrebuilts = fs.path.join(tools, 'dart_prebuilts'); + final File devFinder = fs.file(fs.path.join(tools, 'dev_finder')); + final File platformDill = fs.file(fs.path.join( + dartPrebuilts, 'flutter_runner', 'platform_strong.dill')); + final File patchedSdk = fs.file(fs.path.join( + dartPrebuilts, 'flutter_runner')); + final File kernelCompiler = fs.file(fs.path.join( + dartPrebuilts, 'kernel_compiler.snapshot')); + final File pm = fs.file(fs.path.join(tools, 'pm')); + // If FUCHSIA_BUILD_DIR is defined, then look for the ssh_config dir // relative to it. Next, if FUCHSIA_SSH_CONFIG is defined, then use it. // TODO(zra): Consider passing the ssh config path in with a flag. @@ -113,14 +136,11 @@ class FuchsiaArtifacts { } return FuchsiaArtifacts( sshConfig: sshConfig, - devFinder: fs.file(fs.path.join(tools, 'dev_finder')), - platformKernelDill: fs.file(fs.path.join( - dartPrebuilts, 'flutter_runner', 'platform_strong.dill')), - flutterPatchedSdk: fs.file(fs.path.join( - dartPrebuilts, 'flutter_runner')), - kernelCompiler: fs.file(fs.path.join( - dartPrebuilts, 'kernel_compiler.snapshot')), - pm: fs.file(fs.path.join(tools, 'pm')), + devFinder: devFinder.existsSync() ? devFinder : null, + platformKernelDill: platformDill.existsSync() ? platformDill : null, + flutterPatchedSdk: patchedSdk.existsSync() ? patchedSdk : null, + kernelCompiler: kernelCompiler.existsSync() ? kernelCompiler : null, + pm: pm.existsSync() ? pm : null, ); } diff --git a/packages/flutter_tools/lib/src/fuchsia/tiles_ctl.dart b/packages/flutter_tools/lib/src/fuchsia/tiles_ctl.dart new file mode 100644 index 00000000000..f1088634631 --- /dev/null +++ b/packages/flutter_tools/lib/src/fuchsia/tiles_ctl.dart @@ -0,0 +1,113 @@ +// Copyright 2019 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 '../base/process.dart'; +import '../globals.dart'; + +import 'fuchsia_device.dart'; + +// Usage: tiles_ctl +// Supported commands: +// start +// add [--disable-focus] [...] +// remove +// list +// quit + +/// A simple wrapper around the 'tiles_ctl' tool running on the Fuchsia device. +class FuchsiaTilesCtl { + /// Finds the key for the app called [appName], or returns -1 if it can't be + /// found. + static Future findAppKey(FuchsiaDevice device, String appName) async { + final FuchsiaTilesCtl tilesCtl = fuchsiaDeviceTools.tilesCtl; + final Map runningApps = await tilesCtl.list(device); + if (runningApps == null) { + printTrace('tiles_ctl is not running'); + return -1; + } + for (MapEntry entry in runningApps.entries) { + if (entry.value.contains('$appName#meta')) { + return entry.key; + } + } + return -1; + } + + /// Ensures that tiles is running on the device. + static Future ensureStarted(FuchsiaDevice device) async { + final FuchsiaTilesCtl tilesCtl = fuchsiaDeviceTools.tilesCtl; + final Map runningApps = await tilesCtl.list(device); + if (runningApps == null) { + return tilesCtl.start(device); + } + return true; + } + + /// Instructs 'tiles' to start on the device. + /// + /// Returns true on success and false on failure. + Future start(FuchsiaDevice device) async { + final RunResult result = await device.shell('tiles_ctl start'); + return result.exitCode == 0; + } + + /// Returns a mapping of tile keys to app urls. + /// + /// Returns an empty mapping if tiles_ctl is running but no apps are running. + /// Returns null if tiles_ctl is not running. + Future> list(FuchsiaDevice device) async { + // Output of tiles_ctl list has the format: + // Found 1 tiles: + // Tile key 1 url fuchsia-pkg://fuchsia.com/stocks#meta/stocks.cmx ... + final Map tiles = {}; + final RunResult result = await device.shell('tiles_ctl list'); + if (result.exitCode != 0) { + return null; + } + // Look for evidence that tiles_ctl is not running. + if (result.stdout.contains("Couldn't find tiles component in realm")) { + return null; + } + // Find lines beginning with 'Tile' + for (String line in result.stdout.split('\n')) { + final List words = line.split(' '); + if (words.isNotEmpty && words[0] == 'Tile') { + final int key = int.tryParse(words[2]); + final String url = words[4]; + tiles[key] = url; + } + } + return tiles; + } + + /// Instructs tiles on the device to begin running the app at [url] in a new + /// tile. + /// + /// The app is passed the arguemnts in [args]. Flutter apps receive these + /// arguments as arguments to `main()`. [url] should be formatted as a + /// Fuchsia-style package url, e.g.: + /// fuchsia-pkg://fuchsia.com/flutter_gallery#meta/flutter_gallery.cmx + /// Returns true on success and false on failure. + Future add(FuchsiaDevice device, String url, List args) async { + final RunResult result = await device.shell( + 'tiles_ctl add $url ${args.join(" ")}'); + return result.exitCode == 0; + } + + /// Instructs tiles on the device to remove the app with key [key]. + /// + /// Returns true on success and false on failure. + Future remove(FuchsiaDevice device, int key) async { + final RunResult result = await device.shell('tiles_ctl remove $key'); + return result.exitCode == 0; + } + + /// Instructs tiles on the device to quit. + /// + /// Returns true on success and false on failure. + Future quit(FuchsiaDevice device) async { + final RunResult result = await device.shell('tiles_ctl quit'); + return result.exitCode == 0; + } +} diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index c902802d316..eca8c28fa7e 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -313,9 +313,15 @@ class FlutterDevice { } void startEchoingDeviceLog() { - if (_loggingSubscription != null) + if (_loggingSubscription != null) { return; - _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) { + } + final Stream logStream = device.getLogReader(app: package).logLines; + if (logStream == null) { + printError('Failed to read device log stream'); + return; + } + _loggingSubscription = logStream.listen((String line) { if (!line.contains('Observatory listening on http')) printStatus(line, wrap: false); }); diff --git a/packages/flutter_tools/test/commands/build_fuchsia_test.dart b/packages/flutter_tools/test/commands/build_fuchsia_test.dart index a31f5969c16..d67e5433ef2 100644 --- a/packages/flutter_tools/test/commands/build_fuchsia_test.dart +++ b/packages/flutter_tools/test/commands/build_fuchsia_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart'; import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; @@ -25,69 +26,99 @@ void main() { MemoryFileSystem memoryFileSystem; MockPlatform linuxPlatform; MockPlatform windowsPlatform; - MockFuchsiaPM fuchsiaPM; - MockFuchsiaKernelCompiler fuchsiaKernelCompiler; + MockFuchsiaSdk fuchsiaSdk; + MockFuchsiaArtifacts fuchsiaArtifacts; + MockFuchsiaArtifacts fuchsiaArtifactsNoCompiler; setUp(() { memoryFileSystem = MemoryFileSystem(); linuxPlatform = MockPlatform(); windowsPlatform = MockPlatform(); - fuchsiaPM = MockFuchsiaPM(); - fuchsiaKernelCompiler = MockFuchsiaKernelCompiler(); + fuchsiaSdk = MockFuchsiaSdk(); + fuchsiaArtifacts = MockFuchsiaArtifacts(); + fuchsiaArtifactsNoCompiler = MockFuchsiaArtifacts(); when(linuxPlatform.isLinux).thenReturn(true); when(windowsPlatform.isWindows).thenReturn(true); when(windowsPlatform.isLinux).thenReturn(false); when(windowsPlatform.isMacOS).thenReturn(false); + when(fuchsiaArtifacts.kernelCompiler).thenReturn(MockFile()); + when(fuchsiaArtifactsNoCompiler.kernelCompiler).thenReturn(null); }); - testUsingContext('Fuchsia build fails when there is no fuchsia project', - () async { - final BuildCommand command = BuildCommand(); - applyMocksToCommand(command); - expect( - createTestCommandRunner(command) - .run(const ['build', 'fuchsia']), - throwsA(isInstanceOf())); - }, overrides: { - Platform: () => linuxPlatform, - FileSystem: () => memoryFileSystem, - }); + group('Fuchsia build fails gracefully when', () { + testUsingContext('there is no Fuchsia project', + () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + expect( + createTestCommandRunner(command) + .run(const ['build', 'fuchsia']), + throwsA(isInstanceOf())); + }, overrides: { + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); - testUsingContext('Fuchsia build fails when there is no cmx file', () async { - final BuildCommand command = BuildCommand(); - applyMocksToCommand(command); - fs.directory('fuchsia').createSync(recursive: true); - fs.file('.packages').createSync(); - fs.file('pubspec.yaml').createSync(); + testUsingContext('there is no cmx file', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + fs.directory('fuchsia').createSync(recursive: true); + fs.file('.packages').createSync(); + fs.file('pubspec.yaml').createSync(); - expect( - createTestCommandRunner(command) - .run(const ['build', 'fuchsia']), - throwsA(isInstanceOf())); - }, overrides: { - Platform: () => linuxPlatform, - FileSystem: () => memoryFileSystem, - }); + expect( + createTestCommandRunner(command) + .run(const ['build', 'fuchsia']), + throwsA(isInstanceOf())); + }, overrides: { + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); - testUsingContext('Fuchsia build fails on Windows platform', () async { - final BuildCommand command = BuildCommand(); - applyMocksToCommand(command); - const String appName = 'app_name'; - fs - .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) - .createSync(recursive: true); - fs.file('.packages').createSync(); - final File pubspecFile = fs.file('pubspec.yaml')..createSync(); - pubspecFile.writeAsStringSync('name: $appName'); + testUsingContext('on Windows platform', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + const String appName = 'app_name'; + fs + .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) + .createSync(recursive: true); + fs.file('.packages').createSync(); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); - expect( - createTestCommandRunner(command) - .run(const ['build', 'fuchsia']), - throwsA(isInstanceOf())); - }, overrides: { - Platform: () => windowsPlatform, - FileSystem: () => memoryFileSystem, + expect( + createTestCommandRunner(command) + .run(const ['build', 'fuchsia']), + throwsA(isInstanceOf())); + }, overrides: { + Platform: () => windowsPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifacts, + }); + + testUsingContext('there is no Fuchsia kernel compiler', () async { + final BuildCommand command = BuildCommand(); + applyMocksToCommand(command); + const String appName = 'app_name'; + fs + .file(fs.path.join('fuchsia', 'meta', '$appName.cmx')) + .createSync(recursive: true); + fs.file('.packages').createSync(); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + expect( + createTestCommandRunner(command) + .run(const ['build', 'fuchsia']), + throwsA(isInstanceOf())); + }, overrides: { + Platform: () => linuxPlatform, + FileSystem: () => memoryFileSystem, + FuchsiaArtifacts: () => fuchsiaArtifactsNoCompiler, + }); }); testUsingContext('Fuchsia build parts fit together right', () async { @@ -110,8 +141,7 @@ void main() { }, overrides: { Platform: () => linuxPlatform, FileSystem: () => memoryFileSystem, - FuchsiaPM: () => fuchsiaPM, - FuchsiaKernelCompiler: () => fuchsiaKernelCompiler, + FuchsiaSdk: () => fuchsiaSdk, }); } @@ -189,3 +219,16 @@ class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler { fs.file(manifestPath).createSync(recursive: true); } } + +class MockFuchsiaSdk extends Mock implements FuchsiaSdk { + @override + final FuchsiaPM fuchsiaPM = MockFuchsiaPM(); + + @override + final FuchsiaKernelCompiler fuchsiaKernelCompiler = + MockFuchsiaKernelCompiler(); +} + +class MockFile extends Mock implements File {} + +class MockFuchsiaArtifacts extends Mock implements FuchsiaArtifacts {} diff --git a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart index 0b5a72702bd..735df55c968 100644 --- a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart +++ b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart @@ -5,26 +5,42 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/vmservice.dart'; -import 'package:mockito/mockito.dart'; -import 'package:process/process.dart'; - +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/time.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/fuchsia/application_package.dart'; +import 'package:flutter_tools/src/fuchsia/amber_ctl.dart'; import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_dev_finder.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart'; +import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart'; import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart'; +import 'package:flutter_tools/src/fuchsia/tiles_ctl.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/vmservice.dart'; +import 'package:meta/meta.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; import '../src/common.dart'; import '../src/context.dart'; void main() { group('fuchsia device', () { + MemoryFileSystem memoryFileSystem; + setUp(() { + memoryFileSystem = MemoryFileSystem(); + }); + testUsingContext('stores the requested id and name', () { const String deviceId = 'e80::0000:a00a:f00f:2002/3'; const String name = 'halfbaked'; @@ -49,14 +65,34 @@ void main() { expect(names.length, 0); }); - test('default capabilities', () async { + testUsingContext('default capabilities', () async { final FuchsiaDevice device = FuchsiaDevice('123'); + fs.directory('fuchsia').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); expect(device.supportsHotReload, true); expect(device.supportsHotRestart, false); expect(device.supportsStopApp, false); - expect(device.isSupportedForProject(null), true); - expect(await device.stopApp(null), false); + expect(device.isSupportedForProject(FlutterProject.current()), true); + }, overrides: { + FileSystem: () => memoryFileSystem, + }); + + testUsingContext('supported for project', () async { + final FuchsiaDevice device = FuchsiaDevice('123'); + fs.directory('fuchsia').createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + expect(device.isSupportedForProject(FlutterProject.current()), true); + }, overrides: { + FileSystem: () => memoryFileSystem, + }); + + testUsingContext('not supported for project', () async { + final FuchsiaDevice device = FuchsiaDevice('123'); + fs.file('pubspec.yaml').createSync(); + expect(device.isSupportedForProject(FlutterProject.current()), false); + }, overrides: { + FileSystem: () => memoryFileSystem, }); }); @@ -215,7 +251,7 @@ void main() { testUsingContext('can be parsed for an app', () async { final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester'); final DeviceLogReader reader = device.getLogReader( - app: FuchsiaModulePackage(name: 'example_app.cmx')); + app: FuchsiaModulePackage(name: 'example_app')); final List logLines = []; final Completer lock = Completer(); reader.logLines.listen((String line) { @@ -244,7 +280,7 @@ void main() { testUsingContext('cuts off prior logs', () async { final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester'); final DeviceLogReader reader = device.getLogReader( - app: FuchsiaModulePackage(name: 'example_app.cmx')); + app: FuchsiaModulePackage(name: 'example_app')); final List logLines = []; final Completer lock = Completer(); reader.logLines.listen((String line) { @@ -367,16 +403,119 @@ void main() { Logger: () => StdoutLogger(), }); }); + + group('fuchsia app start and stop: ', () { + MemoryFileSystem memoryFileSystem; + MockOperatingSystemUtils osUtils; + MockFuchsiaDeviceTools fuchsiaDeviceTools; + MockFuchsiaSdk fuchsiaSdk; + setUp(() { + memoryFileSystem = MemoryFileSystem(); + osUtils = MockOperatingSystemUtils(); + fuchsiaDeviceTools = MockFuchsiaDeviceTools(); + fuchsiaSdk = MockFuchsiaSdk(); + + when(osUtils.findFreePort()).thenAnswer((_) => Future.value(12345)); + }); + + testUsingContext('start prebuilt app in release mode', () async { + const String appName = 'app_name'; + final FuchsiaDevice device = FuchsiaDevice('123'); + fs.directory('fuchsia').createSync(recursive: true); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + final File far = fs.file('app_name-0.far')..createSync(); + + final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far); + final DebuggingOptions debuggingOptions = + DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)); + final LaunchResult launchResult = await device.startApp(app, + prebuiltApplication: true, + debuggingOptions: debuggingOptions); + + expect(launchResult.started, isTrue); + expect(launchResult.hasObservatory, isFalse); + }, overrides: { + FileSystem: () => memoryFileSystem, + FuchsiaDeviceTools: () => fuchsiaDeviceTools, + FuchsiaSdk: () => fuchsiaSdk, + OperatingSystemUtils: () => osUtils, + }); + + testUsingContext('start and stop prebuilt app in release mode', () async { + const String appName = 'app_name'; + final FuchsiaDevice device = FuchsiaDevice('123'); + fs.directory('fuchsia').createSync(recursive: true); + final File pubspecFile = fs.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync('name: $appName'); + final File far = fs.file('app_name-0.far')..createSync(); + + final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far); + final DebuggingOptions debuggingOptions = + DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)); + final LaunchResult launchResult = await device.startApp(app, + prebuiltApplication: true, + debuggingOptions: debuggingOptions); + + expect(launchResult.started, isTrue); + expect(launchResult.hasObservatory, isFalse); + expect(await device.stopApp(app), isTrue); + }, overrides: { + FileSystem: () => memoryFileSystem, + FuchsiaDeviceTools: () => fuchsiaDeviceTools, + FuchsiaSdk: () => fuchsiaSdk, + OperatingSystemUtils: () => osUtils, + }); + }); +} + +class FuchsiaModulePackage extends ApplicationPackage { + FuchsiaModulePackage({@required this.name}) : super(id: name); + + @override + final String name; } class MockProcessManager extends Mock implements ProcessManager {} class MockProcessResult extends Mock implements ProcessResult {} +class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} + class MockFile extends Mock implements File {} class MockProcess extends Mock implements Process {} +Process _createMockProcess({ + int exitCode = 0, + String stdout = '', + String stderr = '', + bool persistent = false, + }) { + final Stream> stdoutStream = Stream>.fromIterable(>[ + utf8.encode(stdout), + ]); + final Stream> stderrStream = Stream>.fromIterable(>[ + utf8.encode(stderr), + ]); + final Process process = MockProcess(); + + when(process.stdout).thenAnswer((_) => stdoutStream); + when(process.stderr).thenAnswer((_) => stderrStream); + + if (persistent) { + final Completer exitCodeCompleter = Completer(); + when(process.kill()).thenAnswer((_) { + exitCodeCompleter.complete(-11); + return true; + }); + when(process.exitCode).thenAnswer((_) => exitCodeCompleter.future); + } else { + when(process.exitCode).thenAnswer((_) => Future.value(exitCode)); + } + return process; +} + class MockFuchsiaDevice extends Mock implements FuchsiaDevice { MockFuchsiaDevice(this.id, this.portForwarder, this.ipv6); @@ -419,3 +558,192 @@ class MockIsolate extends Mock implements Isolate { @override final String name; } + +class MockFuchsiaAmberCtl extends Mock implements FuchsiaAmberCtl { + @override + Future addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async { + return true; + } + + @override + Future rmSrc(FuchsiaDevice device, FuchsiaPackageServer server) async { + return true; + } + + @override + Future getUp(FuchsiaDevice device, String packageName) async { + return true; + } +} + +class MockFuchsiaTilesCtl extends Mock implements FuchsiaTilesCtl { + final Map _runningApps = {}; + bool _started = false; + int _nextAppId = 1; + + @override + Future start(FuchsiaDevice device) async { + _started = true; + return true; + } + + @override + Future> list(FuchsiaDevice device) async { + if (!_started) { + return null; + } + return _runningApps; + } + + @override + Future add(FuchsiaDevice device, String url, List args) async { + if (!_started) { + return false; + } + _runningApps[_nextAppId] = url; + _nextAppId++; + return true; + } + + @override + Future remove(FuchsiaDevice device, int key) async { + if (!_started) { + return false; + } + _runningApps.remove(key); + return true; + } + + @override + Future quit(FuchsiaDevice device) async { + if (!_started) { + return false; + } + _started = false; + return true; + } +} + +class MockFuchsiaDeviceTools extends Mock implements FuchsiaDeviceTools { + @override + final FuchsiaAmberCtl amberCtl = MockFuchsiaAmberCtl(); + + @override + final FuchsiaTilesCtl tilesCtl = MockFuchsiaTilesCtl(); +} + +class MockFuchsiaPM extends Mock implements FuchsiaPM { + String _appName; + + @override + Future init(String buildPath, String appName) async { + if (!fs.directory(buildPath).existsSync()) { + return false; + } + fs + .file(fs.path.join(buildPath, 'meta', 'package')) + .createSync(recursive: true); + _appName = appName; + return true; + } + + @override + Future genkey(String buildPath, String outKeyPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync()) { + return false; + } + fs.file(outKeyPath).createSync(recursive: true); + return true; + } + + @override + Future build( + String buildPath, String keyPath, String manifestPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() || + !fs.file(keyPath).existsSync() || + !fs.file(manifestPath).existsSync()) { + return false; + } + fs.file(fs.path.join(buildPath, 'meta.far')).createSync(recursive: true); + return true; + } + + @override + Future archive( + String buildPath, String keyPath, String manifestPath) async { + if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() || + !fs.file(keyPath).existsSync() || + !fs.file(manifestPath).existsSync()) { + return false; + } + if (_appName == null) { + return false; + } + fs + .file(fs.path.join(buildPath, '$_appName-0.far')) + .createSync(recursive: true); + return true; + } + + @override + Future newrepo(String repoPath) async { + if (!fs.directory(repoPath).existsSync()) { + return false; + } + return true; + } + + @override + Future serve(String repoPath, String host, int port) async { + return _createMockProcess(persistent: true); + } + + @override + Future publish(String repoPath, String packagePath) async { + if (!fs.directory(repoPath).existsSync()) { + return false; + } + if (!fs.file(packagePath).existsSync()) { + return false; + } + return true; + } +} + +class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler { + @override + Future build({ + @required FuchsiaProject fuchsiaProject, + @required String target, // E.g., lib/main.dart + BuildInfo buildInfo = BuildInfo.debug, + }) async { + final String outDir = getFuchsiaBuildDirectory(); + final String appName = fuchsiaProject.project.manifest.appName; + final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest'); + fs.file(manifestPath).createSync(recursive: true); + } +} + +class MockFuchsiaDevFinder extends Mock implements FuchsiaDevFinder { + @override + Future> list() async { + return ['192.168.42.172 scare-cable-skip-joy']; + } + + @override + Future resolve(String deviceName) async { + return '192.168.42.10'; + } +} + +class MockFuchsiaSdk extends Mock implements FuchsiaSdk { + @override + final FuchsiaPM fuchsiaPM = MockFuchsiaPM(); + + @override + final FuchsiaKernelCompiler fuchsiaKernelCompiler = + MockFuchsiaKernelCompiler(); + + @override + final FuchsiaDevFinder fuchsiaDevFinder = MockFuchsiaDevFinder(); +}