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 check_ssh_config_file, find_image_in_sdk, get_system_info, \ 17 run_ffx_command, SDK_ROOT 18from compatible_utils import get_host_arch, get_sdk_hash 19 20_EMU_COMMAND_RETRIES = 3 21 22 23class FfxEmulator(AbstractContextManager): 24 """A helper for managing emulators.""" 25 def __init__(self, args: argparse.Namespace) -> None: 26 if args.product_bundle: 27 self._product_bundle = args.product_bundle 28 else: 29 self._product_bundle = 'terminal.qemu-' + get_host_arch() 30 31 self._enable_graphics = args.enable_graphics 32 self._hardware_gpu = args.hardware_gpu 33 self._logs_dir = args.logs_dir 34 self._with_network = args.with_network 35 if args.everlasting: 36 # Do not change the name, it will break the logic. 37 # ffx has a prefix-matching logic, so 'fuchsia-emulator' is not 38 # usable to avoid breaking local development workflow. I.e. 39 # developers can create an everlasting emulator and an ephemeral one 40 # without interfering each other. 41 self._node_name = 'fuchsia-everlasting-emulator' 42 assert self._everlasting() 43 else: 44 self._node_name = 'fuchsia-emulator-' + str(random.randint( 45 1, 9999)) 46 47 # Set the download path parallel to Fuchsia SDK directory 48 # permanently so that scripts can always find the product bundles. 49 run_ffx_command(('config', 'set', 'pbms.storage.path', 50 os.path.join(SDK_ROOT, os.pardir, 'images'))) 51 52 def _everlasting(self) -> bool: 53 return self._node_name == 'fuchsia-everlasting-emulator' 54 55 def _start_emulator(self) -> None: 56 """Start the emulator.""" 57 logging.info('Starting emulator %s', self._node_name) 58 check_ssh_config_file() 59 emu_command = [ 60 'emu', 'start', self._product_bundle, '--name', self._node_name 61 ] 62 if not self._enable_graphics: 63 emu_command.append('-H') 64 if self._hardware_gpu: 65 emu_command.append('--gpu') 66 if self._logs_dir: 67 emu_command.extend( 68 ('-l', os.path.join(self._logs_dir, 'emulator_log'))) 69 if self._with_network: 70 emu_command.extend(('--net', 'tap')) 71 72 # TODO(https://crbug.com/1336776): remove when ffx has native support 73 # for starting emulator on arm64 host. 74 if get_host_arch() == 'arm64': 75 76 arm64_qemu_dir = os.path.join(SDK_ROOT, 'tools', 'arm64', 77 'qemu_internal') 78 79 # The arm64 emulator binaries are downloaded separately, so add 80 # a symlink to the expected location inside the SDK. 81 if not os.path.isdir(arm64_qemu_dir): 82 os.symlink( 83 os.path.join(SDK_ROOT, '..', '..', 'qemu-linux-arm64'), 84 arm64_qemu_dir) 85 86 # Add the arm64 emulator binaries to the SDK's manifest.json file. 87 sdk_manifest = os.path.join(SDK_ROOT, 'meta', 'manifest.json') 88 with open(sdk_manifest, 'r+') as f: 89 data = json.load(f) 90 for part in data['parts']: 91 if part['meta'] == 'tools/x64/qemu_internal-meta.json': 92 part['meta'] = 'tools/arm64/qemu_internal-meta.json' 93 break 94 f.seek(0) 95 json.dump(data, f) 96 f.truncate() 97 98 # Generate a meta file for the arm64 emulator binaries using its 99 # x64 counterpart. 100 qemu_arm64_meta_file = os.path.join(SDK_ROOT, 'tools', 'arm64', 101 'qemu_internal-meta.json') 102 qemu_x64_meta_file = os.path.join(SDK_ROOT, 'tools', 'x64', 103 'qemu_internal-meta.json') 104 with open(qemu_x64_meta_file) as f: 105 data = str(json.load(f)) 106 qemu_arm64_meta = data.replace(r'tools/x64', 'tools/arm64') 107 with open(qemu_arm64_meta_file, "w+") as f: 108 json.dump(ast.literal_eval(qemu_arm64_meta), f) 109 emu_command.extend(['--engine', 'qemu']) 110 111 for i in range(_EMU_COMMAND_RETRIES): 112 113 # If the ffx daemon fails to establish a connection with 114 # the emulator after 85 seconds, that means the emulator 115 # failed to be brought up and a retry is needed. 116 # TODO(fxb/103540): Remove retry when start up issue is fixed. 117 try: 118 # TODO(fxb/125872): Debug is added for examining flakiness. 119 configs = ['emu.start.timeout=90'] 120 if i > 0: 121 logging.warning( 122 'Emulator failed to start. Turning on debug') 123 configs.append('log.level=debug') 124 run_ffx_command(emu_command, timeout=85, configs=configs) 125 break 126 except (subprocess.TimeoutExpired, subprocess.CalledProcessError): 127 run_ffx_command(('emu', 'stop')) 128 129 def _shutdown_emulator(self) -> None: 130 """Shutdown the emulator.""" 131 132 logging.info('Stopping the emulator %s', self._node_name) 133 # The emulator might have shut down unexpectedly, so this command 134 # might fail. 135 run_ffx_command(('emu', 'stop', self._node_name), check=False) 136 137 def __enter__(self) -> str: 138 """Start the emulator if necessary. 139 140 Returns: 141 The node name of the emulator. 142 """ 143 144 if self._everlasting(): 145 sdk_hash = get_sdk_hash(find_image_in_sdk(self._product_bundle)) 146 sys_info = get_system_info(self._node_name) 147 if sdk_hash == sys_info: 148 return self._node_name 149 logging.info( 150 ('The emulator version [%s] does not match the SDK [%s], ' 151 'updating...'), sys_info, sdk_hash) 152 153 self._start_emulator() 154 return self._node_name 155 156 def __exit__(self, exc_type, exc_value, traceback) -> bool: 157 """Shutdown the emulator if necessary.""" 158 159 if not self._everlasting(): 160 self._shutdown_emulator() 161 # Do not suppress exceptions. 162 return False 163