1# 2# Copyright (C) 2016 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16"""Provides functionality to interact with a device via `fastboot`.""" 17 18import os 19import re 20import subprocess 21 22 23class FastbootError(Exception): 24 """Something went wrong interacting with fastboot.""" 25 26 27class FastbootDevice(object): 28 """Class to interact with a fastboot device.""" 29 30 # Prefix for INFO-type messages when printed by fastboot. If we want 31 # to parse the output from an INFO message we need to strip this off. 32 INFO_PREFIX = '(bootloader) ' 33 34 def __init__(self, path='fastboot'): 35 """Initialization. 36 37 Args: 38 path: path to the fastboot executable to test with. 39 40 Raises: 41 FastbootError: Failed to find a device in fastboot mode. 42 """ 43 self.path = path 44 45 # Make sure the fastboot executable is available. 46 try: 47 _subprocess_check_output([self.path, '--version']) 48 except OSError: 49 raise FastbootError('Could not execute `{}`'.format(self.path)) 50 51 # Make sure exactly 1 fastboot device is available if <specific device> 52 # was not given as an argument. Do not try to find an adb device and 53 # put it in fastboot mode, it would be too easy to accidentally 54 # download to the wrong device. 55 if not self._check_single_device(): 56 raise FastbootError('Requires exactly 1 device in fastboot mode') 57 58 def _check_single_device(self): 59 """Returns True if there is exactly one fastboot device attached. 60 When ANDROID_SERIAL is set it checks that the device is available. 61 """ 62 63 if 'ANDROID_SERIAL' in os.environ: 64 try: 65 self.getvar('product') 66 return True 67 except subprocess.CalledProcessError: 68 return False 69 devices = _subprocess_check_output([self.path, 'devices']).splitlines() 70 return len(devices) == 1 and devices[0].split()[1] == 'fastboot' 71 72 def getvar(self, name): 73 """Calls `fastboot getvar`. 74 75 To query all variables (fastboot getvar all) use getvar_all() 76 instead. 77 78 Args: 79 name: variable name to access. 80 81 Returns: 82 String value of variable |name| or None if not found. 83 """ 84 try: 85 output = _subprocess_check_output([self.path, 'getvar', name], 86 stderr=subprocess.STDOUT).splitlines() 87 except subprocess.CalledProcessError: 88 return None 89 # Output format is <name>:<whitespace><value>. 90 out = 0 91 if output[0] == "< waiting for any device >": 92 out = 1 93 result = re.search(r'{}:\s*(.*)'.format(name), output[out]) 94 if result: 95 return result.group(1) 96 else: 97 return None 98 99 def getvar_all(self): 100 """Calls `fastboot getvar all`. 101 102 Returns: 103 A {name, value} dictionary of variables. 104 """ 105 output = _subprocess_check_output([self.path, 'getvar', 'all'], 106 stderr=subprocess.STDOUT).splitlines() 107 all_vars = {} 108 for line in output: 109 result = re.search(r'(.*):\s*(.*)', line) 110 if result: 111 var_name = result.group(1) 112 113 # `getvar all` works by sending one INFO message per variable 114 # so we need to strip out the info prefix string. 115 if var_name.startswith(self.INFO_PREFIX): 116 var_name = var_name[len(self.INFO_PREFIX):] 117 118 # In addition to returning all variables the bootloader may 119 # also think it's supposed to query a return a variable named 120 # "all", so ignore this line if so. Fastboot also prints a 121 # summary line that we want to ignore. 122 if var_name != 'all' and 'total time' not in var_name: 123 all_vars[var_name] = result.group(2) 124 return all_vars 125 126 def flashall(self, wipe_user=True, slot=None, skip_secondary=False, quiet=True): 127 """Calls `fastboot [-w] flashall`. 128 129 Args: 130 wipe_user: whether to set the -w flag or not. 131 slot: slot to flash if device supports A/B, otherwise default will be used. 132 skip_secondary: on A/B devices, flashes only the primary images if true. 133 quiet: True to hide output, false to send it to stdout. 134 """ 135 func = (_subprocess_check_output if quiet else subprocess.check_call) 136 command = [self.path, 'flashall'] 137 if slot: 138 command.extend(['--slot', slot]) 139 if skip_secondary: 140 command.append("--skip-secondary") 141 if wipe_user: 142 command.append('-w') 143 func(command, stderr=subprocess.STDOUT) 144 145 def flash(self, partition='cache', img=None, slot=None, quiet=True): 146 """Calls `fastboot flash`. 147 148 Args: 149 partition: which partition to flash. 150 img: path to .img file, otherwise the default will be used. 151 slot: slot to flash if device supports A/B, otherwise default will be used. 152 quiet: True to hide output, false to send it to stdout. 153 """ 154 func = (_subprocess_check_output if quiet else subprocess.check_call) 155 command = [self.path, 'flash', partition] 156 if img: 157 command.append(img) 158 if slot: 159 command.extend(['--slot', slot]) 160 if skip_secondary: 161 command.append("--skip-secondary") 162 func(command, stderr=subprocess.STDOUT) 163 164 def reboot(self, bootloader=False): 165 """Calls `fastboot reboot [bootloader]`. 166 167 Args: 168 bootloader: True to reboot back to the bootloader. 169 """ 170 command = [self.path, 'reboot'] 171 if bootloader: 172 command.append('bootloader') 173 _subprocess_check_output(command, stderr=subprocess.STDOUT) 174 175 def set_active(self, slot): 176 """Calls `fastboot set_active <slot>`. 177 178 Args: 179 slot: The slot to set as the current slot.""" 180 command = [self.path, 'set_active', slot] 181 _subprocess_check_output(command, stderr=subprocess.STDOUT) 182 183# If necessary, modifies subprocess.check_output() or subprocess.Popen() args 184# to run the subprocess via Windows PowerShell to work-around an issue in 185# Python 2's subprocess class on Windows where it doesn't support Unicode. 186def _get_subprocess_args(args): 187 # Only do this slow work-around if Unicode is in the cmd line on Windows. 188 # PowerShell takes 600-700ms to startup on a 2013-2014 machine, which is 189 # very slow. 190 if os.name != 'nt' or all(not isinstance(arg, unicode) for arg in args[0]): 191 return args 192 193 def escape_arg(arg): 194 # Escape for the parsing that the C Runtime does in Windows apps. In 195 # particular, this will take care of double-quotes. 196 arg = subprocess.list2cmdline([arg]) 197 # Escape single-quote with another single-quote because we're about 198 # to... 199 arg = arg.replace(u"'", u"''") 200 # ...put the arg in a single-quoted string for PowerShell to parse. 201 arg = u"'" + arg + u"'" 202 return arg 203 204 # Escape command line args. 205 argv = map(escape_arg, args[0]) 206 # Cause script errors (such as adb not found) to stop script immediately 207 # with an error. 208 ps_code = u'$ErrorActionPreference = "Stop"\r\n' 209 # Add current directory to the PATH var, to match cmd.exe/CreateProcess() 210 # behavior. 211 ps_code += u'$env:Path = ".;" + $env:Path\r\n' 212 # Precede by &, the PowerShell call operator, and separate args by space. 213 ps_code += u'& ' + u' '.join(argv) 214 # Make the PowerShell exit code the exit code of the subprocess. 215 ps_code += u'\r\nExit $LastExitCode' 216 # Encode as UTF-16LE (without Byte-Order-Mark) which Windows natively 217 # understands. 218 ps_code = ps_code.encode('utf-16le') 219 220 # Encode the PowerShell command as base64 and use the special 221 # -EncodedCommand option that base64 decodes. Base64 is just plain ASCII, 222 # so it should have no problem passing through Win32 CreateProcessA() 223 # (which python erroneously calls instead of CreateProcessW()). 224 return (['powershell.exe', '-NoProfile', '-NonInteractive', 225 '-EncodedCommand', base64.b64encode(ps_code)],) + args[1:] 226 227# Call this instead of subprocess.check_output() to work-around issue in Python 228# 2's subprocess class on Windows where it doesn't support Unicode. 229def _subprocess_check_output(*args, **kwargs): 230 try: 231 return subprocess.check_output(*_get_subprocess_args(args), **kwargs) 232 except subprocess.CalledProcessError as e: 233 # Show real command line instead of the powershell.exe command line. 234 raise subprocess.CalledProcessError(e.returncode, args[0], 235 output=e.output) 236