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 12 13from contextlib import AbstractContextManager 14 15from common import run_ffx_command, IMAGES_ROOT, INTERNAL_IMAGES_ROOT, SDK_ROOT 16from compatible_utils import get_host_arch 17 18 19class FfxEmulator(AbstractContextManager): 20 """A helper for managing emulators.""" 21 # pylint: disable=too-many-branches 22 def __init__(self, args: argparse.Namespace) -> None: 23 if args.product: 24 self._product = args.product 25 else: 26 if get_host_arch() == 'x64': 27 self._product = 'terminal.x64' 28 else: 29 self._product = 'terminal.qemu-arm64' 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 self._device_spec = args.device_spec 47 48 def _everlasting(self) -> bool: 49 return self._node_name == 'fuchsia-everlasting-emulator' 50 51 def __enter__(self) -> str: 52 """Start the emulator. 53 54 Returns: 55 The node name of the emulator. 56 """ 57 logging.info('Starting emulator %s', self._node_name) 58 prod, board = self._product.split('.', 1) 59 image_dir = os.path.join(IMAGES_ROOT, prod, board) 60 if not os.path.isdir(image_dir): 61 image_dir = os.path.join(INTERNAL_IMAGES_ROOT, prod, board) 62 emu_command = ['emu', 'start', image_dir, '--name', self._node_name] 63 if not self._enable_graphics: 64 emu_command.append('-H') 65 if self._hardware_gpu: 66 emu_command.append('--gpu') 67 if self._logs_dir: 68 emu_command.extend( 69 ('-l', os.path.join(self._logs_dir, 'emulator_log'))) 70 if self._with_network: 71 emu_command.extend(['--net', 'tap']) 72 else: 73 emu_command.extend(['--net', 'user']) 74 if self._everlasting(): 75 emu_command.extend(['--reuse-with-check']) 76 if self._device_spec: 77 emu_command.extend(['--device', self._device_spec]) 78 79 # TODO(https://fxbug.dev/99321): remove when ffx has native support 80 # for starting emulator on arm64 host. 81 if get_host_arch() == 'arm64': 82 83 arm64_qemu_dir = os.path.join(SDK_ROOT, 'tools', 'arm64', 84 'qemu_internal') 85 86 # The arm64 emulator binaries are downloaded separately, so add 87 # a symlink to the expected location inside the SDK. 88 if not os.path.isdir(arm64_qemu_dir): 89 os.symlink( 90 os.path.join(SDK_ROOT, '..', '..', 'qemu-linux-arm64'), 91 arm64_qemu_dir) 92 93 # Add the arm64 emulator binaries to the SDK's manifest.json file. 94 sdk_manifest = os.path.join(SDK_ROOT, 'meta', 'manifest.json') 95 with open(sdk_manifest, 'r+') as f: 96 data = json.load(f) 97 for part in data['parts']: 98 if part['meta'] == 'tools/x64/qemu_internal-meta.json': 99 part['meta'] = 'tools/arm64/qemu_internal-meta.json' 100 break 101 f.seek(0) 102 json.dump(data, f) 103 f.truncate() 104 105 # Generate a meta file for the arm64 emulator binaries using its 106 # x64 counterpart. 107 qemu_arm64_meta_file = os.path.join(SDK_ROOT, 'tools', 'arm64', 108 'qemu_internal-meta.json') 109 qemu_x64_meta_file = os.path.join(SDK_ROOT, 'tools', 'x64', 110 'qemu_internal-meta.json') 111 with open(qemu_x64_meta_file) as f: 112 data = str(json.load(f)) 113 qemu_arm64_meta = data.replace(r'tools/x64', 'tools/arm64') 114 with open(qemu_arm64_meta_file, "w+") as f: 115 json.dump(ast.literal_eval(qemu_arm64_meta), f) 116 117 # Always use qemu for arm64 images, no matter it runs on arm64 hosts or 118 # x64 hosts with simulation. 119 if self._product.endswith('arm64'): 120 emu_command.extend(['--engine', 'qemu']) 121 122 run_ffx_command(cmd=emu_command, 123 timeout=310, 124 configs=['emu.start.timeout=300']) 125 126 return self._node_name 127 128 def __exit__(self, exc_type, exc_value, traceback) -> bool: 129 """Shutdown the emulator.""" 130 131 logging.info('Stopping the emulator %s', self._node_name) 132 cmd = ['emu', 'stop', self._node_name] 133 if self._everlasting(): 134 cmd.extend(['--persist']) 135 # The emulator might have shut down unexpectedly, so this command 136 # might fail. 137 run_ffx_command(cmd=cmd, check=False) 138 # Do not suppress exceptions. 139 return False 140