From 83bef81a9c168ff328c42d4a3900dfaa8eb7ff78 Mon Sep 17 00:00:00 2001 From: Daniel Rosenberg Date: Fri, 12 Aug 2016 13:12:21 -0700 Subject: [PATCH] Add python fastboot package Change-Id: I310b259c59e2a1c609254ea60e8fc6438d3b2dc5 --- python-packages/fastboot/README.md | 2 + python-packages/fastboot/__init__.py | 17 ++ python-packages/fastboot/device.py | 235 +++++++++++++++++++++++++++ python-packages/fastboot/setup.py | 32 ++++ 4 files changed, 286 insertions(+) create mode 100644 python-packages/fastboot/README.md create mode 100644 python-packages/fastboot/__init__.py create mode 100644 python-packages/fastboot/device.py create mode 100644 python-packages/fastboot/setup.py diff --git a/python-packages/fastboot/README.md b/python-packages/fastboot/README.md new file mode 100644 index 00000000000..87090c23478 --- /dev/null +++ b/python-packages/fastboot/README.md @@ -0,0 +1,2 @@ +This library provides access to the fastboot utility. +For fastboot bootloader tests, see platform/system/extra/tests/bootloader diff --git a/python-packages/fastboot/__init__.py b/python-packages/fastboot/__init__.py new file mode 100644 index 00000000000..ed4a8d30c18 --- /dev/null +++ b/python-packages/fastboot/__init__.py @@ -0,0 +1,17 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import absolute_import +from .device import * # pylint: disable=wildcard-import diff --git a/python-packages/fastboot/device.py b/python-packages/fastboot/device.py new file mode 100644 index 00000000000..d5082c5b1f1 --- /dev/null +++ b/python-packages/fastboot/device.py @@ -0,0 +1,235 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Provides functionality to interact with a device via `fastboot`.""" + +import os +import re +import subprocess + + +class FastbootError(Exception): + """Something went wrong interacting with fastboot.""" + + +class FastbootDevice(object): + """Class to interact with a fastboot device.""" + + # Prefix for INFO-type messages when printed by fastboot. If we want + # to parse the output from an INFO message we need to strip this off. + INFO_PREFIX = '(bootloader) ' + + def __init__(self, path='fastboot'): + """Initialization. + + Args: + path: path to the fastboot executable to test with. + + Raises: + FastbootError: Failed to find a device in fastboot mode. + """ + self.path = path + + # Make sure the fastboot executable is available. + try: + _subprocess_check_output([self.path, '--version']) + except OSError: + raise FastbootError('Could not execute `{}`'.format(self.path)) + + # Make sure exactly 1 fastboot device is available if + # was not given as an argument. Do not try to find an adb device and + # put it in fastboot mode, it would be too easy to accidentally + # download to the wrong device. + if not self._check_single_device(): + raise FastbootError('Requires exactly 1 device in fastboot mode') + + def _check_single_device(self): + """Returns True if there is exactly one fastboot device attached. + When ANDROID_SERIAL is set it checks that the device is available. + """ + + if 'ANDROID_SERIAL' in os.environ: + try: + self.getvar('product') + return True + except subprocess.CalledProcessError: + return False + devices = _subprocess_check_output([self.path, 'devices']).splitlines() + return len(devices) == 1 and devices[0].split()[1] == 'fastboot' + + def getvar(self, name): + """Calls `fastboot getvar`. + + To query all variables (fastboot getvar all) use getvar_all() + instead. + + Args: + name: variable name to access. + + Returns: + String value of variable |name| or None if not found. + """ + try: + output = _subprocess_check_output([self.path, 'getvar', name], + stderr=subprocess.STDOUT).splitlines() + except subprocess.CalledProcessError: + return None + # Output format is :. + out = 0 + if output[0] == "< waiting for any device >": + out = 1 + result = re.search(r'{}:\s*(.*)'.format(name), output[out]) + if result: + return result.group(1) + else: + return None + + def getvar_all(self): + """Calls `fastboot getvar all`. + + Returns: + A {name, value} dictionary of variables. + """ + output = _subprocess_check_output([self.path, 'getvar', 'all'], + stderr=subprocess.STDOUT).splitlines() + all_vars = {} + for line in output: + result = re.search(r'(.*):\s*(.*)', line) + if result: + var_name = result.group(1) + + # `getvar all` works by sending one INFO message per variable + # so we need to strip out the info prefix string. + if var_name.startswith(self.INFO_PREFIX): + var_name = var_name[len(self.INFO_PREFIX):] + + # In addition to returning all variables the bootloader may + # also think it's supposed to query a return a variable named + # "all", so ignore this line if so. Fastboot also prints a + # summary line that we want to ignore. + if var_name != 'all' and 'total time' not in var_name: + all_vars[var_name] = result.group(2) + return all_vars + + def flashall(self, wipe_user=True, slot=None, skip_secondary=False, quiet=True): + """Calls `fastboot [-w] flashall`. + + Args: + wipe_user: whether to set the -w flag or not. + slot: slot to flash if device supports A/B, otherwise default will be used. + skip_secondary: on A/B devices, flashes only the primary images if true. + quiet: True to hide output, false to send it to stdout. + """ + func = (_subprocess_check_output if quiet else subprocess.check_call) + command = [self.path, 'flashall'] + if slot: + command.extend(['--slot', slot]) + if skip_secondary: + command.append("--skip-secondary") + if wipe_user: + command.append('-w') + func(command, stderr=subprocess.STDOUT) + + def flash(self, partition='cache', img=None, slot=None, quiet=True): + """Calls `fastboot flash`. + + Args: + partition: which partition to flash. + img: path to .img file, otherwise the default will be used. + slot: slot to flash if device supports A/B, otherwise default will be used. + quiet: True to hide output, false to send it to stdout. + """ + func = (_subprocess_check_output if quiet else subprocess.check_call) + command = [self.path, 'flash', partition] + if img: + command.append(img) + if slot: + command.extend(['--slot', slot]) + if skip_secondary: + command.append("--skip-secondary") + func(command, stderr=subprocess.STDOUT) + + def reboot(self, bootloader=False): + """Calls `fastboot reboot [bootloader]`. + + Args: + bootloader: True to reboot back to the bootloader. + """ + command = [self.path, 'reboot'] + if bootloader: + command.append('bootloader') + _subprocess_check_output(command, stderr=subprocess.STDOUT) + + def set_active(self, slot): + """Calls `fastboot set_active `. + + Args: + slot: The slot to set as the current slot.""" + command = [self.path, 'set_active', slot] + _subprocess_check_output(command, stderr=subprocess.STDOUT) + +# If necessary, modifies subprocess.check_output() or subprocess.Popen() args +# to run the subprocess via Windows PowerShell to work-around an issue in +# Python 2's subprocess class on Windows where it doesn't support Unicode. +def _get_subprocess_args(args): + # Only do this slow work-around if Unicode is in the cmd line on Windows. + # PowerShell takes 600-700ms to startup on a 2013-2014 machine, which is + # very slow. + if os.name != 'nt' or all(not isinstance(arg, unicode) for arg in args[0]): + return args + + def escape_arg(arg): + # Escape for the parsing that the C Runtime does in Windows apps. In + # particular, this will take care of double-quotes. + arg = subprocess.list2cmdline([arg]) + # Escape single-quote with another single-quote because we're about + # to... + arg = arg.replace(u"'", u"''") + # ...put the arg in a single-quoted string for PowerShell to parse. + arg = u"'" + arg + u"'" + return arg + + # Escape command line args. + argv = map(escape_arg, args[0]) + # Cause script errors (such as adb not found) to stop script immediately + # with an error. + ps_code = u'$ErrorActionPreference = "Stop"\r\n' + # Add current directory to the PATH var, to match cmd.exe/CreateProcess() + # behavior. + ps_code += u'$env:Path = ".;" + $env:Path\r\n' + # Precede by &, the PowerShell call operator, and separate args by space. + ps_code += u'& ' + u' '.join(argv) + # Make the PowerShell exit code the exit code of the subprocess. + ps_code += u'\r\nExit $LastExitCode' + # Encode as UTF-16LE (without Byte-Order-Mark) which Windows natively + # understands. + ps_code = ps_code.encode('utf-16le') + + # Encode the PowerShell command as base64 and use the special + # -EncodedCommand option that base64 decodes. Base64 is just plain ASCII, + # so it should have no problem passing through Win32 CreateProcessA() + # (which python erroneously calls instead of CreateProcessW()). + return (['powershell.exe', '-NoProfile', '-NonInteractive', + '-EncodedCommand', base64.b64encode(ps_code)],) + args[1:] + +# Call this instead of subprocess.check_output() to work-around issue in Python +# 2's subprocess class on Windows where it doesn't support Unicode. +def _subprocess_check_output(*args, **kwargs): + try: + return subprocess.check_output(*_get_subprocess_args(args), **kwargs) + except subprocess.CalledProcessError as e: + # Show real command line instead of the powershell.exe command line. + raise subprocess.CalledProcessError(e.returncode, args[0], + output=e.output) diff --git a/python-packages/fastboot/setup.py b/python-packages/fastboot/setup.py new file mode 100644 index 00000000000..d189ecacacd --- /dev/null +++ b/python-packages/fastboot/setup.py @@ -0,0 +1,32 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from distutils.core import setup + + +setup( + name='fastboot', + version='0.0.1', + description='A Python interface to the Fastboot utility.', + license='Apache 2.0', + keywords='fastboot android', + package_dir={'fastboot': ''}, + packages=['fastboot'], + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + ] +)