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