xref: /aosp_15_r20/external/angle/build/fuchsia/test/flash_device.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1#!/usr/bin/env vpython3
2# Copyright 2022 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Implements commands for flashing a Fuchsia device."""
6
7import argparse
8import logging
9import os
10import subprocess
11import sys
12
13from typing import Optional, Tuple
14
15import common
16from boot_device import BootMode, StateTransitionError, boot_device
17from common import get_system_info, find_image_in_sdk, \
18                   register_device_args
19from compatible_utils import get_sdk_hash, running_unattended
20from lockfile import lock
21
22# Flash-file lock. Used to restrict number of flash operations per host.
23# File lock should be marked as stale after 15 mins.
24_FF_LOCK = os.path.join('/tmp', 'flash.lock')
25_FF_LOCK_STALE_SECS = 60 * 15
26_FF_LOCK_ACQ_TIMEOUT = _FF_LOCK_STALE_SECS
27
28
29def _get_system_info(target: Optional[str],
30                     serial_num: Optional[str]) -> Tuple[str, str]:
31    """Retrieves installed OS version from device.
32
33    Args:
34        target: Target to get system info of.
35        serial_num: Serial number of device to get system info of.
36    Returns:
37        Tuple of strings, containing (product, version number).
38    """
39
40    if running_unattended():
41        try:
42            boot_device(target, BootMode.REGULAR, serial_num)
43        except (subprocess.CalledProcessError, StateTransitionError):
44            logging.warning('Could not boot device. Assuming in fastboot')
45            return ('', '')
46
47    return get_system_info(target)
48
49
50def _update_required(
51        os_check,
52        system_image_dir: Optional[str],
53        target: Optional[str],
54        serial_num: Optional[str] = None) -> Tuple[bool, Optional[str]]:
55    """Returns True if a system update is required and path to image dir."""
56
57    if os_check == 'ignore':
58        return False, system_image_dir
59    if not system_image_dir:
60        raise ValueError('System image directory must be specified.')
61    if not os.path.exists(system_image_dir):
62        logging.warning(
63            'System image directory does not exist. Assuming it\'s '
64            'a product-bundle name and dynamically searching for '
65            'image directory')
66        path = find_image_in_sdk(system_image_dir)
67        if not path:
68            raise FileNotFoundError(
69                f'System image directory {system_image_dir} could not'
70                'be found')
71        system_image_dir = path
72    if (os_check == 'check'
73            and get_sdk_hash(system_image_dir) == _get_system_info(
74                target, serial_num)):
75        return False, system_image_dir
76    return True, system_image_dir
77
78
79def _run_flash_command(system_image_dir: str, target_id: Optional[str]):
80    """Helper function for running `ffx target flash`."""
81    logging.info('Flashing %s to %s', system_image_dir, target_id)
82    # Flash only with a file lock acquired.
83    # This prevents multiple fastboot binaries from flashing concurrently,
84    # which should increase the odds of flashing success.
85    with lock(_FF_LOCK, timeout=_FF_LOCK_ACQ_TIMEOUT):
86        # The ffx.fastboot.inline_target has negative impact when ffx
87        # discovering devices in fastboot, so it's inlined here to limit its
88        # scope. See the discussion in https://fxbug.dev/issues/317228141.
89        logging.info(
90            'Flash result %s',
91            common.run_ffx_command(cmd=('target', 'flash', '-b',
92                                        system_image_dir,
93                                        '--no-bootloader-reboot'),
94                                   target_id=target_id,
95                                   configs=['ffx.fastboot.inline_target=true'],
96                                   capture_output=True).stdout)
97
98
99def update(system_image_dir: str,
100           os_check: str,
101           target: Optional[str],
102           serial_num: Optional[str] = None) -> None:
103    """Conditionally updates target given.
104
105    Args:
106        system_image_dir: string, path to image directory.
107        os_check: <check|ignore|update>, which decides how to update the device.
108        target: Node-name string indicating device that should be updated.
109        serial_num: String of serial number of device that should be updated.
110    """
111    needs_update, actual_image_dir = _update_required(os_check,
112                                                      system_image_dir, target,
113                                                      serial_num)
114    logging.info('update_required %s, actual_image_dir %s', needs_update,
115                 actual_image_dir)
116    if not needs_update:
117        return
118    if serial_num:
119        boot_device(target, BootMode.BOOTLOADER, serial_num)
120        _run_flash_command(system_image_dir, serial_num)
121    else:
122        _run_flash_command(system_image_dir, target)
123
124
125def register_update_args(arg_parser: argparse.ArgumentParser,
126                         default_os_check: Optional[str] = 'check') -> None:
127    """Register common arguments for device updating."""
128    serve_args = arg_parser.add_argument_group('update',
129                                               'device updating arguments')
130    serve_args.add_argument('--system-image-dir',
131                            help='Specify the directory that contains the '
132                            'Fuchsia image used to flash the device. Only '
133                            'needs to be specified if "os_check" is not '
134                            '"ignore".')
135    serve_args.add_argument('--serial-num',
136                            default=os.environ.get('FUCHSIA_FASTBOOT_SERNUM'),
137                            help='Serial number of the device. Should be '
138                            'specified for devices that do not have an image '
139                            'flashed.')
140    serve_args.add_argument('--os-check',
141                            choices=['check', 'update', 'ignore'],
142                            default=default_os_check,
143                            help='Sets the OS version enforcement policy. If '
144                            '"check", then the deployment process will halt '
145                            'if the target\'s version does not match. If '
146                            '"update", then the target device will '
147                            'be reflashed. If "ignore", then the OS version '
148                            'will not be checked.')
149
150
151def main():
152    """Stand-alone function for flashing a device."""
153    parser = argparse.ArgumentParser()
154    register_device_args(parser)
155    register_update_args(parser, default_os_check='update')
156    args = parser.parse_args()
157    update(args.system_image_dir, args.os_check, args.target_id,
158           args.serial_num)
159
160
161if __name__ == '__main__':
162    sys.exit(main())
163