| // Copyright 2019 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:fuchsia_ctl/fuchsia_ctl.dart'; |
| import 'package:fuchsia_ctl/src/amber_ctl.dart'; |
| import 'package:fuchsia_ctl/src/operation_result.dart'; |
| import 'package:fuchsia_ctl/src/tar.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:uuid/uuid.dart'; |
| |
| typedef AsyncResult = Future<OperationResult> Function( |
| String, DevFinder, ArgResults); |
| |
| const Map<String, AsyncResult> commands = <String, AsyncResult>{ |
| 'pave': pave, |
| 'pm': pm, |
| 'ssh': ssh, |
| 'test': test, |
| 'push-packages': pushPackages, |
| }; |
| |
| Future<void> main(List<String> args) async { |
| if (!Platform.isLinux) { |
| throw UnsupportedError('This tool only supports Linux.'); |
| } |
| |
| final ArgParser parser = ArgParser(); |
| parser |
| ..addOption('device-name', |
| abbr: 'd', |
| help: 'The device node name to use. ' |
| 'If not specified, the first discoverable device will be used.') |
| ..addOption('dev-finder-path', |
| defaultsTo: './dev_finder', |
| help: 'The path to the dev_finder executable.'); |
| parser.addCommand('ssh') |
| ..addFlag('interactive', |
| abbr: 'i', |
| help: 'Whether to ssh in interactive mode. ' |
| 'If --comand is specified, this is ignored.') |
| ..addOption('command', |
| abbr: 'c', |
| help: 'The command to run on the device. ' |
| 'If specified, --interactive is ignored.') |
| ..addOption('identity-file', |
| defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.'); |
| parser.addCommand('pave') |
| ..addOption('image', |
| abbr: 'i', help: 'The system image tgz to unpack and pave.'); |
| |
| final ArgParser pmSubCommand = parser.addCommand('pm') |
| ..addOption('pm-path', |
| defaultsTo: './pm', help: 'The path to the pm executable.') |
| ..addOption('repo', |
| abbr: 'r', |
| help: 'The location of the repository folder to create, ' |
| 'publish, or serve.') |
| ..addCommand('serve') |
| ..addCommand('newRepo'); |
| pmSubCommand |
| .addCommand('publishRepo') |
| .addMultiOption('far', abbr: 'f', help: 'The .far files to publish.'); |
| |
| parser.addCommand('push-packages') |
| ..addOption('pm-path', |
| defaultsTo: './pm', help: 'The path to the pm executable.') |
| ..addOption('repoArchive', help: 'The path to the repo tar.gz archive.') |
| ..addOption('identity-file', |
| defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.') |
| ..addMultiOption('packages', |
| abbr: 'p', |
| help: 'Packages from the repo that need to be pushed to the device.'); |
| |
| parser.addCommand('test') |
| ..addOption('pm-path', |
| defaultsTo: './pm', help: 'The path to the pm executable.') |
| ..addOption('identity-file', |
| defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.') |
| ..addOption('target', |
| abbr: 't', help: 'The name of the target to pass to runtests.') |
| ..addOption('arguments', |
| abbr: 'a', |
| help: 'Command line arguments to pass when invoking the tests') |
| ..addMultiOption('far', |
| abbr: 'f', help: 'The .far files to include for the test.'); |
| |
| final ArgResults results = parser.parse(args); |
| |
| if (results.command == null) { |
| stderr.writeln('Unknown command, expected one of: ${parser.commands.keys}'); |
| stderr.writeln(parser.usage); |
| exit(-1); |
| } |
| final AsyncResult command = commands[results.command.name]; |
| if (command == null) { |
| stderr.writeln('Unkown command ${results.command.name}.'); |
| stderr.writeln(parser.usage); |
| exit(-1); |
| } |
| final OperationResult result = await command( |
| results['device-name'], |
| DevFinder(results['dev-finder-path']), |
| results.command, |
| ); |
| if (!result.success) { |
| exit(-1); |
| } |
| } |
| |
| @visibleForTesting |
| Future<OperationResult> ssh( |
| String deviceName, |
| DevFinder devFinder, |
| ArgResults args, |
| ) async { |
| const SshClient sshClient = SshClient(); |
| final String targetIp = await devFinder.getTargetAddress(deviceName); |
| final String identityFile = args['identity-file']; |
| if (args['interactive']) { |
| return await sshClient.interactive( |
| targetIp, |
| identityFilePath: identityFile, |
| ); |
| } |
| final OperationResult result = await sshClient.runCommand( |
| targetIp, |
| identityFilePath: identityFile, |
| command: args['command'].split(' '), |
| ); |
| stdout.writeln( |
| '==================================== STDOUT ===================================='); |
| stdout.writeln(result.info); |
| stderr.writeln( |
| '==================================== STDERR ===================================='); |
| stderr.writeln(result.error); |
| return result; |
| } |
| |
| @visibleForTesting |
| Future<OperationResult> pave( |
| String deviceName, |
| DevFinder devFinder, |
| ArgResults args, |
| ) async { |
| const ImagePaver paver = ImagePaver(); |
| return await paver.pave(args['image'], deviceName); |
| } |
| |
| @visibleForTesting |
| Future<OperationResult> pm( |
| String deviceName, |
| DevFinder devFinder, |
| ArgResults args, |
| ) async { |
| final PackageServer server = PackageServer(args['pm-path']); |
| switch (args.command.name) { |
| case 'serve': |
| await server.serveRepo(args['repo']); |
| await Future<void>.delayed(const Duration(seconds: 15)); |
| return await server.close(); |
| case 'newRepo': |
| return await server.newRepo(args['repo']); |
| case 'publishRepo': |
| return await server.publishRepo(args['repo'], args['far']); |
| default: |
| throw ArgumentError('Command ${args.command.name} unknown.'); |
| } |
| } |
| |
| @visibleForTesting |
| Future<OperationResult> pushPackages( |
| String deviceName, |
| DevFinder devFinder, |
| ArgResults args, |
| ) async { |
| final PackageServer server = PackageServer(args['pm-path']); |
| final String repoArchive = args['repoArchive']; |
| final List<String> packages = args['packages']; |
| final String identityFile = args['identity-file']; |
| |
| const FileSystem fs = LocalFileSystem(); |
| final String uuid = Uuid().v4(); |
| final Directory repo = fs.systemTempDirectory.childDirectory('repo_$uuid'); |
| const Tar tar = SystemTar(); |
| try { |
| final String targetIp = await devFinder.getTargetAddress(deviceName); |
| final AmberCtl amberCtl = AmberCtl(targetIp, identityFile); |
| |
| stdout.writeln('Untaring $repoArchive to ${repo.path}'); |
| repo.createSync(recursive: true); |
| await tar.untar(repoArchive, repo.path); |
| |
| final String repositoryBase = path.join(repo.path, 'amber-files'); |
| stdout.writeln('Serving $repositoryBase to $targetIp'); |
| await server.serveRepo(repositoryBase, port: 0); |
| await amberCtl.addSrc(server.serverPort); |
| |
| stdout.writeln('Pushing packages $packages to $targetIp'); |
| for (String packageName in packages) { |
| stdout.writeln('Attempting to add package $packageName.'); |
| await amberCtl.addPackage(packageName); |
| } |
| |
| return OperationResult.success( |
| info: 'Successfully pushed $packages to $targetIp.'); |
| } finally { |
| // We may not have created the repo if dev finder errored first. |
| if (repo.existsSync()) { |
| repo.deleteSync(recursive: true); |
| } |
| if (server.serving) { |
| await server.close(); |
| } |
| } |
| } |
| |
| @visibleForTesting |
| Future<OperationResult> test( |
| String deviceName, |
| DevFinder devFinder, |
| ArgResults args, |
| ) async { |
| const FileSystem fs = LocalFileSystem(); |
| final String uuid = Uuid().v4(); |
| final String identityFile = args['identity-file']; |
| final Directory repo = fs.systemTempDirectory.childDirectory('repo_$uuid'); |
| final PackageServer server = PackageServer(args['pm-path']); |
| const SshClient ssh = SshClient(); |
| final List<String> farFiles = args['far']; |
| final String target = args['target']; |
| final String arguments = args['arguments']; |
| try { |
| final String targetIp = await devFinder.getTargetAddress(deviceName); |
| final AmberCtl amberCtl = AmberCtl(targetIp, identityFile); |
| stdout.writeln('Using ${repo.path} as repo to serve to $targetIp...'); |
| repo.createSync(recursive: true); |
| OperationResult result = await server.newRepo(repo.path); |
| |
| if (!result.success) { |
| stderr.writeln('Failed to create repo at $repo.'); |
| return result; |
| } |
| |
| await server.serveRepo(repo.path, port: 0); |
| await amberCtl.addSrc(server.serverPort); |
| |
| for (String farFile in farFiles) { |
| result = await server.publishRepo(repo.path, farFile); |
| if (!result.success) { |
| stderr.writeln('Failed to publish repo at $repo with $farFiles.'); |
| stderr.writeln(result.error); |
| return result; |
| } |
| final String packageName = |
| fs.file(farFile).basename.replaceFirst('-0.far', ''); |
| await amberCtl.addPackage(packageName); |
| } |
| |
| final OperationResult testResult = await ssh.runCommand( |
| targetIp, |
| identityFilePath: identityFile, |
| command: <String>[ |
| 'run', |
| 'fuchsia-pkg://fuchsia.com/$target#meta/$target.cmx', |
| arguments |
| ], |
| ); |
| stdout.writeln('Test results (passed: ${testResult.success}):'); |
| if (result.info != null) { |
| stdout.writeln(testResult.info); |
| } |
| if (result.error != null) { |
| stderr.writeln(testResult.error); |
| } |
| return testResult; |
| } finally { |
| // We may not have created the repo if dev finder errored first. |
| if (repo.existsSync()) { |
| repo.deleteSync(recursive: true); |
| } |
| if (server.serving) { |
| await server.close(); |
| } |
| } |
| } |