diff options
Diffstat (limited to 'catapult/devil/devil/android/fastboot_utils.py')
-rw-r--r-- | catapult/devil/devil/android/fastboot_utils.py | 282 |
1 files changed, 200 insertions, 82 deletions
diff --git a/catapult/devil/devil/android/fastboot_utils.py b/catapult/devil/devil/android/fastboot_utils.py index 3621d7fb..d8ca7d20 100644 --- a/catapult/devil/devil/android/fastboot_utils.py +++ b/catapult/devil/devil/android/fastboot_utils.py @@ -1,7 +1,6 @@ # Copyright 2015 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. - """Provides a variety of device interactions based on fastboot.""" # pylint: disable=unused-argument @@ -14,6 +13,7 @@ import re from devil.android import decorators from devil.android import device_errors +from devil.android import device_utils from devil.android.sdk import fastboot from devil.utils import timeout_retry @@ -23,58 +23,62 @@ _DEFAULT_TIMEOUT = 30 _DEFAULT_RETRIES = 3 _FASTBOOT_REBOOT_TIMEOUT = 10 * _DEFAULT_TIMEOUT _KNOWN_PARTITIONS = collections.OrderedDict([ - ('bootloader', {'image': 'bootloader*.img', 'restart': True}), - ('radio', {'image': 'radio*.img', 'restart': True}), - ('boot', {'image': 'boot.img'}), - ('recovery', {'image': 'recovery.img'}), - ('system', {'image': 'system.img'}), - ('userdata', {'image': 'userdata.img', 'wipe_only': True}), - ('cache', {'image': 'cache.img', 'wipe_only': True}), - ('vendor', {'image': 'vendor*.img', 'optional': True}), - ]) + ('bootloader', { + 'image': 'bootloader*.img', + 'restart': True + }), + ('radio', { + 'image': 'radio*.img', + 'restart': True + }), + ('boot', { + 'image': 'boot.img' + }), + # recovery.img moved into boot.img for A/B devices. See: + # https://source.android.com/devices/tech/ota/ab/ab_implement#recovery + ('recovery', { + 'image': 'recovery.img', + 'optional': lambda fu: fu.supports_ab + }), + ('system', { + 'image': 'system.img' + }), + ('userdata', { + 'image': 'userdata.img', + 'wipe_only': True + }), + # cache.img deprecated for A/B devices. See: + # https://source.android.com/devices/tech/ota/ab/ab_implement#cache + ('cache', { + 'image': 'cache.img', + 'wipe_only': True, + 'optional': lambda fu: fu.supports_ab + }), + ('vendor', { + 'image': 'vendor*.img', + 'optional': lambda _: True + }), + ('dtbo', { + 'image': 'dtbo.img', + 'optional': lambda fu: not fu.requires_dtbo + }), + ('vbmeta', { + 'image': 'vbmeta.img', + 'optional': lambda fu: not fu.requires_vbmeta + }), +]) ALL_PARTITIONS = _KNOWN_PARTITIONS.keys() -def _FindAndVerifyPartitionsAndImages(partitions, directory): - """Validate partitions and images. - - Validate all partition names and partition directories. Cannot stop mid - flash so its important to validate everything first. - - Args: - Partitions: partitions to be tested. - directory: directory containing the images. - - Returns: - Dictionary with exact partition, image name mapping. - """ - - files = os.listdir(directory) - return_dict = collections.OrderedDict() - - def find_file(pattern): - for filename in files: - if fnmatch.fnmatch(filename, pattern): - return os.path.join(directory, filename) - return None - for partition in partitions: - partition_info = _KNOWN_PARTITIONS[partition] - image_file = find_file(partition_info['image']) - if image_file: - return_dict[partition] = image_file - elif not partition_info.get('optional'): - raise device_errors.FastbootCommandFailedError( - 'Failed to flash device. Could not find image for %s.', - partition_info['image']) - return return_dict - - class FastbootUtils(object): _FASTBOOT_WAIT_TIME = 1 _BOARD_VERIFICATION_FILE = 'android-info.txt' - def __init__(self, device, fastbooter=None, default_timeout=_DEFAULT_TIMEOUT, + def __init__(self, + device=None, + fastbooter=None, + default_timeout=_DEFAULT_TIMEOUT, default_retries=_DEFAULT_RETRIES): """FastbootUtils constructor. @@ -83,23 +87,99 @@ class FastbootUtils(object): fastboot.FlashDevice('/path/to/build/directory') Args: - device: A DeviceUtils instance. - fastbooter: Optional fastboot object. If none is passed, one will - be created. + device: A DeviceUtils instance. Optional if a Fastboot instance was + passed. + fastbooter: A fastboot.Fastboot instance. Optional if a DeviceUtils + instance was passed. default_timeout: An integer containing the default number of seconds to wait for an operation to complete if no explicit value is provided. default_retries: An integer containing the default number or times an operation should be retried on failure if no explicit value is provided. """ - self._device = device - self._board = device.product_board - self._serial = str(device) - self._default_timeout = default_timeout - self._default_retries = default_retries + if not device and not fastbooter: + raise ValueError("One of 'device' or 'fastbooter' must be passed.") + + if device: + self._device = device + self._serial = str(device) + self._board = device.product_board + if not fastbooter: + self.fastboot = fastboot.Fastboot(self._serial) + if fastbooter: + self._serial = str(fastbooter) self.fastboot = fastbooter - else: - self.fastboot = fastboot.Fastboot(self._serial) + self._board = fastbooter.GetVar('product') + if not device: + self._device = device_utils.DeviceUtils(self._serial) + + self._default_timeout = default_timeout + self._default_retries = default_retries + + self._supports_ab = None + self._requires_dtbo = None + self._requires_vbmeta = None + + @property + def supports_ab(self): + """returns boolean to indicate if a device supports A/B updates. + + It appears that boards which support A/B updates have different partition + requirements when flashing. + """ + if self._supports_ab is None: + if self.IsFastbootMode(): + try: + # According to https://bit.ly/2XIuICQ, slot-count is used to + # determine if a device supports A/B updates. + slot_count = self.fastboot.GetVar('slot-count') or '0' + self._supports_ab = int(slot_count) >= 2 + except device_errors.FastbootCommandFailedError: + self._supports_ab = False + else: + # According to https://bit.ly/2UlJkGa and https://bit.ly/2MG8CL0, + # the property 'ro.build.ab_update' will be defined if the device + # supports A/B system updates. + self._supports_ab = self._device.GetProp('ro.build.ab_update') == 'true' + + return self._supports_ab + + @property + def requires_dtbo(self): + if self._requires_dtbo is None: + if self.IsFastbootMode(): + try: + self._requires_dtbo = self.fastboot.GetVar('has-slot:dtbo') == 'yes' + except device_errors.FastbootCommandFailedError: + self._requires_dtbo = False + else: + # This prop will be set when a device supports dtbo. + # See https://bit.ly/2VUjBp0. + # Checking if this prop has a non-empty value should be good enough. + self._requires_dtbo = len(self._device.GetProp('ro.boot.dtbo_idx')) > 0 + + return self._requires_dtbo + + @property + def requires_vbmeta(self): + if self._requires_vbmeta is None: + if self.IsFastbootMode(): + try: + self._requires_vbmeta = self.fastboot.GetVar( + 'has-slot:vbmeta') == 'yes' + except device_errors.FastbootCommandFailedError: + self._requires_vbmeta = False + else: + # This prop will be set when a device uses Android Verified Boot (avb). + # See https://bit.ly/2CbsO5z. + # Checking if this prop has a non-empty value should be good enough. + self._requires_vbmeta = len( + self._device.GetProp('ro.boot.vbmeta.digest')) > 0 + + return self._requires_vbmeta + + def IsFastbootMode(self): + return self._serial in (str(d) for d in self.fastboot.Devices()) @decorators.WithTimeoutAndRetriesFromInstance() def WaitForFastbootMode(self, timeout=None, retries=None): @@ -107,10 +187,8 @@ class FastbootUtils(object): This waits for the device serial to show up in fastboot devices output. """ - def fastboot_mode(): - return any(self._serial == str(d) for d in self.fastboot.Devices()) - - timeout_retry.WaitFor(fastboot_mode, wait_period=self._FASTBOOT_WAIT_TIME) + timeout_retry.WaitFor(self.IsFastbootMode, + wait_period=self._FASTBOOT_WAIT_TIME) @decorators.WithTimeoutAndRetriesFromInstance( min_default_timeout=_FASTBOOT_REBOOT_TIMEOUT) @@ -119,14 +197,19 @@ class FastbootUtils(object): Roots phone if needed, then reboots phone into fastboot mode and waits. """ + if self.IsFastbootMode(): + return self._device.EnableRoot() self._device.adb.Reboot(to_bootloader=True) self.WaitForFastbootMode() @decorators.WithTimeoutAndRetriesFromInstance( min_default_timeout=_FASTBOOT_REBOOT_TIMEOUT) - def Reboot( - self, bootloader=False, wait_for_reboot=True, timeout=None, retries=None): + def Reboot(self, + bootloader=False, + wait_for_reboot=True, + timeout=None, + retries=None): """Reboots out of fastboot mode. It reboots the phone either back into fastboot, or to a regular boot. It @@ -153,20 +236,16 @@ class FastbootUtils(object): directory: directory where build files are located. """ files = os.listdir(directory) - board_regex = re.compile(r'require board=(\w+)') + board_regex = re.compile(r'require board=([\w|]+)') if self._BOARD_VERIFICATION_FILE in files: with open(os.path.join(directory, self._BOARD_VERIFICATION_FILE)) as f: for line in f: m = board_regex.match(line) - if m: - board_name = m.group(1) - if board_name == self._board: - return True - elif board_name: - return False - else: - logger.warning('No board type found in %s.', - self._BOARD_VERIFICATION_FILE) + if m and m.group(1): + return self._board in m.group(1).split('|') + else: + logger.warning('No board type found in %s.', + self._BOARD_VERIFICATION_FILE) else: logger.warning('%s not found. Unable to use it to verify device.', self._BOARD_VERIFICATION_FILE) @@ -203,19 +282,58 @@ class FastbootUtils(object): 'device type. Run again with force=True to force flashing with an ' 'unverified board.') - flash_image_files = _FindAndVerifyPartitionsAndImages(partitions, directory) + flash_image_files = self._FindAndVerifyPartitionsAndImages( + partitions, directory) partitions = flash_image_files.keys() for partition in partitions: if _KNOWN_PARTITIONS[partition].get('wipe_only') and not wipe: - logger.info( - 'Not flashing in wipe mode. Skipping partition %s.', partition) + logger.info('Not flashing in wipe mode. Skipping partition %s.', + partition) else: - logger.info( - 'Flashing %s with %s', partition, flash_image_files[partition]) + logger.info('Flashing %s with %s', partition, + flash_image_files[partition]) self.fastboot.Flash(partition, flash_image_files[partition]) if _KNOWN_PARTITIONS[partition].get('restart', False): self.Reboot(bootloader=True) + def _FindAndVerifyPartitionsAndImages(self, partitions, directory): + """Validate partitions and images. + + Validate all partition names and partition directories. Cannot stop mid + flash so its important to validate everything first. + + Args: + Partitions: partitions to be tested. + directory: directory containing the images. + + Returns: + Dictionary with exact partition, image name mapping. + """ + + files = os.listdir(directory) + return_dict = collections.OrderedDict() + + def find_file(pattern): + for filename in files: + if fnmatch.fnmatch(filename, pattern): + return os.path.join(directory, filename) + return None + + for partition in partitions: + partition_info = _KNOWN_PARTITIONS[partition] + image_file = find_file(partition_info['image']) + if image_file: + return_dict[partition] = image_file + elif (not 'optional' in partition_info + or not partition_info['optional'](self)): + raise device_errors.FastbootCommandFailedError( + [], + '', + message='Failed to flash device%s. Could not find image for %s.' % + (' which supports A/B updates' if self.supports_ab else '', + partition_info['image'])) + return return_dict + @contextlib.contextmanager def FastbootMode(self, wait_for_reboot=True, timeout=None, retries=None): """Context manager that enables fastboot mode, and reboots after. @@ -227,11 +345,12 @@ class FastbootUtils(object): """ self.EnableFastbootMode() self.fastboot.SetOemOffModeCharge(False) - try: - yield self - finally: - self.fastboot.SetOemOffModeCharge(True) - self.Reboot(wait_for_reboot=wait_for_reboot) + yield self + # If something went wrong while it was in fastboot mode (eg: a failed + # flash) rebooting may be harmful or cause boot loops. So only reboot if + # no exception was thrown. + self.fastboot.SetOemOffModeCharge(True) + self.Reboot(wait_for_reboot=wait_for_reboot) def FlashDevice(self, directory, partitions=None, wipe=False): """Flash device with build in |directory|. @@ -241,7 +360,6 @@ class FastbootUtils(object): use with care. Args: - fastboot: A FastbootUtils instance. directory: Directory with build files. wipe: Wipes cache and userdata if set to true. partitions: List of partitions to flash. Defaults to all. |