chromium/build/fuchsia/test/ffx_emulator.py

# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provide helpers for running Fuchsia's `ffx emu`."""

import argparse
import logging
import os
import random

from contextlib import AbstractContextManager

import monitors

from common import run_ffx_command, IMAGES_ROOT, INTERNAL_IMAGES_ROOT, \
                   DIR_SRC_ROOT
from compatible_utils import get_host_arch


class FfxEmulator(AbstractContextManager):
    """A helper for managing emulators."""
    # pylint: disable=too-many-branches
    def __init__(self, args: argparse.Namespace) -> None:
        if args.product:
            self._product = args.product
        else:
            if get_host_arch() == 'x64':
                self._product = 'terminal.x64'
            else:
                self._product = 'terminal.qemu-arm64'

        self._enable_graphics = args.enable_graphics
        self._logs_dir = args.logs_dir
        self._with_network = args.with_network
        if args.everlasting:
            # Do not change the name, it will break the logic.
            # ffx has a prefix-matching logic, so 'fuchsia-emulator' is not
            # usable to avoid breaking local development workflow. I.e.
            # developers can create an everlasting emulator and an ephemeral one
            # without interfering each other.
            self._node_name = 'fuchsia-everlasting-emulator'
            assert self._everlasting()
        else:
            self._node_name = 'fuchsia-emulator-' + str(random.randint(
                1, 9999))
        self._device_spec = args.device_spec

    def _everlasting(self) -> bool:
        return self._node_name == 'fuchsia-everlasting-emulator'

    def __enter__(self) -> str:
        """Start the emulator.

        Returns:
            The node name of the emulator.
        """
        logging.info('Starting emulator %s', self._node_name)
        prod, board = self._product.split('.', 1)
        image_dir = os.path.join(IMAGES_ROOT, prod, board)
        if not os.path.isdir(image_dir):
            image_dir = os.path.join(INTERNAL_IMAGES_ROOT, prod, board)
        emu_command = ['emu', 'start', image_dir, '--name', self._node_name]
        configs = ['emu.start.timeout=300']
        if not self._enable_graphics:
            emu_command.append('-H')
        if self._logs_dir:
            emu_command.extend(
                ('-l', os.path.join(self._logs_dir, 'emulator_log')))
        if self._with_network:
            emu_command.extend(['--net', 'tap'])
        else:
            emu_command.extend(['--net', 'user'])
        if self._everlasting():
            emu_command.extend(['--reuse-with-check'])
        if self._device_spec:
            emu_command.extend(['--device', self._device_spec])

        # fuchsia-sdk does not carry arm64 qemu binaries, so use overrides to
        # allow it using the qemu-arm64 being downloaded separately.
        if get_host_arch() == 'arm64':
            configs.append(
                'sdk.overrides.qemu_internal=' +
                os.path.join(DIR_SRC_ROOT, 'third_party', 'qemu-linux-arm64',
                             'bin', 'qemu-system-aarch64'))

        # Always use qemu for arm64 images, no matter it runs on arm64 hosts or
        # x64 hosts with simulation.
        if self._product.endswith('arm64'):
            emu_command.extend(['--engine', 'qemu'])

        with monitors.time_consumption('emulator', 'startup_time'):
            run_ffx_command(cmd=emu_command, timeout=310, configs=configs)

        return self._node_name

    def __exit__(self, exc_type, exc_value, traceback) -> bool:
        """Shutdown the emulator."""

        logging.info('Stopping the emulator %s', self._node_name)
        cmd = ['emu', 'stop', self._node_name]
        if self._everlasting():
            cmd.extend(['--persist'])
        # The emulator might have shut down unexpectedly, so this command
        # might fail.
        run_ffx_command(cmd=cmd, check=False)
        # Do not suppress exceptions.
        return False