# 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 common import json import logging import os import shutil import subprocess import tempfile import time # Maximum amount of time to block while waiting for "pm serve" to come up. _PM_SERVE_LISTEN_TIMEOUT_SECS = 10 # Amount of time to sleep in between busywaits for "pm serve"'s port file. _PM_SERVE_POLL_INTERVAL = 0.1 _MANAGED_REPO_NAME = 'chromium-test-package-server' class PkgRepo(object): """Abstract interface for a repository used to serve packages to devices.""" def __init__(self): pass def PublishPackage(self, package_path): pm_tool = common.GetHostToolPathFromPlatform('pm') # Flags for `pm publish`: # https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/src/sys/pkg/bin/pm/cmd/pm/publish/publish.go # https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/src/sys/pkg/bin/pm/repo/config.go # -a: Publish archived package # -f : Path to packages # -r : Path to repository # -vt: Repo versioning based on time rather than monotonic version number # increase # -v: Verbose output subprocess.check_call([ pm_tool, 'publish', '-a', '-f', package_path, '-r', self.GetPath(), '-vt', '-v' ], stderr=subprocess.STDOUT) def GetPath(self): pass class ManagedPkgRepo(PkgRepo): """Creates and serves packages from an ephemeral repository.""" def __init__(self, target): super(ManagedPkgRepo, self).__init__() self._with_count = 0 self._target = target self._pkg_root = tempfile.mkdtemp() pm_tool = common.GetHostToolPathFromPlatform('pm') subprocess.check_call([pm_tool, 'newrepo', '-repo', self._pkg_root]) logging.debug('Creating and serving temporary package root: {}.'.format( self._pkg_root)) with tempfile.NamedTemporaryFile() as pm_port_file: # Flags for `pm serve`: # https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/src/sys/pkg/bin/pm/cmd/pm/serve/serve.go self._pm_serve_task = subprocess.Popen([ pm_tool, 'serve', '-d', os.path.join(self._pkg_root, 'repository'), '-c', '2', # Use config.json format v2, the default for pkgctl. '-q', # Don't log transfer activity. '-l', ':0', # Bind to ephemeral port. '-f', pm_port_file.name # Publish port number to |pm_port_file|. ]) # Busywait until 'pm serve' starts the server and publishes its port to # a temporary file. timeout = time.time() + _PM_SERVE_LISTEN_TIMEOUT_SECS serve_port = None while not serve_port: if time.time() > timeout: raise Exception('Timeout waiting for \'pm serve\' to publish its port.') with open(pm_port_file.name, 'r', encoding='utf8') as serve_port_file: serve_port = serve_port_file.read() time.sleep(_PM_SERVE_POLL_INTERVAL) serve_port = int(serve_port) logging.debug('pm serve is active on port {}.'.format(serve_port)) remote_port = common.ConnectPortForwardingTask(target, serve_port, 0) self._RegisterPkgRepository(self._pkg_root, remote_port) def __enter__(self): self._with_count += 1 return self def __exit__(self, type, value, tb): # Allows the repository to delete itself when it leaves the scope of a 'with' block. self._with_count -= 1 if self._with_count > 0: return self._UnregisterPkgRepository() self._pm_serve_task.kill() self._pm_serve_task = None logging.info('Cleaning up package root: ' + self._pkg_root) shutil.rmtree(self._pkg_root) self._pkg_root = None def GetPath(self): return self._pkg_root def _RegisterPkgRepository(self, tuf_repo, remote_port): """Configures a device to use a local TUF repository as an installation source for packages. |tuf_repo|: The host filesystem path to the TUF repository. |remote_port|: The reverse-forwarded port used to connect to instance of `pm serve` that is serving the contents of |tuf_repo|.""" # Extract the public signing key for inclusion in the config file. root_keys = [] root_json_path = os.path.join(tuf_repo, 'repository', 'root.json') root_json = json.load(open(root_json_path, 'r')) for root_key_id in root_json['signed']['roles']['root']['keyids']: root_keys.append({ 'type': root_json['signed']['keys'][root_key_id]['keytype'], 'value': root_json['signed']['keys'][root_key_id]['keyval']['public'] }) # "pm serve" can automatically generate a "config.json" file at query time, # but the file is unusable because it specifies URLs with port # numbers that are unreachable from across the port forwarding boundary. # So instead, we generate our own config file with the forwarded port # numbers instead. config_file = open(os.path.join(tuf_repo, 'repository', 'repo_config.json'), 'w') json.dump( { 'repo_url': "fuchsia-pkg://%s" % _MANAGED_REPO_NAME, 'root_keys': root_keys, 'mirrors': [{ "mirror_url": "http://127.0.0.1:%d" % remote_port, "subscribe": True }], 'root_threshold': 1, 'root_version': 1 }, config_file) config_file.close() # Register the repo. return_code = self._target.RunCommand([ ('pkgctl repo rm fuchsia-pkg://%s; ' + 'pkgctl repo add url http://127.0.0.1:%d/repo_config.json; ') % (_MANAGED_REPO_NAME, remote_port) ]) if return_code != 0: raise Exception('Error code %d when running pkgctl repo add.' % return_code) rule_template = """'{"version":"1","content":[{"host_match":"fuchsia.com","host_replacement":"%s","path_prefix_match":"/","path_prefix_replacement":"/"}]}'""" return_code = self._target.RunCommand([ ('pkgctl rule replace json %s') % (rule_template % (_MANAGED_REPO_NAME)) ]) if return_code != 0: raise Exception('Error code %d when running pkgctl rule replace.' % return_code) def _UnregisterPkgRepository(self): """Unregisters the package repository.""" logging.debug('Unregistering package repository.') self._target.RunCommand( ['pkgctl', 'repo', 'rm', 'fuchsia-pkg://%s' % (_MANAGED_REPO_NAME)]) # Re-enable 'devhost' repo if it's present. This is useful for devices that # were booted with 'fx serve'. self._target.RunCommand([ 'pkgctl', 'rule', 'replace', 'json', """'{"version":"1","content":[{"host_match":"fuchsia.com","host_replacement":"devhost","path_prefix_match":"/","path_prefix_replacement":"/"}]}'""" ], silent=True) class ExternalPkgRepo(PkgRepo): """Publishes packages to a package repository located and served externally (ie. located under a Fuchsia build directory and served by "fx serve".""" def __init__(self, pkg_root, symbol_root): super(PkgRepo, self).__init__() self._pkg_root = pkg_root self._symbol_root = symbol_root logging.info('Using existing package root: {}'.format(pkg_root)) logging.info( 'ATTENTION: This will not start a package server. Please run "fx serve" manually.' ) def GetPath(self): return self._pkg_root def PublishPackage(self, package_path): super(ExternalPkgRepo, self).PublishPackage(package_path) self._InstallSymbols(os.path.join(os.path.dirname(package_path), 'ids.txt')) def __enter__(self): return self def __exit__(self, type, value, tb): pass def _InstallSymbols(self, package_path): """Installs debug symbols for a packageinto the GDB-standard symbol directory located at |self.symbol_root|.""" ids_txt_path = os.path.join(os.path.dirname(package_path), 'ids.txt') for entry in open(ids_txt_path, 'r'): build_id, binary_relpath = entry.strip().split(' ') binary_abspath = os.path.abspath( os.path.join(os.path.dirname(ids_txt_path), binary_relpath)) symbol_dir = os.path.join(self._symbol_root, build_id[:2]) symbol_file = os.path.join(symbol_dir, build_id[2:] + '.debug') if not os.path.exists(symbol_dir): os.makedirs(symbol_dir) if os.path.islink(symbol_file) or os.path.exists(symbol_file): # Clobber the existing entry to ensure that the symlink's target is # up to date. os.unlink(symbol_file) os.symlink(os.path.relpath(binary_abspath, symbol_dir), symbol_file)