aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/fastboot_utils.py
blob: d8ca7d20c35affeb86258a90db78bdeb5c34e1f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# 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

import collections
import contextlib
import fnmatch
import logging
import os
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

logger = logging.getLogger(__name__)

_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.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()


class FastbootUtils(object):

  _FASTBOOT_WAIT_TIME = 1
  _BOARD_VERIFICATION_FILE = 'android-info.txt'

  def __init__(self,
               device=None,
               fastbooter=None,
               default_timeout=_DEFAULT_TIMEOUT,
               default_retries=_DEFAULT_RETRIES):
    """FastbootUtils constructor.

    Example Usage to flash a device:
      fastboot = fastboot_utils.FastbootUtils(device)
      fastboot.FlashDevice('/path/to/build/directory')

    Args:
      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.
    """
    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
      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):
    """Wait for device to boot into fastboot mode.

    This waits for the device serial to show up in fastboot devices output.
    """
    timeout_retry.WaitFor(self.IsFastbootMode,
                          wait_period=self._FASTBOOT_WAIT_TIME)

  @decorators.WithTimeoutAndRetriesFromInstance(
      min_default_timeout=_FASTBOOT_REBOOT_TIMEOUT)
  def EnableFastbootMode(self, timeout=None, retries=None):
    """Reboots phone into fastboot mode.

    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):
    """Reboots out of fastboot mode.

    It reboots the phone either back into fastboot, or to a regular boot. It
    then blocks until the device is ready.

    Args:
      bootloader: If set to True, reboots back into bootloader.
    """
    if bootloader:
      self.fastboot.RebootBootloader()
      self.WaitForFastbootMode()
    else:
      self.fastboot.Reboot()
      if wait_for_reboot:
        self._device.WaitUntilFullyBooted(timeout=_FASTBOOT_REBOOT_TIMEOUT)

  def _VerifyBoard(self, directory):
    """Validate as best as possible that the android build matches the device.

    Goes through build files and checks if the board name is mentioned in the
    |self._BOARD_VERIFICATION_FILE| or in the build archive.

    Args:
      directory: directory where build files are located.
    """
    files = os.listdir(directory)
    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 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)

    zip_regex = re.compile(r'.*%s.*\.zip' % re.escape(self._board))
    for f in files:
      if zip_regex.match(f):
        return True

    return False

  def _FlashPartitions(self, partitions, directory, wipe=False, force=False):
    """Flashes all given partiitons with all given images.

    Args:
      partitions: List of partitions to flash.
      directory: Directory where all partitions can be found.
      wipe: If set to true, will automatically detect if cache and userdata
          partitions are sent, and if so ignore them.
      force: boolean to decide to ignore board name safety checks.

    Raises:
      device_errors.CommandFailedError(): If image cannot be found or if bad
          partition name is give.
    """
    if not self._VerifyBoard(directory):
      if force:
        logger.warning('Could not verify build is meant to be installed on '
                       'the current device type, but force flag is set. '
                       'Flashing device. Possibly dangerous operation.')
      else:
        raise device_errors.CommandFailedError(
            'Could not verify build is meant to be installed on the current '
            'device type. Run again with force=True to force flashing with an '
            'unverified board.')

    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)
      else:
        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.

    Example usage:
      with FastbootMode():
        Flash Device
      # Anything that runs after flashing.
    """
    self.EnableFastbootMode()
    self.fastboot.SetOemOffModeCharge(False)
    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|.

    Directory must contain bootloader, radio, boot, recovery, system, userdata,
    and cache .img files from an android build. This is a dangerous operation so
    use with care.

    Args:
      directory: Directory with build files.
      wipe: Wipes cache and userdata if set to true.
      partitions: List of partitions to flash. Defaults to all.
    """
    if partitions is None:
      partitions = ALL_PARTITIONS
    # If a device is wiped, then it will no longer have adb keys so it cannot be
    # communicated with to verify that it is rebooted. It is up to the user of
    # this script to ensure that the adb keys are set on the device after using
    # this to wipe a device.
    with self.FastbootMode(wait_for_reboot=not wipe):
      self._FlashPartitions(partitions, directory, wipe=wipe)