Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
michael@0 | 2 | # Use of this source code is governed by a BSD-style license that can be |
michael@0 | 3 | # found in the LICENSE file. |
michael@0 | 4 | |
michael@0 | 5 | """Provides an interface to communicate with the device via the adb command. |
michael@0 | 6 | |
michael@0 | 7 | Assumes adb binary is currently on system path. |
michael@0 | 8 | """ |
michael@0 | 9 | |
michael@0 | 10 | import collections |
michael@0 | 11 | import datetime |
michael@0 | 12 | import logging |
michael@0 | 13 | import os |
michael@0 | 14 | import re |
michael@0 | 15 | import shlex |
michael@0 | 16 | import subprocess |
michael@0 | 17 | import sys |
michael@0 | 18 | import tempfile |
michael@0 | 19 | import time |
michael@0 | 20 | |
michael@0 | 21 | import io_stats_parser |
michael@0 | 22 | from pylib import pexpect |
michael@0 | 23 | |
michael@0 | 24 | CHROME_SRC = os.path.join( |
michael@0 | 25 | os.path.abspath(os.path.dirname(__file__)), '..', '..', '..') |
michael@0 | 26 | |
michael@0 | 27 | sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner')) |
michael@0 | 28 | import adb_interface |
michael@0 | 29 | |
michael@0 | 30 | import cmd_helper |
michael@0 | 31 | import errors # is under ../../../third_party/android_testrunner/errors.py |
michael@0 | 32 | |
michael@0 | 33 | |
michael@0 | 34 | # Pattern to search for the next whole line of pexpect output and capture it |
michael@0 | 35 | # into a match group. We can't use ^ and $ for line start end with pexpect, |
michael@0 | 36 | # see http://www.noah.org/python/pexpect/#doc for explanation why. |
michael@0 | 37 | PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') |
michael@0 | 38 | |
michael@0 | 39 | # Set the adb shell prompt to be a unique marker that will [hopefully] not |
michael@0 | 40 | # appear at the start of any line of a command's output. |
michael@0 | 41 | SHELL_PROMPT = '~+~PQ\x17RS~+~' |
michael@0 | 42 | |
michael@0 | 43 | # Java properties file |
michael@0 | 44 | LOCAL_PROPERTIES_PATH = '/data/local.prop' |
michael@0 | 45 | |
michael@0 | 46 | # Property in /data/local.prop that controls Java assertions. |
michael@0 | 47 | JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' |
michael@0 | 48 | |
michael@0 | 49 | MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$') |
michael@0 | 50 | NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*' |
michael@0 | 51 | '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$') |
michael@0 | 52 | |
michael@0 | 53 | # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). |
michael@0 | 54 | KEYCODE_HOME = 3 |
michael@0 | 55 | KEYCODE_BACK = 4 |
michael@0 | 56 | KEYCODE_DPAD_UP = 19 |
michael@0 | 57 | KEYCODE_DPAD_DOWN = 20 |
michael@0 | 58 | KEYCODE_DPAD_RIGHT = 22 |
michael@0 | 59 | KEYCODE_ENTER = 66 |
michael@0 | 60 | KEYCODE_MENU = 82 |
michael@0 | 61 | |
michael@0 | 62 | MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin' |
michael@0 | 63 | |
michael@0 | 64 | def GetEmulators(): |
michael@0 | 65 | """Returns a list of emulators. Does not filter by status (e.g. offline). |
michael@0 | 66 | |
michael@0 | 67 | Both devices starting with 'emulator' will be returned in below output: |
michael@0 | 68 | |
michael@0 | 69 | * daemon not running. starting it now on port 5037 * |
michael@0 | 70 | * daemon started successfully * |
michael@0 | 71 | List of devices attached |
michael@0 | 72 | 027c10494100b4d7 device |
michael@0 | 73 | emulator-5554 offline |
michael@0 | 74 | emulator-5558 device |
michael@0 | 75 | """ |
michael@0 | 76 | re_device = re.compile('^emulator-[0-9]+', re.MULTILINE) |
michael@0 | 77 | devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
michael@0 | 78 | return devices |
michael@0 | 79 | |
michael@0 | 80 | |
michael@0 | 81 | def GetAVDs(): |
michael@0 | 82 | """Returns a list of AVDs.""" |
michael@0 | 83 | re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE) |
michael@0 | 84 | avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd'])) |
michael@0 | 85 | return avds |
michael@0 | 86 | |
michael@0 | 87 | |
michael@0 | 88 | def GetAttachedDevices(): |
michael@0 | 89 | """Returns a list of attached, online android devices. |
michael@0 | 90 | |
michael@0 | 91 | If a preferred device has been set with ANDROID_SERIAL, it will be first in |
michael@0 | 92 | the returned list. |
michael@0 | 93 | |
michael@0 | 94 | Example output: |
michael@0 | 95 | |
michael@0 | 96 | * daemon not running. starting it now on port 5037 * |
michael@0 | 97 | * daemon started successfully * |
michael@0 | 98 | List of devices attached |
michael@0 | 99 | 027c10494100b4d7 device |
michael@0 | 100 | emulator-5554 offline |
michael@0 | 101 | """ |
michael@0 | 102 | re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) |
michael@0 | 103 | devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
michael@0 | 104 | preferred_device = os.environ.get('ANDROID_SERIAL') |
michael@0 | 105 | if preferred_device in devices: |
michael@0 | 106 | devices.remove(preferred_device) |
michael@0 | 107 | devices.insert(0, preferred_device) |
michael@0 | 108 | return devices |
michael@0 | 109 | |
michael@0 | 110 | def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): |
michael@0 | 111 | """Gets a list of files from `ls` command output. |
michael@0 | 112 | |
michael@0 | 113 | Python's os.walk isn't used because it doesn't work over adb shell. |
michael@0 | 114 | |
michael@0 | 115 | Args: |
michael@0 | 116 | path: The path to list. |
michael@0 | 117 | ls_output: A list of lines returned by an `ls -lR` command. |
michael@0 | 118 | re_file: A compiled regular expression which parses a line into named groups |
michael@0 | 119 | consisting of at minimum "filename", "date", "time", "size" and |
michael@0 | 120 | optionally "timezone". |
michael@0 | 121 | utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a |
michael@0 | 122 | 2-digit string giving the number of UTC offset hours, and MM is a |
michael@0 | 123 | 2-digit string giving the number of UTC offset minutes. If the input |
michael@0 | 124 | utc_offset is None, will try to look for the value of "timezone" if it |
michael@0 | 125 | is specified in re_file. |
michael@0 | 126 | |
michael@0 | 127 | Returns: |
michael@0 | 128 | A dict of {"name": (size, lastmod), ...} where: |
michael@0 | 129 | name: The file name relative to |path|'s directory. |
michael@0 | 130 | size: The file size in bytes (0 for directories). |
michael@0 | 131 | lastmod: The file last modification date in UTC. |
michael@0 | 132 | """ |
michael@0 | 133 | re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path)) |
michael@0 | 134 | path_dir = os.path.dirname(path) |
michael@0 | 135 | |
michael@0 | 136 | current_dir = '' |
michael@0 | 137 | files = {} |
michael@0 | 138 | for line in ls_output: |
michael@0 | 139 | directory_match = re_directory.match(line) |
michael@0 | 140 | if directory_match: |
michael@0 | 141 | current_dir = directory_match.group('dir') |
michael@0 | 142 | continue |
michael@0 | 143 | file_match = re_file.match(line) |
michael@0 | 144 | if file_match: |
michael@0 | 145 | filename = os.path.join(current_dir, file_match.group('filename')) |
michael@0 | 146 | if filename.startswith(path_dir): |
michael@0 | 147 | filename = filename[len(path_dir)+1:] |
michael@0 | 148 | lastmod = datetime.datetime.strptime( |
michael@0 | 149 | file_match.group('date') + ' ' + file_match.group('time')[:5], |
michael@0 | 150 | '%Y-%m-%d %H:%M') |
michael@0 | 151 | if not utc_offset and 'timezone' in re_file.groupindex: |
michael@0 | 152 | utc_offset = file_match.group('timezone') |
michael@0 | 153 | if isinstance(utc_offset, str) and len(utc_offset) == 5: |
michael@0 | 154 | utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), |
michael@0 | 155 | minutes=int(utc_offset[3:5])) |
michael@0 | 156 | if utc_offset[0:1] == '-': |
michael@0 | 157 | utc_delta = -utc_delta |
michael@0 | 158 | lastmod -= utc_delta |
michael@0 | 159 | files[filename] = (int(file_match.group('size')), lastmod) |
michael@0 | 160 | return files |
michael@0 | 161 | |
michael@0 | 162 | def _ComputeFileListHash(md5sum_output): |
michael@0 | 163 | """Returns a list of MD5 strings from the provided md5sum output.""" |
michael@0 | 164 | return [line.split(' ')[0] for line in md5sum_output] |
michael@0 | 165 | |
michael@0 | 166 | def _HasAdbPushSucceeded(command_output): |
michael@0 | 167 | """Returns whether adb push has succeeded from the provided output.""" |
michael@0 | 168 | if not command_output: |
michael@0 | 169 | return False |
michael@0 | 170 | # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" |
michael@0 | 171 | # Errors look like this: "failed to copy ... " |
michael@0 | 172 | if not re.search('^[0-9]', command_output.splitlines()[-1]): |
michael@0 | 173 | logging.critical('PUSH FAILED: ' + command_output) |
michael@0 | 174 | return False |
michael@0 | 175 | return True |
michael@0 | 176 | |
michael@0 | 177 | def GetLogTimestamp(log_line, year): |
michael@0 | 178 | """Returns the timestamp of the given |log_line| in the given year.""" |
michael@0 | 179 | try: |
michael@0 | 180 | return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), |
michael@0 | 181 | '%Y-%m-%d %H:%M:%S.%f') |
michael@0 | 182 | except (ValueError, IndexError): |
michael@0 | 183 | logging.critical('Error reading timestamp from ' + log_line) |
michael@0 | 184 | return None |
michael@0 | 185 | |
michael@0 | 186 | |
michael@0 | 187 | class AndroidCommands(object): |
michael@0 | 188 | """Helper class for communicating with Android device via adb. |
michael@0 | 189 | |
michael@0 | 190 | Args: |
michael@0 | 191 | device: If given, adb commands are only send to the device of this ID. |
michael@0 | 192 | Otherwise commands are sent to all attached devices. |
michael@0 | 193 | """ |
michael@0 | 194 | |
michael@0 | 195 | def __init__(self, device=None): |
michael@0 | 196 | self._adb = adb_interface.AdbInterface() |
michael@0 | 197 | if device: |
michael@0 | 198 | self._adb.SetTargetSerial(device) |
michael@0 | 199 | self._logcat = None |
michael@0 | 200 | self.logcat_process = None |
michael@0 | 201 | self._pushed_files = [] |
michael@0 | 202 | self._device_utc_offset = self.RunShellCommand('date +%z')[0] |
michael@0 | 203 | self._md5sum_path = '' |
michael@0 | 204 | self._external_storage = '' |
michael@0 | 205 | |
michael@0 | 206 | def Adb(self): |
michael@0 | 207 | """Returns our AdbInterface to avoid us wrapping all its methods.""" |
michael@0 | 208 | return self._adb |
michael@0 | 209 | |
michael@0 | 210 | def IsRootEnabled(self): |
michael@0 | 211 | """Checks if root is enabled on the device.""" |
michael@0 | 212 | root_test_output = self.RunShellCommand('ls /root') or [''] |
michael@0 | 213 | return not 'Permission denied' in root_test_output[0] |
michael@0 | 214 | |
michael@0 | 215 | def EnableAdbRoot(self): |
michael@0 | 216 | """Enables adb root on the device. |
michael@0 | 217 | |
michael@0 | 218 | Returns: |
michael@0 | 219 | True: if output from executing adb root was as expected. |
michael@0 | 220 | False: otherwise. |
michael@0 | 221 | """ |
michael@0 | 222 | return_value = self._adb.EnableAdbRoot() |
michael@0 | 223 | # EnableAdbRoot inserts a call for wait-for-device only when adb logcat |
michael@0 | 224 | # output matches what is expected. Just to be safe add a call to |
michael@0 | 225 | # wait-for-device. |
michael@0 | 226 | self._adb.SendCommand('wait-for-device') |
michael@0 | 227 | return return_value |
michael@0 | 228 | |
michael@0 | 229 | def GetDeviceYear(self): |
michael@0 | 230 | """Returns the year information of the date on device.""" |
michael@0 | 231 | return self.RunShellCommand('date +%Y')[0] |
michael@0 | 232 | |
michael@0 | 233 | def GetExternalStorage(self): |
michael@0 | 234 | if not self._external_storage: |
michael@0 | 235 | self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0] |
michael@0 | 236 | assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE' |
michael@0 | 237 | return self._external_storage |
michael@0 | 238 | |
michael@0 | 239 | def WaitForDevicePm(self): |
michael@0 | 240 | """Blocks until the device's package manager is available. |
michael@0 | 241 | |
michael@0 | 242 | To workaround http://b/5201039, we restart the shell and retry if the |
michael@0 | 243 | package manager isn't back after 120 seconds. |
michael@0 | 244 | |
michael@0 | 245 | Raises: |
michael@0 | 246 | errors.WaitForResponseTimedOutError after max retries reached. |
michael@0 | 247 | """ |
michael@0 | 248 | last_err = None |
michael@0 | 249 | retries = 3 |
michael@0 | 250 | while retries: |
michael@0 | 251 | try: |
michael@0 | 252 | self._adb.WaitForDevicePm() |
michael@0 | 253 | return # Success |
michael@0 | 254 | except errors.WaitForResponseTimedOutError as e: |
michael@0 | 255 | last_err = e |
michael@0 | 256 | logging.warning('Restarting and retrying after timeout: %s', e) |
michael@0 | 257 | retries -= 1 |
michael@0 | 258 | self.RestartShell() |
michael@0 | 259 | raise last_err # Only reached after max retries, re-raise the last error. |
michael@0 | 260 | |
michael@0 | 261 | def RestartShell(self): |
michael@0 | 262 | """Restarts the shell on the device. Does not block for it to return.""" |
michael@0 | 263 | self.RunShellCommand('stop') |
michael@0 | 264 | self.RunShellCommand('start') |
michael@0 | 265 | |
michael@0 | 266 | def Reboot(self, full_reboot=True): |
michael@0 | 267 | """Reboots the device and waits for the package manager to return. |
michael@0 | 268 | |
michael@0 | 269 | Args: |
michael@0 | 270 | full_reboot: Whether to fully reboot the device or just restart the shell. |
michael@0 | 271 | """ |
michael@0 | 272 | # TODO(torne): hive can't reboot the device either way without breaking the |
michael@0 | 273 | # connection; work out if we can handle this better |
michael@0 | 274 | if os.environ.get('USING_HIVE'): |
michael@0 | 275 | logging.warning('Ignoring reboot request as we are on hive') |
michael@0 | 276 | return |
michael@0 | 277 | if full_reboot or not self.IsRootEnabled(): |
michael@0 | 278 | self._adb.SendCommand('reboot') |
michael@0 | 279 | timeout = 300 |
michael@0 | 280 | else: |
michael@0 | 281 | self.RestartShell() |
michael@0 | 282 | timeout = 120 |
michael@0 | 283 | # To run tests we need at least the package manager and the sd card (or |
michael@0 | 284 | # other external storage) to be ready. |
michael@0 | 285 | self.WaitForDevicePm() |
michael@0 | 286 | self.WaitForSdCardReady(timeout) |
michael@0 | 287 | |
michael@0 | 288 | def Uninstall(self, package): |
michael@0 | 289 | """Uninstalls the specified package from the device. |
michael@0 | 290 | |
michael@0 | 291 | Args: |
michael@0 | 292 | package: Name of the package to remove. |
michael@0 | 293 | |
michael@0 | 294 | Returns: |
michael@0 | 295 | A status string returned by adb uninstall |
michael@0 | 296 | """ |
michael@0 | 297 | uninstall_command = 'uninstall %s' % package |
michael@0 | 298 | |
michael@0 | 299 | logging.info('>>> $' + uninstall_command) |
michael@0 | 300 | return self._adb.SendCommand(uninstall_command, timeout_time=60) |
michael@0 | 301 | |
michael@0 | 302 | def Install(self, package_file_path, reinstall=False): |
michael@0 | 303 | """Installs the specified package to the device. |
michael@0 | 304 | |
michael@0 | 305 | Args: |
michael@0 | 306 | package_file_path: Path to .apk file to install. |
michael@0 | 307 | reinstall: Reinstall an existing apk, keeping the data. |
michael@0 | 308 | |
michael@0 | 309 | Returns: |
michael@0 | 310 | A status string returned by adb install |
michael@0 | 311 | """ |
michael@0 | 312 | assert os.path.isfile(package_file_path), ('<%s> is not file' % |
michael@0 | 313 | package_file_path) |
michael@0 | 314 | |
michael@0 | 315 | install_cmd = ['install'] |
michael@0 | 316 | |
michael@0 | 317 | if reinstall: |
michael@0 | 318 | install_cmd.append('-r') |
michael@0 | 319 | |
michael@0 | 320 | install_cmd.append(package_file_path) |
michael@0 | 321 | install_cmd = ' '.join(install_cmd) |
michael@0 | 322 | |
michael@0 | 323 | logging.info('>>> $' + install_cmd) |
michael@0 | 324 | return self._adb.SendCommand(install_cmd, timeout_time=2*60, retry_count=0) |
michael@0 | 325 | |
michael@0 | 326 | def ManagedInstall(self, apk_path, keep_data=False, package_name=None, |
michael@0 | 327 | reboots_on_failure=2): |
michael@0 | 328 | """Installs specified package and reboots device on timeouts. |
michael@0 | 329 | |
michael@0 | 330 | Args: |
michael@0 | 331 | apk_path: Path to .apk file to install. |
michael@0 | 332 | keep_data: Reinstalls instead of uninstalling first, preserving the |
michael@0 | 333 | application data. |
michael@0 | 334 | package_name: Package name (only needed if keep_data=False). |
michael@0 | 335 | reboots_on_failure: number of time to reboot if package manager is frozen. |
michael@0 | 336 | |
michael@0 | 337 | Returns: |
michael@0 | 338 | A status string returned by adb install |
michael@0 | 339 | """ |
michael@0 | 340 | reboots_left = reboots_on_failure |
michael@0 | 341 | while True: |
michael@0 | 342 | try: |
michael@0 | 343 | if not keep_data: |
michael@0 | 344 | assert package_name |
michael@0 | 345 | self.Uninstall(package_name) |
michael@0 | 346 | install_status = self.Install(apk_path, reinstall=keep_data) |
michael@0 | 347 | if 'Success' in install_status: |
michael@0 | 348 | return install_status |
michael@0 | 349 | except errors.WaitForResponseTimedOutError: |
michael@0 | 350 | print '@@@STEP_WARNINGS@@@' |
michael@0 | 351 | logging.info('Timeout on installing %s' % apk_path) |
michael@0 | 352 | |
michael@0 | 353 | if reboots_left <= 0: |
michael@0 | 354 | raise Exception('Install failure') |
michael@0 | 355 | |
michael@0 | 356 | # Force a hard reboot on last attempt |
michael@0 | 357 | self.Reboot(full_reboot=(reboots_left == 1)) |
michael@0 | 358 | reboots_left -= 1 |
michael@0 | 359 | |
michael@0 | 360 | def MakeSystemFolderWritable(self): |
michael@0 | 361 | """Remounts the /system folder rw.""" |
michael@0 | 362 | out = self._adb.SendCommand('remount') |
michael@0 | 363 | if out.strip() != 'remount succeeded': |
michael@0 | 364 | raise errors.MsgException('Remount failed: %s' % out) |
michael@0 | 365 | |
michael@0 | 366 | def RestartAdbServer(self): |
michael@0 | 367 | """Restart the adb server.""" |
michael@0 | 368 | self.KillAdbServer() |
michael@0 | 369 | self.StartAdbServer() |
michael@0 | 370 | |
michael@0 | 371 | def KillAdbServer(self): |
michael@0 | 372 | """Kill adb server.""" |
michael@0 | 373 | adb_cmd = ['adb', 'kill-server'] |
michael@0 | 374 | return cmd_helper.RunCmd(adb_cmd) |
michael@0 | 375 | |
michael@0 | 376 | def StartAdbServer(self): |
michael@0 | 377 | """Start adb server.""" |
michael@0 | 378 | adb_cmd = ['adb', 'start-server'] |
michael@0 | 379 | return cmd_helper.RunCmd(adb_cmd) |
michael@0 | 380 | |
michael@0 | 381 | def WaitForSystemBootCompleted(self, wait_time): |
michael@0 | 382 | """Waits for targeted system's boot_completed flag to be set. |
michael@0 | 383 | |
michael@0 | 384 | Args: |
michael@0 | 385 | wait_time: time in seconds to wait |
michael@0 | 386 | |
michael@0 | 387 | Raises: |
michael@0 | 388 | WaitForResponseTimedOutError if wait_time elapses and flag still not |
michael@0 | 389 | set. |
michael@0 | 390 | """ |
michael@0 | 391 | logging.info('Waiting for system boot completed...') |
michael@0 | 392 | self._adb.SendCommand('wait-for-device') |
michael@0 | 393 | # Now the device is there, but system not boot completed. |
michael@0 | 394 | # Query the sys.boot_completed flag with a basic command |
michael@0 | 395 | boot_completed = False |
michael@0 | 396 | attempts = 0 |
michael@0 | 397 | wait_period = 5 |
michael@0 | 398 | while not boot_completed and (attempts * wait_period) < wait_time: |
michael@0 | 399 | output = self._adb.SendShellCommand('getprop sys.boot_completed', |
michael@0 | 400 | retry_count=1) |
michael@0 | 401 | output = output.strip() |
michael@0 | 402 | if output == '1': |
michael@0 | 403 | boot_completed = True |
michael@0 | 404 | else: |
michael@0 | 405 | # If 'error: xxx' returned when querying the flag, it means |
michael@0 | 406 | # adb server lost the connection to the emulator, so restart the adb |
michael@0 | 407 | # server. |
michael@0 | 408 | if 'error:' in output: |
michael@0 | 409 | self.RestartAdbServer() |
michael@0 | 410 | time.sleep(wait_period) |
michael@0 | 411 | attempts += 1 |
michael@0 | 412 | if not boot_completed: |
michael@0 | 413 | raise errors.WaitForResponseTimedOutError( |
michael@0 | 414 | 'sys.boot_completed flag was not set after %s seconds' % wait_time) |
michael@0 | 415 | |
michael@0 | 416 | def WaitForSdCardReady(self, timeout_time): |
michael@0 | 417 | """Wait for the SD card ready before pushing data into it.""" |
michael@0 | 418 | logging.info('Waiting for SD card ready...') |
michael@0 | 419 | sdcard_ready = False |
michael@0 | 420 | attempts = 0 |
michael@0 | 421 | wait_period = 5 |
michael@0 | 422 | external_storage = self.GetExternalStorage() |
michael@0 | 423 | while not sdcard_ready and attempts * wait_period < timeout_time: |
michael@0 | 424 | output = self.RunShellCommand('ls ' + external_storage) |
michael@0 | 425 | if output: |
michael@0 | 426 | sdcard_ready = True |
michael@0 | 427 | else: |
michael@0 | 428 | time.sleep(wait_period) |
michael@0 | 429 | attempts += 1 |
michael@0 | 430 | if not sdcard_ready: |
michael@0 | 431 | raise errors.WaitForResponseTimedOutError( |
michael@0 | 432 | 'SD card not ready after %s seconds' % timeout_time) |
michael@0 | 433 | |
michael@0 | 434 | # It is tempting to turn this function into a generator, however this is not |
michael@0 | 435 | # possible without using a private (local) adb_shell instance (to ensure no |
michael@0 | 436 | # other command interleaves usage of it), which would defeat the main aim of |
michael@0 | 437 | # being able to reuse the adb shell instance across commands. |
michael@0 | 438 | def RunShellCommand(self, command, timeout_time=20, log_result=False): |
michael@0 | 439 | """Send a command to the adb shell and return the result. |
michael@0 | 440 | |
michael@0 | 441 | Args: |
michael@0 | 442 | command: String containing the shell command to send. Must not include |
michael@0 | 443 | the single quotes as we use them to escape the whole command. |
michael@0 | 444 | timeout_time: Number of seconds to wait for command to respond before |
michael@0 | 445 | retrying, used by AdbInterface.SendShellCommand. |
michael@0 | 446 | log_result: Boolean to indicate whether we should log the result of the |
michael@0 | 447 | shell command. |
michael@0 | 448 | |
michael@0 | 449 | Returns: |
michael@0 | 450 | list containing the lines of output received from running the command |
michael@0 | 451 | """ |
michael@0 | 452 | logging.info('>>> $' + command) |
michael@0 | 453 | if "'" in command: logging.warning(command + " contains ' quotes") |
michael@0 | 454 | result = self._adb.SendShellCommand( |
michael@0 | 455 | "'%s'" % command, timeout_time).splitlines() |
michael@0 | 456 | if ['error: device not found'] == result: |
michael@0 | 457 | raise errors.DeviceUnresponsiveError('device not found') |
michael@0 | 458 | if log_result: |
michael@0 | 459 | logging.info('\n>>> '.join(result)) |
michael@0 | 460 | return result |
michael@0 | 461 | |
michael@0 | 462 | def KillAll(self, process): |
michael@0 | 463 | """Android version of killall, connected via adb. |
michael@0 | 464 | |
michael@0 | 465 | Args: |
michael@0 | 466 | process: name of the process to kill off |
michael@0 | 467 | |
michael@0 | 468 | Returns: |
michael@0 | 469 | the number of processes killed |
michael@0 | 470 | """ |
michael@0 | 471 | pids = self.ExtractPid(process) |
michael@0 | 472 | if pids: |
michael@0 | 473 | self.RunShellCommand('kill ' + ' '.join(pids)) |
michael@0 | 474 | return len(pids) |
michael@0 | 475 | |
michael@0 | 476 | def KillAllBlocking(self, process, timeout_sec): |
michael@0 | 477 | """Blocking version of killall, connected via adb. |
michael@0 | 478 | |
michael@0 | 479 | This waits until no process matching the corresponding name appears in ps' |
michael@0 | 480 | output anymore. |
michael@0 | 481 | |
michael@0 | 482 | Args: |
michael@0 | 483 | process: name of the process to kill off |
michael@0 | 484 | timeout_sec: the timeout in seconds |
michael@0 | 485 | |
michael@0 | 486 | Returns: |
michael@0 | 487 | the number of processes killed |
michael@0 | 488 | """ |
michael@0 | 489 | processes_killed = self.KillAll(process) |
michael@0 | 490 | if processes_killed: |
michael@0 | 491 | elapsed = 0 |
michael@0 | 492 | wait_period = 0.1 |
michael@0 | 493 | # Note that this doesn't take into account the time spent in ExtractPid(). |
michael@0 | 494 | while self.ExtractPid(process) and elapsed < timeout_sec: |
michael@0 | 495 | time.sleep(wait_period) |
michael@0 | 496 | elapsed += wait_period |
michael@0 | 497 | if elapsed >= timeout_sec: |
michael@0 | 498 | return 0 |
michael@0 | 499 | return processes_killed |
michael@0 | 500 | |
michael@0 | 501 | def StartActivity(self, package, activity, wait_for_completion=False, |
michael@0 | 502 | action='android.intent.action.VIEW', |
michael@0 | 503 | category=None, data=None, |
michael@0 | 504 | extras=None, trace_file_name=None): |
michael@0 | 505 | """Starts |package|'s activity on the device. |
michael@0 | 506 | |
michael@0 | 507 | Args: |
michael@0 | 508 | package: Name of package to start (e.g. 'com.google.android.apps.chrome'). |
michael@0 | 509 | activity: Name of activity (e.g. '.Main' or |
michael@0 | 510 | 'com.google.android.apps.chrome.Main'). |
michael@0 | 511 | wait_for_completion: wait for the activity to finish launching (-W flag). |
michael@0 | 512 | action: string (e.g. "android.intent.action.MAIN"). Default is VIEW. |
michael@0 | 513 | category: string (e.g. "android.intent.category.HOME") |
michael@0 | 514 | data: Data string to pass to activity (e.g. 'http://www.example.com/'). |
michael@0 | 515 | extras: Dict of extras to pass to activity. Values are significant. |
michael@0 | 516 | trace_file_name: If used, turns on and saves the trace to this file name. |
michael@0 | 517 | """ |
michael@0 | 518 | cmd = 'am start -a %s' % action |
michael@0 | 519 | if wait_for_completion: |
michael@0 | 520 | cmd += ' -W' |
michael@0 | 521 | if category: |
michael@0 | 522 | cmd += ' -c %s' % category |
michael@0 | 523 | if package and activity: |
michael@0 | 524 | cmd += ' -n %s/%s' % (package, activity) |
michael@0 | 525 | if data: |
michael@0 | 526 | cmd += ' -d "%s"' % data |
michael@0 | 527 | if extras: |
michael@0 | 528 | for key in extras: |
michael@0 | 529 | value = extras[key] |
michael@0 | 530 | if isinstance(value, str): |
michael@0 | 531 | cmd += ' --es' |
michael@0 | 532 | elif isinstance(value, bool): |
michael@0 | 533 | cmd += ' --ez' |
michael@0 | 534 | elif isinstance(value, int): |
michael@0 | 535 | cmd += ' --ei' |
michael@0 | 536 | else: |
michael@0 | 537 | raise NotImplementedError( |
michael@0 | 538 | 'Need to teach StartActivity how to pass %s extras' % type(value)) |
michael@0 | 539 | cmd += ' %s %s' % (key, value) |
michael@0 | 540 | if trace_file_name: |
michael@0 | 541 | cmd += ' --start-profiler ' + trace_file_name |
michael@0 | 542 | self.RunShellCommand(cmd) |
michael@0 | 543 | |
michael@0 | 544 | def GoHome(self): |
michael@0 | 545 | """Tell the device to return to the home screen. Blocks until completion.""" |
michael@0 | 546 | self.RunShellCommand('am start -W ' |
michael@0 | 547 | '-a android.intent.action.MAIN -c android.intent.category.HOME') |
michael@0 | 548 | |
michael@0 | 549 | def CloseApplication(self, package): |
michael@0 | 550 | """Attempt to close down the application, using increasing violence. |
michael@0 | 551 | |
michael@0 | 552 | Args: |
michael@0 | 553 | package: Name of the process to kill off, e.g. |
michael@0 | 554 | com.google.android.apps.chrome |
michael@0 | 555 | """ |
michael@0 | 556 | self.RunShellCommand('am force-stop ' + package) |
michael@0 | 557 | |
michael@0 | 558 | def ClearApplicationState(self, package): |
michael@0 | 559 | """Closes and clears all state for the given |package|.""" |
michael@0 | 560 | self.CloseApplication(package) |
michael@0 | 561 | self.RunShellCommand('rm -r /data/data/%s/app_*' % package) |
michael@0 | 562 | self.RunShellCommand('rm -r /data/data/%s/cache/*' % package) |
michael@0 | 563 | self.RunShellCommand('rm -r /data/data/%s/files/*' % package) |
michael@0 | 564 | self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package) |
michael@0 | 565 | |
michael@0 | 566 | def SendKeyEvent(self, keycode): |
michael@0 | 567 | """Sends keycode to the device. |
michael@0 | 568 | |
michael@0 | 569 | Args: |
michael@0 | 570 | keycode: Numeric keycode to send (see "enum" at top of file). |
michael@0 | 571 | """ |
michael@0 | 572 | self.RunShellCommand('input keyevent %d' % keycode) |
michael@0 | 573 | |
michael@0 | 574 | def PushIfNeeded(self, local_path, device_path): |
michael@0 | 575 | """Pushes |local_path| to |device_path|. |
michael@0 | 576 | |
michael@0 | 577 | Works for files and directories. This method skips copying any paths in |
michael@0 | 578 | |test_data_paths| that already exist on the device with the same hash. |
michael@0 | 579 | |
michael@0 | 580 | All pushed files can be removed by calling RemovePushedFiles(). |
michael@0 | 581 | """ |
michael@0 | 582 | assert os.path.exists(local_path), 'Local path not found %s' % local_path |
michael@0 | 583 | |
michael@0 | 584 | if not self._md5sum_path: |
michael@0 | 585 | default_build_type = os.environ.get('BUILD_TYPE', 'Debug') |
michael@0 | 586 | md5sum_path = '%s/out/%s/md5sum_bin' % (CHROME_SRC, default_build_type) |
michael@0 | 587 | if not os.path.exists(md5sum_path): |
michael@0 | 588 | md5sum_path = '%s/out/Release/md5sum_bin' % (CHROME_SRC) |
michael@0 | 589 | if not os.path.exists(md5sum_path): |
michael@0 | 590 | print >> sys.stderr, 'Please build md5sum.' |
michael@0 | 591 | sys.exit(1) |
michael@0 | 592 | command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH) |
michael@0 | 593 | assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) |
michael@0 | 594 | self._md5sum_path = md5sum_path |
michael@0 | 595 | |
michael@0 | 596 | self._pushed_files.append(device_path) |
michael@0 | 597 | hashes_on_device = _ComputeFileListHash( |
michael@0 | 598 | self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path)) |
michael@0 | 599 | assert os.path.exists(local_path), 'Local path not found %s' % local_path |
michael@0 | 600 | hashes_on_host = _ComputeFileListHash( |
michael@0 | 601 | subprocess.Popen( |
michael@0 | 602 | '%s_host %s' % (self._md5sum_path, local_path), |
michael@0 | 603 | stdout=subprocess.PIPE, shell=True).stdout) |
michael@0 | 604 | if hashes_on_device == hashes_on_host: |
michael@0 | 605 | return |
michael@0 | 606 | |
michael@0 | 607 | # They don't match, so remove everything first and then create it. |
michael@0 | 608 | if os.path.isdir(local_path): |
michael@0 | 609 | self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) |
michael@0 | 610 | self.RunShellCommand('mkdir -p %s' % device_path) |
michael@0 | 611 | |
michael@0 | 612 | # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of |
michael@0 | 613 | # 60 seconds which isn't sufficient for a lot of users of this method. |
michael@0 | 614 | push_command = 'push %s %s' % (local_path, device_path) |
michael@0 | 615 | logging.info('>>> $' + push_command) |
michael@0 | 616 | output = self._adb.SendCommand(push_command, timeout_time=30*60) |
michael@0 | 617 | assert _HasAdbPushSucceeded(output) |
michael@0 | 618 | |
michael@0 | 619 | |
michael@0 | 620 | def GetFileContents(self, filename, log_result=False): |
michael@0 | 621 | """Gets contents from the file specified by |filename|.""" |
michael@0 | 622 | return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' + |
michael@0 | 623 | filename + '"; fi', log_result=log_result) |
michael@0 | 624 | |
michael@0 | 625 | def SetFileContents(self, filename, contents): |
michael@0 | 626 | """Writes |contents| to the file specified by |filename|.""" |
michael@0 | 627 | with tempfile.NamedTemporaryFile() as f: |
michael@0 | 628 | f.write(contents) |
michael@0 | 629 | f.flush() |
michael@0 | 630 | self._adb.Push(f.name, filename) |
michael@0 | 631 | |
michael@0 | 632 | def RemovePushedFiles(self): |
michael@0 | 633 | """Removes all files pushed with PushIfNeeded() from the device.""" |
michael@0 | 634 | for p in self._pushed_files: |
michael@0 | 635 | self.RunShellCommand('rm -r %s' % p, timeout_time=2*60) |
michael@0 | 636 | |
michael@0 | 637 | def ListPathContents(self, path): |
michael@0 | 638 | """Lists files in all subdirectories of |path|. |
michael@0 | 639 | |
michael@0 | 640 | Args: |
michael@0 | 641 | path: The path to list. |
michael@0 | 642 | |
michael@0 | 643 | Returns: |
michael@0 | 644 | A dict of {"name": (size, lastmod), ...}. |
michael@0 | 645 | """ |
michael@0 | 646 | # Example output: |
michael@0 | 647 | # /foo/bar: |
michael@0 | 648 | # -rw-r----- 1 user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt |
michael@0 | 649 | re_file = re.compile('^-(?P<perms>[^\s]+)\s+' |
michael@0 | 650 | '(?P<user>[^\s]+)\s+' |
michael@0 | 651 | '(?P<group>[^\s]+)\s+' |
michael@0 | 652 | '(?P<size>[^\s]+)\s+' |
michael@0 | 653 | '(?P<date>[^\s]+)\s+' |
michael@0 | 654 | '(?P<time>[^\s]+)\s+' |
michael@0 | 655 | '(?P<filename>[^\s]+)$') |
michael@0 | 656 | return _GetFilesFromRecursiveLsOutput( |
michael@0 | 657 | path, self.RunShellCommand('ls -lR %s' % path), re_file, |
michael@0 | 658 | self._device_utc_offset) |
michael@0 | 659 | |
michael@0 | 660 | |
michael@0 | 661 | def SetJavaAssertsEnabled(self, enable): |
michael@0 | 662 | """Sets or removes the device java assertions property. |
michael@0 | 663 | |
michael@0 | 664 | Args: |
michael@0 | 665 | enable: If True the property will be set. |
michael@0 | 666 | |
michael@0 | 667 | Returns: |
michael@0 | 668 | True if the file was modified (reboot is required for it to take effect). |
michael@0 | 669 | """ |
michael@0 | 670 | # First ensure the desired property is persisted. |
michael@0 | 671 | temp_props_file = tempfile.NamedTemporaryFile() |
michael@0 | 672 | properties = '' |
michael@0 | 673 | if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name): |
michael@0 | 674 | properties = file(temp_props_file.name).read() |
michael@0 | 675 | re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + |
michael@0 | 676 | r'\s*=\s*all\s*$', re.MULTILINE) |
michael@0 | 677 | if enable != bool(re.search(re_search, properties)): |
michael@0 | 678 | re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + |
michael@0 | 679 | r'\s*=\s*\w+\s*$', re.MULTILINE) |
michael@0 | 680 | properties = re.sub(re_replace, '', properties) |
michael@0 | 681 | if enable: |
michael@0 | 682 | properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY |
michael@0 | 683 | |
michael@0 | 684 | file(temp_props_file.name, 'w').write(properties) |
michael@0 | 685 | self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) |
michael@0 | 686 | |
michael@0 | 687 | # Next, check the current runtime value is what we need, and |
michael@0 | 688 | # if not, set it and report that a reboot is required. |
michael@0 | 689 | was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY) |
michael@0 | 690 | if was_set == enable: |
michael@0 | 691 | return False |
michael@0 | 692 | |
michael@0 | 693 | self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY, |
michael@0 | 694 | enable and 'all' or '')) |
michael@0 | 695 | return True |
michael@0 | 696 | |
michael@0 | 697 | def GetBuildId(self): |
michael@0 | 698 | """Returns the build ID of the system (e.g. JRM79C).""" |
michael@0 | 699 | build_id = self.RunShellCommand('getprop ro.build.id')[0] |
michael@0 | 700 | assert build_id |
michael@0 | 701 | return build_id |
michael@0 | 702 | |
michael@0 | 703 | def GetBuildType(self): |
michael@0 | 704 | """Returns the build type of the system (e.g. eng).""" |
michael@0 | 705 | build_type = self.RunShellCommand('getprop ro.build.type')[0] |
michael@0 | 706 | assert build_type |
michael@0 | 707 | return build_type |
michael@0 | 708 | |
michael@0 | 709 | def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None, |
michael@0 | 710 | filters=None): |
michael@0 | 711 | """Starts monitoring the output of logcat, for use with WaitForLogMatch. |
michael@0 | 712 | |
michael@0 | 713 | Args: |
michael@0 | 714 | clear: If True the existing logcat output will be cleared, to avoiding |
michael@0 | 715 | matching historical output lurking in the log. |
michael@0 | 716 | timeout: How long WaitForLogMatch will wait for the given match |
michael@0 | 717 | filters: A list of logcat filters to be used. |
michael@0 | 718 | """ |
michael@0 | 719 | if clear: |
michael@0 | 720 | self.RunShellCommand('logcat -c') |
michael@0 | 721 | args = [] |
michael@0 | 722 | if self._adb._target_arg: |
michael@0 | 723 | args += shlex.split(self._adb._target_arg) |
michael@0 | 724 | args += ['logcat', '-v', 'threadtime'] |
michael@0 | 725 | if filters: |
michael@0 | 726 | args.extend(filters) |
michael@0 | 727 | else: |
michael@0 | 728 | args.append('*:v') |
michael@0 | 729 | |
michael@0 | 730 | if logfile: |
michael@0 | 731 | logfile = NewLineNormalizer(logfile) |
michael@0 | 732 | |
michael@0 | 733 | # Spawn logcat and syncronize with it. |
michael@0 | 734 | for _ in range(4): |
michael@0 | 735 | self._logcat = pexpect.spawn('adb', args, timeout=timeout, |
michael@0 | 736 | logfile=logfile) |
michael@0 | 737 | self.RunShellCommand('log startup_sync') |
michael@0 | 738 | if self._logcat.expect(['startup_sync', pexpect.EOF, |
michael@0 | 739 | pexpect.TIMEOUT]) == 0: |
michael@0 | 740 | break |
michael@0 | 741 | self._logcat.close(force=True) |
michael@0 | 742 | else: |
michael@0 | 743 | logging.critical('Error reading from logcat: ' + str(self._logcat.match)) |
michael@0 | 744 | sys.exit(1) |
michael@0 | 745 | |
michael@0 | 746 | def GetMonitoredLogCat(self): |
michael@0 | 747 | """Returns an "adb logcat" command as created by pexpected.spawn.""" |
michael@0 | 748 | if not self._logcat: |
michael@0 | 749 | self.StartMonitoringLogcat(clear=False) |
michael@0 | 750 | return self._logcat |
michael@0 | 751 | |
michael@0 | 752 | def WaitForLogMatch(self, success_re, error_re, clear=False): |
michael@0 | 753 | """Blocks until a matching line is logged or a timeout occurs. |
michael@0 | 754 | |
michael@0 | 755 | Args: |
michael@0 | 756 | success_re: A compiled re to search each line for. |
michael@0 | 757 | error_re: A compiled re which, if found, terminates the search for |
michael@0 | 758 | |success_re|. If None is given, no error condition will be detected. |
michael@0 | 759 | clear: If True the existing logcat output will be cleared, defaults to |
michael@0 | 760 | false. |
michael@0 | 761 | |
michael@0 | 762 | Raises: |
michael@0 | 763 | pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat(). |
michael@0 | 764 | |
michael@0 | 765 | Returns: |
michael@0 | 766 | The re match object if |success_re| is matched first or None if |error_re| |
michael@0 | 767 | is matched first. |
michael@0 | 768 | """ |
michael@0 | 769 | logging.info('<<< Waiting for logcat:' + str(success_re.pattern)) |
michael@0 | 770 | t0 = time.time() |
michael@0 | 771 | while True: |
michael@0 | 772 | if not self._logcat: |
michael@0 | 773 | self.StartMonitoringLogcat(clear) |
michael@0 | 774 | try: |
michael@0 | 775 | while True: |
michael@0 | 776 | # Note this will block for upto the timeout _per log line_, so we need |
michael@0 | 777 | # to calculate the overall timeout remaining since t0. |
michael@0 | 778 | time_remaining = t0 + self._logcat.timeout - time.time() |
michael@0 | 779 | if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat) |
michael@0 | 780 | self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining) |
michael@0 | 781 | line = self._logcat.match.group(1) |
michael@0 | 782 | if error_re: |
michael@0 | 783 | error_match = error_re.search(line) |
michael@0 | 784 | if error_match: |
michael@0 | 785 | return None |
michael@0 | 786 | success_match = success_re.search(line) |
michael@0 | 787 | if success_match: |
michael@0 | 788 | return success_match |
michael@0 | 789 | logging.info('<<< Skipped Logcat Line:' + str(line)) |
michael@0 | 790 | except pexpect.TIMEOUT: |
michael@0 | 791 | raise pexpect.TIMEOUT( |
michael@0 | 792 | 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv ' |
michael@0 | 793 | 'to debug)' % |
michael@0 | 794 | (self._logcat.timeout, success_re.pattern)) |
michael@0 | 795 | except pexpect.EOF: |
michael@0 | 796 | # It seems that sometimes logcat can end unexpectedly. This seems |
michael@0 | 797 | # to happen during Chrome startup after a reboot followed by a cache |
michael@0 | 798 | # clean. I don't understand why this happens, but this code deals with |
michael@0 | 799 | # getting EOF in logcat. |
michael@0 | 800 | logging.critical('Found EOF in adb logcat. Restarting...') |
michael@0 | 801 | # Rerun spawn with original arguments. Note that self._logcat.args[0] is |
michael@0 | 802 | # the path of adb, so we don't want it in the arguments. |
michael@0 | 803 | self._logcat = pexpect.spawn('adb', |
michael@0 | 804 | self._logcat.args[1:], |
michael@0 | 805 | timeout=self._logcat.timeout, |
michael@0 | 806 | logfile=self._logcat.logfile) |
michael@0 | 807 | |
michael@0 | 808 | def StartRecordingLogcat(self, clear=True, filters=['*:v']): |
michael@0 | 809 | """Starts recording logcat output to eventually be saved as a string. |
michael@0 | 810 | |
michael@0 | 811 | This call should come before some series of tests are run, with either |
michael@0 | 812 | StopRecordingLogcat or SearchLogcatRecord following the tests. |
michael@0 | 813 | |
michael@0 | 814 | Args: |
michael@0 | 815 | clear: True if existing log output should be cleared. |
michael@0 | 816 | filters: A list of logcat filters to be used. |
michael@0 | 817 | """ |
michael@0 | 818 | if clear: |
michael@0 | 819 | self._adb.SendCommand('logcat -c') |
michael@0 | 820 | logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg, |
michael@0 | 821 | ' '.join(filters)) |
michael@0 | 822 | self.logcat_process = subprocess.Popen(logcat_command, shell=True, |
michael@0 | 823 | stdout=subprocess.PIPE) |
michael@0 | 824 | |
michael@0 | 825 | def StopRecordingLogcat(self): |
michael@0 | 826 | """Stops an existing logcat recording subprocess and returns output. |
michael@0 | 827 | |
michael@0 | 828 | Returns: |
michael@0 | 829 | The logcat output as a string or an empty string if logcat was not |
michael@0 | 830 | being recorded at the time. |
michael@0 | 831 | """ |
michael@0 | 832 | if not self.logcat_process: |
michael@0 | 833 | return '' |
michael@0 | 834 | # Cannot evaluate directly as 0 is a possible value. |
michael@0 | 835 | # Better to read the self.logcat_process.stdout before killing it, |
michael@0 | 836 | # Otherwise the communicate may return incomplete output due to pipe break. |
michael@0 | 837 | if self.logcat_process.poll() is None: |
michael@0 | 838 | self.logcat_process.kill() |
michael@0 | 839 | (output, _) = self.logcat_process.communicate() |
michael@0 | 840 | self.logcat_process = None |
michael@0 | 841 | return output |
michael@0 | 842 | |
michael@0 | 843 | def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None, |
michael@0 | 844 | log_level=None, component=None): |
michael@0 | 845 | """Searches the specified logcat output and returns results. |
michael@0 | 846 | |
michael@0 | 847 | This method searches through the logcat output specified by record for a |
michael@0 | 848 | certain message, narrowing results by matching them against any other |
michael@0 | 849 | specified criteria. It returns all matching lines as described below. |
michael@0 | 850 | |
michael@0 | 851 | Args: |
michael@0 | 852 | record: A string generated by Start/StopRecordingLogcat to search. |
michael@0 | 853 | message: An output string to search for. |
michael@0 | 854 | thread_id: The thread id that is the origin of the message. |
michael@0 | 855 | proc_id: The process that is the origin of the message. |
michael@0 | 856 | log_level: The log level of the message. |
michael@0 | 857 | component: The name of the component that would create the message. |
michael@0 | 858 | |
michael@0 | 859 | Returns: |
michael@0 | 860 | A list of dictionaries represeting matching entries, each containing keys |
michael@0 | 861 | thread_id, proc_id, log_level, component, and message. |
michael@0 | 862 | """ |
michael@0 | 863 | if thread_id: |
michael@0 | 864 | thread_id = str(thread_id) |
michael@0 | 865 | if proc_id: |
michael@0 | 866 | proc_id = str(proc_id) |
michael@0 | 867 | results = [] |
michael@0 | 868 | reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$', |
michael@0 | 869 | re.MULTILINE) |
michael@0 | 870 | log_list = reg.findall(record) |
michael@0 | 871 | for (tid, pid, log_lev, comp, msg) in log_list: |
michael@0 | 872 | if ((not thread_id or thread_id == tid) and |
michael@0 | 873 | (not proc_id or proc_id == pid) and |
michael@0 | 874 | (not log_level or log_level == log_lev) and |
michael@0 | 875 | (not component or component == comp) and msg.find(message) > -1): |
michael@0 | 876 | match = dict({'thread_id': tid, 'proc_id': pid, |
michael@0 | 877 | 'log_level': log_lev, 'component': comp, |
michael@0 | 878 | 'message': msg}) |
michael@0 | 879 | results.append(match) |
michael@0 | 880 | return results |
michael@0 | 881 | |
michael@0 | 882 | def ExtractPid(self, process_name): |
michael@0 | 883 | """Extracts Process Ids for a given process name from Android Shell. |
michael@0 | 884 | |
michael@0 | 885 | Args: |
michael@0 | 886 | process_name: name of the process on the device. |
michael@0 | 887 | |
michael@0 | 888 | Returns: |
michael@0 | 889 | List of all the process ids (as strings) that match the given name. |
michael@0 | 890 | If the name of a process exactly matches the given name, the pid of |
michael@0 | 891 | that process will be inserted to the front of the pid list. |
michael@0 | 892 | """ |
michael@0 | 893 | pids = [] |
michael@0 | 894 | for line in self.RunShellCommand('ps', log_result=False): |
michael@0 | 895 | data = line.split() |
michael@0 | 896 | try: |
michael@0 | 897 | if process_name in data[-1]: # name is in the last column |
michael@0 | 898 | if process_name == data[-1]: |
michael@0 | 899 | pids.insert(0, data[1]) # PID is in the second column |
michael@0 | 900 | else: |
michael@0 | 901 | pids.append(data[1]) |
michael@0 | 902 | except IndexError: |
michael@0 | 903 | pass |
michael@0 | 904 | return pids |
michael@0 | 905 | |
michael@0 | 906 | def GetIoStats(self): |
michael@0 | 907 | """Gets cumulative disk IO stats since boot (for all processes). |
michael@0 | 908 | |
michael@0 | 909 | Returns: |
michael@0 | 910 | Dict of {num_reads, num_writes, read_ms, write_ms} or None if there |
michael@0 | 911 | was an error. |
michael@0 | 912 | """ |
michael@0 | 913 | for line in self.GetFileContents('/proc/diskstats', log_result=False): |
michael@0 | 914 | stats = io_stats_parser.ParseIoStatsLine(line) |
michael@0 | 915 | if stats.device == 'mmcblk0': |
michael@0 | 916 | return { |
michael@0 | 917 | 'num_reads': stats.num_reads_issued, |
michael@0 | 918 | 'num_writes': stats.num_writes_completed, |
michael@0 | 919 | 'read_ms': stats.ms_spent_reading, |
michael@0 | 920 | 'write_ms': stats.ms_spent_writing, |
michael@0 | 921 | } |
michael@0 | 922 | logging.warning('Could not find disk IO stats.') |
michael@0 | 923 | return None |
michael@0 | 924 | |
michael@0 | 925 | def GetMemoryUsageForPid(self, pid): |
michael@0 | 926 | """Returns the memory usage for given pid. |
michael@0 | 927 | |
michael@0 | 928 | Args: |
michael@0 | 929 | pid: The pid number of the specific process running on device. |
michael@0 | 930 | |
michael@0 | 931 | Returns: |
michael@0 | 932 | A tuple containg: |
michael@0 | 933 | [0]: Dict of {metric:usage_kb}, for the process which has specified pid. |
michael@0 | 934 | The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, |
michael@0 | 935 | Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, |
michael@0 | 936 | KernelPageSize, MMUPageSize, Nvidia (tablet only). |
michael@0 | 937 | [1]: Detailed /proc/[PID]/smaps information. |
michael@0 | 938 | """ |
michael@0 | 939 | usage_dict = collections.defaultdict(int) |
michael@0 | 940 | smaps = collections.defaultdict(dict) |
michael@0 | 941 | current_smap = '' |
michael@0 | 942 | for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False): |
michael@0 | 943 | items = line.split() |
michael@0 | 944 | # See man 5 proc for more details. The format is: |
michael@0 | 945 | # address perms offset dev inode pathname |
michael@0 | 946 | if len(items) > 5: |
michael@0 | 947 | current_smap = ' '.join(items[5:]) |
michael@0 | 948 | elif len(items) > 3: |
michael@0 | 949 | current_smap = ' '.join(items[3:]) |
michael@0 | 950 | match = re.match(MEMORY_INFO_RE, line) |
michael@0 | 951 | if match: |
michael@0 | 952 | key = match.group('key') |
michael@0 | 953 | usage_kb = int(match.group('usage_kb')) |
michael@0 | 954 | usage_dict[key] += usage_kb |
michael@0 | 955 | if key not in smaps[current_smap]: |
michael@0 | 956 | smaps[current_smap][key] = 0 |
michael@0 | 957 | smaps[current_smap][key] += usage_kb |
michael@0 | 958 | if not usage_dict or not any(usage_dict.values()): |
michael@0 | 959 | # Presumably the process died between ps and calling this method. |
michael@0 | 960 | logging.warning('Could not find memory usage for pid ' + str(pid)) |
michael@0 | 961 | |
michael@0 | 962 | for line in self.GetFileContents('/d/nvmap/generic-0/clients', |
michael@0 | 963 | log_result=False): |
michael@0 | 964 | match = re.match(NVIDIA_MEMORY_INFO_RE, line) |
michael@0 | 965 | if match and match.group('pid') == pid: |
michael@0 | 966 | usage_bytes = int(match.group('usage_bytes')) |
michael@0 | 967 | usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB |
michael@0 | 968 | break |
michael@0 | 969 | |
michael@0 | 970 | return (usage_dict, smaps) |
michael@0 | 971 | |
michael@0 | 972 | def GetMemoryUsageForPackage(self, package): |
michael@0 | 973 | """Returns the memory usage for all processes whose name contains |pacakge|. |
michael@0 | 974 | |
michael@0 | 975 | Args: |
michael@0 | 976 | package: A string holding process name to lookup pid list for. |
michael@0 | 977 | |
michael@0 | 978 | Returns: |
michael@0 | 979 | A tuple containg: |
michael@0 | 980 | [0]: Dict of {metric:usage_kb}, summed over all pids associated with |
michael@0 | 981 | |name|. |
michael@0 | 982 | The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, |
michael@0 | 983 | Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, |
michael@0 | 984 | KernelPageSize, MMUPageSize, Nvidia (tablet only). |
michael@0 | 985 | [1]: a list with detailed /proc/[PID]/smaps information. |
michael@0 | 986 | """ |
michael@0 | 987 | usage_dict = collections.defaultdict(int) |
michael@0 | 988 | pid_list = self.ExtractPid(package) |
michael@0 | 989 | smaps = collections.defaultdict(dict) |
michael@0 | 990 | |
michael@0 | 991 | for pid in pid_list: |
michael@0 | 992 | usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid) |
michael@0 | 993 | smaps[pid] = smaps_per_pid |
michael@0 | 994 | for (key, value) in usage_dict_per_pid.items(): |
michael@0 | 995 | usage_dict[key] += value |
michael@0 | 996 | |
michael@0 | 997 | return usage_dict, smaps |
michael@0 | 998 | |
michael@0 | 999 | def ProcessesUsingDevicePort(self, device_port): |
michael@0 | 1000 | """Lists processes using the specified device port on loopback interface. |
michael@0 | 1001 | |
michael@0 | 1002 | Args: |
michael@0 | 1003 | device_port: Port on device we want to check. |
michael@0 | 1004 | |
michael@0 | 1005 | Returns: |
michael@0 | 1006 | A list of (pid, process_name) tuples using the specified port. |
michael@0 | 1007 | """ |
michael@0 | 1008 | tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False) |
michael@0 | 1009 | tcp_address = '0100007F:%04X' % device_port |
michael@0 | 1010 | pids = [] |
michael@0 | 1011 | for single_connect in tcp_results: |
michael@0 | 1012 | connect_results = single_connect.split() |
michael@0 | 1013 | # Column 1 is the TCP port, and Column 9 is the inode of the socket |
michael@0 | 1014 | if connect_results[1] == tcp_address: |
michael@0 | 1015 | socket_inode = connect_results[9] |
michael@0 | 1016 | socket_name = 'socket:[%s]' % socket_inode |
michael@0 | 1017 | lsof_results = self.RunShellCommand('lsof', log_result=False) |
michael@0 | 1018 | for single_process in lsof_results: |
michael@0 | 1019 | process_results = single_process.split() |
michael@0 | 1020 | # Ignore the line if it has less than nine columns in it, which may |
michael@0 | 1021 | # be the case when a process stops while lsof is executing. |
michael@0 | 1022 | if len(process_results) <= 8: |
michael@0 | 1023 | continue |
michael@0 | 1024 | # Column 0 is the executable name |
michael@0 | 1025 | # Column 1 is the pid |
michael@0 | 1026 | # Column 8 is the Inode in use |
michael@0 | 1027 | if process_results[8] == socket_name: |
michael@0 | 1028 | pids.append((int(process_results[1]), process_results[0])) |
michael@0 | 1029 | break |
michael@0 | 1030 | logging.info('PidsUsingDevicePort: %s', pids) |
michael@0 | 1031 | return pids |
michael@0 | 1032 | |
michael@0 | 1033 | def FileExistsOnDevice(self, file_name): |
michael@0 | 1034 | """Checks whether the given file exists on the device. |
michael@0 | 1035 | |
michael@0 | 1036 | Args: |
michael@0 | 1037 | file_name: Full path of file to check. |
michael@0 | 1038 | |
michael@0 | 1039 | Returns: |
michael@0 | 1040 | True if the file exists, False otherwise. |
michael@0 | 1041 | """ |
michael@0 | 1042 | assert '"' not in file_name, 'file_name cannot contain double quotes' |
michael@0 | 1043 | status = self._adb.SendShellCommand( |
michael@0 | 1044 | '\'test -e "%s"; echo $?\'' % (file_name)) |
michael@0 | 1045 | if 'test: not found' not in status: |
michael@0 | 1046 | return int(status) == 0 |
michael@0 | 1047 | |
michael@0 | 1048 | status = self._adb.SendShellCommand( |
michael@0 | 1049 | '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name)) |
michael@0 | 1050 | return int(status) == 0 |
michael@0 | 1051 | |
michael@0 | 1052 | |
michael@0 | 1053 | class NewLineNormalizer(object): |
michael@0 | 1054 | """A file-like object to normalize EOLs to '\n'. |
michael@0 | 1055 | |
michael@0 | 1056 | Pexpect runs adb within a pseudo-tty device (see |
michael@0 | 1057 | http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written |
michael@0 | 1058 | as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate |
michael@0 | 1059 | lines, the log ends up having '\r\r\n' at the end of each line. This |
michael@0 | 1060 | filter replaces the above with a single '\n' in the data stream. |
michael@0 | 1061 | """ |
michael@0 | 1062 | def __init__(self, output): |
michael@0 | 1063 | self._output = output |
michael@0 | 1064 | |
michael@0 | 1065 | def write(self, data): |
michael@0 | 1066 | data = data.replace('\r\r\n', '\n') |
michael@0 | 1067 | self._output.write(data) |
michael@0 | 1068 | |
michael@0 | 1069 | def flush(self): |
michael@0 | 1070 | self._output.flush() |
michael@0 | 1071 |