1# Copyright 2023 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Provide helpers for running Fuchsia's `ffx emu`.""" 5 6import argparse 7import ast 8import logging 9import os 10import json 11import random 12import subprocess 13 14from contextlib import AbstractContextManager 15 16from common import run_ffx_command, IMAGES_ROOT, SDK_ROOT 17from compatible_utils import get_host_arch 18 19_EMU_COMMAND_RETRIES = 3 20 21 22class FfxEmulator(AbstractContextManager): 23 """A helper for managing emulators.""" 24 # pylint: disable=too-many-branches 25 def __init__(self, args: argparse.Namespace) -> None: 26 if args.product: 27 self._product = args.product 28 else: 29 if get_host_arch() == 'x64': 30 self._product = 'terminal.x64' 31 else: 32 self._product = 'terminal.qemu-arm64' 33 34 self._enable_graphics = args.enable_graphics 35 self._hardware_gpu = args.hardware_gpu 36 self._logs_dir = args.logs_dir 37 self._with_network = args.with_network 38 if args.everlasting: 39 # Do not change the name, it will break the logic. 40 # ffx has a prefix-matching logic, so 'fuchsia-emulator' is not 41 # usable to avoid breaking local development workflow. I.e. 42 # developers can create an everlasting emulator and an ephemeral one 43 # without interfering each other. 44 self._node_name = 'fuchsia-everlasting-emulator' 45 assert self._everlasting() 46 else: 47 self._node_name = 'fuchsia-emulator-' + str(random.randint( 48 1, 9999)) 49 self._device_spec = args.device_spec 50 51 def _everlasting(self) -> bool: 52 return self._node_name == 'fuchsia-everlasting-emulator' 53 54 def __enter__(self) -> str: 55 """Start the emulator. 56 57 Returns: 58 The node name of the emulator. 59 """ 60 logging.info('Starting emulator %s', self._node_name) 61 prod, board = self._product.split('.', 1) 62 image_dir = os.path.join(IMAGES_ROOT, prod, board) 63 emu_command = ['emu', 'start', image_dir, '--name', self._node_name] 64 if not self._enable_graphics: 65 emu_command.append('-H') 66 if self._hardware_gpu: 67 emu_command.append('--gpu') 68 if self._logs_dir: 69 emu_command.extend( 70 ('-l', os.path.join(self._logs_dir, 'emulator_log'))) 71 if self._with_network: 72 emu_command.extend(['--net', 'tap']) 73 else: 74 emu_command.extend(['--net', 'user']) 75 if self._everlasting(): 76 emu_command.extend(['--reuse-with-check']) 77 if self._device_spec: 78 emu_command.extend(['--device', self._device_spec]) 79 80 # TODO(https://fxbug.dev/99321): remove when ffx has native support 81 # for starting emulator on arm64 host. 82 if get_host_arch() == 'arm64': 83 84 arm64_qemu_dir = os.path.join(SDK_ROOT, 'tools', 'arm64', 85 'qemu_internal') 86 87 # The arm64 emulator binaries are downloaded separately, so add 88 # a symlink to the expected location inside the SDK. 89 if not os.path.isdir(arm64_qemu_dir): 90 os.symlink( 91 os.path.join(SDK_ROOT, '..', '..', 'qemu-linux-arm64'), 92 arm64_qemu_dir) 93 94 # Add the arm64 emulator binaries to the SDK's manifest.json file. 95 sdk_manifest = os.path.join(SDK_ROOT, 'meta', 'manifest.json') 96 with open(sdk_manifest, 'r+') as f: 97 data = json.load(f) 98 for part in data['parts']: 99 if part['meta'] == 'tools/x64/qemu_internal-meta.json': 100 part['meta'] = 'tools/arm64/qemu_internal-meta.json' 101 break 102 f.seek(0) 103 json.dump(data, f) 104 f.truncate() 105 106 # Generate a meta file for the arm64 emulator binaries using its 107 # x64 counterpart. 108 qemu_arm64_meta_file = os.path.join(SDK_ROOT, 'tools', 'arm64', 109 'qemu_internal-meta.json') 110 qemu_x64_meta_file = os.path.join(SDK_ROOT, 'tools', 'x64', 111 'qemu_internal-meta.json') 112 with open(qemu_x64_meta_file) as f: 113 data = str(json.load(f)) 114 qemu_arm64_meta = data.replace(r'tools/x64', 'tools/arm64') 115 with open(qemu_arm64_meta_file, "w+") as f: 116 json.dump(ast.literal_eval(qemu_arm64_meta), f) 117 emu_command.extend(['--engine', 'qemu']) 118 119 for i in range(_EMU_COMMAND_RETRIES): 120 121 # If the ffx daemon fails to establish a connection with 122 # the emulator after 85 seconds, that means the emulator 123 # failed to be brought up and a retry is needed. 124 # TODO(fxb/103540): Remove retry when start up issue is fixed. 125 try: 126 if i > 0: 127 logging.warning( 128 'Emulator failed to start.') 129 run_ffx_command(cmd=emu_command, 130 timeout=100, 131 configs=['emu.start.timeout=90']) 132 break 133 except (subprocess.TimeoutExpired, subprocess.CalledProcessError): 134 run_ffx_command(cmd=('emu', 'stop')) 135 136 return self._node_name 137 138 def __exit__(self, exc_type, exc_value, traceback) -> bool: 139 """Shutdown the emulator.""" 140 141 logging.info('Stopping the emulator %s', self._node_name) 142 cmd = ['emu', 'stop', self._node_name] 143 if self._everlasting(): 144 cmd.extend(['--persist']) 145 # The emulator might have shut down unexpectedly, so this command 146 # might fail. 147 run_ffx_command(cmd=cmd, check=False) 148 # Do not suppress exceptions. 149 return False 150