• 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 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