1 # Copyright 2013-2015 ARM Limited 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # 15 16 17 """ 18 Utility functions for working with Android devices through adb. 19 20 """ 21 # pylint: disable=E1103 22 import os 23 import pexpect 24 import time 25 import subprocess 26 import logging 27 import re 28 import threading 29 import tempfile 30 import Queue 31 from collections import defaultdict 32 33 from devlib.exception import TargetError, HostError, DevlibError 34 from devlib.utils.misc import check_output, which, memoized, ABI_MAP 35 from devlib.utils.misc import escape_single_quotes, escape_double_quotes 36 from devlib import host 37 38 39 logger = logging.getLogger('android') 40 41 MAX_ATTEMPTS = 5 42 AM_START_ERROR = re.compile(r"Error: Activity.*") 43 44 # See: 45 # http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels 46 ANDROID_VERSION_MAP = { 47 23: 'MARSHMALLOW', 48 22: 'LOLLYPOP_MR1', 49 21: 'LOLLYPOP', 50 20: 'KITKAT_WATCH', 51 19: 'KITKAT', 52 18: 'JELLY_BEAN_MR2', 53 17: 'JELLY_BEAN_MR1', 54 16: 'JELLY_BEAN', 55 15: 'ICE_CREAM_SANDWICH_MR1', 56 14: 'ICE_CREAM_SANDWICH', 57 13: 'HONEYCOMB_MR2', 58 12: 'HONEYCOMB_MR1', 59 11: 'HONEYCOMB', 60 10: 'GINGERBREAD_MR1', 61 9: 'GINGERBREAD', 62 8: 'FROYO', 63 7: 'ECLAIR_MR1', 64 6: 'ECLAIR_0_1', 65 5: 'ECLAIR', 66 4: 'DONUT', 67 3: 'CUPCAKE', 68 2: 'BASE_1_1', 69 1: 'BASE', 70 } 71 72 73 # Initialized in functions near the botton of the file 74 android_home = None 75 platform_tools = None 76 adb = None 77 aapt = None 78 fastboot = None 79 80 81 class AndroidProperties(object): 82 83 def __init__(self, text): 84 self._properties = {} 85 self.parse(text) 86 87 def parse(self, text): 88 self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text)) 89 90 def iteritems(self): 91 return self._properties.iteritems() 92 93 def __iter__(self): 94 return iter(self._properties) 95 96 def __getattr__(self, name): 97 return self._properties.get(name) 98 99 __getitem__ = __getattr__ 100 101 102 class AdbDevice(object): 103 104 def __init__(self, name, status): 105 self.name = name 106 self.status = status 107 108 def __cmp__(self, other): 109 if isinstance(other, AdbDevice): 110 return cmp(self.name, other.name) 111 else: 112 return cmp(self.name, other) 113 114 def __str__(self): 115 return 'AdbDevice({}, {})'.format(self.name, self.status) 116 117 __repr__ = __str__ 118 119 120 class ApkInfo(object): 121 122 version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'") 123 name_regex = re.compile(r"name='(?P<name>[^']+)'") 124 125 def __init__(self, path=None): 126 self.path = path 127 self.package = None 128 self.activity = None 129 self.label = None 130 self.version_name = None 131 self.version_code = None 132 self.native_code = None 133 self.parse(path) 134 135 def parse(self, apk_path): 136 _check_env() 137 command = [aapt, 'dump', 'badging', apk_path] 138 logger.debug(' '.join(command)) 139 try: 140 output = subprocess.check_output(command, stderr=subprocess.STDOUT) 141 except subprocess.CalledProcessError as e: 142 raise HostError('Error parsing APK file {}. `aapt` says:\n{}' 143 .format(apk_path, e.output)) 144 for line in output.split('\n'): 145 if line.startswith('application-label:'): 146 self.label = line.split(':')[1].strip().replace('\'', '') 147 elif line.startswith('package:'): 148 match = self.version_regex.search(line) 149 if match: 150 self.package = match.group('name') 151 self.version_code = match.group('vcode') 152 self.version_name = match.group('vname') 153 elif line.startswith('launchable-activity:'): 154 match = self.name_regex.search(line) 155 self.activity = match.group('name') 156 elif line.startswith('native-code'): 157 apk_abis = [entry.strip() for entry in line.split(':')[1].split("'") if entry.strip()] 158 mapped_abis = [] 159 for apk_abi in apk_abis: 160 found = False 161 for abi, architectures in ABI_MAP.iteritems(): 162 if apk_abi in architectures: 163 mapped_abis.append(abi) 164 found = True 165 break 166 if not found: 167 mapped_abis.append(apk_abi) 168 self.native_code = mapped_abis 169 else: 170 pass # not interested 171 172 173 class AdbConnection(object): 174 175 # maintains the count of parallel active connections to a device, so that 176 # adb disconnect is not invoked untill all connections are closed 177 active_connections = defaultdict(int) 178 default_timeout = 10 179 ls_command = 'ls' 180 181 @property 182 def name(self): 183 return self.device 184 185 @property 186 @memoized 187 def newline_separator(self): 188 output = adb_command(self.device, 189 "shell '({}); echo \"\n$?\"'".format(self.ls_command), adb_server=self.adb_server) 190 if output.endswith('\r\n'): 191 return '\r\n' 192 elif output.endswith('\n'): 193 return '\n' 194 else: 195 raise DevlibError("Unknown line ending") 196 197 # Again, we need to handle boards where the default output format from ls is 198 # single column *and* boards where the default output is multi-column. 199 # We need to do this purely because the '-1' option causes errors on older 200 # versions of the ls tool in Android pre-v7. 201 def _setup_ls(self): 202 command = "shell '(ls -1); echo \"\n$?\"'" 203 try: 204 output = adb_command(self.device, command, timeout=self.timeout, adb_server=self.adb_server) 205 except subprocess.CalledProcessError as e: 206 raise HostError( 207 'Failed to set up ls command on Android device. Output:\n' 208 + e.output) 209 lines = output.splitlines() 210 retval = lines[-1].strip() 211 if int(retval) == 0: 212 self.ls_command = 'ls -1' 213 else: 214 self.ls_command = 'ls' 215 logger.debug("ls command is set to {}".format(self.ls_command)) 216 217 def __init__(self, device=None, timeout=None, platform=None, adb_server=None): 218 self.timeout = timeout if timeout is not None else self.default_timeout 219 if device is None: 220 device = adb_get_device(timeout=timeout, adb_server=adb_server) 221 self.device = device 222 self.adb_server = adb_server 223 adb_connect(self.device) 224 AdbConnection.active_connections[self.device] += 1 225 self._setup_ls() 226 227 def push(self, source, dest, timeout=None): 228 if timeout is None: 229 timeout = self.timeout 230 command = "push '{}' '{}'".format(source, dest) 231 if not os.path.exists(source): 232 raise HostError('No such file "{}"'.format(source)) 233 return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server) 234 235 def pull(self, source, dest, timeout=None): 236 if timeout is None: 237 timeout = self.timeout 238 # Pull all files matching a wildcard expression 239 if os.path.isdir(dest) and \ 240 ('*' in source or '?' in source): 241 command = 'shell {} {}'.format(self.ls_command, source) 242 output = adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server) 243 for line in output.splitlines(): 244 command = "pull '{}' '{}'".format(line.strip(), dest) 245 adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server) 246 return 247 command = "pull '{}' '{}'".format(source, dest) 248 return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server) 249 250 def execute(self, command, timeout=None, check_exit_code=False, 251 as_root=False, strip_colors=True): 252 return adb_shell(self.device, command, timeout, check_exit_code, 253 as_root, self.newline_separator,adb_server=self.adb_server) 254 255 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): 256 return adb_background_shell(self.device, command, stdout, stderr, as_root) 257 258 def close(self): 259 AdbConnection.active_connections[self.device] -= 1 260 if AdbConnection.active_connections[self.device] <= 0: 261 adb_disconnect(self.device) 262 del AdbConnection.active_connections[self.device] 263 264 def cancel_running_command(self): 265 # adbd multiplexes commands so that they don't interfer with each 266 # other, so there is no need to explicitly cancel a running command 267 # before the next one can be issued. 268 pass 269 270 271 def fastboot_command(command, timeout=None, device=None): 272 _check_env() 273 target = '-s {}'.format(device) if device else '' 274 full_command = 'fastboot {} {}'.format(target, command) 275 logger.debug(full_command) 276 output, _ = check_output(full_command, timeout, shell=True) 277 return output 278 279 280 def fastboot_flash_partition(partition, path_to_image): 281 command = 'flash {} {}'.format(partition, path_to_image) 282 fastboot_command(command) 283 284 285 def adb_get_device(timeout=None, adb_server=None): 286 """ 287 Returns the serial number of a connected android device. 288 289 If there are more than one device connected to the machine, or it could not 290 find any device connected, :class:`devlib.exceptions.HostError` is raised. 291 """ 292 # TODO this is a hacky way to issue a adb command to all listed devices 293 294 # Ensure server is started so the 'daemon started successfully' message 295 # doesn't confuse the parsing below 296 adb_command(None, 'start-server', adb_server=adb_server) 297 298 # The output of calling adb devices consists of a heading line then 299 # a list of the devices sperated by new line 300 # The last line is a blank new line. in otherwords, if there is a device found 301 # then the output length is 2 + (1 for each device) 302 start = time.time() 303 while True: 304 output = adb_command(None, "devices", adb_server=adb_server).splitlines() # pylint: disable=E1103 305 output_length = len(output) 306 if output_length == 3: 307 # output[1] is the 2nd line in the output which has the device name 308 # Splitting the line by '\t' gives a list of two indexes, which has 309 # device serial in 0 number and device type in 1. 310 return output[1].split('\t')[0] 311 elif output_length > 3: 312 message = '{} Android devices found; either explicitly specify ' +\ 313 'the device you want, or make sure only one is connected.' 314 raise HostError(message.format(output_length - 2)) 315 else: 316 if timeout < time.time() - start: 317 raise HostError('No device is connected and available') 318 time.sleep(1) 319 320 321 def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS): 322 _check_env() 323 # Connect is required only for ADB-over-IP 324 if "." not in device: 325 logger.debug('Device connected via USB, connect not required') 326 return 327 tries = 0 328 output = None 329 while tries <= attempts: 330 tries += 1 331 if device: 332 command = 'adb connect {}'.format(device) 333 logger.debug(command) 334 output, _ = check_output(command, shell=True, timeout=timeout) 335 if _ping(device): 336 break 337 time.sleep(10) 338 else: # did not connect to the device 339 message = 'Could not connect to {}'.format(device or 'a device') 340 if output: 341 message += '; got: "{}"'.format(output) 342 raise HostError(message) 343 344 345 def adb_disconnect(device): 346 _check_env() 347 if not device: 348 return 349 if ":" in device and device in adb_list_devices(): 350 command = "adb disconnect " + device 351 logger.debug(command) 352 retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True) 353 if retval: 354 raise TargetError('"{}" returned {}'.format(command, retval)) 355 356 357 def _ping(device): 358 _check_env() 359 device_string = ' -s {}'.format(device) if device else '' 360 command = "adb{} shell \"ls / > /dev/null\"".format(device_string) 361 logger.debug(command) 362 result = subprocess.call(command, stderr=subprocess.PIPE, shell=True) 363 if not result: 364 return True 365 else: 366 return False 367 368 369 def adb_shell(device, command, timeout=None, check_exit_code=False, 370 as_root=False, newline_separator='\r\n', adb_server=None): # NOQA 371 _check_env() 372 if as_root: 373 command = 'echo \'{}\' | su'.format(escape_single_quotes(command)) 374 device_part = [] 375 if adb_server: 376 device_part = ['-H', adb_server] 377 device_part += ['-s', device] if device else [] 378 379 # On older combinations of ADB/Android versions, the adb host command always 380 # exits with 0 if it was able to run the command on the target, even if the 381 # command failed (https://code.google.com/p/android/issues/detail?id=3254). 382 # Homogenise this behaviour by running the command then echoing the exit 383 # code. 384 adb_shell_command = '({}); echo \"\n$?\"'.format(command) 385 actual_command = ['adb'] + device_part + ['shell', adb_shell_command] 386 logger.debug('adb {} shell {}'.format(' '.join(device_part), command)) 387 raw_output, error = check_output(actual_command, timeout, shell=False) 388 if raw_output: 389 try: 390 output, exit_code, _ = raw_output.rsplit(newline_separator, 2) 391 except ValueError: 392 exit_code, _ = raw_output.rsplit(newline_separator, 1) 393 output = '' 394 else: # raw_output is empty 395 exit_code = '969696' # just because 396 output = '' 397 398 if check_exit_code: 399 exit_code = exit_code.strip() 400 re_search = AM_START_ERROR.findall('{}\n{}'.format(output, error)) 401 if exit_code.isdigit(): 402 if int(exit_code): 403 message = ('Got exit code {}\nfrom target command: {}\n' 404 'STDOUT: {}\nSTDERR: {}') 405 raise TargetError(message.format(exit_code, command, output, error)) 406 elif re_search: 407 message = 'Could not start activity; got the following:\n{}' 408 raise TargetError(message.format(re_search[0])) 409 else: # not all digits 410 if re_search: 411 message = 'Could not start activity; got the following:\n{}' 412 raise TargetError(message.format(re_search[0])) 413 else: 414 message = 'adb has returned early; did not get an exit code. '\ 415 'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\ 416 '-----\nERROR:\n-----\n{}\n-----' 417 raise TargetError(message.format(raw_output, error)) 418 419 return output 420 421 422 def adb_background_shell(device, command, 423 stdout=subprocess.PIPE, 424 stderr=subprocess.PIPE, 425 as_root=False): 426 """Runs the sepcified command in a subprocess, returning the the Popen object.""" 427 _check_env() 428 if as_root: 429 command = 'echo \'{}\' | su'.format(escape_single_quotes(command)) 430 device_string = ' -s {}'.format(device) if device else '' 431 full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command)) 432 logger.debug(full_command) 433 return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True) 434 435 436 def adb_list_devices(adb_server=None): 437 output = adb_command(None, 'devices',adb_server=adb_server) 438 devices = [] 439 for line in output.splitlines(): 440 parts = [p.strip() for p in line.split()] 441 if len(parts) == 2: 442 devices.append(AdbDevice(*parts)) 443 return devices 444 445 446 def get_adb_command(device, command, timeout=None,adb_server=None): 447 _check_env() 448 device_string = "" 449 if adb_server != None: 450 device_string = ' -H {}'.format(adb_server) 451 device_string += ' -s {}'.format(device) if device else '' 452 return "adb{} {}".format(device_string, command) 453 454 def adb_command(device, command, timeout=None,adb_server=None): 455 full_command = get_adb_command(device, command, timeout, adb_server) 456 logger.debug(full_command) 457 output, _ = check_output(full_command, timeout, shell=True) 458 return output 459 460 def grant_app_permissions(target, package): 461 """ 462 Grant an app all the permissions it may ask for 463 """ 464 dumpsys = target.execute('dumpsys package {}'.format(package)) 465 466 permissions = re.search( 467 'requested permissions:\s*(?P<permissions>(android.permission.+\s*)+)', dumpsys 468 ) 469 if permissions is None: 470 return 471 permissions = permissions.group('permissions').replace(" ", "").splitlines() 472 473 for permission in permissions: 474 try: 475 target.execute('pm grant {} {}'.format(package, permission)) 476 except TargetError: 477 logger.debug('Cannot grant {}'.format(permission)) 478 479 480 # Messy environment initialisation stuff... 481 482 class _AndroidEnvironment(object): 483 484 def __init__(self): 485 self.android_home = None 486 self.platform_tools = None 487 self.adb = None 488 self.aapt = None 489 self.fastboot = None 490 491 492 def _initialize_with_android_home(env): 493 logger.debug('Using ANDROID_HOME from the environment.') 494 env.android_home = android_home 495 env.platform_tools = os.path.join(android_home, 'platform-tools') 496 os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH'] 497 _init_common(env) 498 return env 499 500 501 def _initialize_without_android_home(env): 502 adb_full_path = which('adb') 503 if adb_full_path: 504 env.adb = 'adb' 505 else: 506 raise HostError('ANDROID_HOME is not set and adb is not in PATH. ' 507 'Have you installed Android SDK?') 508 logger.debug('Discovering ANDROID_HOME from adb path.') 509 env.platform_tools = os.path.dirname(adb_full_path) 510 env.android_home = os.path.dirname(env.platform_tools) 511 try: 512 _init_common(env) 513 except: 514 env.aapt = which('aapt') 515 if env.aapt: 516 logger.info('Using aapt: ' + env.aapt) 517 else: 518 raise RuntimeError('aapt not found, try setting ANDROID_HOME to \ 519 Android SDK or run LISA from android environment') 520 return env 521 522 523 def _init_common(env): 524 logger.debug('ANDROID_HOME: {}'.format(env.android_home)) 525 build_tools_directory = os.path.join(env.android_home, 'build-tools') 526 if not os.path.isdir(build_tools_directory): 527 msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install 528 (cannot find build-tools)''' 529 raise HostError(msg.format(env.android_home)) 530 versions = os.listdir(build_tools_directory) 531 for version in reversed(sorted(versions)): 532 aapt_path = os.path.join(build_tools_directory, version, 'aapt') 533 if os.path.isfile(aapt_path): 534 logger.debug('Using aapt for version {}'.format(version)) 535 env.aapt = aapt_path 536 break 537 else: 538 raise HostError('aapt not found. Please make sure at least one Android ' 539 'platform is installed.') 540 541 542 def _check_env(): 543 global android_home, platform_tools, adb, aapt # pylint: disable=W0603 544 if not android_home: 545 android_home = os.getenv('ANDROID_HOME') 546 if android_home: 547 _env = _initialize_with_android_home(_AndroidEnvironment()) 548 else: 549 _env = _initialize_without_android_home(_AndroidEnvironment()) 550 android_home = _env.android_home 551 platform_tools = _env.platform_tools 552 adb = _env.adb 553 aapt = _env.aapt 554 555 class LogcatMonitor(object): 556 """ 557 Helper class for monitoring Anroid's logcat 558 559 :param target: Android target to monitor 560 :type target: :class:`AndroidTarget` 561 562 :param regexps: List of uncompiled regular expressions to filter on the 563 device. Logcat entries that don't match any will not be 564 seen. If omitted, all entries will be sent to host. 565 :type regexps: list(str) 566 """ 567 568 @property 569 def logfile(self): 570 return self._logfile 571 572 def __init__(self, target, regexps=None): 573 super(LogcatMonitor, self).__init__() 574 575 self.target = target 576 self._regexps = regexps 577 578 def start(self, outfile=None): 579 """ 580 Start logcat and begin monitoring 581 582 :param outfile: Optional path to file to store all logcat entries 583 :type outfile: str 584 """ 585 if outfile: 586 self._logfile = open(outfile, 'w') 587 else: 588 self._logfile = tempfile.NamedTemporaryFile() 589 590 self.target.clear_logcat() 591 592 logcat_cmd = 'logcat' 593 594 # Join all requested regexps with an 'or' 595 if self._regexps: 596 regexp = '{}'.format('|'.join(self._regexps)) 597 if len(self._regexps) > 1: 598 regexp = '({})'.format(regexp) 599 logcat_cmd = '{} -e "{}"'.format(logcat_cmd, regexp) 600 601 logcat_cmd = get_adb_command(self.target.conn.device, logcat_cmd) 602 603 logger.debug('logcat command ="{}"'.format(logcat_cmd)) 604 self._logcat = pexpect.spawn(logcat_cmd, logfile=self._logfile) 605 606 def stop(self): 607 self._logcat.terminate() 608 self._logfile.close() 609 610 def get_log(self): 611 """ 612 Return the list of lines found by the monitor 613 """ 614 with open(self._logfile.name) as fh: 615 return [line for line in fh] 616 617 def clear_log(self): 618 with open(self._logfile.name, 'w') as fh: 619 pass 620 621 def search(self, regexp): 622 """ 623 Search a line that matches a regexp in the logcat log 624 Return immediatly 625 """ 626 return [line for line in self.get_log() if re.match(regexp, line)] 627 628 def wait_for(self, regexp, timeout=30): 629 """ 630 Search a line that matches a regexp in the logcat log 631 Wait for it to appear if it's not found 632 633 :param regexp: regexp to search 634 :type regexp: str 635 636 :param timeout: Timeout in seconds, before rasing RuntimeError. 637 ``None`` means wait indefinitely 638 :type timeout: number 639 640 :returns: List of matched strings 641 """ 642 log = self.get_log() 643 res = [line for line in log if re.match(regexp, line)] 644 645 # Found some matches, return them 646 if len(res) > 0: 647 return res 648 649 # Store the number of lines we've searched already, so we don't have to 650 # re-grep them after 'expect' returns 651 next_line_num = len(log) 652 653 try: 654 self._logcat.expect(regexp, timeout=timeout) 655 except pexpect.TIMEOUT: 656 raise RuntimeError('Logcat monitor timeout ({}s)'.format(timeout)) 657 658 return [line for line in self.get_log()[next_line_num:] 659 if re.match(regexp, line)] 660