| Index: build/android/pylib/android_commands.py
|
| diff --git a/build/android/pylib/android_commands.py b/build/android/pylib/android_commands.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f7191f7935da25cbd12b4c11447277fbf7e9bc34
|
| --- /dev/null
|
| +++ b/build/android/pylib/android_commands.py
|
| @@ -0,0 +1,1976 @@
|
| +# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Provides an interface to communicate with the device via the adb command.
|
| +
|
| +Assumes adb binary is currently on system path.
|
| +
|
| +Note that this module is deprecated.
|
| +"""
|
| +# TODO(jbudorick): Delete this file once no clients use it.
|
| +
|
| +# pylint: skip-file
|
| +
|
| +import collections
|
| +import datetime
|
| +import inspect
|
| +import logging
|
| +import os
|
| +import random
|
| +import re
|
| +import shlex
|
| +import signal
|
| +import subprocess
|
| +import sys
|
| +import tempfile
|
| +import time
|
| +
|
| +import cmd_helper
|
| +import constants
|
| +import system_properties
|
| +from utils import host_utils
|
| +
|
| +try:
|
| + from pylib import pexpect
|
| +except ImportError:
|
| + pexpect = None
|
| +
|
| +sys.path.append(os.path.join(
|
| + constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner'))
|
| +import adb_interface
|
| +import am_instrument_parser
|
| +import errors
|
| +
|
| +from pylib.device import device_blacklist
|
| +from pylib.device import device_errors
|
| +
|
| +# Pattern to search for the next whole line of pexpect output and capture it
|
| +# into a match group. We can't use ^ and $ for line start end with pexpect,
|
| +# see http://www.noah.org/python/pexpect/#doc for explanation why.
|
| +PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r')
|
| +
|
| +# Set the adb shell prompt to be a unique marker that will [hopefully] not
|
| +# appear at the start of any line of a command's output.
|
| +SHELL_PROMPT = '~+~PQ\x17RS~+~'
|
| +
|
| +# Java properties file
|
| +LOCAL_PROPERTIES_PATH = constants.DEVICE_LOCAL_PROPERTIES_PATH
|
| +
|
| +# Property in /data/local.prop that controls Java assertions.
|
| +JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
|
| +
|
| +# Keycode "enum" suitable for passing to AndroidCommands.SendKey().
|
| +KEYCODE_HOME = 3
|
| +KEYCODE_BACK = 4
|
| +KEYCODE_DPAD_UP = 19
|
| +KEYCODE_DPAD_DOWN = 20
|
| +KEYCODE_DPAD_RIGHT = 22
|
| +KEYCODE_ENTER = 66
|
| +KEYCODE_MENU = 82
|
| +
|
| +MD5SUM_DEVICE_FOLDER = constants.TEST_EXECUTABLE_DIR + '/md5sum/'
|
| +MD5SUM_DEVICE_PATH = MD5SUM_DEVICE_FOLDER + 'md5sum_bin'
|
| +
|
| +PIE_WRAPPER_PATH = constants.TEST_EXECUTABLE_DIR + '/run_pie'
|
| +
|
| +CONTROL_USB_CHARGING_COMMANDS = [
|
| + {
|
| + # Nexus 4
|
| + 'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
|
| + 'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled',
|
| + 'disable_command':
|
| + 'echo 1 > /sys/module/pm8921_charger/parameters/disabled',
|
| + },
|
| + {
|
| + # Nexus 5
|
| + # Setting the HIZ bit of the bq24192 causes the charger to actually ignore
|
| + # energy coming from USB. Setting the power_supply offline just updates the
|
| + # Android system to reflect that.
|
| + 'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
|
| + 'enable_command': (
|
| + 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
|
| + 'echo 1 > /sys/class/power_supply/usb/online'),
|
| + 'disable_command': (
|
| + 'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
|
| + 'chmod 644 /sys/class/power_supply/usb/online && '
|
| + 'echo 0 > /sys/class/power_supply/usb/online'),
|
| + },
|
| +]
|
| +
|
| +class DeviceTempFile(object):
|
| + def __init__(self, android_commands, prefix='temp_file', suffix=''):
|
| + """Find an unused temporary file path in the devices external directory.
|
| +
|
| + When this object is closed, the file will be deleted on the device.
|
| + """
|
| + self.android_commands = android_commands
|
| + while True:
|
| + # TODO(cjhopman): This could actually return the same file in multiple
|
| + # calls if the caller doesn't write to the files immediately. This is
|
| + # expected to never happen.
|
| + i = random.randint(0, 1000000)
|
| + self.name = '%s/%s-%d-%010d%s' % (
|
| + android_commands.GetExternalStorage(),
|
| + prefix, int(time.time()), i, suffix)
|
| + if not android_commands.FileExistsOnDevice(self.name):
|
| + break
|
| +
|
| + def __enter__(self):
|
| + return self
|
| +
|
| + def __exit__(self, type, value, traceback):
|
| + self.close()
|
| +
|
| + def close(self):
|
| + self.android_commands.RunShellCommand('rm ' + self.name)
|
| +
|
| +
|
| +def GetAVDs():
|
| + """Returns a list of AVDs."""
|
| + re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE)
|
| + avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd']))
|
| + return avds
|
| +
|
| +def ResetBadDevices():
|
| + """Removes the blacklist that keeps track of bad devices for a current
|
| + build.
|
| + """
|
| + device_blacklist.ResetBlacklist()
|
| +
|
| +def ExtendBadDevices(devices):
|
| + """Adds devices to the blacklist that keeps track of bad devices for a
|
| + current build.
|
| +
|
| + The devices listed in the bad devices file will not be returned by
|
| + GetAttachedDevices.
|
| +
|
| + Args:
|
| + devices: list of bad devices to be added to the bad devices file.
|
| + """
|
| + device_blacklist.ExtendBlacklist(devices)
|
| +
|
| +
|
| +def GetAttachedDevices(hardware=True, emulator=True, offline=False):
|
| + """Returns a list of attached, android devices and emulators.
|
| +
|
| + If a preferred device has been set with ANDROID_SERIAL, it will be first in
|
| + the returned list. The arguments specify what devices to include in the list.
|
| +
|
| + Example output:
|
| +
|
| + * daemon not running. starting it now on port 5037 *
|
| + * daemon started successfully *
|
| + List of devices attached
|
| + 027c10494100b4d7 device
|
| + emulator-5554 offline
|
| +
|
| + Args:
|
| + hardware: Include attached actual devices that are online.
|
| + emulator: Include emulators (i.e. AVD's) currently on host.
|
| + offline: Include devices and emulators that are offline.
|
| +
|
| + Returns: List of devices.
|
| + """
|
| + adb_devices_output = cmd_helper.GetCmdOutput([constants.GetAdbPath(),
|
| + 'devices'])
|
| +
|
| + re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE)
|
| + online_devices = re_device.findall(adb_devices_output)
|
| +
|
| + re_device = re.compile('^(emulator-[0-9]+)\tdevice', re.MULTILINE)
|
| + emulator_devices = re_device.findall(adb_devices_output)
|
| +
|
| + re_device = re.compile('^([a-zA-Z0-9_:.-]+)\t(?:offline|unauthorized)$',
|
| + re.MULTILINE)
|
| + offline_devices = re_device.findall(adb_devices_output)
|
| +
|
| + devices = []
|
| + # First determine list of online devices (e.g. hardware and/or emulator).
|
| + if hardware and emulator:
|
| + devices = online_devices
|
| + elif hardware:
|
| + devices = [device for device in online_devices
|
| + if device not in emulator_devices]
|
| + elif emulator:
|
| + devices = emulator_devices
|
| +
|
| + # Now add offline devices if offline is true
|
| + if offline:
|
| + devices = devices + offline_devices
|
| +
|
| + # Remove any devices in the blacklist.
|
| + blacklist = device_blacklist.ReadBlacklist()
|
| + if len(blacklist):
|
| + logging.info('Avoiding bad devices %s', ' '.join(blacklist))
|
| + devices = [device for device in devices if device not in blacklist]
|
| +
|
| + preferred_device = os.environ.get('ANDROID_SERIAL')
|
| + if preferred_device in devices:
|
| + devices.remove(preferred_device)
|
| + devices.insert(0, preferred_device)
|
| + return devices
|
| +
|
| +
|
| +def IsDeviceAttached(device):
|
| + """Return true if the device is attached and online."""
|
| + return device in GetAttachedDevices()
|
| +
|
| +
|
| +def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
|
| + """Gets a list of files from `ls` command output.
|
| +
|
| + Python's os.walk isn't used because it doesn't work over adb shell.
|
| +
|
| + Args:
|
| + path: The path to list.
|
| + ls_output: A list of lines returned by an `ls -lR` command.
|
| + re_file: A compiled regular expression which parses a line into named groups
|
| + consisting of at minimum "filename", "date", "time", "size" and
|
| + optionally "timezone".
|
| + utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a
|
| + 2-digit string giving the number of UTC offset hours, and MM is a
|
| + 2-digit string giving the number of UTC offset minutes. If the input
|
| + utc_offset is None, will try to look for the value of "timezone" if it
|
| + is specified in re_file.
|
| +
|
| + Returns:
|
| + A dict of {"name": (size, lastmod), ...} where:
|
| + name: The file name relative to |path|'s directory.
|
| + size: The file size in bytes (0 for directories).
|
| + lastmod: The file last modification date in UTC.
|
| + """
|
| + re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path))
|
| + path_dir = os.path.dirname(path)
|
| +
|
| + current_dir = ''
|
| + files = {}
|
| + for line in ls_output:
|
| + directory_match = re_directory.match(line)
|
| + if directory_match:
|
| + current_dir = directory_match.group('dir')
|
| + continue
|
| + file_match = re_file.match(line)
|
| + if file_match:
|
| + filename = os.path.join(current_dir, file_match.group('filename'))
|
| + if filename.startswith(path_dir):
|
| + filename = filename[len(path_dir) + 1:]
|
| + lastmod = datetime.datetime.strptime(
|
| + file_match.group('date') + ' ' + file_match.group('time')[:5],
|
| + '%Y-%m-%d %H:%M')
|
| + if not utc_offset and 'timezone' in re_file.groupindex:
|
| + utc_offset = file_match.group('timezone')
|
| + if isinstance(utc_offset, str) and len(utc_offset) == 5:
|
| + utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]),
|
| + minutes=int(utc_offset[3:5]))
|
| + if utc_offset[0:1] == '-':
|
| + utc_delta = -utc_delta
|
| + lastmod -= utc_delta
|
| + files[filename] = (int(file_match.group('size')), lastmod)
|
| + return files
|
| +
|
| +
|
| +def _ParseMd5SumOutput(md5sum_output):
|
| + """Returns a list of tuples from the provided md5sum output.
|
| +
|
| + Args:
|
| + md5sum_output: output directly from md5sum binary.
|
| +
|
| + Returns:
|
| + List of namedtuples with attributes |hash| and |path|, where |path| is the
|
| + absolute path to the file with an Md5Sum of |hash|.
|
| + """
|
| + HashAndPath = collections.namedtuple('HashAndPath', ['hash', 'path'])
|
| + split_lines = [line.split(' ') for line in md5sum_output]
|
| + return [HashAndPath._make(s) for s in split_lines if len(s) == 2]
|
| +
|
| +
|
| +def _HasAdbPushSucceeded(command_output):
|
| + """Returns whether adb push has succeeded from the provided output."""
|
| + # TODO(frankf): We should look at the return code instead of the command
|
| + # output for many of the commands in this file.
|
| + if not command_output:
|
| + return True
|
| + # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)"
|
| + # Errors look like this: "failed to copy ... "
|
| + if not re.search('^[0-9]', command_output.splitlines()[-1]):
|
| + logging.critical('PUSH FAILED: ' + command_output)
|
| + return False
|
| + return True
|
| +
|
| +
|
| +def GetLogTimestamp(log_line, year):
|
| + """Returns the timestamp of the given |log_line| in the given year."""
|
| + try:
|
| + return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
|
| + '%Y-%m-%d %H:%M:%S.%f')
|
| + except (ValueError, IndexError):
|
| + logging.critical('Error reading timestamp from ' + log_line)
|
| + return None
|
| +
|
| +
|
| +class AndroidCommands(object):
|
| + """Helper class for communicating with Android device via adb."""
|
| +
|
| + def __init__(self, device=None):
|
| + """Constructor.
|
| +
|
| + Args:
|
| + device: If given, adb commands are only send to the device of this ID.
|
| + Otherwise commands are sent to all attached devices.
|
| + """
|
| + self._adb = adb_interface.AdbInterface(constants.GetAdbPath())
|
| + if device:
|
| + self._adb.SetTargetSerial(device)
|
| + self._device = device
|
| + self._logcat = None
|
| + self.logcat_process = None
|
| + self._logcat_tmpoutfile = None
|
| + self._pushed_files = []
|
| + self._device_utc_offset = None
|
| + self._potential_push_size = 0
|
| + self._actual_push_size = 0
|
| + self._external_storage = ''
|
| + self._util_wrapper = ''
|
| + self._system_properties = system_properties.SystemProperties(self.Adb())
|
| + self._push_if_needed_cache = {}
|
| + self._control_usb_charging_command = {
|
| + 'command': None,
|
| + 'cached': False,
|
| + }
|
| + self._protected_file_access_method_initialized = None
|
| + self._privileged_command_runner = None
|
| + self._pie_wrapper = None
|
| +
|
| + @property
|
| + def system_properties(self):
|
| + return self._system_properties
|
| +
|
| + def _LogShell(self, cmd):
|
| + """Logs the adb shell command."""
|
| + if self._device:
|
| + device_repr = self._device[-4:]
|
| + else:
|
| + device_repr = '????'
|
| + logging.info('[%s]> %s', device_repr, cmd)
|
| +
|
| + def Adb(self):
|
| + """Returns our AdbInterface to avoid us wrapping all its methods."""
|
| + # TODO(tonyg): Goal should be to git rid of this method by making this API
|
| + # complete and alleviating the need.
|
| + return self._adb
|
| +
|
| + def GetDevice(self):
|
| + """Returns the device serial."""
|
| + return self._device
|
| +
|
| + def IsOnline(self):
|
| + """Checks whether the device is online.
|
| +
|
| + Returns:
|
| + True if device is in 'device' mode, False otherwise.
|
| + """
|
| + # TODO(aurimas): revert to using adb get-state when android L adb is fixed.
|
| + #out = self._adb.SendCommand('get-state')
|
| + #return out.strip() == 'device'
|
| +
|
| + out = self._adb.SendCommand('devices')
|
| + for line in out.split('\n'):
|
| + if self._device in line and 'device' in line:
|
| + return True
|
| + return False
|
| +
|
| + def IsRootEnabled(self):
|
| + """Checks if root is enabled on the device."""
|
| + root_test_output = self.RunShellCommand('ls /root') or ['']
|
| + return not 'Permission denied' in root_test_output[0]
|
| +
|
| + def EnableAdbRoot(self):
|
| + """Enables adb root on the device.
|
| +
|
| + Returns:
|
| + True: if output from executing adb root was as expected.
|
| + False: otherwise.
|
| + """
|
| + if self.GetBuildType() == 'user':
|
| + logging.warning("Can't enable root in production builds with type user")
|
| + return False
|
| + else:
|
| + return_value = self._adb.EnableAdbRoot()
|
| + # EnableAdbRoot inserts a call for wait-for-device only when adb logcat
|
| + # output matches what is expected. Just to be safe add a call to
|
| + # wait-for-device.
|
| + self._adb.SendCommand('wait-for-device')
|
| + return return_value
|
| +
|
| + def GetDeviceYear(self):
|
| + """Returns the year information of the date on device."""
|
| + return self.RunShellCommand('date +%Y')[0]
|
| +
|
| + def GetExternalStorage(self):
|
| + if not self._external_storage:
|
| + self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0]
|
| + if not self._external_storage:
|
| + raise device_errors.CommandFailedError(
|
| + ['shell', "'echo $EXTERNAL_STORAGE'"],
|
| + 'Unable to find $EXTERNAL_STORAGE')
|
| + return self._external_storage
|
| +
|
| + def WaitForDevicePm(self, timeout=120):
|
| + """Blocks until the device's package manager is available.
|
| +
|
| + To workaround http://b/5201039, we restart the shell and retry if the
|
| + package manager isn't back after 120 seconds.
|
| +
|
| + Raises:
|
| + errors.WaitForResponseTimedOutError after max retries reached.
|
| + """
|
| + last_err = None
|
| + retries = 3
|
| + while retries:
|
| + try:
|
| + self._adb.WaitForDevicePm(wait_time=timeout)
|
| + return # Success
|
| + except errors.WaitForResponseTimedOutError as e:
|
| + last_err = e
|
| + logging.warning('Restarting and retrying after timeout: %s', e)
|
| + retries -= 1
|
| + self.RestartShell()
|
| + raise last_err # Only reached after max retries, re-raise the last error.
|
| +
|
| + def RestartShell(self):
|
| + """Restarts the shell on the device. Does not block for it to return."""
|
| + self.RunShellCommand('stop')
|
| + self.RunShellCommand('start')
|
| +
|
| + def Reboot(self, full_reboot=True):
|
| + """Reboots the device and waits for the package manager to return.
|
| +
|
| + Args:
|
| + full_reboot: Whether to fully reboot the device or just restart the shell.
|
| + """
|
| + # TODO(torne): hive can't reboot the device either way without breaking the
|
| + # connection; work out if we can handle this better
|
| + if os.environ.get('USING_HIVE'):
|
| + logging.warning('Ignoring reboot request as we are on hive')
|
| + return
|
| + if full_reboot or not self.IsRootEnabled():
|
| + self._adb.SendCommand('reboot')
|
| + self._system_properties = system_properties.SystemProperties(self.Adb())
|
| + timeout = 300
|
| + retries = 1
|
| + # Wait for the device to disappear.
|
| + while retries < 10 and self.IsOnline():
|
| + time.sleep(1)
|
| + retries += 1
|
| + else:
|
| + self.RestartShell()
|
| + timeout = 120
|
| + # To run tests we need at least the package manager and the sd card (or
|
| + # other external storage) to be ready.
|
| + self.WaitForDevicePm(timeout)
|
| + self.WaitForSdCardReady(timeout)
|
| +
|
| + def Shutdown(self):
|
| + """Shuts down the device."""
|
| + self._adb.SendCommand('reboot -p')
|
| + self._system_properties = system_properties.SystemProperties(self.Adb())
|
| +
|
| + def Uninstall(self, package):
|
| + """Uninstalls the specified package from the device.
|
| +
|
| + Args:
|
| + package: Name of the package to remove.
|
| +
|
| + Returns:
|
| + A status string returned by adb uninstall
|
| + """
|
| + uninstall_command = 'uninstall %s' % package
|
| +
|
| + self._LogShell(uninstall_command)
|
| + return self._adb.SendCommand(uninstall_command, timeout_time=60)
|
| +
|
| + def Install(self, package_file_path, reinstall=False):
|
| + """Installs the specified package to the device.
|
| +
|
| + Args:
|
| + package_file_path: Path to .apk file to install.
|
| + reinstall: Reinstall an existing apk, keeping the data.
|
| +
|
| + Returns:
|
| + A status string returned by adb install
|
| + """
|
| + assert os.path.isfile(package_file_path), ('<%s> is not file' %
|
| + package_file_path)
|
| +
|
| + install_cmd = ['install']
|
| +
|
| + if reinstall:
|
| + install_cmd.append('-r')
|
| +
|
| + install_cmd.append(package_file_path)
|
| + install_cmd = ' '.join(install_cmd)
|
| +
|
| + self._LogShell(install_cmd)
|
| + return self._adb.SendCommand(install_cmd,
|
| + timeout_time=2 * 60,
|
| + retry_count=0)
|
| +
|
| + def ManagedInstall(self, apk_path, keep_data=False, package_name=None,
|
| + reboots_on_timeout=2):
|
| + """Installs specified package and reboots device on timeouts.
|
| +
|
| + If package_name is supplied, checks if the package is already installed and
|
| + doesn't reinstall if the apk md5sums match.
|
| +
|
| + Args:
|
| + apk_path: Path to .apk file to install.
|
| + keep_data: Reinstalls instead of uninstalling first, preserving the
|
| + application data.
|
| + package_name: Package name (only needed if keep_data=False).
|
| + reboots_on_timeout: number of time to reboot if package manager is frozen.
|
| + """
|
| + # Check if package is already installed and up to date.
|
| + if package_name:
|
| + installed_apk_path = self.GetApplicationPath(package_name)
|
| + if (installed_apk_path and
|
| + not self.GetFilesChanged(apk_path, installed_apk_path,
|
| + ignore_filenames=True)):
|
| + logging.info('Skipped install: identical %s APK already installed' %
|
| + package_name)
|
| + return
|
| + # Install.
|
| + reboots_left = reboots_on_timeout
|
| + while True:
|
| + try:
|
| + if not keep_data:
|
| + assert package_name
|
| + self.Uninstall(package_name)
|
| + install_status = self.Install(apk_path, reinstall=keep_data)
|
| + if 'Success' in install_status:
|
| + return
|
| + else:
|
| + raise Exception('Install failure: %s' % install_status)
|
| + except errors.WaitForResponseTimedOutError:
|
| + print '@@@STEP_WARNINGS@@@'
|
| + logging.info('Timeout on installing %s on device %s', apk_path,
|
| + self._device)
|
| +
|
| + if reboots_left <= 0:
|
| + raise Exception('Install timed out')
|
| +
|
| + # Force a hard reboot on last attempt
|
| + self.Reboot(full_reboot=(reboots_left == 1))
|
| + reboots_left -= 1
|
| +
|
| + def MakeSystemFolderWritable(self):
|
| + """Remounts the /system folder rw."""
|
| + out = self._adb.SendCommand('remount')
|
| + if out.strip() != 'remount succeeded':
|
| + raise errors.MsgException('Remount failed: %s' % out)
|
| +
|
| + def RestartAdbdOnDevice(self):
|
| + logging.info('Restarting adbd on the device...')
|
| + with DeviceTempFile(self, suffix=".sh") as temp_script_file:
|
| + host_script_path = os.path.join(constants.DIR_SOURCE_ROOT,
|
| + 'build',
|
| + 'android',
|
| + 'pylib',
|
| + 'restart_adbd.sh')
|
| + self._adb.Push(host_script_path, temp_script_file.name)
|
| + self.RunShellCommand('. %s' % temp_script_file.name)
|
| + self._adb.SendCommand('wait-for-device')
|
| +
|
| + def RestartAdbServer(self):
|
| + """Restart the adb server."""
|
| + ret = self.KillAdbServer()
|
| + if ret != 0:
|
| + raise errors.MsgException('KillAdbServer: %d' % ret)
|
| +
|
| + ret = self.StartAdbServer()
|
| + if ret != 0:
|
| + raise errors.MsgException('StartAdbServer: %d' % ret)
|
| +
|
| + @staticmethod
|
| + def KillAdbServer():
|
| + """Kill adb server."""
|
| + adb_cmd = [constants.GetAdbPath(), 'kill-server']
|
| + ret = cmd_helper.RunCmd(adb_cmd)
|
| + retry = 0
|
| + while retry < 3:
|
| + ret, _ = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb'])
|
| + if ret != 0:
|
| + # pgrep didn't find adb, kill-server succeeded.
|
| + return 0
|
| + retry += 1
|
| + time.sleep(retry)
|
| + return ret
|
| +
|
| + def StartAdbServer(self):
|
| + """Start adb server."""
|
| + adb_cmd = ['taskset', '-c', '0', constants.GetAdbPath(), 'start-server']
|
| + ret, _ = cmd_helper.GetCmdStatusAndOutput(adb_cmd)
|
| + retry = 0
|
| + while retry < 3:
|
| + ret, _ = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb'])
|
| + if ret == 0:
|
| + # pgrep found adb, start-server succeeded.
|
| + # Waiting for device to reconnect before returning success.
|
| + self._adb.SendCommand('wait-for-device')
|
| + return 0
|
| + retry += 1
|
| + time.sleep(retry)
|
| + return ret
|
| +
|
| + def WaitForSystemBootCompleted(self, wait_time):
|
| + """Waits for targeted system's boot_completed flag to be set.
|
| +
|
| + Args:
|
| + wait_time: time in seconds to wait
|
| +
|
| + Raises:
|
| + WaitForResponseTimedOutError if wait_time elapses and flag still not
|
| + set.
|
| + """
|
| + logging.info('Waiting for system boot completed...')
|
| + self._adb.SendCommand('wait-for-device')
|
| + # Now the device is there, but system not boot completed.
|
| + # Query the sys.boot_completed flag with a basic command
|
| + boot_completed = False
|
| + attempts = 0
|
| + wait_period = 5
|
| + while not boot_completed and (attempts * wait_period) < wait_time:
|
| + output = self.system_properties['sys.boot_completed']
|
| + output = output.strip()
|
| + if output == '1':
|
| + boot_completed = True
|
| + else:
|
| + # If 'error: xxx' returned when querying the flag, it means
|
| + # adb server lost the connection to the emulator, so restart the adb
|
| + # server.
|
| + if 'error:' in output:
|
| + self.RestartAdbServer()
|
| + time.sleep(wait_period)
|
| + attempts += 1
|
| + if not boot_completed:
|
| + raise errors.WaitForResponseTimedOutError(
|
| + 'sys.boot_completed flag was not set after %s seconds' % wait_time)
|
| +
|
| + def WaitForSdCardReady(self, timeout_time):
|
| + """Wait for the SD card ready before pushing data into it."""
|
| + logging.info('Waiting for SD card ready...')
|
| + sdcard_ready = False
|
| + attempts = 0
|
| + wait_period = 5
|
| + external_storage = self.GetExternalStorage()
|
| + while not sdcard_ready and attempts * wait_period < timeout_time:
|
| + output = self.RunShellCommand('ls ' + external_storage)
|
| + if output:
|
| + sdcard_ready = True
|
| + else:
|
| + time.sleep(wait_period)
|
| + attempts += 1
|
| + if not sdcard_ready:
|
| + raise errors.WaitForResponseTimedOutError(
|
| + 'SD card not ready after %s seconds' % timeout_time)
|
| +
|
| + def GetAndroidToolStatusAndOutput(self, command, lib_path=None, *args, **kw):
|
| + """Runs a native Android binary, wrapping the command as necessary.
|
| +
|
| + This is a specialization of GetShellCommandStatusAndOutput, which is meant
|
| + for running tools/android/ binaries and handle properly: (1) setting the
|
| + lib path (for component=shared_library), (2) using the PIE wrapper on ICS.
|
| + See crbug.com/373219 for more context.
|
| +
|
| + Args:
|
| + command: String containing the command to send.
|
| + lib_path: (optional) path to the folder containing the dependent libs.
|
| + Same other arguments of GetCmdStatusAndOutput.
|
| + """
|
| + # The first time this command is run the device is inspected to check
|
| + # whether a wrapper for running PIE executable is needed (only Android ICS)
|
| + # or not. The results is cached, so the wrapper is pushed only once.
|
| + if self._pie_wrapper is None:
|
| + # None: did not check; '': did check and not needed; '/path': use /path.
|
| + self._pie_wrapper = ''
|
| + if self.GetBuildId().startswith('I'): # Ixxxx = Android ICS.
|
| + run_pie_dist_path = os.path.join(constants.GetOutDirectory(), 'run_pie')
|
| + assert os.path.exists(run_pie_dist_path), 'Please build run_pie'
|
| + # The PIE loader must be pushed manually (i.e. no PushIfNeeded) because
|
| + # PushIfNeeded requires md5sum and md5sum requires the wrapper as well.
|
| + adb_command = 'push %s %s' % (run_pie_dist_path, PIE_WRAPPER_PATH)
|
| + assert _HasAdbPushSucceeded(self._adb.SendCommand(adb_command))
|
| + self._pie_wrapper = PIE_WRAPPER_PATH
|
| +
|
| + if self._pie_wrapper:
|
| + command = '%s %s' % (self._pie_wrapper, command)
|
| + if lib_path:
|
| + command = 'LD_LIBRARY_PATH=%s %s' % (lib_path, command)
|
| + return self.GetShellCommandStatusAndOutput(command, *args, **kw)
|
| +
|
| + # It is tempting to turn this function into a generator, however this is not
|
| + # possible without using a private (local) adb_shell instance (to ensure no
|
| + # other command interleaves usage of it), which would defeat the main aim of
|
| + # being able to reuse the adb shell instance across commands.
|
| + def RunShellCommand(self, command, timeout_time=20, log_result=False):
|
| + """Send a command to the adb shell and return the result.
|
| +
|
| + Args:
|
| + command: String containing the shell command to send.
|
| + timeout_time: Number of seconds to wait for command to respond before
|
| + retrying, used by AdbInterface.SendShellCommand.
|
| + log_result: Boolean to indicate whether we should log the result of the
|
| + shell command.
|
| +
|
| + Returns:
|
| + list containing the lines of output received from running the command
|
| + """
|
| + self._LogShell(command)
|
| + if "'" in command:
|
| + command = command.replace('\'', '\'\\\'\'')
|
| + result = self._adb.SendShellCommand(
|
| + "'%s'" % command, timeout_time).splitlines()
|
| + # TODO(b.kelemen): we should really be able to drop the stderr of the
|
| + # command or raise an exception based on what the caller wants.
|
| + result = [ l for l in result if not l.startswith('WARNING') ]
|
| + if ['error: device not found'] == result:
|
| + raise errors.DeviceUnresponsiveError('device not found')
|
| + if log_result:
|
| + self._LogShell('\n'.join(result))
|
| + return result
|
| +
|
| + def GetShellCommandStatusAndOutput(self, command, timeout_time=20,
|
| + log_result=False):
|
| + """See RunShellCommand() above.
|
| +
|
| + Returns:
|
| + The tuple (exit code, list of output lines).
|
| + """
|
| + lines = self.RunShellCommand(
|
| + command + '; echo %$?', timeout_time, log_result)
|
| + last_line = lines[-1]
|
| + status_pos = last_line.rfind('%')
|
| + assert status_pos >= 0
|
| + status = int(last_line[status_pos + 1:])
|
| + if status_pos == 0:
|
| + lines = lines[:-1]
|
| + else:
|
| + lines = lines[:-1] + [last_line[:status_pos]]
|
| + return (status, lines)
|
| +
|
| + def KillAll(self, process, signum=9, with_su=False):
|
| + """Android version of killall, connected via adb.
|
| +
|
| + Args:
|
| + process: name of the process to kill off.
|
| + signum: signal to use, 9 (SIGKILL) by default.
|
| + with_su: wether or not to use su to kill the processes.
|
| +
|
| + Returns:
|
| + the number of processes killed
|
| + """
|
| + pids = self.ExtractPid(process)
|
| + if pids:
|
| + cmd = 'kill -%d %s' % (signum, ' '.join(pids))
|
| + if with_su:
|
| + self.RunShellCommandWithSU(cmd)
|
| + else:
|
| + self.RunShellCommand(cmd)
|
| + return len(pids)
|
| +
|
| + def KillAllBlocking(self, process, timeout_sec, signum=9, with_su=False):
|
| + """Blocking version of killall, connected via adb.
|
| +
|
| + This waits until no process matching the corresponding name appears in ps'
|
| + output anymore.
|
| +
|
| + Args:
|
| + process: name of the process to kill off
|
| + timeout_sec: the timeout in seconds
|
| + signum: same as |KillAll|
|
| + with_su: same as |KillAll|
|
| + Returns:
|
| + the number of processes killed
|
| + """
|
| + processes_killed = self.KillAll(process, signum=signum, with_su=with_su)
|
| + if processes_killed:
|
| + elapsed = 0
|
| + wait_period = 0.1
|
| + # Note that this doesn't take into account the time spent in ExtractPid().
|
| + while self.ExtractPid(process) and elapsed < timeout_sec:
|
| + time.sleep(wait_period)
|
| + elapsed += wait_period
|
| + if elapsed >= timeout_sec:
|
| + return processes_killed - self.ExtractPid(process)
|
| + return processes_killed
|
| +
|
| + @staticmethod
|
| + def _GetActivityCommand(package, activity, wait_for_completion, action,
|
| + category, data, extras, trace_file_name, force_stop,
|
| + flags):
|
| + """Creates command to start |package|'s activity on the device.
|
| +
|
| + Args - as for StartActivity
|
| +
|
| + Returns:
|
| + the command to run on the target to start the activity
|
| + """
|
| + cmd = 'am start -a %s' % action
|
| + if force_stop:
|
| + cmd += ' -S'
|
| + if wait_for_completion:
|
| + cmd += ' -W'
|
| + if category:
|
| + cmd += ' -c %s' % category
|
| + if package and activity:
|
| + cmd += ' -n %s/%s' % (package, activity)
|
| + if data:
|
| + cmd += ' -d "%s"' % data
|
| + if extras:
|
| + for key in extras:
|
| + value = extras[key]
|
| + if isinstance(value, str):
|
| + cmd += ' --es'
|
| + elif isinstance(value, bool):
|
| + cmd += ' --ez'
|
| + elif isinstance(value, int):
|
| + cmd += ' --ei'
|
| + else:
|
| + raise NotImplementedError(
|
| + 'Need to teach StartActivity how to pass %s extras' % type(value))
|
| + cmd += ' %s %s' % (key, value)
|
| + if trace_file_name:
|
| + cmd += ' --start-profiler ' + trace_file_name
|
| + if flags:
|
| + cmd += ' -f %s' % flags
|
| + return cmd
|
| +
|
| + def StartActivity(self, package, activity, wait_for_completion=False,
|
| + action='android.intent.action.VIEW',
|
| + category=None, data=None,
|
| + extras=None, trace_file_name=None,
|
| + force_stop=False, flags=None):
|
| + """Starts |package|'s activity on the device.
|
| +
|
| + Args:
|
| + package: Name of package to start (e.g. 'com.google.android.apps.chrome').
|
| + activity: Name of activity (e.g. '.Main' or
|
| + 'com.google.android.apps.chrome.Main').
|
| + wait_for_completion: wait for the activity to finish launching (-W flag).
|
| + action: string (e.g. "android.intent.action.MAIN"). Default is VIEW.
|
| + category: string (e.g. "android.intent.category.HOME")
|
| + data: Data string to pass to activity (e.g. 'http://www.example.com/').
|
| + extras: Dict of extras to pass to activity. Values are significant.
|
| + trace_file_name: If used, turns on and saves the trace to this file name.
|
| + force_stop: force stop the target app before starting the activity (-S
|
| + flag).
|
| + Returns:
|
| + The output of the underlying command as a list of lines.
|
| + """
|
| + cmd = self._GetActivityCommand(package, activity, wait_for_completion,
|
| + action, category, data, extras,
|
| + trace_file_name, force_stop, flags)
|
| + return self.RunShellCommand(cmd)
|
| +
|
| + def StartActivityTimed(self, package, activity, wait_for_completion=False,
|
| + action='android.intent.action.VIEW',
|
| + category=None, data=None,
|
| + extras=None, trace_file_name=None,
|
| + force_stop=False, flags=None):
|
| + """Starts |package|'s activity on the device, returning the start time
|
| +
|
| + Args - as for StartActivity
|
| +
|
| + Returns:
|
| + A tuple containing:
|
| + - the output of the underlying command as a list of lines, and
|
| + - a timestamp string for the time at which the activity started
|
| + """
|
| + cmd = self._GetActivityCommand(package, activity, wait_for_completion,
|
| + action, category, data, extras,
|
| + trace_file_name, force_stop, flags)
|
| + self.StartMonitoringLogcat()
|
| + out = self.RunShellCommand('log starting activity; ' + cmd)
|
| + activity_started_re = re.compile('.*starting activity.*')
|
| + m = self.WaitForLogMatch(activity_started_re, None)
|
| + assert m
|
| + start_line = m.group(0)
|
| + return (out, GetLogTimestamp(start_line, self.GetDeviceYear()))
|
| +
|
| + def StartCrashUploadService(self, package):
|
| + # TODO(frankf): We really need a python wrapper around Intent
|
| + # to be shared with StartActivity/BroadcastIntent.
|
| + cmd = (
|
| + 'am startservice -a %s.crash.ACTION_FIND_ALL -n '
|
| + '%s/%s.crash.MinidumpUploadService' %
|
| + (constants.PACKAGE_INFO['chrome'].package,
|
| + package,
|
| + constants.PACKAGE_INFO['chrome'].package))
|
| + am_output = self.RunShellCommandWithSU(cmd)
|
| + assert am_output and 'Starting' in am_output[-1], (
|
| + 'Service failed to start: %s' % am_output)
|
| + time.sleep(15)
|
| +
|
| + def BroadcastIntent(self, package, intent, *args):
|
| + """Send a broadcast intent.
|
| +
|
| + Args:
|
| + package: Name of package containing the intent.
|
| + intent: Name of the intent.
|
| + args: Optional extra arguments for the intent.
|
| + """
|
| + cmd = 'am broadcast -a %s.%s %s' % (package, intent, ' '.join(args))
|
| + self.RunShellCommand(cmd)
|
| +
|
| + def GoHome(self):
|
| + """Tell the device to return to the home screen. Blocks until completion."""
|
| + self.RunShellCommand('am start -W '
|
| + '-a android.intent.action.MAIN -c android.intent.category.HOME')
|
| +
|
| + def CloseApplication(self, package):
|
| + """Attempt to close down the application, using increasing violence.
|
| +
|
| + Args:
|
| + package: Name of the process to kill off, e.g.
|
| + com.google.android.apps.chrome
|
| + """
|
| + self.RunShellCommand('am force-stop ' + package)
|
| +
|
| + def GetApplicationPath(self, package):
|
| + """Get the installed apk path on the device for the given package.
|
| +
|
| + Args:
|
| + package: Name of the package.
|
| +
|
| + Returns:
|
| + Path to the apk on the device if it exists, None otherwise.
|
| + """
|
| + pm_path_output = self.RunShellCommand('pm path ' + package)
|
| + # The path output contains anything if and only if the package
|
| + # exists.
|
| + if pm_path_output:
|
| + # pm_path_output is of the form: "package:/path/to/foo.apk"
|
| + return pm_path_output[0].split(':')[1]
|
| + else:
|
| + return None
|
| +
|
| + def ClearApplicationState(self, package):
|
| + """Closes and clears all state for the given |package|."""
|
| + # Check that the package exists before clearing it. Necessary because
|
| + # calling pm clear on a package that doesn't exist may never return.
|
| + pm_path_output = self.RunShellCommand('pm path ' + package)
|
| + # The path output only contains anything if and only if the package exists.
|
| + if pm_path_output:
|
| + self.RunShellCommand('pm clear ' + package)
|
| +
|
| + def SendKeyEvent(self, keycode):
|
| + """Sends keycode to the device.
|
| +
|
| + Args:
|
| + keycode: Numeric keycode to send (see "enum" at top of file).
|
| + """
|
| + self.RunShellCommand('input keyevent %d' % keycode)
|
| +
|
| + def _RunMd5Sum(self, host_path, device_path):
|
| + """Gets the md5sum of a host path and device path.
|
| +
|
| + Args:
|
| + host_path: Path (file or directory) on the host.
|
| + device_path: Path on the device.
|
| +
|
| + Returns:
|
| + A tuple containing lists of the host and device md5sum results as
|
| + created by _ParseMd5SumOutput().
|
| + """
|
| + md5sum_dist_path = os.path.join(constants.GetOutDirectory(),
|
| + 'md5sum_dist')
|
| + assert os.path.exists(md5sum_dist_path), 'Please build md5sum.'
|
| + md5sum_dist_mtime = os.stat(md5sum_dist_path).st_mtime
|
| + if (md5sum_dist_path not in self._push_if_needed_cache or
|
| + self._push_if_needed_cache[md5sum_dist_path] != md5sum_dist_mtime):
|
| + command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER)
|
| + assert _HasAdbPushSucceeded(self._adb.SendCommand(command))
|
| + self._push_if_needed_cache[md5sum_dist_path] = md5sum_dist_mtime
|
| +
|
| + (_, md5_device_output) = self.GetAndroidToolStatusAndOutput(
|
| + self._util_wrapper + ' ' + MD5SUM_DEVICE_PATH + ' ' + device_path,
|
| + lib_path=MD5SUM_DEVICE_FOLDER,
|
| + timeout_time=2 * 60)
|
| + device_hash_tuples = _ParseMd5SumOutput(md5_device_output)
|
| + assert os.path.exists(host_path), 'Local path not found %s' % host_path
|
| + md5sum_output = cmd_helper.GetCmdOutput(
|
| + [os.path.join(constants.GetOutDirectory(), 'md5sum_bin_host'),
|
| + host_path])
|
| + host_hash_tuples = _ParseMd5SumOutput(md5sum_output.splitlines())
|
| + return (host_hash_tuples, device_hash_tuples)
|
| +
|
| + def GetFilesChanged(self, host_path, device_path, ignore_filenames=False):
|
| + """Compares the md5sum of a host path against a device path.
|
| +
|
| + Note: Ignores extra files on the device.
|
| +
|
| + Args:
|
| + host_path: Path (file or directory) on the host.
|
| + device_path: Path on the device.
|
| + ignore_filenames: If True only the file contents are considered when
|
| + checking whether a file has changed, otherwise the relative path
|
| + must also match.
|
| +
|
| + Returns:
|
| + A list of tuples of the form (host_path, device_path) for files whose
|
| + md5sums do not match.
|
| + """
|
| +
|
| + # Md5Sum resolves symbolic links in path names so the calculation of
|
| + # relative path names from its output will need the real path names of the
|
| + # base directories. Having calculated these they are used throughout the
|
| + # function since this makes us less subject to any future changes to Md5Sum.
|
| + real_host_path = os.path.realpath(host_path)
|
| + real_device_path = self.RunShellCommand('realpath "%s"' % device_path)[0]
|
| +
|
| + host_hash_tuples, device_hash_tuples = self._RunMd5Sum(
|
| + real_host_path, real_device_path)
|
| +
|
| + if len(host_hash_tuples) > len(device_hash_tuples):
|
| + logging.info('%s files do not exist on the device' %
|
| + (len(host_hash_tuples) - len(device_hash_tuples)))
|
| +
|
| + host_rel = [(os.path.relpath(os.path.normpath(t.path), real_host_path),
|
| + t.hash)
|
| + for t in host_hash_tuples]
|
| +
|
| + if os.path.isdir(real_host_path):
|
| + def RelToRealPaths(rel_path):
|
| + return (os.path.join(real_host_path, rel_path),
|
| + os.path.join(real_device_path, rel_path))
|
| + else:
|
| + assert len(host_rel) == 1
|
| + def RelToRealPaths(_):
|
| + return (real_host_path, real_device_path)
|
| +
|
| + if ignore_filenames:
|
| + # If we are ignoring file names, then we want to push any file for which
|
| + # a file with an equivalent MD5 sum does not exist on the device.
|
| + device_hashes = set([h.hash for h in device_hash_tuples])
|
| + ShouldPush = lambda p, h: h not in device_hashes
|
| + else:
|
| + # Otherwise, we want to push any file on the host for which a file with
|
| + # an equivalent MD5 sum does not exist at the same relative path on the
|
| + # device.
|
| + device_rel = dict([(os.path.relpath(os.path.normpath(t.path),
|
| + real_device_path),
|
| + t.hash)
|
| + for t in device_hash_tuples])
|
| + ShouldPush = lambda p, h: p not in device_rel or h != device_rel[p]
|
| +
|
| + return [RelToRealPaths(path) for path, host_hash in host_rel
|
| + if ShouldPush(path, host_hash)]
|
| +
|
| + def PushIfNeeded(self, host_path, device_path):
|
| + """Pushes |host_path| to |device_path|.
|
| +
|
| + Works for files and directories. This method skips copying any paths in
|
| + |test_data_paths| that already exist on the device with the same hash.
|
| +
|
| + All pushed files can be removed by calling RemovePushedFiles().
|
| + """
|
| + MAX_INDIVIDUAL_PUSHES = 50
|
| + if not os.path.exists(host_path):
|
| + raise device_errors.CommandFailedError(
|
| + 'Local path not found %s' % host_path, device=str(self))
|
| +
|
| + # See if the file on the host changed since the last push (if any) and
|
| + # return early if it didn't. Note that this shortcut assumes that the tests
|
| + # on the device don't modify the files.
|
| + if not os.path.isdir(host_path):
|
| + if host_path in self._push_if_needed_cache:
|
| + host_path_mtime = self._push_if_needed_cache[host_path]
|
| + if host_path_mtime == os.stat(host_path).st_mtime:
|
| + return
|
| +
|
| + size = host_utils.GetRecursiveDiskUsage(host_path)
|
| + self._pushed_files.append(device_path)
|
| + self._potential_push_size += size
|
| +
|
| + if os.path.isdir(host_path):
|
| + self.RunShellCommand('mkdir -p "%s"' % device_path)
|
| +
|
| + changed_files = self.GetFilesChanged(host_path, device_path)
|
| + logging.info('Found %d files that need to be pushed to %s',
|
| + len(changed_files), device_path)
|
| + if not changed_files:
|
| + return
|
| +
|
| + def Push(host, device):
|
| + # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout
|
| + # of 60 seconds which isn't sufficient for a lot of users of this method.
|
| + push_command = 'push %s %s' % (host, device)
|
| + self._LogShell(push_command)
|
| +
|
| + # Retry push with increasing backoff if the device is busy.
|
| + retry = 0
|
| + while True:
|
| + output = self._adb.SendCommand(push_command, timeout_time=30 * 60)
|
| + if _HasAdbPushSucceeded(output):
|
| + if not os.path.isdir(host_path):
|
| + self._push_if_needed_cache[host] = os.stat(host).st_mtime
|
| + return
|
| + if retry < 3:
|
| + retry += 1
|
| + wait_time = 5 * retry
|
| + logging.error('Push failed, retrying in %d seconds: %s' %
|
| + (wait_time, output))
|
| + time.sleep(wait_time)
|
| + else:
|
| + raise Exception('Push failed: %s' % output)
|
| +
|
| + diff_size = 0
|
| + if len(changed_files) <= MAX_INDIVIDUAL_PUSHES:
|
| + diff_size = sum(host_utils.GetRecursiveDiskUsage(f[0])
|
| + for f in changed_files)
|
| +
|
| + # TODO(craigdh): Replace this educated guess with a heuristic that
|
| + # approximates the push time for each method.
|
| + if len(changed_files) > MAX_INDIVIDUAL_PUSHES or diff_size > 0.5 * size:
|
| + self._actual_push_size += size
|
| + Push(host_path, device_path)
|
| + else:
|
| + for f in changed_files:
|
| + Push(f[0], f[1])
|
| + self._actual_push_size += diff_size
|
| +
|
| + def GetPushSizeInfo(self):
|
| + """Get total size of pushes to the device done via PushIfNeeded()
|
| +
|
| + Returns:
|
| + A tuple:
|
| + 1. Total size of push requests to PushIfNeeded (MB)
|
| + 2. Total size that was actually pushed (MB)
|
| + """
|
| + return (self._potential_push_size, self._actual_push_size)
|
| +
|
| + def GetFileContents(self, filename, log_result=False):
|
| + """Gets contents from the file specified by |filename|."""
|
| + return self.RunShellCommand('cat "%s" 2>/dev/null' % filename,
|
| + log_result=log_result)
|
| +
|
| + def SetFileContents(self, filename, contents):
|
| + """Writes |contents| to the file specified by |filename|."""
|
| + with tempfile.NamedTemporaryFile() as f:
|
| + f.write(contents)
|
| + f.flush()
|
| + self._adb.Push(f.name, filename)
|
| +
|
| + def RunShellCommandWithSU(self, command, timeout_time=20, log_result=False):
|
| + return self.RunShellCommand('su -c %s' % command, timeout_time, log_result)
|
| +
|
| + def CanAccessProtectedFileContents(self):
|
| + """Returns True if Get/SetProtectedFileContents would work via "su" or adb
|
| + shell running as root.
|
| +
|
| + Devices running user builds don't have adb root, but may provide "su" which
|
| + can be used for accessing protected files.
|
| + """
|
| + return (self._GetProtectedFileCommandRunner() != None)
|
| +
|
| + def _GetProtectedFileCommandRunner(self):
|
| + """Finds the best method to access protected files on the device.
|
| +
|
| + Returns:
|
| + 1. None when privileged files cannot be accessed on the device.
|
| + 2. Otherwise: A function taking a single parameter: a string with command
|
| + line arguments. Running that function executes the command with
|
| + the appropriate method.
|
| + """
|
| + if self._protected_file_access_method_initialized:
|
| + return self._privileged_command_runner
|
| +
|
| + self._privileged_command_runner = None
|
| + self._protected_file_access_method_initialized = True
|
| +
|
| + for cmd in [self.RunShellCommand, self.RunShellCommandWithSU]:
|
| + # Get contents of the auxv vector for the init(8) process from a small
|
| + # binary file that always exists on linux and is always read-protected.
|
| + contents = cmd('cat /proc/1/auxv')
|
| + # The leading 4 or 8-bytes of auxv vector is a_type. There are not many
|
| + # reserved a_type values, hence byte 2 must always be '\0' for a realistic
|
| + # auxv. See /usr/include/elf.h.
|
| + if len(contents) > 0 and (contents[0][2] == '\0'):
|
| + self._privileged_command_runner = cmd
|
| + break
|
| + return self._privileged_command_runner
|
| +
|
| + def GetProtectedFileContents(self, filename):
|
| + """Gets contents from the protected file specified by |filename|.
|
| +
|
| + This is potentially less efficient than GetFileContents.
|
| + """
|
| + command = 'cat "%s" 2> /dev/null' % filename
|
| + command_runner = self._GetProtectedFileCommandRunner()
|
| + if command_runner:
|
| + return command_runner(command)
|
| + else:
|
| + logging.warning('Could not access protected file: %s' % filename)
|
| + return []
|
| +
|
| + def SetProtectedFileContents(self, filename, contents):
|
| + """Writes |contents| to the protected file specified by |filename|.
|
| +
|
| + This is less efficient than SetFileContents.
|
| + """
|
| + with DeviceTempFile(self) as temp_file:
|
| + with DeviceTempFile(self, suffix=".sh") as temp_script:
|
| + # Put the contents in a temporary file
|
| + self.SetFileContents(temp_file.name, contents)
|
| + # Create a script to copy the file contents to its final destination
|
| + self.SetFileContents(temp_script.name,
|
| + 'cat %s > %s' % (temp_file.name, filename))
|
| +
|
| + command = 'sh %s' % temp_script.name
|
| + command_runner = self._GetProtectedFileCommandRunner()
|
| + if command_runner:
|
| + return command_runner(command)
|
| + else:
|
| + logging.warning(
|
| + 'Could not set contents of protected file: %s' % filename)
|
| +
|
| +
|
| + def RemovePushedFiles(self):
|
| + """Removes all files pushed with PushIfNeeded() from the device."""
|
| + for p in self._pushed_files:
|
| + self.RunShellCommand('rm -r %s' % p, timeout_time=2 * 60)
|
| +
|
| + def ListPathContents(self, path):
|
| + """Lists files in all subdirectories of |path|.
|
| +
|
| + Args:
|
| + path: The path to list.
|
| +
|
| + Returns:
|
| + A dict of {"name": (size, lastmod), ...}.
|
| + """
|
| + # Example output:
|
| + # /foo/bar:
|
| + # -rw-r----- user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt
|
| + re_file = re.compile('^-(?P<perms>[^\s]+)\s+'
|
| + '(?P<user>[^\s]+)\s+'
|
| + '(?P<group>[^\s]+)\s+'
|
| + '(?P<size>[^\s]+)\s+'
|
| + '(?P<date>[^\s]+)\s+'
|
| + '(?P<time>[^\s]+)\s+'
|
| + '(?P<filename>[^\s]+)$')
|
| + return _GetFilesFromRecursiveLsOutput(
|
| + path, self.RunShellCommand('ls -lR %s' % path), re_file,
|
| + self.GetUtcOffset())
|
| +
|
| + def GetUtcOffset(self):
|
| + if not self._device_utc_offset:
|
| + self._device_utc_offset = self.RunShellCommand('date +%z')[0]
|
| + return self._device_utc_offset
|
| +
|
| + def SetJavaAssertsEnabled(self, enable):
|
| + """Sets or removes the device java assertions property.
|
| +
|
| + Args:
|
| + enable: If True the property will be set.
|
| +
|
| + Returns:
|
| + True if the file was modified (reboot is required for it to take effect).
|
| + """
|
| + # First ensure the desired property is persisted.
|
| + temp_props_file = tempfile.NamedTemporaryFile()
|
| + properties = ''
|
| + if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name):
|
| + with open(temp_props_file.name) as f:
|
| + properties = f.read()
|
| + re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
|
| + r'\s*=\s*all\s*$', re.MULTILINE)
|
| + if enable != bool(re.search(re_search, properties)):
|
| + re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
|
| + r'\s*=\s*\w+\s*$', re.MULTILINE)
|
| + properties = re.sub(re_replace, '', properties)
|
| + if enable:
|
| + properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY
|
| +
|
| + file(temp_props_file.name, 'w').write(properties)
|
| + self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH)
|
| +
|
| + # Next, check the current runtime value is what we need, and
|
| + # if not, set it and report that a reboot is required.
|
| + was_set = 'all' in self.system_properties[JAVA_ASSERT_PROPERTY]
|
| + if was_set == enable:
|
| + return False
|
| + self.system_properties[JAVA_ASSERT_PROPERTY] = enable and 'all' or ''
|
| + return True
|
| +
|
| + def GetBuildId(self):
|
| + """Returns the build ID of the system (e.g. JRM79C)."""
|
| + build_id = self.system_properties['ro.build.id']
|
| + assert build_id
|
| + return build_id
|
| +
|
| + def GetBuildType(self):
|
| + """Returns the build type of the system (e.g. eng)."""
|
| + build_type = self.system_properties['ro.build.type']
|
| + assert build_type
|
| + return build_type
|
| +
|
| + def GetBuildProduct(self):
|
| + """Returns the build product of the device (e.g. maguro)."""
|
| + build_product = self.system_properties['ro.build.product']
|
| + assert build_product
|
| + return build_product
|
| +
|
| + def GetProductName(self):
|
| + """Returns the product name of the device (e.g. takju)."""
|
| + name = self.system_properties['ro.product.name']
|
| + assert name
|
| + return name
|
| +
|
| + def GetBuildFingerprint(self):
|
| + """Returns the build fingerprint of the device."""
|
| + build_fingerprint = self.system_properties['ro.build.fingerprint']
|
| + assert build_fingerprint
|
| + return build_fingerprint
|
| +
|
| + def GetDescription(self):
|
| + """Returns the description of the system.
|
| +
|
| + For example, "yakju-userdebug 4.1 JRN54F 364167 dev-keys".
|
| + """
|
| + description = self.system_properties['ro.build.description']
|
| + assert description
|
| + return description
|
| +
|
| + def GetProductModel(self):
|
| + """Returns the name of the product model (e.g. "Galaxy Nexus") """
|
| + model = self.system_properties['ro.product.model']
|
| + assert model
|
| + return model
|
| +
|
| + def GetWifiIP(self):
|
| + """Returns the wifi IP on the device."""
|
| + wifi_ip = self.system_properties['dhcp.wlan0.ipaddress']
|
| + # Do not assert here. Devices (e.g. emulators) may not have a WifiIP.
|
| + return wifi_ip
|
| +
|
| + def GetSubscriberInfo(self):
|
| + """Returns the device subscriber info (e.g. GSM and device ID) as string."""
|
| + iphone_sub = self.RunShellCommand('dumpsys iphonesubinfo')
|
| + # Do not assert here. Devices (e.g. Nakasi on K) may not have iphonesubinfo.
|
| + return '\n'.join(iphone_sub)
|
| +
|
| + def GetBatteryInfo(self):
|
| + """Returns a {str: str} dict of battery info (e.g. status, level, etc)."""
|
| + battery = self.RunShellCommand('dumpsys battery')
|
| + assert battery
|
| + battery_info = {}
|
| + for line in battery[1:]:
|
| + k, _, v = line.partition(': ')
|
| + battery_info[k.strip()] = v.strip()
|
| + return battery_info
|
| +
|
| + def GetSetupWizardStatus(self):
|
| + """Returns the status of the device setup wizard (e.g. DISABLED)."""
|
| + status = self.system_properties['ro.setupwizard.mode']
|
| + # On some devices, the status is empty if not otherwise set. In such cases
|
| + # the caller should expect an empty string to be returned.
|
| + return status
|
| +
|
| + def StartMonitoringLogcat(self, clear=True, logfile=None, filters=None):
|
| + """Starts monitoring the output of logcat, for use with WaitForLogMatch.
|
| +
|
| + Args:
|
| + clear: If True the existing logcat output will be cleared, to avoiding
|
| + matching historical output lurking in the log.
|
| + filters: A list of logcat filters to be used.
|
| + """
|
| + if clear:
|
| + self.RunShellCommand('logcat -c')
|
| + args = []
|
| + if self._adb._target_arg:
|
| + args += shlex.split(self._adb._target_arg)
|
| + args += ['logcat', '-v', 'threadtime']
|
| + if filters:
|
| + args.extend(filters)
|
| + else:
|
| + args.append('*:v')
|
| +
|
| + if logfile:
|
| + logfile = NewLineNormalizer(logfile)
|
| +
|
| + # Spawn logcat and synchronize with it.
|
| + for _ in range(4):
|
| + self._logcat = pexpect.spawn(constants.GetAdbPath(), args, timeout=10,
|
| + logfile=logfile)
|
| + if not clear or self.SyncLogCat():
|
| + break
|
| + self._logcat.close(force=True)
|
| + else:
|
| + logging.critical('Error reading from logcat: ' + str(self._logcat.match))
|
| + sys.exit(1)
|
| +
|
| + def SyncLogCat(self):
|
| + """Synchronize with logcat.
|
| +
|
| + Synchronize with the monitored logcat so that WaitForLogMatch will only
|
| + consider new message that are received after this point in time.
|
| +
|
| + Returns:
|
| + True if the synchronization succeeded.
|
| + """
|
| + assert self._logcat
|
| + tag = 'logcat_sync_%s' % time.time()
|
| + self.RunShellCommand('log ' + tag)
|
| + return self._logcat.expect([tag, pexpect.EOF, pexpect.TIMEOUT]) == 0
|
| +
|
| + def GetMonitoredLogCat(self):
|
| + """Returns an "adb logcat" command as created by pexpected.spawn."""
|
| + if not self._logcat:
|
| + self.StartMonitoringLogcat(clear=False)
|
| + return self._logcat
|
| +
|
| + def WaitForLogMatch(self, success_re, error_re, clear=False, timeout=10):
|
| + """Blocks until a matching line is logged or a timeout occurs.
|
| +
|
| + Args:
|
| + success_re: A compiled re to search each line for.
|
| + error_re: A compiled re which, if found, terminates the search for
|
| + |success_re|. If None is given, no error condition will be detected.
|
| + clear: If True the existing logcat output will be cleared, defaults to
|
| + false.
|
| + timeout: Timeout in seconds to wait for a log match.
|
| +
|
| + Raises:
|
| + pexpect.TIMEOUT after |timeout| seconds without a match for |success_re|
|
| + or |error_re|.
|
| +
|
| + Returns:
|
| + The re match object if |success_re| is matched first or None if |error_re|
|
| + is matched first.
|
| + """
|
| + logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
|
| + t0 = time.time()
|
| + while True:
|
| + if not self._logcat:
|
| + self.StartMonitoringLogcat(clear)
|
| + try:
|
| + while True:
|
| + # Note this will block for upto the timeout _per log line_, so we need
|
| + # to calculate the overall timeout remaining since t0.
|
| + time_remaining = t0 + timeout - time.time()
|
| + if time_remaining < 0:
|
| + raise pexpect.TIMEOUT(self._logcat)
|
| + self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining)
|
| + line = self._logcat.match.group(1)
|
| + if error_re:
|
| + error_match = error_re.search(line)
|
| + if error_match:
|
| + return None
|
| + success_match = success_re.search(line)
|
| + if success_match:
|
| + return success_match
|
| + logging.info('<<< Skipped Logcat Line:' + str(line))
|
| + except pexpect.TIMEOUT:
|
| + raise pexpect.TIMEOUT(
|
| + 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv '
|
| + 'to debug)' %
|
| + (timeout, success_re.pattern))
|
| + except pexpect.EOF:
|
| + # It seems that sometimes logcat can end unexpectedly. This seems
|
| + # to happen during Chrome startup after a reboot followed by a cache
|
| + # clean. I don't understand why this happens, but this code deals with
|
| + # getting EOF in logcat.
|
| + logging.critical('Found EOF in adb logcat. Restarting...')
|
| + # Rerun spawn with original arguments. Note that self._logcat.args[0] is
|
| + # the path of adb, so we don't want it in the arguments.
|
| + self._logcat = pexpect.spawn(constants.GetAdbPath(),
|
| + self._logcat.args[1:],
|
| + timeout=self._logcat.timeout,
|
| + logfile=self._logcat.logfile)
|
| +
|
| + def StartRecordingLogcat(self, clear=True, filters=None):
|
| + """Starts recording logcat output to eventually be saved as a string.
|
| +
|
| + This call should come before some series of tests are run, with either
|
| + StopRecordingLogcat or SearchLogcatRecord following the tests.
|
| +
|
| + Args:
|
| + clear: True if existing log output should be cleared.
|
| + filters: A list of logcat filters to be used.
|
| + """
|
| + if not filters:
|
| + filters = ['*:v']
|
| + if clear:
|
| + self._adb.SendCommand('logcat -c')
|
| + logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
|
| + ' '.join(filters))
|
| + self._logcat_tmpoutfile = tempfile.NamedTemporaryFile(bufsize=0)
|
| + self.logcat_process = subprocess.Popen(logcat_command, shell=True,
|
| + stdout=self._logcat_tmpoutfile)
|
| +
|
| + def GetCurrentRecordedLogcat(self):
|
| + """Return the current content of the logcat being recorded.
|
| + Call this after StartRecordingLogcat() and before StopRecordingLogcat().
|
| + This can be useful to perform timed polling/parsing.
|
| + Returns:
|
| + Current logcat output as a single string, or None if
|
| + StopRecordingLogcat() was already called.
|
| + """
|
| + if not self._logcat_tmpoutfile:
|
| + return None
|
| +
|
| + with open(self._logcat_tmpoutfile.name) as f:
|
| + return f.read()
|
| +
|
| + def StopRecordingLogcat(self):
|
| + """Stops an existing logcat recording subprocess and returns output.
|
| +
|
| + Returns:
|
| + The logcat output as a string or an empty string if logcat was not
|
| + being recorded at the time.
|
| + """
|
| + if not self.logcat_process:
|
| + return ''
|
| + # Cannot evaluate directly as 0 is a possible value.
|
| + # Better to read the self.logcat_process.stdout before killing it,
|
| + # Otherwise the communicate may return incomplete output due to pipe break.
|
| + if self.logcat_process.poll() is None:
|
| + self.logcat_process.kill()
|
| + self.logcat_process.wait()
|
| + self.logcat_process = None
|
| + self._logcat_tmpoutfile.seek(0)
|
| + output = self._logcat_tmpoutfile.read()
|
| + self._logcat_tmpoutfile.close()
|
| + self._logcat_tmpoutfile = None
|
| + return output
|
| +
|
| + @staticmethod
|
| + def SearchLogcatRecord(record, message, thread_id=None, proc_id=None,
|
| + log_level=None, component=None):
|
| + """Searches the specified logcat output and returns results.
|
| +
|
| + This method searches through the logcat output specified by record for a
|
| + certain message, narrowing results by matching them against any other
|
| + specified criteria. It returns all matching lines as described below.
|
| +
|
| + Args:
|
| + record: A string generated by Start/StopRecordingLogcat to search.
|
| + message: An output string to search for.
|
| + thread_id: The thread id that is the origin of the message.
|
| + proc_id: The process that is the origin of the message.
|
| + log_level: The log level of the message.
|
| + component: The name of the component that would create the message.
|
| +
|
| + Returns:
|
| + A list of dictionaries represeting matching entries, each containing keys
|
| + thread_id, proc_id, log_level, component, and message.
|
| + """
|
| + if thread_id:
|
| + thread_id = str(thread_id)
|
| + if proc_id:
|
| + proc_id = str(proc_id)
|
| + results = []
|
| + reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$',
|
| + re.MULTILINE)
|
| + log_list = reg.findall(record)
|
| + for (tid, pid, log_lev, comp, msg) in log_list:
|
| + if ((not thread_id or thread_id == tid) and
|
| + (not proc_id or proc_id == pid) and
|
| + (not log_level or log_level == log_lev) and
|
| + (not component or component == comp) and msg.find(message) > -1):
|
| + match = dict({'thread_id': tid, 'proc_id': pid,
|
| + 'log_level': log_lev, 'component': comp,
|
| + 'message': msg})
|
| + results.append(match)
|
| + return results
|
| +
|
| + def ExtractPid(self, process_name):
|
| + """Extracts Process Ids for a given process name from Android Shell.
|
| +
|
| + Args:
|
| + process_name: name of the process on the device.
|
| +
|
| + Returns:
|
| + List of all the process ids (as strings) that match the given name.
|
| + If the name of a process exactly matches the given name, the pid of
|
| + that process will be inserted to the front of the pid list.
|
| + """
|
| + pids = []
|
| + for line in self.RunShellCommand('ps', log_result=False):
|
| + data = line.split()
|
| + try:
|
| + if process_name in data[-1]: # name is in the last column
|
| + if process_name == data[-1]:
|
| + pids.insert(0, data[1]) # PID is in the second column
|
| + else:
|
| + pids.append(data[1])
|
| + except IndexError:
|
| + pass
|
| + return pids
|
| +
|
| + def GetIoStats(self):
|
| + """Gets cumulative disk IO stats since boot (for all processes).
|
| +
|
| + Returns:
|
| + Dict of {num_reads, num_writes, read_ms, write_ms} or None if there
|
| + was an error.
|
| + """
|
| + IoStats = collections.namedtuple(
|
| + 'IoStats',
|
| + ['device',
|
| + 'num_reads_issued',
|
| + 'num_reads_merged',
|
| + 'num_sectors_read',
|
| + 'ms_spent_reading',
|
| + 'num_writes_completed',
|
| + 'num_writes_merged',
|
| + 'num_sectors_written',
|
| + 'ms_spent_writing',
|
| + 'num_ios_in_progress',
|
| + 'ms_spent_doing_io',
|
| + 'ms_spent_doing_io_weighted',
|
| + ])
|
| +
|
| + for line in self.GetFileContents('/proc/diskstats', log_result=False):
|
| + fields = line.split()
|
| + stats = IoStats._make([fields[2]] + [int(f) for f in fields[3:]])
|
| + if stats.device == 'mmcblk0':
|
| + return {
|
| + 'num_reads': stats.num_reads_issued,
|
| + 'num_writes': stats.num_writes_completed,
|
| + 'read_ms': stats.ms_spent_reading,
|
| + 'write_ms': stats.ms_spent_writing,
|
| + }
|
| + logging.warning('Could not find disk IO stats.')
|
| + return None
|
| +
|
| + def GetMemoryUsageForPid(self, pid):
|
| + """Returns the memory usage for given pid.
|
| +
|
| + Args:
|
| + pid: The pid number of the specific process running on device.
|
| +
|
| + Returns:
|
| + Dict of {metric:usage_kb}, for the process which has specified pid.
|
| + The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
|
| + Shared_Dirty, Private_Clean, Private_Dirty, VmHWM.
|
| + """
|
| + showmap = self.RunShellCommand('showmap %d' % pid)
|
| + if not showmap or not showmap[-1].endswith('TOTAL'):
|
| + logging.warning('Invalid output for showmap %s', str(showmap))
|
| + return {}
|
| + items = showmap[-1].split()
|
| + if len(items) != 9:
|
| + logging.warning('Invalid TOTAL for showmap %s', str(items))
|
| + return {}
|
| + usage_dict = collections.defaultdict(int)
|
| + usage_dict.update({
|
| + 'Size': int(items[0].strip()),
|
| + 'Rss': int(items[1].strip()),
|
| + 'Pss': int(items[2].strip()),
|
| + 'Shared_Clean': int(items[3].strip()),
|
| + 'Shared_Dirty': int(items[4].strip()),
|
| + 'Private_Clean': int(items[5].strip()),
|
| + 'Private_Dirty': int(items[6].strip()),
|
| + })
|
| + peak_value_kb = 0
|
| + for line in self.GetProtectedFileContents('/proc/%s/status' % pid):
|
| + if not line.startswith('VmHWM:'): # Format: 'VmHWM: +[0-9]+ kB'
|
| + continue
|
| + peak_value_kb = int(line.split(':')[1].strip().split(' ')[0])
|
| + break
|
| + usage_dict['VmHWM'] = peak_value_kb
|
| + if not peak_value_kb:
|
| + logging.warning('Could not find memory peak value for pid ' + str(pid))
|
| +
|
| + return usage_dict
|
| +
|
| + def ProcessesUsingDevicePort(self, device_port):
|
| + """Lists processes using the specified device port on loopback interface.
|
| +
|
| + Args:
|
| + device_port: Port on device we want to check.
|
| +
|
| + Returns:
|
| + A list of (pid, process_name) tuples using the specified port.
|
| + """
|
| + tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False)
|
| + tcp_address = '0100007F:%04X' % device_port
|
| + pids = []
|
| + for single_connect in tcp_results:
|
| + connect_results = single_connect.split()
|
| + # Column 1 is the TCP port, and Column 9 is the inode of the socket
|
| + if connect_results[1] == tcp_address:
|
| + socket_inode = connect_results[9]
|
| + socket_name = 'socket:[%s]' % socket_inode
|
| + lsof_results = self.RunShellCommand('lsof', log_result=False)
|
| + for single_process in lsof_results:
|
| + process_results = single_process.split()
|
| + # Ignore the line if it has less than nine columns in it, which may
|
| + # be the case when a process stops while lsof is executing.
|
| + if len(process_results) <= 8:
|
| + continue
|
| + # Column 0 is the executable name
|
| + # Column 1 is the pid
|
| + # Column 8 is the Inode in use
|
| + if process_results[8] == socket_name:
|
| + pids.append((int(process_results[1]), process_results[0]))
|
| + break
|
| + logging.info('PidsUsingDevicePort: %s', pids)
|
| + return pids
|
| +
|
| + def FileExistsOnDevice(self, file_name):
|
| + """Checks whether the given file exists on the device.
|
| +
|
| + Args:
|
| + file_name: Full path of file to check.
|
| +
|
| + Returns:
|
| + True if the file exists, False otherwise.
|
| + """
|
| + assert '"' not in file_name, 'file_name cannot contain double quotes'
|
| + try:
|
| + status = self._adb.SendShellCommand(
|
| + '\'test -e "%s"; echo $?\'' % (file_name))
|
| + if 'test: not found' not in status:
|
| + return int(status) == 0
|
| +
|
| + status = self._adb.SendShellCommand(
|
| + '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name))
|
| + return int(status) == 0
|
| + except ValueError:
|
| + if IsDeviceAttached(self._device):
|
| + raise errors.DeviceUnresponsiveError('Device may be offline.')
|
| +
|
| + return False
|
| +
|
| + def IsFileWritableOnDevice(self, file_name):
|
| + """Checks whether the given file (or directory) is writable on the device.
|
| +
|
| + Args:
|
| + file_name: Full path of file/directory to check.
|
| +
|
| + Returns:
|
| + True if writable, False otherwise.
|
| + """
|
| + assert '"' not in file_name, 'file_name cannot contain double quotes'
|
| + try:
|
| + status = self._adb.SendShellCommand(
|
| + '\'test -w "%s"; echo $?\'' % (file_name))
|
| + if 'test: not found' not in status:
|
| + return int(status) == 0
|
| + raise errors.AbortError('"test" binary not found. OS too old.')
|
| +
|
| + except ValueError:
|
| + if IsDeviceAttached(self._device):
|
| + raise errors.DeviceUnresponsiveError('Device may be offline.')
|
| +
|
| + return False
|
| +
|
| + @staticmethod
|
| + def GetTimestamp():
|
| + return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
|
| +
|
| + @staticmethod
|
| + def EnsureHostDirectory(host_file):
|
| + host_dir = os.path.dirname(os.path.abspath(host_file))
|
| + if not os.path.exists(host_dir):
|
| + os.makedirs(host_dir)
|
| +
|
| + def TakeScreenshot(self, host_file=None):
|
| + """Saves a screenshot image to |host_file| on the host.
|
| +
|
| + Args:
|
| + host_file: Absolute path to the image file to store on the host or None to
|
| + use an autogenerated file name.
|
| +
|
| + Returns:
|
| + Resulting host file name of the screenshot.
|
| + """
|
| + host_file = os.path.abspath(host_file or
|
| + 'screenshot-%s.png' % self.GetTimestamp())
|
| + self.EnsureHostDirectory(host_file)
|
| + device_file = '%s/screenshot.png' % self.GetExternalStorage()
|
| + self.RunShellCommand(
|
| + '/system/bin/screencap -p %s' % device_file)
|
| + self.PullFileFromDevice(device_file, host_file)
|
| + self.RunShellCommand('rm -f "%s"' % device_file)
|
| + return host_file
|
| +
|
| + def PullFileFromDevice(self, device_file, host_file):
|
| + """Download |device_file| on the device from to |host_file| on the host.
|
| +
|
| + Args:
|
| + device_file: Absolute path to the file to retrieve from the device.
|
| + host_file: Absolute path to the file to store on the host.
|
| + """
|
| + if not self._adb.Pull(device_file, host_file):
|
| + raise device_errors.AdbCommandFailedError(
|
| + ['pull', device_file, host_file], 'Failed to pull file from device.')
|
| + assert os.path.exists(host_file)
|
| +
|
| + def SetUtilWrapper(self, util_wrapper):
|
| + """Sets a wrapper prefix to be used when running a locally-built
|
| + binary on the device (ex.: md5sum_bin).
|
| + """
|
| + self._util_wrapper = util_wrapper
|
| +
|
| + def RunUIAutomatorTest(self, test, test_package, timeout):
|
| + """Runs a single uiautomator test.
|
| +
|
| + Args:
|
| + test: Test class/method.
|
| + test_package: Name of the test jar.
|
| + timeout: Timeout time in seconds.
|
| +
|
| + Returns:
|
| + An instance of am_instrument_parser.TestResult object.
|
| + """
|
| + cmd = 'uiautomator runtest %s -e class %s' % (test_package, test)
|
| + self._LogShell(cmd)
|
| + output = self._adb.SendShellCommand(cmd, timeout_time=timeout)
|
| + # uiautomator doesn't fully conform to the instrumenation test runner
|
| + # convention and doesn't terminate with INSTRUMENTATION_CODE.
|
| + # Just assume the first result is valid.
|
| + (test_results, _) = am_instrument_parser.ParseAmInstrumentOutput(output)
|
| + if not test_results:
|
| + raise errors.InstrumentationError(
|
| + 'no test results... device setup correctly?')
|
| + return test_results[0]
|
| +
|
| + def DismissCrashDialogIfNeeded(self):
|
| + """Dismiss the error/ANR dialog if present.
|
| +
|
| + Returns: Name of the crashed package if a dialog is focused,
|
| + None otherwise.
|
| + """
|
| + re_focus = re.compile(
|
| + r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}')
|
| +
|
| + def _FindFocusedWindow():
|
| + match = None
|
| + for line in self.RunShellCommand('dumpsys window windows'):
|
| + match = re.match(re_focus, line)
|
| + if match:
|
| + break
|
| + return match
|
| +
|
| + match = _FindFocusedWindow()
|
| + if not match:
|
| + return
|
| + package = match.group(2)
|
| + logging.warning('Trying to dismiss %s dialog for %s' % match.groups())
|
| + self.SendKeyEvent(KEYCODE_DPAD_RIGHT)
|
| + self.SendKeyEvent(KEYCODE_DPAD_RIGHT)
|
| + self.SendKeyEvent(KEYCODE_ENTER)
|
| + match = _FindFocusedWindow()
|
| + if match:
|
| + logging.error('Still showing a %s dialog for %s' % match.groups())
|
| + return package
|
| +
|
| + def EfficientDeviceDirectoryCopy(self, source, dest):
|
| + """ Copy a directory efficiently on the device
|
| +
|
| + Uses a shell script running on the target to copy new and changed files the
|
| + source directory to the destination directory and remove added files. This
|
| + is in some cases much faster than cp -r.
|
| +
|
| + Args:
|
| + source: absolute path of source directory
|
| + dest: absolute path of destination directory
|
| + """
|
| + logging.info('In EfficientDeviceDirectoryCopy %s %s', source, dest)
|
| + with DeviceTempFile(self, suffix=".sh") as temp_script_file:
|
| + host_script_path = os.path.join(constants.DIR_SOURCE_ROOT,
|
| + 'build',
|
| + 'android',
|
| + 'pylib',
|
| + 'efficient_android_directory_copy.sh')
|
| + self._adb.Push(host_script_path, temp_script_file.name)
|
| + out = self.RunShellCommand(
|
| + 'sh %s %s %s' % (temp_script_file.name, source, dest),
|
| + timeout_time=120)
|
| + if self._device:
|
| + device_repr = self._device[-4:]
|
| + else:
|
| + device_repr = '????'
|
| + for line in out:
|
| + logging.info('[%s]> %s', device_repr, line)
|
| +
|
| + def _GetControlUsbChargingCommand(self):
|
| + if self._control_usb_charging_command['cached']:
|
| + return self._control_usb_charging_command['command']
|
| + self._control_usb_charging_command['cached'] = True
|
| + if not self.IsRootEnabled():
|
| + return None
|
| + for command in CONTROL_USB_CHARGING_COMMANDS:
|
| + # Assert command is valid.
|
| + assert 'disable_command' in command
|
| + assert 'enable_command' in command
|
| + assert 'witness_file' in command
|
| + witness_file = command['witness_file']
|
| + if self.FileExistsOnDevice(witness_file):
|
| + self._control_usb_charging_command['command'] = command
|
| + return command
|
| + return None
|
| +
|
| + def CanControlUsbCharging(self):
|
| + return self._GetControlUsbChargingCommand() is not None
|
| +
|
| + def DisableUsbCharging(self, timeout=10):
|
| + command = self._GetControlUsbChargingCommand()
|
| + if not command:
|
| + raise Exception('Unable to act on usb charging.')
|
| + disable_command = command['disable_command']
|
| + t0 = time.time()
|
| + # Do not loop directly on self.IsDeviceCharging to cut the number of calls
|
| + # to the device.
|
| + while True:
|
| + if t0 + timeout - time.time() < 0:
|
| + raise pexpect.TIMEOUT('Unable to disable USB charging in time: %s' % (
|
| + self.GetBatteryInfo()))
|
| + self.RunShellCommand(disable_command)
|
| + if not self.IsDeviceCharging():
|
| + break
|
| +
|
| + def EnableUsbCharging(self, timeout=10):
|
| + command = self._GetControlUsbChargingCommand()
|
| + if not command:
|
| + raise Exception('Unable to act on usb charging.')
|
| + disable_command = command['enable_command']
|
| + t0 = time.time()
|
| + # Do not loop directly on self.IsDeviceCharging to cut the number of calls
|
| + # to the device.
|
| + while True:
|
| + if t0 + timeout - time.time() < 0:
|
| + raise pexpect.TIMEOUT('Unable to enable USB charging in time.')
|
| + self.RunShellCommand(disable_command)
|
| + if self.IsDeviceCharging():
|
| + break
|
| +
|
| + def IsDeviceCharging(self):
|
| + for line in self.RunShellCommand('dumpsys battery'):
|
| + if 'powered: ' in line:
|
| + if line.split('powered: ')[1] == 'true':
|
| + return True
|
| +
|
| +
|
| +class NewLineNormalizer(object):
|
| + """A file-like object to normalize EOLs to '\n'.
|
| +
|
| + Pexpect runs adb within a pseudo-tty device (see
|
| + http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written
|
| + as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate
|
| + lines, the log ends up having '\r\r\n' at the end of each line. This
|
| + filter replaces the above with a single '\n' in the data stream.
|
| + """
|
| + def __init__(self, output):
|
| + self._output = output
|
| +
|
| + def write(self, data):
|
| + data = data.replace('\r\r\n', '\n')
|
| + self._output.write(data)
|
| +
|
| + def flush(self):
|
| + self._output.flush()
|
|
|