• 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 get_hash_from_sdk, get_system_info, run_ffx_command, \
17                   IMAGES_ROOT, SDK_ROOT
18from compatible_utils import get_host_arch
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:
27            self._product = args.product
28        else:
29            self._product = '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    def _everlasting(self) -> bool:
48        return self._node_name == 'fuchsia-everlasting-emulator'
49
50    def _start_emulator(self) -> None:
51        """Start the emulator."""
52        logging.info('Starting emulator %s', self._node_name)
53        prod, board = self._product.split('.', 1)
54        image_dir = os.path.join(IMAGES_ROOT, prod, board)
55        emu_command = ['emu', 'start', image_dir, '--name', self._node_name]
56        if not self._enable_graphics:
57            emu_command.append('-H')
58        if self._hardware_gpu:
59            emu_command.append('--gpu')
60        if self._logs_dir:
61            emu_command.extend(
62                ('-l', os.path.join(self._logs_dir, 'emulator_log')))
63        if self._with_network:
64            emu_command.extend(('--net', 'tap'))
65        else:
66            emu_command.extend(('--net', 'user'))
67
68        # TODO(https://crbug.com/1336776): remove when ffx has native support
69        # for starting emulator on arm64 host.
70        if get_host_arch() == 'arm64':
71
72            arm64_qemu_dir = os.path.join(SDK_ROOT, 'tools', 'arm64',
73                                          'qemu_internal')
74
75            # The arm64 emulator binaries are downloaded separately, so add
76            # a symlink to the expected location inside the SDK.
77            if not os.path.isdir(arm64_qemu_dir):
78                os.symlink(
79                    os.path.join(SDK_ROOT, '..', '..', 'qemu-linux-arm64'),
80                    arm64_qemu_dir)
81
82            # Add the arm64 emulator binaries to the SDK's manifest.json file.
83            sdk_manifest = os.path.join(SDK_ROOT, 'meta', 'manifest.json')
84            with open(sdk_manifest, 'r+') as f:
85                data = json.load(f)
86                for part in data['parts']:
87                    if part['meta'] == 'tools/x64/qemu_internal-meta.json':
88                        part['meta'] = 'tools/arm64/qemu_internal-meta.json'
89                        break
90                f.seek(0)
91                json.dump(data, f)
92                f.truncate()
93
94            # Generate a meta file for the arm64 emulator binaries using its
95            # x64 counterpart.
96            qemu_arm64_meta_file = os.path.join(SDK_ROOT, 'tools', 'arm64',
97                                                'qemu_internal-meta.json')
98            qemu_x64_meta_file = os.path.join(SDK_ROOT, 'tools', 'x64',
99                                              'qemu_internal-meta.json')
100            with open(qemu_x64_meta_file) as f:
101                data = str(json.load(f))
102            qemu_arm64_meta = data.replace(r'tools/x64', 'tools/arm64')
103            with open(qemu_arm64_meta_file, "w+") as f:
104                json.dump(ast.literal_eval(qemu_arm64_meta), f)
105            emu_command.extend(['--engine', 'qemu'])
106
107        for i in range(_EMU_COMMAND_RETRIES):
108
109            # If the ffx daemon fails to establish a connection with
110            # the emulator after 85 seconds, that means the emulator
111            # failed to be brought up and a retry is needed.
112            # TODO(fxb/103540): Remove retry when start up issue is fixed.
113            try:
114                # TODO(fxb/125872): Debug is added for examining flakiness.
115                configs = ['emu.start.timeout=90']
116                if i > 0:
117                    logging.warning(
118                        'Emulator failed to start.')
119                run_ffx_command(cmd=emu_command, timeout=100, configs=configs)
120                break
121            except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
122                run_ffx_command(cmd=('emu', 'stop'))
123
124    def _shutdown_emulator(self) -> None:
125        """Shutdown the emulator."""
126
127        logging.info('Stopping the emulator %s', self._node_name)
128        # The emulator might have shut down unexpectedly, so this command
129        # might fail.
130        run_ffx_command(cmd=('emu', 'stop', self._node_name), check=False)
131
132    def __enter__(self) -> str:
133        """Start the emulator if necessary.
134
135        Returns:
136            The node name of the emulator.
137        """
138
139        if self._everlasting():
140            sdk_hash = get_hash_from_sdk()
141            sys_info = get_system_info(self._node_name)
142            if sdk_hash == sys_info:
143                return self._node_name
144            logging.info(
145                ('The emulator version [%s] does not match the SDK [%s], '
146                 'updating...'), sys_info, sdk_hash)
147
148        self._start_emulator()
149        return self._node_name
150
151    def __exit__(self, exc_type, exc_value, traceback) -> bool:
152        """Shutdown the emulator if necessary."""
153
154        if not self._everlasting():
155            self._shutdown_emulator()
156        # Do not suppress exceptions.
157        return False
158