• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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