# Copyright 2022 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Implements commands for serving a TUF repository.""" import argparse import contextlib import json import logging from typing import Iterator, Optional import monitors from common import run_ffx_command, REPO_ALIAS _REPO_NAME = 'chromium-test-package-server' def _stop_serving(repo_name: str, target: Optional[str]) -> None: """Stop serving a repository.""" # Attempt to clean up. with monitors.time_consumption('repository', 'deregister'): run_ffx_command( cmd=['target', 'repository', 'deregister', '-r', repo_name], target_id=target, check=False) with monitors.time_consumption('repository', 'stop'): run_ffx_command(cmd=['repository', 'server', 'stop', repo_name], check=False) def _start_serving(repo_dir: str, repo_name: str, target: Optional[str]) -> None: """Start serving a repository to a target device. Args: repo_dir: directory the repository is served from. repo_name: repository name. target: Fuchsia device the repository is served to. """ cmd = [ 'repository', 'server', 'start', '--background', '--address', '[::]:0', '--repository', repo_name, '--repo-path', repo_dir, '--no-device' ] with monitors.time_consumption('repository', 'start'): start_cmd = run_ffx_command(cmd=cmd, check=False) logging.warning('ffx repository server start returns %d: %s %s', start_cmd.returncode, start_cmd.stderr, start_cmd.stdout) _assert_server_running(repo_name) cmd = [ 'target', 'repository', 'register', '-r', repo_name, '--alias', REPO_ALIAS ] with monitors.time_consumption('repository', 'register'): run_ffx_command(cmd=cmd, target_id=target) def _assert_server_running(repo_name: str) -> None: """Raises RuntimeError if the repository server is not running.""" with monitors.time_consumption('repository', 'list'): list_cmd = run_ffx_command(cmd=[ '--machine', 'json', 'repository', 'server', 'list', '--name', repo_name ], check=False, capture_output=True) try: response = json.loads(list_cmd.stdout.strip()) if 'ok' in response and response['ok']['data']: if response['ok']['data'][0]['name'] != repo_name: raise RuntimeError( 'Repository server %s is not running. Output: %s stderr: %s' % (repo_name, list_cmd.stdout, list_cmd.stderr)) return except json.decoder.JSONDecodeError as error: # Log the json parsing error, but don't raise an exception since it # does not have the full context of the error. logging.error('Unexpected json string: %s, exception: %s, stderr: %s', list_cmd.stdout, error, list_cmd.stderr) raise RuntimeError( 'Repository server %s is not running. Output: %s stderr: %s' % (repo_name, list_cmd.stdout, list_cmd.stderr)) def register_serve_args(arg_parser: argparse.ArgumentParser) -> None: """Register common arguments for repository serving.""" serve_args = arg_parser.add_argument_group('serve', 'repo serving arguments') serve_args.add_argument('--serve-repo', dest='repo', help='Directory the repository is served from.') serve_args.add_argument('--repo-name', default=_REPO_NAME, help='Name of the repository.') @contextlib.contextmanager def serve_repository(args: argparse.Namespace) -> Iterator[None]: """Context manager for serving a repository.""" _start_serving(args.repo, args.repo_name, args.target_id) try: yield None finally: _stop_serving(args.repo_name, args.target_id)