# 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 ast import logging import os import json import random import subprocess from contextlib import AbstractContextManager from common import run_ffx_command, IMAGES_ROOT, SDK_ROOT from compatible_utils import get_host_arch _EMU_COMMAND_RETRIES = 3 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._hardware_gpu = args.hardware_gpu 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) emu_command = ['emu', 'start', image_dir, '--name', self._node_name] if not self._enable_graphics: emu_command.append('-H') if self._hardware_gpu: emu_command.append('--gpu') 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]) # TODO(https://fxbug.dev/99321): remove when ffx has native support # for starting emulator on arm64 host. if get_host_arch() == 'arm64': arm64_qemu_dir = os.path.join(SDK_ROOT, 'tools', 'arm64', 'qemu_internal') # The arm64 emulator binaries are downloaded separately, so add # a symlink to the expected location inside the SDK. if not os.path.isdir(arm64_qemu_dir): os.symlink( os.path.join(SDK_ROOT, '..', '..', 'qemu-linux-arm64'), arm64_qemu_dir) # Add the arm64 emulator binaries to the SDK's manifest.json file. sdk_manifest = os.path.join(SDK_ROOT, 'meta', 'manifest.json') with open(sdk_manifest, 'r+') as f: data = json.load(f) for part in data['parts']: if part['meta'] == 'tools/x64/qemu_internal-meta.json': part['meta'] = 'tools/arm64/qemu_internal-meta.json' break f.seek(0) json.dump(data, f) f.truncate() # Generate a meta file for the arm64 emulator binaries using its # x64 counterpart. qemu_arm64_meta_file = os.path.join(SDK_ROOT, 'tools', 'arm64', 'qemu_internal-meta.json') qemu_x64_meta_file = os.path.join(SDK_ROOT, 'tools', 'x64', 'qemu_internal-meta.json') with open(qemu_x64_meta_file) as f: data = str(json.load(f)) qemu_arm64_meta = data.replace(r'tools/x64', 'tools/arm64') with open(qemu_arm64_meta_file, "w+") as f: json.dump(ast.literal_eval(qemu_arm64_meta), f) emu_command.extend(['--engine', 'qemu']) for i in range(_EMU_COMMAND_RETRIES): # If the ffx daemon fails to establish a connection with # the emulator after 85 seconds, that means the emulator # failed to be brought up and a retry is needed. # TODO(fxb/103540): Remove retry when start up issue is fixed. try: if i > 0: logging.warning( 'Emulator failed to start.') run_ffx_command(cmd=emu_command, timeout=100, configs=['emu.start.timeout=90']) break except (subprocess.TimeoutExpired, subprocess.CalledProcessError): run_ffx_command(cmd=('emu', 'stop')) 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