1529 lines
55 KiB
Python
1529 lines
55 KiB
Python
# 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.
|
|
"""
|
|
|
|
import collections
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
import cmd_helper
|
|
import constants
|
|
import io_stats_parser
|
|
try:
|
|
import pexpect
|
|
except:
|
|
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
|
|
|
|
|
|
# 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 = '/data/local.prop'
|
|
|
|
# Property in /data/local.prop that controls Java assertions.
|
|
JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
|
|
|
|
MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$')
|
|
NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*'
|
|
'(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$')
|
|
|
|
# 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'
|
|
MD5SUM_LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % MD5SUM_DEVICE_FOLDER
|
|
|
|
|
|
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 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.ADB_PATH, '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_:.-]+)\toffline$', 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
|
|
|
|
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.
|
|
|
|
Args:
|
|
device: If given, adb commands are only send to the device of this ID.
|
|
Otherwise commands are sent to all attached devices.
|
|
"""
|
|
|
|
def __init__(self, device=None):
|
|
adb_dir = os.path.dirname(constants.ADB_PATH)
|
|
if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep):
|
|
# Required by third_party/android_testrunner to call directly 'adb'.
|
|
os.environ['PATH'] += os.pathsep + adb_dir
|
|
self._adb = adb_interface.AdbInterface()
|
|
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._md5sum_build_dir = ''
|
|
self._external_storage = ''
|
|
self._util_wrapper = ''
|
|
|
|
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."""
|
|
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.
|
|
"""
|
|
out = self._adb.SendCommand('get-state')
|
|
return out.strip() == 'device'
|
|
|
|
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]
|
|
assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE'
|
|
return self._external_storage
|
|
|
|
def WaitForDevicePm(self):
|
|
"""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()
|
|
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')
|
|
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()
|
|
self.WaitForSdCardReady(timeout)
|
|
|
|
def Shutdown(self):
|
|
"""Shuts down the device."""
|
|
self._adb.SendCommand('reboot -p')
|
|
|
|
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_failure=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_failure: 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)):
|
|
logging.info('Skipped install: identical %s APK already installed' %
|
|
package_name)
|
|
return
|
|
# Install.
|
|
reboots_left = reboots_on_failure
|
|
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
|
|
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 failure')
|
|
|
|
# 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 RestartAdbServer(self):
|
|
"""Restart the adb server."""
|
|
self.KillAdbServer()
|
|
self.StartAdbServer()
|
|
|
|
def KillAdbServer(self):
|
|
"""Kill adb server."""
|
|
adb_cmd = [constants.ADB_PATH, 'kill-server']
|
|
return cmd_helper.RunCmd(adb_cmd)
|
|
|
|
def StartAdbServer(self):
|
|
"""Start adb server."""
|
|
adb_cmd = [constants.ADB_PATH, 'start-server']
|
|
return cmd_helper.RunCmd(adb_cmd)
|
|
|
|
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._adb.SendShellCommand('getprop sys.boot_completed',
|
|
retry_count=1)
|
|
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)
|
|
|
|
# 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. Must not include
|
|
the single quotes as we use them to escape the whole command.
|
|
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: logging.warning(command + " contains ' quotes")
|
|
result = self._adb.SendShellCommand(
|
|
"'%s'" % command, timeout_time).splitlines()
|
|
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):
|
|
"""Android version of killall, connected via adb.
|
|
|
|
Args:
|
|
process: name of the process to kill off
|
|
|
|
Returns:
|
|
the number of processes killed
|
|
"""
|
|
pids = self.ExtractPid(process)
|
|
if pids:
|
|
self.RunShellCommand('kill -9 ' + ' '.join(pids))
|
|
return len(pids)
|
|
|
|
def KillAllBlocking(self, process, timeout_sec):
|
|
"""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
|
|
|
|
Returns:
|
|
the number of processes killed
|
|
"""
|
|
processes_killed = self.KillAll(process)
|
|
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 0
|
|
return processes_killed
|
|
|
|
def _GetActivityCommand(self, package, activity, wait_for_completion, action,
|
|
category, data, extras, trace_file_name, force_stop):
|
|
"""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
|
|
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):
|
|
"""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).
|
|
"""
|
|
cmd = self._GetActivityCommand(package, activity, wait_for_completion,
|
|
action, category, data, extras,
|
|
trace_file_name, force_stop)
|
|
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):
|
|
"""Starts |package|'s activity on the device, returning the start time
|
|
|
|
Args - as for StartActivity
|
|
|
|
Returns:
|
|
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)
|
|
self.StartMonitoringLogcat()
|
|
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 GetLogTimestamp(start_line, self.GetDeviceYear())
|
|
|
|
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().
|
|
"""
|
|
if not self._md5sum_build_dir:
|
|
default_build_type = os.environ.get('BUILD_TYPE', 'Debug')
|
|
build_dir = '%s/%s/' % (
|
|
cmd_helper.OutDirectory().get(), default_build_type)
|
|
md5sum_dist_path = '%s/md5sum_dist' % build_dir
|
|
if not os.path.exists(md5sum_dist_path):
|
|
build_dir = '%s/Release/' % cmd_helper.OutDirectory().get()
|
|
md5sum_dist_path = '%s/md5sum_dist' % build_dir
|
|
assert os.path.exists(md5sum_dist_path), 'Please build md5sum.'
|
|
command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER)
|
|
assert _HasAdbPushSucceeded(self._adb.SendCommand(command))
|
|
self._md5sum_build_dir = build_dir
|
|
|
|
cmd = (MD5SUM_LD_LIBRARY_PATH + ' ' + self._util_wrapper + ' ' +
|
|
MD5SUM_DEVICE_PATH + ' ' + device_path)
|
|
device_hash_tuples = _ParseMd5SumOutput(
|
|
self.RunShellCommand(cmd, timeout_time=2 * 60))
|
|
assert os.path.exists(host_path), 'Local path not found %s' % host_path
|
|
md5sum_output = cmd_helper.GetCmdOutput(
|
|
['%s/md5sum_bin_host' % self._md5sum_build_dir, host_path])
|
|
host_hash_tuples = _ParseMd5SumOutput(md5sum_output.splitlines())
|
|
return (host_hash_tuples, device_hash_tuples)
|
|
|
|
def GetFilesChanged(self, host_path, device_path):
|
|
"""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.
|
|
|
|
Returns:
|
|
A list of tuples of the form (host_path, device_path) for files whose
|
|
md5sums do not match.
|
|
"""
|
|
host_hash_tuples, device_hash_tuples = self._RunMd5Sum(
|
|
host_path, device_path)
|
|
|
|
# Ignore extra files on the device.
|
|
if len(device_hash_tuples) > len(host_hash_tuples):
|
|
host_files = [os.path.relpath(os.path.normpath(p.path),
|
|
os.path.normpath(host_path)) for p in host_hash_tuples]
|
|
|
|
def HostHas(fname):
|
|
return any(path in fname for path in host_files)
|
|
|
|
device_hash_tuples = [h for h in device_hash_tuples if HostHas(h.path)]
|
|
|
|
# Constructs the target device path from a given host path. Don't use when
|
|
# only a single file is given as the base name given in device_path may
|
|
# differ from that in host_path.
|
|
def HostToDevicePath(host_file_path):
|
|
return os.path.join(os.path.dirname(device_path), os.path.relpath(
|
|
host_file_path, os.path.dirname(os.path.normpath(host_path))))
|
|
|
|
device_hashes = [h.hash for h in device_hash_tuples]
|
|
return [(t.path, HostToDevicePath(t.path) if os.path.isdir(host_path) else
|
|
device_path)
|
|
for t in host_hash_tuples if t.hash not in device_hashes]
|
|
|
|
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
|
|
assert os.path.exists(host_path), 'Local path not found %s' % host_path
|
|
|
|
def GetHostSize(path):
|
|
return int(cmd_helper.GetCmdOutput(['du', '-sb', path]).split()[0])
|
|
|
|
size = GetHostSize(host_path)
|
|
self._pushed_files.append(device_path)
|
|
self._potential_push_size += size
|
|
|
|
changed_files = self.GetFilesChanged(host_path, 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):
|
|
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(GetHostSize(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:
|
|
# We're pushing everything, remove everything first and then create it.
|
|
self._actual_push_size += size
|
|
if os.path.isdir(host_path):
|
|
self.RunShellCommand('rm -r %s' % device_path, timeout_time=2 * 60)
|
|
self.RunShellCommand('mkdir -p %s' % device_path)
|
|
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)
|
|
|
|
_TEMP_FILE_BASE_FMT = 'temp_file_%d'
|
|
_TEMP_SCRIPT_FILE_BASE_FMT = 'temp_script_file_%d.sh'
|
|
|
|
def _GetDeviceTempFileName(self, base_name):
|
|
i = 0
|
|
while self.FileExistsOnDevice(
|
|
self.GetExternalStorage() + '/' + base_name % i):
|
|
i += 1
|
|
return self.GetExternalStorage() + '/' + base_name % i
|
|
|
|
def CanAccessProtectedFileContents(self):
|
|
"""Returns True if Get/SetProtectedFileContents would work via "su".
|
|
|
|
Devices running user builds don't have adb root, but may provide "su" which
|
|
can be used for accessing protected files.
|
|
"""
|
|
r = self.RunShellCommand('su -c cat /dev/null')
|
|
return r == [] or r[0].strip() == ''
|
|
|
|
def GetProtectedFileContents(self, filename, log_result=False):
|
|
"""Gets contents from the protected file specified by |filename|.
|
|
|
|
This is less efficient than GetFileContents, but will work for protected
|
|
files and device files.
|
|
"""
|
|
# Run the script as root
|
|
return self.RunShellCommand('su -c cat "%s" 2> /dev/null' % filename)
|
|
|
|
def SetProtectedFileContents(self, filename, contents):
|
|
"""Writes |contents| to the protected file specified by |filename|.
|
|
|
|
This is less efficient than SetFileContents, but will work for protected
|
|
files and device files.
|
|
"""
|
|
temp_file = self._GetDeviceTempFileName(AndroidCommands._TEMP_FILE_BASE_FMT)
|
|
temp_script = self._GetDeviceTempFileName(
|
|
AndroidCommands._TEMP_SCRIPT_FILE_BASE_FMT)
|
|
|
|
# Put the contents in a temporary file
|
|
self.SetFileContents(temp_file, contents)
|
|
# Create a script to copy the file contents to its final destination
|
|
self.SetFileContents(temp_script, 'cat %s > %s' % (temp_file, filename))
|
|
# Run the script as root
|
|
self.RunShellCommand('su -c sh %s' % temp_script)
|
|
# And remove the temporary files
|
|
self.RunShellCommand('rm ' + temp_file)
|
|
self.RunShellCommand('rm ' + temp_script)
|
|
|
|
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----- 1 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):
|
|
properties = file(temp_props_file.name).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.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY)
|
|
if was_set == enable:
|
|
return False
|
|
|
|
self.RunShellCommand('setprop %s "%s"' % (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.RunShellCommand('getprop ro.build.id')[0]
|
|
assert build_id
|
|
return build_id
|
|
|
|
def GetBuildType(self):
|
|
"""Returns the build type of the system (e.g. eng)."""
|
|
build_type = self.RunShellCommand('getprop ro.build.type')[0]
|
|
assert build_type
|
|
return build_type
|
|
|
|
def GetBuildProduct(self):
|
|
"""Returns the build product of the device (e.g. maguro)."""
|
|
build_product = self.RunShellCommand('getprop ro.build.product')[0]
|
|
assert build_product
|
|
return build_product
|
|
|
|
def GetProductName(self):
|
|
"""Returns the product name of the device (e.g. takju)."""
|
|
name = self.RunShellCommand('getprop ro.product.name')[0]
|
|
assert name
|
|
return name
|
|
|
|
def GetBuildFingerprint(self):
|
|
"""Returns the build fingerprint of the device."""
|
|
build_fingerprint = self.RunShellCommand('getprop ro.build.fingerprint')[0]
|
|
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.RunShellCommand('getprop ro.build.description')[0]
|
|
assert description
|
|
return description
|
|
|
|
def GetProductModel(self):
|
|
"""Returns the name of the product model (e.g. "Galaxy Nexus") """
|
|
model = self.RunShellCommand('getprop ro.product.model')[0]
|
|
assert model
|
|
return model
|
|
|
|
def GetWifiIP(self):
|
|
"""Returns the wifi IP on the device."""
|
|
wifi_ip = self.RunShellCommand('getprop dhcp.wlan0.ipaddress')[0]
|
|
# 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')
|
|
assert iphone_sub
|
|
return '\n'.join(iphone_sub)
|
|
|
|
def GetBatteryInfo(self):
|
|
"""Returns the device battery info (e.g. status, level, etc) as string."""
|
|
battery = self.RunShellCommand('dumpsys battery')
|
|
assert battery
|
|
return '\n'.join(battery)
|
|
|
|
def GetSetupWizardStatus(self):
|
|
"""Returns the status of the device setup wizard (e.g. DISABLED)."""
|
|
status = self.RunShellCommand('getprop ro.setupwizard.mode')[0]
|
|
assert status
|
|
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 syncronize with it.
|
|
for _ in range(4):
|
|
self._logcat = pexpect.spawn(constants.ADB_PATH, args, timeout=10,
|
|
logfile=logfile)
|
|
self.RunShellCommand('log startup_sync')
|
|
if self._logcat.expect(['startup_sync', pexpect.EOF,
|
|
pexpect.TIMEOUT]) == 0:
|
|
break
|
|
self._logcat.close(force=True)
|
|
else:
|
|
logging.critical('Error reading from logcat: ' + str(self._logcat.match))
|
|
sys.exit(1)
|
|
|
|
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.ADB_PATH,
|
|
self._logcat.args[1:],
|
|
timeout=self._logcat.timeout,
|
|
logfile=self._logcat.logfile)
|
|
|
|
def StartRecordingLogcat(self, clear=True, filters=['*:v']):
|
|
"""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 clear:
|
|
self._adb.SendCommand('logcat -c')
|
|
logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
|
|
' '.join(filters))
|
|
self._logcat_tmpoutfile = tempfile.TemporaryFile(bufsize=0)
|
|
self.logcat_process = subprocess.Popen(logcat_command, shell=True,
|
|
stdout=self._logcat_tmpoutfile)
|
|
|
|
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()
|
|
return output
|
|
|
|
def SearchLogcatRecord(self, 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.
|
|
"""
|
|
for line in self.GetFileContents('/proc/diskstats', log_result=False):
|
|
stats = io_stats_parser.ParseIoStatsLine(line)
|
|
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:
|
|
A tuple containg:
|
|
[0]: 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, Referenced, Swap,
|
|
KernelPageSize, MMUPageSize, Nvidia (tablet only).
|
|
[1]: Detailed /proc/[PID]/smaps information.
|
|
"""
|
|
usage_dict = collections.defaultdict(int)
|
|
smaps = collections.defaultdict(dict)
|
|
current_smap = ''
|
|
for line in self.GetProtectedFileContents('/proc/%s/smaps' % pid,
|
|
log_result=False):
|
|
items = line.split()
|
|
# See man 5 proc for more details. The format is:
|
|
# address perms offset dev inode pathname
|
|
if len(items) > 5:
|
|
current_smap = ' '.join(items[5:])
|
|
elif len(items) > 3:
|
|
current_smap = ' '.join(items[3:])
|
|
match = re.match(MEMORY_INFO_RE, line)
|
|
if match:
|
|
key = match.group('key')
|
|
usage_kb = int(match.group('usage_kb'))
|
|
usage_dict[key] += usage_kb
|
|
if key not in smaps[current_smap]:
|
|
smaps[current_smap][key] = 0
|
|
smaps[current_smap][key] += usage_kb
|
|
if not usage_dict or not any(usage_dict.values()):
|
|
# Presumably the process died between ps and calling this method.
|
|
logging.warning('Could not find memory usage for pid ' + str(pid))
|
|
|
|
for line in self.GetProtectedFileContents('/d/nvmap/generic-0/clients',
|
|
log_result=False):
|
|
match = re.match(NVIDIA_MEMORY_INFO_RE, line)
|
|
if match and match.group('pid') == pid:
|
|
usage_bytes = int(match.group('usage_bytes'))
|
|
usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB
|
|
break
|
|
|
|
return (usage_dict, smaps)
|
|
|
|
def GetMemoryUsageForPackage(self, package):
|
|
"""Returns the memory usage for all processes whose name contains |pacakge|.
|
|
|
|
Args:
|
|
package: A string holding process name to lookup pid list for.
|
|
|
|
Returns:
|
|
A tuple containg:
|
|
[0]: Dict of {metric:usage_kb}, summed over all pids associated with
|
|
|name|.
|
|
The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
|
|
Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
|
|
KernelPageSize, MMUPageSize, Nvidia (tablet only).
|
|
[1]: a list with detailed /proc/[PID]/smaps information.
|
|
"""
|
|
usage_dict = collections.defaultdict(int)
|
|
pid_list = self.ExtractPid(package)
|
|
smaps = collections.defaultdict(dict)
|
|
|
|
for pid in pid_list:
|
|
usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid)
|
|
smaps[pid] = smaps_per_pid
|
|
for (key, value) in usage_dict_per_pid.items():
|
|
usage_dict[key] += value
|
|
|
|
return usage_dict, smaps
|
|
|
|
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 TakeScreenshot(self, host_file):
|
|
"""Saves a screenshot image to |host_file| on the host.
|
|
|
|
Args:
|
|
host_file: Absolute path to the image file to store on the host.
|
|
"""
|
|
host_dir = os.path.dirname(host_file)
|
|
if not os.path.exists(host_dir):
|
|
os.makedirs(host_dir)
|
|
device_file = '%s/screenshot.png' % self.GetExternalStorage()
|
|
self.RunShellCommand('/system/bin/screencap -p %s' % device_file)
|
|
assert self._adb.Pull(device_file, host_file)
|
|
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 RunInstrumentationTest(self, test, test_package, instr_args, timeout):
|
|
"""Runs a single instrumentation test.
|
|
|
|
Args:
|
|
test: Test class/method.
|
|
test_package: Package name of test apk.
|
|
instr_args: Extra key/value to pass to am instrument.
|
|
timeout: Timeout time in seconds.
|
|
|
|
Returns:
|
|
An instance of am_instrument_parser.TestResult object.
|
|
"""
|
|
instrumentation_path = ('%s/android.test.InstrumentationTestRunner' %
|
|
test_package)
|
|
args_with_filter = dict(instr_args)
|
|
args_with_filter['class'] = test
|
|
logging.info(args_with_filter)
|
|
(raw_results, _) = self._adb.StartInstrumentation(
|
|
instrumentation_path=instrumentation_path,
|
|
instrumentation_args=args_with_filter,
|
|
timeout_time=timeout)
|
|
assert len(raw_results) == 1
|
|
return raw_results[0]
|
|
|
|
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]
|
|
|
|
|
|
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()
|