1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Provides an interface to communicate with the device via the adb command. 6 7 Assumes adb binary is currently on system path. 8 """ 9 10 import collections 11 import datetime 12 import inspect 13 import logging 14 import os 15 import re 16 import shlex 17 import signal 18 import subprocess 19 import sys 20 import tempfile 21 import time 22 23 import cmd_helper 24 import constants 25 import screenshot 26 import system_properties 27 28 from utils import host_path_finder 29 30 try: 31 from pylib import pexpect 32 except: 33 pexpect = None 34 35 sys.path.append(os.path.join( 36 constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner')) 37 import adb_interface 38 import am_instrument_parser 39 import errors 40 41 42 # Pattern to search for the next whole line of pexpect output and capture it 43 # into a match group. We can't use ^ and $ for line start end with pexpect, 44 # see http://www.noah.org/python/pexpect/#doc for explanation why. 45 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') 46 47 # Set the adb shell prompt to be a unique marker that will [hopefully] not 48 # appear at the start of any line of a command's output. 49 SHELL_PROMPT = '~+~PQ\x17RS~+~' 50 51 # Java properties file 52 LOCAL_PROPERTIES_PATH = '/data/local.prop' 53 54 # Property in /data/local.prop that controls Java assertions. 55 JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' 56 57 MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$') 58 NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*' 59 '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$') 60 61 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). 62 KEYCODE_HOME = 3 63 KEYCODE_BACK = 4 64 KEYCODE_DPAD_UP = 19 65 KEYCODE_DPAD_DOWN = 20 66 KEYCODE_DPAD_RIGHT = 22 67 KEYCODE_ENTER = 66 68 KEYCODE_MENU = 82 69 70 MD5SUM_DEVICE_FOLDER = constants.TEST_EXECUTABLE_DIR + '/md5sum/' 71 MD5SUM_DEVICE_PATH = MD5SUM_DEVICE_FOLDER + 'md5sum_bin' 72 MD5SUM_LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % MD5SUM_DEVICE_FOLDER 73 74 75 def GetAVDs(): 76 """Returns a list of AVDs.""" 77 re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE) 78 avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd'])) 79 return avds 80 81 82 def GetAttachedDevices(hardware=True, emulator=True, offline=False): 83 """Returns a list of attached, android devices and emulators. 84 85 If a preferred device has been set with ANDROID_SERIAL, it will be first in 86 the returned list. The arguments specify what devices to include in the list. 87 88 Example output: 89 90 * daemon not running. starting it now on port 5037 * 91 * daemon started successfully * 92 List of devices attached 93 027c10494100b4d7 device 94 emulator-5554 offline 95 96 Args: 97 hardware: Include attached actual devices that are online. 98 emulator: Include emulators (i.e. AVD's) currently on host. 99 offline: Include devices and emulators that are offline. 100 101 Returns: List of devices. 102 """ 103 adb_devices_output = cmd_helper.GetCmdOutput([constants.ADB_PATH, 'devices']) 104 105 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) 106 online_devices = re_device.findall(adb_devices_output) 107 108 re_device = re.compile('^(emulator-[0-9]+)\tdevice', re.MULTILINE) 109 emulator_devices = re_device.findall(adb_devices_output) 110 111 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\toffline$', re.MULTILINE) 112 offline_devices = re_device.findall(adb_devices_output) 113 114 devices = [] 115 # First determine list of online devices (e.g. hardware and/or emulator). 116 if hardware and emulator: 117 devices = online_devices 118 elif hardware: 119 devices = [device for device in online_devices 120 if device not in emulator_devices] 121 elif emulator: 122 devices = emulator_devices 123 124 # Now add offline devices if offline is true 125 if offline: 126 devices = devices + offline_devices 127 128 preferred_device = os.environ.get('ANDROID_SERIAL') 129 if preferred_device in devices: 130 devices.remove(preferred_device) 131 devices.insert(0, preferred_device) 132 return devices 133 134 135 def IsDeviceAttached(device): 136 """Return true if the device is attached and online.""" 137 return device in GetAttachedDevices() 138 139 140 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): 141 """Gets a list of files from `ls` command output. 142 143 Python's os.walk isn't used because it doesn't work over adb shell. 144 145 Args: 146 path: The path to list. 147 ls_output: A list of lines returned by an `ls -lR` command. 148 re_file: A compiled regular expression which parses a line into named groups 149 consisting of at minimum "filename", "date", "time", "size" and 150 optionally "timezone". 151 utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a 152 2-digit string giving the number of UTC offset hours, and MM is a 153 2-digit string giving the number of UTC offset minutes. If the input 154 utc_offset is None, will try to look for the value of "timezone" if it 155 is specified in re_file. 156 157 Returns: 158 A dict of {"name": (size, lastmod), ...} where: 159 name: The file name relative to |path|'s directory. 160 size: The file size in bytes (0 for directories). 161 lastmod: The file last modification date in UTC. 162 """ 163 re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path)) 164 path_dir = os.path.dirname(path) 165 166 current_dir = '' 167 files = {} 168 for line in ls_output: 169 directory_match = re_directory.match(line) 170 if directory_match: 171 current_dir = directory_match.group('dir') 172 continue 173 file_match = re_file.match(line) 174 if file_match: 175 filename = os.path.join(current_dir, file_match.group('filename')) 176 if filename.startswith(path_dir): 177 filename = filename[len(path_dir) + 1:] 178 lastmod = datetime.datetime.strptime( 179 file_match.group('date') + ' ' + file_match.group('time')[:5], 180 '%Y-%m-%d %H:%M') 181 if not utc_offset and 'timezone' in re_file.groupindex: 182 utc_offset = file_match.group('timezone') 183 if isinstance(utc_offset, str) and len(utc_offset) == 5: 184 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), 185 minutes=int(utc_offset[3:5])) 186 if utc_offset[0:1] == '-': 187 utc_delta = -utc_delta 188 lastmod -= utc_delta 189 files[filename] = (int(file_match.group('size')), lastmod) 190 return files 191 192 193 def _ParseMd5SumOutput(md5sum_output): 194 """Returns a list of tuples from the provided md5sum output. 195 196 Args: 197 md5sum_output: output directly from md5sum binary. 198 199 Returns: 200 List of namedtuples with attributes |hash| and |path|, where |path| is the 201 absolute path to the file with an Md5Sum of |hash|. 202 """ 203 HashAndPath = collections.namedtuple('HashAndPath', ['hash', 'path']) 204 split_lines = [line.split(' ') for line in md5sum_output] 205 return [HashAndPath._make(s) for s in split_lines if len(s) == 2] 206 207 208 def _HasAdbPushSucceeded(command_output): 209 """Returns whether adb push has succeeded from the provided output.""" 210 # TODO(frankf): We should look at the return code instead of the command 211 # output for many of the commands in this file. 212 if not command_output: 213 return True 214 # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" 215 # Errors look like this: "failed to copy ... " 216 if not re.search('^[0-9]', command_output.splitlines()[-1]): 217 logging.critical('PUSH FAILED: ' + command_output) 218 return False 219 return True 220 221 222 def GetLogTimestamp(log_line, year): 223 """Returns the timestamp of the given |log_line| in the given year.""" 224 try: 225 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), 226 '%Y-%m-%d %H:%M:%S.%f') 227 except (ValueError, IndexError): 228 logging.critical('Error reading timestamp from ' + log_line) 229 return None 230 231 232 class AndroidCommands(object): 233 """Helper class for communicating with Android device via adb.""" 234 235 def __init__(self, device=None, api_strict_mode=False): 236 """Constructor. 237 238 Args: 239 device: If given, adb commands are only send to the device of this ID. 240 Otherwise commands are sent to all attached devices. 241 api_strict_mode: A boolean indicating whether fatal errors should be 242 raised if this API is used improperly. 243 """ 244 adb_dir = os.path.dirname(constants.ADB_PATH) 245 if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep): 246 # Required by third_party/android_testrunner to call directly 'adb'. 247 os.environ['PATH'] += os.pathsep + adb_dir 248 self._adb = adb_interface.AdbInterface() 249 if device: 250 self._adb.SetTargetSerial(device) 251 self._device = device 252 self._logcat = None 253 self.logcat_process = None 254 self._logcat_tmpoutfile = None 255 self._pushed_files = [] 256 self._device_utc_offset = None 257 self._potential_push_size = 0 258 self._actual_push_size = 0 259 self._external_storage = '' 260 self._util_wrapper = '' 261 self._api_strict_mode = api_strict_mode 262 self._system_properties = system_properties.SystemProperties(self.Adb()) 263 self._push_if_needed_cache = {} 264 265 if not self._api_strict_mode: 266 logging.warning( 267 'API STRICT MODE IS DISABLED.\n' 268 'It should be enabled as soon as possible as it will eventually ' 269 'become the default.') 270 271 @property 272 def system_properties(self): 273 return self._system_properties 274 275 def _LogShell(self, cmd): 276 """Logs the adb shell command.""" 277 if self._device: 278 device_repr = self._device[-4:] 279 else: 280 device_repr = '????' 281 logging.info('[%s]> %s', device_repr, cmd) 282 283 def Adb(self): 284 """Returns our AdbInterface to avoid us wrapping all its methods.""" 285 # TODO(tonyg): Disable this method when in _api_strict_mode. 286 return self._adb 287 288 def GetDevice(self): 289 """Returns the device serial.""" 290 return self._device 291 292 def IsOnline(self): 293 """Checks whether the device is online. 294 295 Returns: 296 True if device is in 'device' mode, False otherwise. 297 """ 298 out = self._adb.SendCommand('get-state') 299 return out.strip() == 'device' 300 301 def IsRootEnabled(self): 302 """Checks if root is enabled on the device.""" 303 root_test_output = self.RunShellCommand('ls /root') or [''] 304 return not 'Permission denied' in root_test_output[0] 305 306 def EnableAdbRoot(self): 307 """Enables adb root on the device. 308 309 Returns: 310 True: if output from executing adb root was as expected. 311 False: otherwise. 312 """ 313 if self.GetBuildType() == 'user': 314 logging.warning("Can't enable root in production builds with type user") 315 return False 316 else: 317 return_value = self._adb.EnableAdbRoot() 318 # EnableAdbRoot inserts a call for wait-for-device only when adb logcat 319 # output matches what is expected. Just to be safe add a call to 320 # wait-for-device. 321 self._adb.SendCommand('wait-for-device') 322 return return_value 323 324 def GetDeviceYear(self): 325 """Returns the year information of the date on device.""" 326 return self.RunShellCommand('date +%Y')[0] 327 328 def GetExternalStorage(self): 329 if not self._external_storage: 330 self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0] 331 assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE' 332 return self._external_storage 333 334 def WaitForDevicePm(self): 335 """Blocks until the device's package manager is available. 336 337 To workaround http://b/5201039, we restart the shell and retry if the 338 package manager isn't back after 120 seconds. 339 340 Raises: 341 errors.WaitForResponseTimedOutError after max retries reached. 342 """ 343 last_err = None 344 retries = 3 345 while retries: 346 try: 347 self._adb.WaitForDevicePm() 348 return # Success 349 except errors.WaitForResponseTimedOutError as e: 350 last_err = e 351 logging.warning('Restarting and retrying after timeout: %s', e) 352 retries -= 1 353 self.RestartShell() 354 raise last_err # Only reached after max retries, re-raise the last error. 355 356 def RestartShell(self): 357 """Restarts the shell on the device. Does not block for it to return.""" 358 self.RunShellCommand('stop') 359 self.RunShellCommand('start') 360 361 def Reboot(self, full_reboot=True): 362 """Reboots the device and waits for the package manager to return. 363 364 Args: 365 full_reboot: Whether to fully reboot the device or just restart the shell. 366 """ 367 # TODO(torne): hive can't reboot the device either way without breaking the 368 # connection; work out if we can handle this better 369 if os.environ.get('USING_HIVE'): 370 logging.warning('Ignoring reboot request as we are on hive') 371 return 372 if full_reboot or not self.IsRootEnabled(): 373 self._adb.SendCommand('reboot') 374 self._system_properties = system_properties.SystemProperties(self.Adb()) 375 timeout = 300 376 retries = 1 377 # Wait for the device to disappear. 378 while retries < 10 and self.IsOnline(): 379 time.sleep(1) 380 retries += 1 381 else: 382 self.RestartShell() 383 timeout = 120 384 # To run tests we need at least the package manager and the sd card (or 385 # other external storage) to be ready. 386 self.WaitForDevicePm() 387 self.WaitForSdCardReady(timeout) 388 389 def Shutdown(self): 390 """Shuts down the device.""" 391 self._adb.SendCommand('reboot -p') 392 self._system_properties = system_properties.SystemProperties(self.Adb()) 393 394 def Uninstall(self, package): 395 """Uninstalls the specified package from the device. 396 397 Args: 398 package: Name of the package to remove. 399 400 Returns: 401 A status string returned by adb uninstall 402 """ 403 uninstall_command = 'uninstall %s' % package 404 405 self._LogShell(uninstall_command) 406 return self._adb.SendCommand(uninstall_command, timeout_time=60) 407 408 def Install(self, package_file_path, reinstall=False): 409 """Installs the specified package to the device. 410 411 Args: 412 package_file_path: Path to .apk file to install. 413 reinstall: Reinstall an existing apk, keeping the data. 414 415 Returns: 416 A status string returned by adb install 417 """ 418 assert os.path.isfile(package_file_path), ('<%s> is not file' % 419 package_file_path) 420 421 install_cmd = ['install'] 422 423 if reinstall: 424 install_cmd.append('-r') 425 426 install_cmd.append(package_file_path) 427 install_cmd = ' '.join(install_cmd) 428 429 self._LogShell(install_cmd) 430 return self._adb.SendCommand(install_cmd, 431 timeout_time=2 * 60, 432 retry_count=0) 433 434 def ManagedInstall(self, apk_path, keep_data=False, package_name=None, 435 reboots_on_timeout=2): 436 """Installs specified package and reboots device on timeouts. 437 438 If package_name is supplied, checks if the package is already installed and 439 doesn't reinstall if the apk md5sums match. 440 441 Args: 442 apk_path: Path to .apk file to install. 443 keep_data: Reinstalls instead of uninstalling first, preserving the 444 application data. 445 package_name: Package name (only needed if keep_data=False). 446 reboots_on_timeout: number of time to reboot if package manager is frozen. 447 """ 448 # Check if package is already installed and up to date. 449 if package_name: 450 installed_apk_path = self.GetApplicationPath(package_name) 451 if (installed_apk_path and 452 not self.GetFilesChanged(apk_path, installed_apk_path, 453 ignore_filenames=True)): 454 logging.info('Skipped install: identical %s APK already installed' % 455 package_name) 456 return 457 # Install. 458 reboots_left = reboots_on_timeout 459 while True: 460 try: 461 if not keep_data: 462 assert package_name 463 self.Uninstall(package_name) 464 install_status = self.Install(apk_path, reinstall=keep_data) 465 if 'Success' in install_status: 466 return 467 else: 468 raise Exception('Install failure: %s' % install_status) 469 except errors.WaitForResponseTimedOutError: 470 print '@@@STEP_WARNINGS@@@' 471 logging.info('Timeout on installing %s on device %s', apk_path, 472 self._device) 473 474 if reboots_left <= 0: 475 raise Exception('Install timed out') 476 477 # Force a hard reboot on last attempt 478 self.Reboot(full_reboot=(reboots_left == 1)) 479 reboots_left -= 1 480 481 def MakeSystemFolderWritable(self): 482 """Remounts the /system folder rw.""" 483 out = self._adb.SendCommand('remount') 484 if out.strip() != 'remount succeeded': 485 raise errors.MsgException('Remount failed: %s' % out) 486 487 def RestartAdbdOnDevice(self): 488 logging.info('Killing adbd on the device...') 489 adb_pids = self.ExtractPid('adbd') 490 if not adb_pids: 491 raise errors.MsgException('Unable to obtain adbd pid') 492 try: 493 self.KillAll('adbd', signal=signal.SIGTERM, with_su=True) 494 logging.info('Waiting for device to settle...') 495 self._adb.SendCommand('wait-for-device') 496 new_adb_pids = self.ExtractPid('adbd') 497 if new_adb_pids == adb_pids: 498 logging.error('adbd on the device may not have been restarted.') 499 except Exception as e: 500 logging.error('Exception when trying to kill adbd on the device [%s]', e) 501 502 def RestartAdbServer(self): 503 """Restart the adb server.""" 504 ret = self.KillAdbServer() 505 if ret != 0: 506 raise errors.MsgException('KillAdbServer: %d' % ret) 507 508 ret = self.StartAdbServer() 509 if ret != 0: 510 raise errors.MsgException('StartAdbServer: %d' % ret) 511 512 def KillAdbServer(self): 513 """Kill adb server.""" 514 adb_cmd = [constants.ADB_PATH, 'kill-server'] 515 ret = cmd_helper.RunCmd(adb_cmd) 516 retry = 0 517 while retry < 3: 518 ret = cmd_helper.RunCmd(['pgrep', 'adb']) 519 if ret != 0: 520 # pgrep didn't find adb, kill-server succeeded. 521 return 0 522 retry += 1 523 time.sleep(retry) 524 return ret 525 526 def StartAdbServer(self): 527 """Start adb server.""" 528 adb_cmd = ['taskset', '-c', '0', constants.ADB_PATH, 'start-server'] 529 ret = cmd_helper.RunCmd(adb_cmd) 530 retry = 0 531 while retry < 3: 532 ret = cmd_helper.RunCmd(['pgrep', 'adb']) 533 if ret == 0: 534 # pgrep found adb, start-server succeeded. 535 # Waiting for device to reconnect before returning success. 536 self._adb.SendCommand('wait-for-device') 537 return 0 538 retry += 1 539 time.sleep(retry) 540 return ret 541 542 def WaitForSystemBootCompleted(self, wait_time): 543 """Waits for targeted system's boot_completed flag to be set. 544 545 Args: 546 wait_time: time in seconds to wait 547 548 Raises: 549 WaitForResponseTimedOutError if wait_time elapses and flag still not 550 set. 551 """ 552 logging.info('Waiting for system boot completed...') 553 self._adb.SendCommand('wait-for-device') 554 # Now the device is there, but system not boot completed. 555 # Query the sys.boot_completed flag with a basic command 556 boot_completed = False 557 attempts = 0 558 wait_period = 5 559 while not boot_completed and (attempts * wait_period) < wait_time: 560 output = self.system_properties['sys.boot_completed'] 561 output = output.strip() 562 if output == '1': 563 boot_completed = True 564 else: 565 # If 'error: xxx' returned when querying the flag, it means 566 # adb server lost the connection to the emulator, so restart the adb 567 # server. 568 if 'error:' in output: 569 self.RestartAdbServer() 570 time.sleep(wait_period) 571 attempts += 1 572 if not boot_completed: 573 raise errors.WaitForResponseTimedOutError( 574 'sys.boot_completed flag was not set after %s seconds' % wait_time) 575 576 def WaitForSdCardReady(self, timeout_time): 577 """Wait for the SD card ready before pushing data into it.""" 578 logging.info('Waiting for SD card ready...') 579 sdcard_ready = False 580 attempts = 0 581 wait_period = 5 582 external_storage = self.GetExternalStorage() 583 while not sdcard_ready and attempts * wait_period < timeout_time: 584 output = self.RunShellCommand('ls ' + external_storage) 585 if output: 586 sdcard_ready = True 587 else: 588 time.sleep(wait_period) 589 attempts += 1 590 if not sdcard_ready: 591 raise errors.WaitForResponseTimedOutError( 592 'SD card not ready after %s seconds' % timeout_time) 593 594 def _CheckCommandIsValid(self, command): 595 """Raises a ValueError if the command is not valid.""" 596 597 # A dict of commands the user should not run directly and a mapping to the 598 # API they should use instead. 599 preferred_apis = { 600 'getprop': 'system_properties[<PROPERTY>]', 601 'setprop': 'system_properties[<PROPERTY>]', 602 'su': 'RunShellCommandWithSU()', 603 } 604 605 # A dict of commands to methods that may call them. 606 whitelisted_callers = { 607 'su': 'RunShellCommandWithSU', 608 } 609 610 base_command = shlex.split(command)[0] 611 if (base_command in preferred_apis and 612 (base_command not in whitelisted_callers or 613 whitelisted_callers[base_command] not in [ 614 f[3] for f in inspect.stack()])): 615 error_msg = ('%s cannot be run directly. Instead use: %s' % 616 (base_command, preferred_apis[base_command])) 617 if self._api_strict_mode: 618 raise ValueError(error_msg) 619 else: 620 logging.warning(error_msg) 621 622 # It is tempting to turn this function into a generator, however this is not 623 # possible without using a private (local) adb_shell instance (to ensure no 624 # other command interleaves usage of it), which would defeat the main aim of 625 # being able to reuse the adb shell instance across commands. 626 def RunShellCommand(self, command, timeout_time=20, log_result=False): 627 """Send a command to the adb shell and return the result. 628 629 Args: 630 command: String containing the shell command to send. Must not include 631 the single quotes as we use them to escape the whole command. 632 timeout_time: Number of seconds to wait for command to respond before 633 retrying, used by AdbInterface.SendShellCommand. 634 log_result: Boolean to indicate whether we should log the result of the 635 shell command. 636 637 Returns: 638 list containing the lines of output received from running the command 639 """ 640 self._CheckCommandIsValid(command) 641 self._LogShell(command) 642 if "'" in command: logging.warning(command + " contains ' quotes") 643 result = self._adb.SendShellCommand( 644 "'%s'" % command, timeout_time).splitlines() 645 if ['error: device not found'] == result: 646 raise errors.DeviceUnresponsiveError('device not found') 647 if log_result: 648 self._LogShell('\n'.join(result)) 649 return result 650 651 def GetShellCommandStatusAndOutput(self, command, timeout_time=20, 652 log_result=False): 653 """See RunShellCommand() above. 654 655 Returns: 656 The tuple (exit code, list of output lines). 657 """ 658 lines = self.RunShellCommand( 659 command + '; echo %$?', timeout_time, log_result) 660 last_line = lines[-1] 661 status_pos = last_line.rfind('%') 662 assert status_pos >= 0 663 status = int(last_line[status_pos + 1:]) 664 if status_pos == 0: 665 lines = lines[:-1] 666 else: 667 lines = lines[:-1] + [last_line[:status_pos]] 668 return (status, lines) 669 670 def KillAll(self, process, signal=9, with_su=False): 671 """Android version of killall, connected via adb. 672 673 Args: 674 process: name of the process to kill off. 675 signal: signal to use, 9 (SIGKILL) by default. 676 with_su: wether or not to use su to kill the processes. 677 678 Returns: 679 the number of processes killed 680 """ 681 pids = self.ExtractPid(process) 682 if pids: 683 cmd = 'kill -%d %s' % (signal, ' '.join(pids)) 684 if with_su: 685 self.RunShellCommandWithSU(cmd) 686 else: 687 self.RunShellCommand(cmd) 688 return len(pids) 689 690 def KillAllBlocking(self, process, timeout_sec): 691 """Blocking version of killall, connected via adb. 692 693 This waits until no process matching the corresponding name appears in ps' 694 output anymore. 695 696 Args: 697 process: name of the process to kill off 698 timeout_sec: the timeout in seconds 699 700 Returns: 701 the number of processes killed 702 """ 703 processes_killed = self.KillAll(process) 704 if processes_killed: 705 elapsed = 0 706 wait_period = 0.1 707 # Note that this doesn't take into account the time spent in ExtractPid(). 708 while self.ExtractPid(process) and elapsed < timeout_sec: 709 time.sleep(wait_period) 710 elapsed += wait_period 711 if elapsed >= timeout_sec: 712 return 0 713 return processes_killed 714 715 def _GetActivityCommand(self, package, activity, wait_for_completion, action, 716 category, data, extras, trace_file_name, force_stop, 717 flags): 718 """Creates command to start |package|'s activity on the device. 719 720 Args - as for StartActivity 721 722 Returns: 723 the command to run on the target to start the activity 724 """ 725 cmd = 'am start -a %s' % action 726 if force_stop: 727 cmd += ' -S' 728 if wait_for_completion: 729 cmd += ' -W' 730 if category: 731 cmd += ' -c %s' % category 732 if package and activity: 733 cmd += ' -n %s/%s' % (package, activity) 734 if data: 735 cmd += ' -d "%s"' % data 736 if extras: 737 for key in extras: 738 value = extras[key] 739 if isinstance(value, str): 740 cmd += ' --es' 741 elif isinstance(value, bool): 742 cmd += ' --ez' 743 elif isinstance(value, int): 744 cmd += ' --ei' 745 else: 746 raise NotImplementedError( 747 'Need to teach StartActivity how to pass %s extras' % type(value)) 748 cmd += ' %s %s' % (key, value) 749 if trace_file_name: 750 cmd += ' --start-profiler ' + trace_file_name 751 if flags: 752 cmd += ' -f %s' % flags 753 return cmd 754 755 def StartActivity(self, package, activity, wait_for_completion=False, 756 action='android.intent.action.VIEW', 757 category=None, data=None, 758 extras=None, trace_file_name=None, 759 force_stop=False, flags=None): 760 """Starts |package|'s activity on the device. 761 762 Args: 763 package: Name of package to start (e.g. 'com.google.android.apps.chrome'). 764 activity: Name of activity (e.g. '.Main' or 765 'com.google.android.apps.chrome.Main'). 766 wait_for_completion: wait for the activity to finish launching (-W flag). 767 action: string (e.g. "android.intent.action.MAIN"). Default is VIEW. 768 category: string (e.g. "android.intent.category.HOME") 769 data: Data string to pass to activity (e.g. 'http://www.example.com/'). 770 extras: Dict of extras to pass to activity. Values are significant. 771 trace_file_name: If used, turns on and saves the trace to this file name. 772 force_stop: force stop the target app before starting the activity (-S 773 flag). 774 """ 775 cmd = self._GetActivityCommand(package, activity, wait_for_completion, 776 action, category, data, extras, 777 trace_file_name, force_stop, flags) 778 self.RunShellCommand(cmd) 779 780 def StartActivityTimed(self, package, activity, wait_for_completion=False, 781 action='android.intent.action.VIEW', 782 category=None, data=None, 783 extras=None, trace_file_name=None, 784 force_stop=False, flags=None): 785 """Starts |package|'s activity on the device, returning the start time 786 787 Args - as for StartActivity 788 789 Returns: 790 a timestamp string for the time at which the activity started 791 """ 792 cmd = self._GetActivityCommand(package, activity, wait_for_completion, 793 action, category, data, extras, 794 trace_file_name, force_stop, flags) 795 self.StartMonitoringLogcat() 796 self.RunShellCommand('log starting activity; ' + cmd) 797 activity_started_re = re.compile('.*starting activity.*') 798 m = self.WaitForLogMatch(activity_started_re, None) 799 assert m 800 start_line = m.group(0) 801 return GetLogTimestamp(start_line, self.GetDeviceYear()) 802 803 def StartCrashUploadService(self, package): 804 # TODO(frankf): We really need a python wrapper around Intent 805 # to be shared with StartActivity/BroadcastIntent. 806 cmd = ( 807 'am startservice -a %s.crash.ACTION_FIND_ALL -n ' 808 '%s/%s.crash.MinidumpUploadService' % 809 (constants.PACKAGE_INFO['chrome'].package, 810 package, 811 constants.PACKAGE_INFO['chrome'].package)) 812 am_output = self.RunShellCommandWithSU(cmd) 813 assert am_output and 'Starting' in am_output[-1], ( 814 'Service failed to start: %s' % am_output) 815 time.sleep(15) 816 817 def BroadcastIntent(self, package, intent, *args): 818 """Send a broadcast intent. 819 820 Args: 821 package: Name of package containing the intent. 822 intent: Name of the intent. 823 args: Optional extra arguments for the intent. 824 """ 825 cmd = 'am broadcast -a %s.%s %s' % (package, intent, ' '.join(args)) 826 self.RunShellCommand(cmd) 827 828 def GoHome(self): 829 """Tell the device to return to the home screen. Blocks until completion.""" 830 self.RunShellCommand('am start -W ' 831 '-a android.intent.action.MAIN -c android.intent.category.HOME') 832 833 def CloseApplication(self, package): 834 """Attempt to close down the application, using increasing violence. 835 836 Args: 837 package: Name of the process to kill off, e.g. 838 com.google.android.apps.chrome 839 """ 840 self.RunShellCommand('am force-stop ' + package) 841 842 def GetApplicationPath(self, package): 843 """Get the installed apk path on the device for the given package. 844 845 Args: 846 package: Name of the package. 847 848 Returns: 849 Path to the apk on the device if it exists, None otherwise. 850 """ 851 pm_path_output = self.RunShellCommand('pm path ' + package) 852 # The path output contains anything if and only if the package 853 # exists. 854 if pm_path_output: 855 # pm_path_output is of the form: "package:/path/to/foo.apk" 856 return pm_path_output[0].split(':')[1] 857 else: 858 return None 859 860 def ClearApplicationState(self, package): 861 """Closes and clears all state for the given |package|.""" 862 # Check that the package exists before clearing it. Necessary because 863 # calling pm clear on a package that doesn't exist may never return. 864 pm_path_output = self.RunShellCommand('pm path ' + package) 865 # The path output only contains anything if and only if the package exists. 866 if pm_path_output: 867 self.RunShellCommand('pm clear ' + package) 868 869 def SendKeyEvent(self, keycode): 870 """Sends keycode to the device. 871 872 Args: 873 keycode: Numeric keycode to send (see "enum" at top of file). 874 """ 875 self.RunShellCommand('input keyevent %d' % keycode) 876 877 def _RunMd5Sum(self, host_path, device_path): 878 """Gets the md5sum of a host path and device path. 879 880 Args: 881 host_path: Path (file or directory) on the host. 882 device_path: Path on the device. 883 884 Returns: 885 A tuple containing lists of the host and device md5sum results as 886 created by _ParseMd5SumOutput(). 887 """ 888 md5sum_dist_path = os.path.join(constants.GetOutDirectory(), 889 'md5sum_dist') 890 assert os.path.exists(md5sum_dist_path), 'Please build md5sum.' 891 command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER) 892 assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) 893 894 cmd = (MD5SUM_LD_LIBRARY_PATH + ' ' + self._util_wrapper + ' ' + 895 MD5SUM_DEVICE_PATH + ' ' + device_path) 896 device_hash_tuples = _ParseMd5SumOutput( 897 self.RunShellCommand(cmd, timeout_time=2 * 60)) 898 assert os.path.exists(host_path), 'Local path not found %s' % host_path 899 md5sum_output = cmd_helper.GetCmdOutput( 900 [os.path.join(constants.GetOutDirectory(), 'md5sum_bin_host'), 901 host_path]) 902 host_hash_tuples = _ParseMd5SumOutput(md5sum_output.splitlines()) 903 return (host_hash_tuples, device_hash_tuples) 904 905 def GetFilesChanged(self, host_path, device_path, ignore_filenames=False): 906 """Compares the md5sum of a host path against a device path. 907 908 Note: Ignores extra files on the device. 909 910 Args: 911 host_path: Path (file or directory) on the host. 912 device_path: Path on the device. 913 ignore_filenames: If True only the file contents are considered when 914 checking whether a file has changed, otherwise the relative path 915 must also match. 916 917 Returns: 918 A list of tuples of the form (host_path, device_path) for files whose 919 md5sums do not match. 920 """ 921 922 # Md5Sum resolves symbolic links in path names so the calculation of 923 # relative path names from its output will need the real path names of the 924 # base directories. Having calculated these they are used throughout the 925 # function since this makes us less subject to any future changes to Md5Sum. 926 real_host_path = os.path.realpath(host_path) 927 real_device_path = self.RunShellCommand('realpath "%s"' % device_path)[0] 928 929 host_hash_tuples, device_hash_tuples = self._RunMd5Sum( 930 real_host_path, real_device_path) 931 932 # Ignore extra files on the device. 933 if not ignore_filenames: 934 host_files = [os.path.relpath(os.path.normpath(p.path), 935 real_host_path) for p in host_hash_tuples] 936 937 def HostHas(fname): 938 return any(path in fname for path in host_files) 939 940 device_hash_tuples = [h for h in device_hash_tuples if HostHas(h.path)] 941 942 if len(host_hash_tuples) > len(device_hash_tuples): 943 logging.info('%s files do not exist on the device' % 944 (len(host_hash_tuples) - len(device_hash_tuples))) 945 946 # Constructs the target device path from a given host path. Don't use when 947 # only a single file is given as the base name given in device_path may 948 # differ from that in host_path. 949 def HostToDevicePath(host_file_path): 950 return os.path.join(device_path, os.path.relpath(host_file_path, 951 real_host_path)) 952 953 device_hashes = [h.hash for h in device_hash_tuples] 954 return [(t.path, HostToDevicePath(t.path) if 955 os.path.isdir(real_host_path) else real_device_path) 956 for t in host_hash_tuples if t.hash not in device_hashes] 957 958 def PushIfNeeded(self, host_path, device_path): 959 """Pushes |host_path| to |device_path|. 960 961 Works for files and directories. This method skips copying any paths in 962 |test_data_paths| that already exist on the device with the same hash. 963 964 All pushed files can be removed by calling RemovePushedFiles(). 965 """ 966 MAX_INDIVIDUAL_PUSHES = 50 967 assert os.path.exists(host_path), 'Local path not found %s' % host_path 968 969 # See if the file on the host changed since the last push (if any) and 970 # return early if it didn't. Note that this shortcut assumes that the tests 971 # on the device don't modify the files. 972 if not os.path.isdir(host_path): 973 if host_path in self._push_if_needed_cache: 974 host_path_mtime = self._push_if_needed_cache[host_path] 975 if host_path_mtime == os.stat(host_path).st_mtime: 976 return 977 978 def GetHostSize(path): 979 return int(cmd_helper.GetCmdOutput(['du', '-sb', path]).split()[0]) 980 981 size = GetHostSize(host_path) 982 self._pushed_files.append(device_path) 983 self._potential_push_size += size 984 985 changed_files = self.GetFilesChanged(host_path, device_path) 986 logging.info('Found %d files that need to be pushed to %s', 987 len(changed_files), device_path) 988 if not changed_files: 989 return 990 991 def Push(host, device): 992 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout 993 # of 60 seconds which isn't sufficient for a lot of users of this method. 994 push_command = 'push %s %s' % (host, device) 995 self._LogShell(push_command) 996 997 # Retry push with increasing backoff if the device is busy. 998 retry = 0 999 while True: 1000 output = self._adb.SendCommand(push_command, timeout_time=30 * 60) 1001 if _HasAdbPushSucceeded(output): 1002 if not os.path.isdir(host_path): 1003 self._push_if_needed_cache[host] = os.stat(host).st_mtime 1004 return 1005 if retry < 3: 1006 retry += 1 1007 wait_time = 5 * retry 1008 logging.error('Push failed, retrying in %d seconds: %s' % 1009 (wait_time, output)) 1010 time.sleep(wait_time) 1011 else: 1012 raise Exception('Push failed: %s' % output) 1013 1014 diff_size = 0 1015 if len(changed_files) <= MAX_INDIVIDUAL_PUSHES: 1016 diff_size = sum(GetHostSize(f[0]) for f in changed_files) 1017 1018 # TODO(craigdh): Replace this educated guess with a heuristic that 1019 # approximates the push time for each method. 1020 if len(changed_files) > MAX_INDIVIDUAL_PUSHES or diff_size > 0.5 * size: 1021 self._actual_push_size += size 1022 if os.path.isdir(host_path): 1023 self.RunShellCommand('mkdir -p %s' % device_path) 1024 Push(host_path, device_path) 1025 else: 1026 for f in changed_files: 1027 Push(f[0], f[1]) 1028 self._actual_push_size += diff_size 1029 1030 def GetPushSizeInfo(self): 1031 """Get total size of pushes to the device done via PushIfNeeded() 1032 1033 Returns: 1034 A tuple: 1035 1. Total size of push requests to PushIfNeeded (MB) 1036 2. Total size that was actually pushed (MB) 1037 """ 1038 return (self._potential_push_size, self._actual_push_size) 1039 1040 def GetFileContents(self, filename, log_result=False): 1041 """Gets contents from the file specified by |filename|.""" 1042 return self.RunShellCommand('cat "%s" 2>/dev/null' % filename, 1043 log_result=log_result) 1044 1045 def SetFileContents(self, filename, contents): 1046 """Writes |contents| to the file specified by |filename|.""" 1047 with tempfile.NamedTemporaryFile() as f: 1048 f.write(contents) 1049 f.flush() 1050 self._adb.Push(f.name, filename) 1051 1052 _TEMP_FILE_BASE_FMT = 'temp_file_%d' 1053 _TEMP_SCRIPT_FILE_BASE_FMT = 'temp_script_file_%d.sh' 1054 1055 def _GetDeviceTempFileName(self, base_name): 1056 i = 0 1057 while self.FileExistsOnDevice( 1058 self.GetExternalStorage() + '/' + base_name % i): 1059 i += 1 1060 return self.GetExternalStorage() + '/' + base_name % i 1061 1062 def RunShellCommandWithSU(self, command, timeout_time=20, log_result=False): 1063 return self.RunShellCommand('su -c %s' % command, timeout_time, log_result) 1064 1065 def CanAccessProtectedFileContents(self): 1066 """Returns True if Get/SetProtectedFileContents would work via "su". 1067 1068 Devices running user builds don't have adb root, but may provide "su" which 1069 can be used for accessing protected files. 1070 """ 1071 r = self.RunShellCommandWithSU('cat /dev/null') 1072 return r == [] or r[0].strip() == '' 1073 1074 def GetProtectedFileContents(self, filename, log_result=False): 1075 """Gets contents from the protected file specified by |filename|. 1076 1077 This is less efficient than GetFileContents, but will work for protected 1078 files and device files. 1079 """ 1080 # Run the script as root 1081 return self.RunShellCommandWithSU('cat "%s" 2> /dev/null' % filename) 1082 1083 def SetProtectedFileContents(self, filename, contents): 1084 """Writes |contents| to the protected file specified by |filename|. 1085 1086 This is less efficient than SetFileContents, but will work for protected 1087 files and device files. 1088 """ 1089 temp_file = self._GetDeviceTempFileName(AndroidCommands._TEMP_FILE_BASE_FMT) 1090 temp_script = self._GetDeviceTempFileName( 1091 AndroidCommands._TEMP_SCRIPT_FILE_BASE_FMT) 1092 1093 # Put the contents in a temporary file 1094 self.SetFileContents(temp_file, contents) 1095 # Create a script to copy the file contents to its final destination 1096 self.SetFileContents(temp_script, 'cat %s > %s' % (temp_file, filename)) 1097 # Run the script as root 1098 self.RunShellCommandWithSU('sh %s' % temp_script) 1099 # And remove the temporary files 1100 self.RunShellCommand('rm ' + temp_file) 1101 self.RunShellCommand('rm ' + temp_script) 1102 1103 def RemovePushedFiles(self): 1104 """Removes all files pushed with PushIfNeeded() from the device.""" 1105 for p in self._pushed_files: 1106 self.RunShellCommand('rm -r %s' % p, timeout_time=2 * 60) 1107 1108 def ListPathContents(self, path): 1109 """Lists files in all subdirectories of |path|. 1110 1111 Args: 1112 path: The path to list. 1113 1114 Returns: 1115 A dict of {"name": (size, lastmod), ...}. 1116 """ 1117 # Example output: 1118 # /foo/bar: 1119 # -rw-r----- 1 user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt 1120 re_file = re.compile('^-(?P<perms>[^\s]+)\s+' 1121 '(?P<user>[^\s]+)\s+' 1122 '(?P<group>[^\s]+)\s+' 1123 '(?P<size>[^\s]+)\s+' 1124 '(?P<date>[^\s]+)\s+' 1125 '(?P<time>[^\s]+)\s+' 1126 '(?P<filename>[^\s]+)$') 1127 return _GetFilesFromRecursiveLsOutput( 1128 path, self.RunShellCommand('ls -lR %s' % path), re_file, 1129 self.GetUtcOffset()) 1130 1131 def GetUtcOffset(self): 1132 if not self._device_utc_offset: 1133 self._device_utc_offset = self.RunShellCommand('date +%z')[0] 1134 return self._device_utc_offset 1135 1136 def SetJavaAssertsEnabled(self, enable): 1137 """Sets or removes the device java assertions property. 1138 1139 Args: 1140 enable: If True the property will be set. 1141 1142 Returns: 1143 True if the file was modified (reboot is required for it to take effect). 1144 """ 1145 # First ensure the desired property is persisted. 1146 temp_props_file = tempfile.NamedTemporaryFile() 1147 properties = '' 1148 if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name): 1149 properties = file(temp_props_file.name).read() 1150 re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + 1151 r'\s*=\s*all\s*$', re.MULTILINE) 1152 if enable != bool(re.search(re_search, properties)): 1153 re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + 1154 r'\s*=\s*\w+\s*$', re.MULTILINE) 1155 properties = re.sub(re_replace, '', properties) 1156 if enable: 1157 properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY 1158 1159 file(temp_props_file.name, 'w').write(properties) 1160 self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) 1161 1162 # Next, check the current runtime value is what we need, and 1163 # if not, set it and report that a reboot is required. 1164 was_set = 'all' in self.system_properties[JAVA_ASSERT_PROPERTY] 1165 if was_set == enable: 1166 return False 1167 self.system_properties[JAVA_ASSERT_PROPERTY] = enable and 'all' or '' 1168 return True 1169 1170 def GetBuildId(self): 1171 """Returns the build ID of the system (e.g. JRM79C).""" 1172 build_id = self.system_properties['ro.build.id'] 1173 assert build_id 1174 return build_id 1175 1176 def GetBuildType(self): 1177 """Returns the build type of the system (e.g. eng).""" 1178 build_type = self.system_properties['ro.build.type'] 1179 assert build_type 1180 return build_type 1181 1182 def GetBuildProduct(self): 1183 """Returns the build product of the device (e.g. maguro).""" 1184 build_product = self.system_properties['ro.build.product'] 1185 assert build_product 1186 return build_product 1187 1188 def GetProductName(self): 1189 """Returns the product name of the device (e.g. takju).""" 1190 name = self.system_properties['ro.product.name'] 1191 assert name 1192 return name 1193 1194 def GetBuildFingerprint(self): 1195 """Returns the build fingerprint of the device.""" 1196 build_fingerprint = self.system_properties['ro.build.fingerprint'] 1197 assert build_fingerprint 1198 return build_fingerprint 1199 1200 def GetDescription(self): 1201 """Returns the description of the system. 1202 1203 For example, "yakju-userdebug 4.1 JRN54F 364167 dev-keys". 1204 """ 1205 description = self.system_properties['ro.build.description'] 1206 assert description 1207 return description 1208 1209 def GetProductModel(self): 1210 """Returns the name of the product model (e.g. "Galaxy Nexus") """ 1211 model = self.system_properties['ro.product.model'] 1212 assert model 1213 return model 1214 1215 def GetWifiIP(self): 1216 """Returns the wifi IP on the device.""" 1217 wifi_ip = self.system_properties['dhcp.wlan0.ipaddress'] 1218 # Do not assert here. Devices (e.g. emulators) may not have a WifiIP. 1219 return wifi_ip 1220 1221 def GetSubscriberInfo(self): 1222 """Returns the device subscriber info (e.g. GSM and device ID) as string.""" 1223 iphone_sub = self.RunShellCommand('dumpsys iphonesubinfo') 1224 assert iphone_sub 1225 return '\n'.join(iphone_sub) 1226 1227 def GetBatteryInfo(self): 1228 """Returns the device battery info (e.g. status, level, etc) as string.""" 1229 battery = self.RunShellCommand('dumpsys battery') 1230 assert battery 1231 return '\n'.join(battery) 1232 1233 def GetSetupWizardStatus(self): 1234 """Returns the status of the device setup wizard (e.g. DISABLED).""" 1235 status = self.system_properties['ro.setupwizard.mode'] 1236 # On some devices, the status is empty if not otherwise set. In such cases 1237 # the caller should expect an empty string to be returned. 1238 return status 1239 1240 def StartMonitoringLogcat(self, clear=True, logfile=None, filters=None): 1241 """Starts monitoring the output of logcat, for use with WaitForLogMatch. 1242 1243 Args: 1244 clear: If True the existing logcat output will be cleared, to avoiding 1245 matching historical output lurking in the log. 1246 filters: A list of logcat filters to be used. 1247 """ 1248 if clear: 1249 self.RunShellCommand('logcat -c') 1250 args = [] 1251 if self._adb._target_arg: 1252 args += shlex.split(self._adb._target_arg) 1253 args += ['logcat', '-v', 'threadtime'] 1254 if filters: 1255 args.extend(filters) 1256 else: 1257 args.append('*:v') 1258 1259 if logfile: 1260 logfile = NewLineNormalizer(logfile) 1261 1262 # Spawn logcat and synchronize with it. 1263 for _ in range(4): 1264 self._logcat = pexpect.spawn(constants.ADB_PATH, args, timeout=10, 1265 logfile=logfile) 1266 if not clear or self.SyncLogCat(): 1267 break 1268 self._logcat.close(force=True) 1269 else: 1270 logging.critical('Error reading from logcat: ' + str(self._logcat.match)) 1271 sys.exit(1) 1272 1273 def SyncLogCat(self): 1274 """Synchronize with logcat. 1275 1276 Synchronize with the monitored logcat so that WaitForLogMatch will only 1277 consider new message that are received after this point in time. 1278 1279 Returns: 1280 True if the synchronization succeeded. 1281 """ 1282 assert self._logcat 1283 tag = 'logcat_sync_%s' % time.time() 1284 self.RunShellCommand('log ' + tag) 1285 return self._logcat.expect([tag, pexpect.EOF, pexpect.TIMEOUT]) == 0 1286 1287 def GetMonitoredLogCat(self): 1288 """Returns an "adb logcat" command as created by pexpected.spawn.""" 1289 if not self._logcat: 1290 self.StartMonitoringLogcat(clear=False) 1291 return self._logcat 1292 1293 def WaitForLogMatch(self, success_re, error_re, clear=False, timeout=10): 1294 """Blocks until a matching line is logged or a timeout occurs. 1295 1296 Args: 1297 success_re: A compiled re to search each line for. 1298 error_re: A compiled re which, if found, terminates the search for 1299 |success_re|. If None is given, no error condition will be detected. 1300 clear: If True the existing logcat output will be cleared, defaults to 1301 false. 1302 timeout: Timeout in seconds to wait for a log match. 1303 1304 Raises: 1305 pexpect.TIMEOUT after |timeout| seconds without a match for |success_re| 1306 or |error_re|. 1307 1308 Returns: 1309 The re match object if |success_re| is matched first or None if |error_re| 1310 is matched first. 1311 """ 1312 logging.info('<<< Waiting for logcat:' + str(success_re.pattern)) 1313 t0 = time.time() 1314 while True: 1315 if not self._logcat: 1316 self.StartMonitoringLogcat(clear) 1317 try: 1318 while True: 1319 # Note this will block for upto the timeout _per log line_, so we need 1320 # to calculate the overall timeout remaining since t0. 1321 time_remaining = t0 + timeout - time.time() 1322 if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat) 1323 self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining) 1324 line = self._logcat.match.group(1) 1325 if error_re: 1326 error_match = error_re.search(line) 1327 if error_match: 1328 return None 1329 success_match = success_re.search(line) 1330 if success_match: 1331 return success_match 1332 logging.info('<<< Skipped Logcat Line:' + str(line)) 1333 except pexpect.TIMEOUT: 1334 raise pexpect.TIMEOUT( 1335 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv ' 1336 'to debug)' % 1337 (timeout, success_re.pattern)) 1338 except pexpect.EOF: 1339 # It seems that sometimes logcat can end unexpectedly. This seems 1340 # to happen during Chrome startup after a reboot followed by a cache 1341 # clean. I don't understand why this happens, but this code deals with 1342 # getting EOF in logcat. 1343 logging.critical('Found EOF in adb logcat. Restarting...') 1344 # Rerun spawn with original arguments. Note that self._logcat.args[0] is 1345 # the path of adb, so we don't want it in the arguments. 1346 self._logcat = pexpect.spawn(constants.ADB_PATH, 1347 self._logcat.args[1:], 1348 timeout=self._logcat.timeout, 1349 logfile=self._logcat.logfile) 1350 1351 def StartRecordingLogcat(self, clear=True, filters=['*:v']): 1352 """Starts recording logcat output to eventually be saved as a string. 1353 1354 This call should come before some series of tests are run, with either 1355 StopRecordingLogcat or SearchLogcatRecord following the tests. 1356 1357 Args: 1358 clear: True if existing log output should be cleared. 1359 filters: A list of logcat filters to be used. 1360 """ 1361 if clear: 1362 self._adb.SendCommand('logcat -c') 1363 logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg, 1364 ' '.join(filters)) 1365 self._logcat_tmpoutfile = tempfile.NamedTemporaryFile(bufsize=0) 1366 self.logcat_process = subprocess.Popen(logcat_command, shell=True, 1367 stdout=self._logcat_tmpoutfile) 1368 1369 def GetCurrentRecordedLogcat(self): 1370 """Return the current content of the logcat being recorded. 1371 Call this after StartRecordingLogcat() and before StopRecordingLogcat(). 1372 This can be useful to perform timed polling/parsing. 1373 Returns: 1374 Current logcat output as a single string, or None if 1375 StopRecordingLogcat() was already called. 1376 """ 1377 if not self._logcat_tmpoutfile: 1378 return None 1379 1380 with open(self._logcat_tmpoutfile.name) as f: 1381 return f.read() 1382 1383 def StopRecordingLogcat(self): 1384 """Stops an existing logcat recording subprocess and returns output. 1385 1386 Returns: 1387 The logcat output as a string or an empty string if logcat was not 1388 being recorded at the time. 1389 """ 1390 if not self.logcat_process: 1391 return '' 1392 # Cannot evaluate directly as 0 is a possible value. 1393 # Better to read the self.logcat_process.stdout before killing it, 1394 # Otherwise the communicate may return incomplete output due to pipe break. 1395 if self.logcat_process.poll() is None: 1396 self.logcat_process.kill() 1397 self.logcat_process.wait() 1398 self.logcat_process = None 1399 self._logcat_tmpoutfile.seek(0) 1400 output = self._logcat_tmpoutfile.read() 1401 self._logcat_tmpoutfile.close() 1402 self._logcat_tmpoutfile = None 1403 return output 1404 1405 def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None, 1406 log_level=None, component=None): 1407 """Searches the specified logcat output and returns results. 1408 1409 This method searches through the logcat output specified by record for a 1410 certain message, narrowing results by matching them against any other 1411 specified criteria. It returns all matching lines as described below. 1412 1413 Args: 1414 record: A string generated by Start/StopRecordingLogcat to search. 1415 message: An output string to search for. 1416 thread_id: The thread id that is the origin of the message. 1417 proc_id: The process that is the origin of the message. 1418 log_level: The log level of the message. 1419 component: The name of the component that would create the message. 1420 1421 Returns: 1422 A list of dictionaries represeting matching entries, each containing keys 1423 thread_id, proc_id, log_level, component, and message. 1424 """ 1425 if thread_id: 1426 thread_id = str(thread_id) 1427 if proc_id: 1428 proc_id = str(proc_id) 1429 results = [] 1430 reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$', 1431 re.MULTILINE) 1432 log_list = reg.findall(record) 1433 for (tid, pid, log_lev, comp, msg) in log_list: 1434 if ((not thread_id or thread_id == tid) and 1435 (not proc_id or proc_id == pid) and 1436 (not log_level or log_level == log_lev) and 1437 (not component or component == comp) and msg.find(message) > -1): 1438 match = dict({'thread_id': tid, 'proc_id': pid, 1439 'log_level': log_lev, 'component': comp, 1440 'message': msg}) 1441 results.append(match) 1442 return results 1443 1444 def ExtractPid(self, process_name): 1445 """Extracts Process Ids for a given process name from Android Shell. 1446 1447 Args: 1448 process_name: name of the process on the device. 1449 1450 Returns: 1451 List of all the process ids (as strings) that match the given name. 1452 If the name of a process exactly matches the given name, the pid of 1453 that process will be inserted to the front of the pid list. 1454 """ 1455 pids = [] 1456 for line in self.RunShellCommand('ps', log_result=False): 1457 data = line.split() 1458 try: 1459 if process_name in data[-1]: # name is in the last column 1460 if process_name == data[-1]: 1461 pids.insert(0, data[1]) # PID is in the second column 1462 else: 1463 pids.append(data[1]) 1464 except IndexError: 1465 pass 1466 return pids 1467 1468 def GetIoStats(self): 1469 """Gets cumulative disk IO stats since boot (for all processes). 1470 1471 Returns: 1472 Dict of {num_reads, num_writes, read_ms, write_ms} or None if there 1473 was an error. 1474 """ 1475 IoStats = collections.namedtuple( 1476 'IoStats', 1477 ['device', 1478 'num_reads_issued', 1479 'num_reads_merged', 1480 'num_sectors_read', 1481 'ms_spent_reading', 1482 'num_writes_completed', 1483 'num_writes_merged', 1484 'num_sectors_written', 1485 'ms_spent_writing', 1486 'num_ios_in_progress', 1487 'ms_spent_doing_io', 1488 'ms_spent_doing_io_weighted', 1489 ]) 1490 1491 for line in self.GetFileContents('/proc/diskstats', log_result=False): 1492 fields = line.split() 1493 stats = IoStats._make([fields[2]] + [int(f) for f in fields[3:]]) 1494 if stats.device == 'mmcblk0': 1495 return { 1496 'num_reads': stats.num_reads_issued, 1497 'num_writes': stats.num_writes_completed, 1498 'read_ms': stats.ms_spent_reading, 1499 'write_ms': stats.ms_spent_writing, 1500 } 1501 logging.warning('Could not find disk IO stats.') 1502 return None 1503 1504 def PurgeUnpinnedAshmem(self): 1505 """Purges the unpinned ashmem memory for the whole system. 1506 1507 This can be used to make memory measurements more stable in particular. 1508 """ 1509 host_path = host_path_finder.GetMostRecentHostPath('purge_ashmem') 1510 if not host_path: 1511 raise Exception('Could not find the purge_ashmem binary.') 1512 device_path = os.path.join(constants.TEST_EXECUTABLE_DIR, 'purge_ashmem') 1513 self.PushIfNeeded(host_path, device_path) 1514 if self.RunShellCommand(device_path, log_result=True): 1515 return 1516 raise Exception('Error while purging ashmem.') 1517 1518 def GetMemoryUsageForPid(self, pid): 1519 """Returns the memory usage for given pid. 1520 1521 Args: 1522 pid: The pid number of the specific process running on device. 1523 1524 Returns: 1525 A tuple containg: 1526 [0]: Dict of {metric:usage_kb}, for the process which has specified pid. 1527 The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, 1528 Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, 1529 KernelPageSize, MMUPageSize, Nvidia (tablet only), VmHWM. 1530 [1]: Detailed /proc/[PID]/smaps information. 1531 """ 1532 usage_dict = collections.defaultdict(int) 1533 smaps = collections.defaultdict(dict) 1534 current_smap = '' 1535 for line in self.GetProtectedFileContents('/proc/%s/smaps' % pid, 1536 log_result=False): 1537 items = line.split() 1538 # See man 5 proc for more details. The format is: 1539 # address perms offset dev inode pathname 1540 if len(items) > 5: 1541 current_smap = ' '.join(items[5:]) 1542 elif len(items) > 3: 1543 current_smap = ' '.join(items[3:]) 1544 match = re.match(MEMORY_INFO_RE, line) 1545 if match: 1546 key = match.group('key') 1547 usage_kb = int(match.group('usage_kb')) 1548 usage_dict[key] += usage_kb 1549 if key not in smaps[current_smap]: 1550 smaps[current_smap][key] = 0 1551 smaps[current_smap][key] += usage_kb 1552 if not usage_dict or not any(usage_dict.values()): 1553 # Presumably the process died between ps and calling this method. 1554 logging.warning('Could not find memory usage for pid ' + str(pid)) 1555 1556 for line in self.GetProtectedFileContents('/d/nvmap/generic-0/clients', 1557 log_result=False): 1558 match = re.match(NVIDIA_MEMORY_INFO_RE, line) 1559 if match and match.group('pid') == pid: 1560 usage_bytes = int(match.group('usage_bytes')) 1561 usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB 1562 break 1563 1564 peak_value_kb = 0 1565 for line in self.GetProtectedFileContents('/proc/%s/status' % pid, 1566 log_result=False): 1567 if not line.startswith('VmHWM:'): # Format: 'VmHWM: +[0-9]+ kB' 1568 continue 1569 peak_value_kb = int(line.split(':')[1].strip().split(' ')[0]) 1570 usage_dict['VmHWM'] = peak_value_kb 1571 if not peak_value_kb: 1572 logging.warning('Could not find memory peak value for pid ' + str(pid)) 1573 1574 return (usage_dict, smaps) 1575 1576 def GetMemoryUsageForPackage(self, package): 1577 """Returns the memory usage for all processes whose name contains |pacakge|. 1578 1579 Args: 1580 package: A string holding process name to lookup pid list for. 1581 1582 Returns: 1583 A tuple containg: 1584 [0]: Dict of {metric:usage_kb}, summed over all pids associated with 1585 |name|. 1586 The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, 1587 Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, 1588 KernelPageSize, MMUPageSize, Nvidia (tablet only). 1589 [1]: a list with detailed /proc/[PID]/smaps information. 1590 """ 1591 usage_dict = collections.defaultdict(int) 1592 pid_list = self.ExtractPid(package) 1593 smaps = collections.defaultdict(dict) 1594 1595 for pid in pid_list: 1596 usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid) 1597 smaps[pid] = smaps_per_pid 1598 for (key, value) in usage_dict_per_pid.items(): 1599 usage_dict[key] += value 1600 1601 return usage_dict, smaps 1602 1603 def ProcessesUsingDevicePort(self, device_port): 1604 """Lists processes using the specified device port on loopback interface. 1605 1606 Args: 1607 device_port: Port on device we want to check. 1608 1609 Returns: 1610 A list of (pid, process_name) tuples using the specified port. 1611 """ 1612 tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False) 1613 tcp_address = '0100007F:%04X' % device_port 1614 pids = [] 1615 for single_connect in tcp_results: 1616 connect_results = single_connect.split() 1617 # Column 1 is the TCP port, and Column 9 is the inode of the socket 1618 if connect_results[1] == tcp_address: 1619 socket_inode = connect_results[9] 1620 socket_name = 'socket:[%s]' % socket_inode 1621 lsof_results = self.RunShellCommand('lsof', log_result=False) 1622 for single_process in lsof_results: 1623 process_results = single_process.split() 1624 # Ignore the line if it has less than nine columns in it, which may 1625 # be the case when a process stops while lsof is executing. 1626 if len(process_results) <= 8: 1627 continue 1628 # Column 0 is the executable name 1629 # Column 1 is the pid 1630 # Column 8 is the Inode in use 1631 if process_results[8] == socket_name: 1632 pids.append((int(process_results[1]), process_results[0])) 1633 break 1634 logging.info('PidsUsingDevicePort: %s', pids) 1635 return pids 1636 1637 def FileExistsOnDevice(self, file_name): 1638 """Checks whether the given file exists on the device. 1639 1640 Args: 1641 file_name: Full path of file to check. 1642 1643 Returns: 1644 True if the file exists, False otherwise. 1645 """ 1646 assert '"' not in file_name, 'file_name cannot contain double quotes' 1647 try: 1648 status = self._adb.SendShellCommand( 1649 '\'test -e "%s"; echo $?\'' % (file_name)) 1650 if 'test: not found' not in status: 1651 return int(status) == 0 1652 1653 status = self._adb.SendShellCommand( 1654 '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name)) 1655 return int(status) == 0 1656 except ValueError: 1657 if IsDeviceAttached(self._device): 1658 raise errors.DeviceUnresponsiveError('Device may be offline.') 1659 1660 return False 1661 1662 def IsFileWritableOnDevice(self, file_name): 1663 """Checks whether the given file (or directory) is writable on the device. 1664 1665 Args: 1666 file_name: Full path of file/directory to check. 1667 1668 Returns: 1669 True if writable, False otherwise. 1670 """ 1671 assert '"' not in file_name, 'file_name cannot contain double quotes' 1672 try: 1673 status = self._adb.SendShellCommand( 1674 '\'test -w "%s"; echo $?\'' % (file_name)) 1675 if 'test: not found' not in status: 1676 return int(status) == 0 1677 raise errors.AbortError('"test" binary not found. OS too old.') 1678 1679 except ValueError: 1680 if IsDeviceAttached(self._device): 1681 raise errors.DeviceUnresponsiveError('Device may be offline.') 1682 1683 return False 1684 1685 def TakeScreenshot(self, host_file): 1686 """Saves a screenshot image to |host_file| on the host. 1687 1688 Args: 1689 host_file: Absolute path to the image file to store on the host or None to 1690 use an autogenerated file name. 1691 1692 Returns: 1693 Resulting host file name of the screenshot. 1694 """ 1695 return screenshot.TakeScreenshot(self, host_file) 1696 1697 def PullFileFromDevice(self, device_file, host_file): 1698 """Download |device_file| on the device from to |host_file| on the host. 1699 1700 Args: 1701 device_file: Absolute path to the file to retrieve from the device. 1702 host_file: Absolute path to the file to store on the host. 1703 """ 1704 assert self._adb.Pull(device_file, host_file) 1705 assert os.path.exists(host_file) 1706 1707 def SetUtilWrapper(self, util_wrapper): 1708 """Sets a wrapper prefix to be used when running a locally-built 1709 binary on the device (ex.: md5sum_bin). 1710 """ 1711 self._util_wrapper = util_wrapper 1712 1713 def RunInstrumentationTest(self, test, test_package, instr_args, timeout): 1714 """Runs a single instrumentation test. 1715 1716 Args: 1717 test: Test class/method. 1718 test_package: Package name of test apk. 1719 instr_args: Extra key/value to pass to am instrument. 1720 timeout: Timeout time in seconds. 1721 1722 Returns: 1723 An instance of am_instrument_parser.TestResult object. 1724 """ 1725 instrumentation_path = ('%s/android.test.InstrumentationTestRunner' % 1726 test_package) 1727 args_with_filter = dict(instr_args) 1728 args_with_filter['class'] = test 1729 logging.info(args_with_filter) 1730 (raw_results, _) = self._adb.StartInstrumentation( 1731 instrumentation_path=instrumentation_path, 1732 instrumentation_args=args_with_filter, 1733 timeout_time=timeout) 1734 assert len(raw_results) == 1 1735 return raw_results[0] 1736 1737 def RunUIAutomatorTest(self, test, test_package, timeout): 1738 """Runs a single uiautomator test. 1739 1740 Args: 1741 test: Test class/method. 1742 test_package: Name of the test jar. 1743 timeout: Timeout time in seconds. 1744 1745 Returns: 1746 An instance of am_instrument_parser.TestResult object. 1747 """ 1748 cmd = 'uiautomator runtest %s -e class %s' % (test_package, test) 1749 self._LogShell(cmd) 1750 output = self._adb.SendShellCommand(cmd, timeout_time=timeout) 1751 # uiautomator doesn't fully conform to the instrumenation test runner 1752 # convention and doesn't terminate with INSTRUMENTATION_CODE. 1753 # Just assume the first result is valid. 1754 (test_results, _) = am_instrument_parser.ParseAmInstrumentOutput(output) 1755 if not test_results: 1756 raise errors.InstrumentationError( 1757 'no test results... device setup correctly?') 1758 return test_results[0] 1759 1760 def DismissCrashDialogIfNeeded(self): 1761 """Dismiss the error/ANR dialog if present. 1762 1763 Returns: Name of the crashed package if a dialog is focused, 1764 None otherwise. 1765 """ 1766 re_focus = re.compile( 1767 r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}') 1768 1769 def _FindFocusedWindow(): 1770 match = None 1771 for line in self.RunShellCommand('dumpsys window windows'): 1772 match = re.match(re_focus, line) 1773 if match: 1774 break 1775 return match 1776 1777 match = _FindFocusedWindow() 1778 if not match: 1779 return 1780 package = match.group(2) 1781 logging.warning('Trying to dismiss %s dialog for %s' % match.groups()) 1782 self.SendKeyEvent(KEYCODE_DPAD_RIGHT) 1783 self.SendKeyEvent(KEYCODE_DPAD_RIGHT) 1784 self.SendKeyEvent(KEYCODE_ENTER) 1785 match = _FindFocusedWindow() 1786 if match: 1787 logging.error('Still showing a %s dialog for %s' % match.groups()) 1788 return package 1789 1790 1791 class NewLineNormalizer(object): 1792 """A file-like object to normalize EOLs to '\n'. 1793 1794 Pexpect runs adb within a pseudo-tty device (see 1795 http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written 1796 as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate 1797 lines, the log ends up having '\r\r\n' at the end of each line. This 1798 filter replaces the above with a single '\n' in the data stream. 1799 """ 1800 def __init__(self, output): 1801 self._output = output 1802 1803 def write(self, data): 1804 data = data.replace('\r\r\n', '\n') 1805 self._output.write(data) 1806 1807 def flush(self): 1808 self._output.flush() 1809