xref: /aosp_15_r20/external/cronet/build/fuchsia/test/ffx_emulator.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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