diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/libwebrtc/build/fuchsia/target.py | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/fuchsia/target.py b/third_party/libwebrtc/build/fuchsia/target.py new file mode 100644 index 0000000000..7a8628266a --- /dev/null +++ b/third_party/libwebrtc/build/fuchsia/target.py @@ -0,0 +1,336 @@ +# Copyright 2018 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 json +import logging +import os +import subprocess +import time + +import common +import remote_cmd +import runner_logs + + +_SHUTDOWN_CMD = ['dm', 'poweroff'] +_ATTACH_RETRY_INTERVAL = 1 +_ATTACH_RETRY_SECONDS = 120 + +# Amount of time to wait for a complete package installation, as a +# mitigation against hangs due to pkg/network-related failures. +_INSTALL_TIMEOUT_SECS = 10 * 60 + + +def _GetPackageUri(package_name): + """Returns the URI for the specified package name.""" + return 'fuchsia-pkg://fuchsia.com/%s' % (package_name) + + +def _GetPackageInfo(package_path): + """Returns a tuple with the name and version of a package.""" + + # Query the metadata file which resides next to the package file. + package_info = json.load( + open(os.path.join(os.path.dirname(package_path), 'package'))) + return package_info['name'], package_info['version'], + + +class _MapIsolatedPathsForPackage: + """Callable object which remaps /data and /tmp paths to their component- + specific locations, based on the package name and test realm path.""" + + def __init__(self, package_name, package_version, realms): + realms_path_fragment = '/r/'.join(['r/sys'] + realms) + package_sub_path = '{2}/fuchsia.com:{0}:{1}#meta:{0}.cmx/'.format( + package_name, package_version, realms_path_fragment) + self.isolated_format = '{0}' + package_sub_path + '{1}' + + def __call__(self, path): + for isolated_directory in ['/data/' , '/tmp/']: + if (path+'/').startswith(isolated_directory): + return self.isolated_format.format(isolated_directory, + path[len(isolated_directory):]) + return path + + +class FuchsiaTargetException(Exception): + def __init__(self, message): + super(FuchsiaTargetException, self).__init__(message) + + +class Target(object): + """Base class representing a Fuchsia deployment target.""" + + def __init__(self, out_dir, target_cpu): + self._out_dir = out_dir + self._started = False + self._dry_run = False + self._target_cpu = target_cpu + self._command_runner = None + self._ffx_path = os.path.join(common.SDK_ROOT, 'tools', + common.GetHostArchFromPlatform(), 'ffx') + + @staticmethod + def CreateFromArgs(args): + raise NotImplementedError() + + @staticmethod + def RegisterArgs(arg_parser): + pass + + # Functions used by the Python context manager for teardown. + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + return + + def Start(self): + """Handles the instantiation and connection process for the Fuchsia + target instance.""" + + def IsStarted(self): + """Returns True if the Fuchsia target instance is ready to accept + commands.""" + return self._started + + def IsNewInstance(self): + """Returns True if the connected target instance is newly provisioned.""" + return True + + def GetCommandRunner(self): + """Returns CommandRunner that can be used to execute commands on the + target. Most clients should prefer RunCommandPiped() and RunCommand().""" + self._AssertIsStarted() + + if self._command_runner is None: + host, port = self._GetEndpoint() + self._command_runner = \ + remote_cmd.CommandRunner(self._GetSshConfigPath(), host, port) + + return self._command_runner + + def RunCommandPiped(self, command, **kwargs): + """Starts a remote command and immediately returns a Popen object for the + command. The caller may interact with the streams, inspect the status code, + wait on command termination, etc. + + command: A list of strings representing the command and arguments. + kwargs: A dictionary of parameters to be passed to subprocess.Popen(). + The parameters can be used to override stdin and stdout, for + example. + + Returns: a Popen object. + + Note: method does not block. + """ + logging.debug('running (non-blocking) \'%s\'.', ' '.join(command)) + return self.GetCommandRunner().RunCommandPiped(command, **kwargs) + + def RunCommand(self, command, silent=False, timeout_secs=None): + """Executes a remote command and waits for it to finish executing. + + Returns the exit code of the command. + """ + logging.debug('running \'%s\'.', ' '.join(command)) + return self.GetCommandRunner().RunCommand(command, silent, + timeout_secs=timeout_secs) + + def EnsureIsolatedPathsExist(self, for_package, for_realms): + """Ensures that the package's isolated /data and /tmp exist.""" + for isolated_directory in ['/data', '/tmp']: + self.RunCommand([ + 'mkdir', '-p', + _MapIsolatedPathsForPackage(for_package, 0, + for_realms)(isolated_directory) + ]) + + def PutFile(self, + source, + dest, + recursive=False, + for_package=None, + for_realms=()): + """Copies a file from the local filesystem to the target filesystem. + + source: The path of the file being copied. + dest: The path on the remote filesystem which will be copied to. + recursive: If true, performs a recursive copy. + for_package: If specified, isolated paths in the |dest| are mapped to their + obsolute paths for the package, on the target. This currently + affects the /data and /tmp directories. + for_realms: If specified, identifies the sub-realm of 'sys' under which + isolated paths (see |for_package|) are stored. + """ + assert type(source) is str + self.PutFiles([source], dest, recursive, for_package, for_realms) + + def PutFiles(self, + sources, + dest, + recursive=False, + for_package=None, + for_realms=()): + """Copies files from the local filesystem to the target filesystem. + + sources: List of local file paths to copy from, or a single path. + dest: The path on the remote filesystem which will be copied to. + recursive: If true, performs a recursive copy. + for_package: If specified, /data in the |dest| is mapped to the package's + isolated /data location. + for_realms: If specified, identifies the sub-realm of 'sys' under which + isolated paths (see |for_package|) are stored. + """ + assert type(sources) is tuple or type(sources) is list + if for_package: + self.EnsureIsolatedPathsExist(for_package, for_realms) + dest = _MapIsolatedPathsForPackage(for_package, 0, for_realms)(dest) + logging.debug('copy local:%s => remote:%s', sources, dest) + self.GetCommandRunner().RunScp(sources, dest, remote_cmd.COPY_TO_TARGET, + recursive) + + def GetFile(self, + source, + dest, + for_package=None, + for_realms=(), + recursive=False): + """Copies a file from the target filesystem to the local filesystem. + + source: The path of the file being copied. + dest: The path on the local filesystem which will be copied to. + for_package: If specified, /data in paths in |sources| is mapped to the + package's isolated /data location. + for_realms: If specified, identifies the sub-realm of 'sys' under which + isolated paths (see |for_package|) are stored. + recursive: If true, performs a recursive copy. + """ + assert type(source) is str + self.GetFiles([source], dest, for_package, for_realms, recursive) + + def GetFiles(self, + sources, + dest, + for_package=None, + for_realms=(), + recursive=False): + """Copies files from the target filesystem to the local filesystem. + + sources: List of remote file paths to copy. + dest: The path on the local filesystem which will be copied to. + for_package: If specified, /data in paths in |sources| is mapped to the + package's isolated /data location. + for_realms: If specified, identifies the sub-realm of 'sys' under which + isolated paths (see |for_package|) are stored. + recursive: If true, performs a recursive copy. + """ + assert type(sources) is tuple or type(sources) is list + self._AssertIsStarted() + if for_package: + sources = map(_MapIsolatedPathsForPackage(for_package, 0, for_realms), + sources) + logging.debug('copy remote:%s => local:%s', sources, dest) + return self.GetCommandRunner().RunScp(sources, dest, + remote_cmd.COPY_FROM_TARGET, + recursive) + + def _GetEndpoint(self): + """Returns a (host, port) tuple for the SSH connection to the target.""" + raise NotImplementedError() + + def _GetTargetSdkArch(self): + """Returns the Fuchsia SDK architecture name for the target CPU.""" + if self._target_cpu == 'arm64' or self._target_cpu == 'x64': + return self._target_cpu + raise FuchsiaTargetException('Unknown target_cpu:' + self._target_cpu) + + def _AssertIsStarted(self): + assert self.IsStarted() + + def _WaitUntilReady(self): + logging.info('Connecting to Fuchsia using SSH.') + + host, port = self._GetEndpoint() + end_time = time.time() + _ATTACH_RETRY_SECONDS + ssh_diagnostic_log = runner_logs.FileStreamFor('ssh_diagnostic_log') + while time.time() < end_time: + runner = remote_cmd.CommandRunner(self._GetSshConfigPath(), host, port) + ssh_proc = runner.RunCommandPiped(['true'], + ssh_args=['-v'], + stdout=ssh_diagnostic_log, + stderr=subprocess.STDOUT) + if ssh_proc.wait() == 0: + logging.info('Connected!') + self._started = True + return True + time.sleep(_ATTACH_RETRY_INTERVAL) + + logging.error('Timeout limit reached.') + + raise FuchsiaTargetException('Couldn\'t connect using SSH.') + + def _GetSshConfigPath(self, path): + raise NotImplementedError() + + def GetPkgRepo(self): + """Returns an PkgRepo instance which serves packages for this Target. + Callers should typically call GetPkgRepo() in a |with| statement, and + install and execute commands inside the |with| block, so that the returned + PkgRepo can teardown correctly, if necessary. + """ + raise NotImplementedError() + + def InstallPackage(self, package_paths): + """Installs a package and it's dependencies on the device. If the package is + already installed then it will be updated to the new version. + + package_paths: Paths to the .far files to install. + """ + with self.GetPkgRepo() as pkg_repo: + # Publish all packages to the serving TUF repository under |tuf_root|. + for package_path in package_paths: + pkg_repo.PublishPackage(package_path) + + # Resolve all packages, to have them pulled into the device/VM cache. + for package_path in package_paths: + package_name, package_version = _GetPackageInfo(package_path) + logging.info('Resolving %s into cache.', package_name) + return_code = self.RunCommand( + ['pkgctl', 'resolve', + _GetPackageUri(package_name), '>/dev/null'], + timeout_secs=_INSTALL_TIMEOUT_SECS) + if return_code != 0: + raise Exception( + 'Error {} while resolving {}.'.format(return_code, package_name)) + + # Verify that the newly resolved versions of packages are reported. + for package_path in package_paths: + # Use pkgctl get-hash to determine which version will be resolved. + package_name, package_version = _GetPackageInfo(package_path) + pkgctl = self.RunCommandPiped( + ['pkgctl', 'get-hash', + _GetPackageUri(package_name)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + pkgctl_out, pkgctl_err = pkgctl.communicate() + + # Read the expected version from the meta.far Merkel hash file alongside + # the package's FAR. + meta_far_path = os.path.join(os.path.dirname(package_path), 'meta.far') + meta_far_merkel = subprocess.check_output( + [common.GetHostToolPathFromPlatform('merkleroot'), + meta_far_path]).split()[0] + if pkgctl_out != meta_far_merkel: + raise Exception('Hash mismatch for %s after resolve (%s vs %s).' % + (package_name, pkgctl_out, meta_far_merkel)) + + def RunFFXCommand(self, ffx_args, **kwargs): + """Automatically gets the FFX path and runs FFX based on the + arguments provided. Extra args can be added to be used with Popen. + + ffx_args: The arguments for a ffx command. + kwargs: A dictionary of parameters to be passed to subprocess.Popen(). + + Returns a Popen object for the command.""" + command = [self._ffx_path] + ffx_args + return subprocess.Popen(command, **kwargs) |