#!/usr/bin/env vpython3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Implements commands for flashing a Fuchsia device."""
import argparse
import logging
import os
import subprocess
import sys
from typing
import Optional, Tuple
import common
from boot_device
import BootMode, StateTransitionError, boot_device
from common
import get_system_info, find_image_in_sdk, \
register_device_args
from compatible_utils
import get_sdk_hash, running_unattended
from lockfile
import lock
# Flash-file lock. Used to restrict number of flash operations per host.
# File lock should be marked as stale after 15 mins.
_FF_LOCK = os.path.join(
'/tmp',
'flash.lock')
_FF_LOCK_STALE_SECS =
60 *
15
_FF_LOCK_ACQ_TIMEOUT = _FF_LOCK_STALE_SECS
def _get_system_info(target: Optional[str],
serial_num: Optional[str]) -> Tuple[str, str]:
"""Retrieves installed OS version from device.
Args:
target: Target to get system info of.
serial_num: Serial number of device to get system info of.
Returns:
Tuple of strings, containing (product, version number).
"""
if running_unattended():
try:
boot_device(target, BootMode.REGULAR, serial_num)
except (subprocess.CalledProcessError, StateTransitionError):
logging.warning(
'Could not boot device. Assuming in fastboot')
return (
'',
'')
return get_system_info(target)
def _update_required(
os_check,
system_image_dir: Optional[str],
target: Optional[str],
serial_num: Optional[str] =
None) -> Tuple[bool, Optional[str]]:
"""Returns True if a system update is required and path to image dir."""
if os_check ==
'ignore':
return False, system_image_dir
if not system_image_dir:
raise ValueError(
'System image directory must be specified.')
if not os.path.exists(system_image_dir):
logging.warning(
'System image directory does not exist. Assuming it\'s
'
'a product-bundle name and dynamically searching for '
'image directory')
path = find_image_in_sdk(system_image_dir)
if not path:
raise FileNotFoundError(
f
'System image directory {system_image_dir} could not'
'be found')
system_image_dir = path
if (os_check ==
'check'
and get_sdk_hash(system_image_dir) == _get_system_info(
target, serial_num)):
return False, system_image_dir
return True, system_image_dir
def _run_flash_command(system_image_dir: str, target_id: Optional[str]):
"""Helper function for running `ffx target flash`."""
logging.info(
'Flashing %s to %s', system_image_dir, target_id)
# Flash only with a file lock acquired.
# This prevents multiple fastboot binaries from flashing concurrently,
# which should increase the odds of flashing success.
with lock(_FF_LOCK, timeout=_FF_LOCK_ACQ_TIMEOUT):
# The ffx.fastboot.inline_target has negative impact when ffx
# discovering devices in fastboot, so it's inlined here to limit its
# scope. See the discussion in https://fxbug.dev/issues/317228141.
logging.info(
'Flash result %s',
common.run_ffx_command(cmd=(
'target',
'flash',
'-b',
system_image_dir,
'--no-bootloader-reboot'),
target_id=target_id,
configs=[
'ffx.fastboot.inline_target=true'],
capture_output=
True).stdout)
def update(system_image_dir: str,
os_check: str,
target: Optional[str],
serial_num: Optional[str] =
None) ->
None:
"""Conditionally updates target given.
Args:
system_image_dir: string, path to image directory.
os_check: <check|ignore|update>, which decides how to update the device.
target: Node-name string indicating device that should be updated.
serial_num: String of serial number of device that should be updated.
"""
needs_update, actual_image_dir = _update_required(os_check,
system_image_dir, target,
serial_num)
logging.info(
'update_required %s, actual_image_dir %s', needs_update,
actual_image_dir)
if not needs_update:
return
if serial_num:
boot_device(target, BootMode.BOOTLOADER, serial_num)
_run_flash_command(system_image_dir, serial_num)
else:
_run_flash_command(system_image_dir, target)
def register_update_args(arg_parser: argparse.ArgumentParser,
default_os_check: Optional[str] =
'check') ->
None:
"""Register common arguments for device updating."""
serve_args = arg_parser.add_argument_group(
'update',
'device updating arguments')
serve_args.add_argument(
'--system-image-dir',
help=
'Specify the directory that contains the '
'Fuchsia image used to flash the device. Only '
'needs to be specified if "os_check" is not '
'"ignore".')
serve_args.add_argument(
'--serial-num',
default=os.environ.get(
'FUCHSIA_FASTBOOT_SERNUM'),
help=
'Serial number of the device. Should be '
'specified for devices that do not have an image '
'flashed.')
serve_args.add_argument(
'--os-check',
choices=[
'check',
'update',
'ignore'],
default=default_os_check,
help=
'Sets the OS version enforcement policy. If '
'"check", then the deployment process will halt '
'if the target\'s version does
not match.
If '
'"update", then the target device will '
'be reflashed. If "ignore", then the OS version '
'will not be checked.')
def main():
"""Stand-alone function for flashing a device."""
parser = argparse.ArgumentParser()
register_device_args(parser)
register_update_args(parser, default_os_check=
'update')
args = parser.parse_args()
update(args.system_image_dir, args.os_check, args.target_id,
args.serial_num)
if __name__ ==
'__main__':
sys.exit(main())