aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/device_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/device_utils.py')
-rw-r--r--catapult/devil/devil/android/device_utils.py532
1 files changed, 343 insertions, 189 deletions
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py
index 51f71949..5a3db413 100644
--- a/catapult/devil/devil/android/device_utils.py
+++ b/catapult/devil/devil/android/device_utils.py
@@ -10,13 +10,13 @@ Eventually, this will be based on adb_wrapper.
import calendar
import collections
-import itertools
+import fnmatch
import json
import logging
-import multiprocessing
import os
import posixpath
import pprint
+import random
import re
import shutil
import stat
@@ -24,7 +24,6 @@ import tempfile
import time
import threading
import uuid
-import zipfile
from devil import base_error
from devil import devil_env
@@ -37,7 +36,6 @@ from devil.android import device_temp_file
from devil.android import install_commands
from devil.android import logcat_monitor
from devil.android import md5sum
-from devil.android.constants import chrome
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
from devil.android.sdk import keyevent
@@ -49,6 +47,8 @@ from devil.utils import reraiser_thread
from devil.utils import timeout_retry
from devil.utils import zip_utils
+from py_utils import tempfile_ext
+
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
@@ -71,11 +71,12 @@ _RESTART_ADBD_SCRIPT = """
"""
# Not all permissions can be set.
-_PERMISSIONS_BLACKLIST = [
+_PERMISSIONS_BLACKLIST_RE = re.compile('|'.join(fnmatch.translate(p) for p in [
'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS',
'android.permission.ACCESS_MOCK_LOCATION',
'android.permission.ACCESS_NETWORK_STATE',
'android.permission.ACCESS_NOTIFICATION_POLICY',
+ 'android.permission.ACCESS_VR_STATE',
'android.permission.ACCESS_WIFI_STATE',
'android.permission.AUTHENTICATE_ACCOUNTS',
'android.permission.BLUETOOTH',
@@ -89,6 +90,7 @@ _PERMISSIONS_BLACKLIST = [
'android.permission.EXPAND_STATUS_BAR',
'android.permission.GET_PACKAGE_SIZE',
'android.permission.INSTALL_SHORTCUT',
+ 'android.permission.INJECT_EVENTS',
'android.permission.INTERNET',
'android.permission.KILL_BACKGROUND_PROCESSES',
'android.permission.MANAGE_ACCOUNTS',
@@ -100,6 +102,7 @@ _PERMISSIONS_BLACKLIST = [
'android.permission.RECORD_VIDEO',
'android.permission.REORDER_TASKS',
'android.permission.REQUEST_INSTALL_PACKAGES',
+ 'android.permission.RESTRICTED_VR_ACCESS',
'android.permission.RUN_INSTRUMENTATION',
'android.permission.SET_ALARM',
'android.permission.SET_TIME_ZONE',
@@ -118,13 +121,15 @@ _PERMISSIONS_BLACKLIST = [
'com.google.android.apps.now.CURRENT_ACCOUNT_ACCESS',
'com.google.android.c2dm.permission.RECEIVE',
'com.google.android.providers.gsf.permission.READ_GSERVICES',
+ 'com.google.vr.vrcore.permission.VRCORE_INTERNAL',
'com.sec.enterprise.knox.MDM_CONTENT_PROVIDER',
-]
-for package_info in chrome.PACKAGE_INFO.itervalues():
- _PERMISSIONS_BLACKLIST.extend([
- '%s.permission.C2D_MESSAGE' % package_info.package,
- '%s.permission.READ_WRITE_BOOKMARK_FOLDERS' % package_info.package,
- '%s.TOS_ACKED' % package_info.package])
+ '*.permission.C2D_MESSAGE',
+ '*.permission.READ_WRITE_BOOKMARK_FOLDERS',
+ '*.TOS_ACKED',
+]))
+_SHELL_OUTPUT_SEPARATOR = '~X~'
+_PERMISSIONS_EXCEPTION_RE = re.compile(
+ r'java\.lang\.\w+Exception: .*$', re.MULTILINE)
_CURRENT_FOCUS_CRASH_RE = re.compile(
r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}')
@@ -165,6 +170,11 @@ _FILE_MODE_SPECIAL = [
('s', stat.S_ISGID),
('t', stat.S_ISVTX),
]
+_PS_COLUMNS = {
+ 'pid': 1,
+ 'ppid': 2,
+ 'name': -1
+}
_SELINUX_MODE = {
'enforcing': True,
'permissive': False,
@@ -172,9 +182,26 @@ _SELINUX_MODE = {
}
# Some devices require different logic for checking if root is necessary
_SPECIAL_ROOT_DEVICE_LIST = [
- 'marlin',
- 'sailfish',
+ 'marlin', # Pixel XL
+ 'sailfish', # Pixel
+ 'taimen', # Pixel 2 XL
+ 'walleye', # Pixel 2
]
+_IMEI_RE = re.compile(r' Device ID = (.+)$')
+# The following regex is used to match result parcels like:
+"""
+Result: Parcel(
+ 0x00000000: 00000000 0000000f 00350033 00360033 '........3.5.3.6.'
+ 0x00000010: 00360032 00370030 00300032 00300039 '2.6.0.7.2.0.9.0.'
+ 0x00000020: 00380033 00000039 '3.8.9... ')
+"""
+_PARCEL_RESULT_RE = re.compile(
+ r'0x[0-9a-f]{8}\: (?:[0-9a-f]{8}\s+){1,4}\'(.{16})\'')
+_EBUSY_RE = re.compile(
+ r'mkdir failed for ([^,]*), Device or resource busy')
+
+PS_COLUMNS = ('name', 'pid', 'ppid')
+ProcessInfo = collections.namedtuple('ProcessInfo', PS_COLUMNS)
@decorators.WithExplicitTimeoutAndRetries(
@@ -507,6 +534,47 @@ class DeviceUtils(object):
return self._cache['external_storage']
@decorators.WithTimeoutAndRetriesFromInstance()
+ def GetIMEI(self, timeout=None, retries=None):
+ """Get the device's IMEI.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The device's IMEI.
+
+ Raises:
+ AdbCommandFailedError on error
+ """
+ if self._cache.get('imei') is not None:
+ return self._cache.get('imei')
+
+ if self.build_version_sdk < 21:
+ out = self.RunShellCommand(['dumpsys', 'iphonesubinfo'],
+ raw_output=True, check_return=True)
+ if out:
+ match = re.search(_IMEI_RE, out)
+ if match:
+ self._cache['imei'] = match.group(1)
+ return self._cache['imei']
+ else:
+ out = self.RunShellCommand(['service', 'call', 'iphonesubinfo', '1'],
+ check_return=True)
+ if out:
+ imei = ''
+ for line in out:
+ match = re.search(_PARCEL_RESULT_RE, line)
+ if match:
+ imei = imei + match.group(1)
+ imei = imei.replace('.', '').strip()
+ if imei:
+ self._cache['imei'] = imei
+ return self._cache['imei']
+
+ raise device_errors.CommandFailedError('Unable to fetch IMEI.')
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def GetApplicationPaths(self, package, timeout=None, retries=None):
"""Get the paths of the installed apks on the device for the given package.
@@ -539,13 +607,22 @@ class DeviceUtils(object):
output = self.RunShellCommand(
['pm', 'path', package], check_return=should_check_return)
apks = []
+ bad_output = False
for line in output:
- if not line.startswith('package:'):
+ if line.startswith('package:'):
+ apks.append(line[len('package:'):])
+ elif line.startswith('WARNING:'):
continue
- apks.append(line[len('package:'):])
+ else:
+ bad_output = True # Unexpected line in output.
if not apks and output:
- raise device_errors.CommandFailedError(
- 'pm path returned: %r' % '\n'.join(output), str(self))
+ if bad_output:
+ raise device_errors.CommandFailedError(
+ 'Unexpected pm path output: %r' % '\n'.join(output), str(self))
+ else:
+ logger.warning('pm returned no paths but the following warnings:')
+ for line in output:
+ logger.warning('- %s', line)
self._cache['package_apk_paths'][package] = list(apks)
return apks
@@ -593,6 +670,25 @@ class DeviceUtils(object):
raise device_errors.CommandFailedError(
'Could not find data directory for %s', package)
+ def TakeBugReport(self, path, timeout=60*5, retries=None):
+ """Takes a bug report and dumps it to the specified path.
+
+ This doesn't use adb's bugreport option since its behavior is dependent on
+ both adb version and device OS version. To make it simpler, this directly
+ runs the bugreport command on the device itself and dumps the stdout to a
+ file.
+
+ Args:
+ path: Path on the host to drop the bug report.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+ """
+ with device_temp_file.DeviceTempFile(self.adb) as device_tmp_file:
+ cmd = '( bugreport )>%s 2>&1' % device_tmp_file.name
+ self.RunShellCommand(
+ cmd, check_return=True, shell=True, timeout=timeout, retries=retries)
+ self.PullFile(device_tmp_file.name, path)
+
@decorators.WithTimeoutAndRetriesFromInstance()
def WaitUntilFullyBooted(self, wifi=False, timeout=None, retries=None):
"""Wait for the device to fully boot.
@@ -787,18 +883,19 @@ class DeviceUtils(object):
else:
self.adb.Install(
base_apk.path, reinstall=reinstall, allow_downgrade=allow_downgrade)
- if (permissions is None
- and self.build_version_sdk >= version_codes.MARSHMALLOW):
- permissions = base_apk.GetPermissions()
- self.GrantPermissions(package_name, permissions)
- # Upon success, we know the device checksums, but not their paths.
- if host_checksums is not None:
- self._cache['package_apk_checksums'][package_name] = host_checksums
else:
# Running adb install terminates running instances of the app, so to be
# consistent, we explicitly terminate it when skipping the install.
self.ForceStop(package_name)
+ if (permissions is None
+ and self.build_version_sdk >= version_codes.MARSHMALLOW):
+ permissions = base_apk.GetPermissions()
+ self.GrantPermissions(package_name, permissions)
+ # Upon success, we know the device checksums, but not their paths.
+ if host_checksums is not None:
+ self._cache['package_apk_checksums'][package_name] = host_checksums
+
@decorators.WithTimeoutAndRetriesFromInstance()
def Uninstall(self, package_name, keep_data=False, timeout=None,
retries=None):
@@ -933,7 +1030,7 @@ class DeviceUtils(object):
def handle_large_output(cmd, large_output_mode):
if large_output_mode:
with device_temp_file.DeviceTempFile(self.adb) as large_output_file:
- cmd = '( %s )>%s' % (cmd, large_output_file.name)
+ cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name)
logger.debug('Large output mode enabled. Will write output to '
'device and read results from file.')
handle_large_command(cmd)
@@ -953,7 +1050,7 @@ class DeviceUtils(object):
if isinstance(cmd, basestring):
if not shell:
- logging.warning(
+ logger.warning(
'The command to run should preferably be passed as a sequence of'
' args. If shell features are needed (pipes, wildcards, variables)'
' clients should explicitly set shell=True.')
@@ -1041,29 +1138,29 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- procs_pids = self.GetPids(process_name)
+ processes = self.ListProcesses(process_name)
if exact:
- procs_pids = {process_name: procs_pids.get(process_name, [])}
- pids = set(itertools.chain(*procs_pids.values()))
- if not pids:
+ processes = [p for p in processes if p.name == process_name]
+ if not processes:
if quiet:
return 0
else:
raise device_errors.CommandFailedError(
- 'No process "%s"' % process_name, str(self))
+ 'No processes matching %r (exact=%r)' % (process_name, exact),
+ str(self))
logger.info(
'KillAll(%r, ...) attempting to kill the following:', process_name)
- for name, ids in procs_pids.iteritems():
- for i in ids:
- logger.info(' %05s %s', str(i), name)
+ for p in processes:
+ logger.info(' %05d %s', p.pid, p.name)
- cmd = ['kill', '-%d' % signum] + sorted(pids)
+ pids = set(p.pid for p in processes)
+ cmd = ['kill', '-%d' % signum] + sorted(str(p) for p in pids)
self.RunShellCommand(cmd, as_root=as_root, check_return=True)
def all_pids_killed():
- procs_pids_remain = self.GetPids(process_name)
- return not pids.intersection(itertools.chain(*procs_pids_remain.values()))
+ pids_left = (p.pid for p in self.ListProcesses(process_name))
+ return not pids.intersection(pids_left)
if blocking:
timeout_retry.WaitFor(all_pids_killed, wait_period=0.1)
@@ -1195,7 +1292,7 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- if self.GetPids(package):
+ if self.GetApplicationPids(package):
self.RunShellCommand(['am', 'force-stop', package], check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -1269,7 +1366,7 @@ class DeviceUtils(object):
all_changed_files = []
all_stale_files = []
- missing_dirs = []
+ missing_dirs = set()
cache_commit_funcs = []
for h, d in host_device_tuples:
assert os.path.isabs(h) and posixpath.isabs(d)
@@ -1281,16 +1378,35 @@ class DeviceUtils(object):
cache_commit_funcs.append(cache_commit_func)
if changed_files and not up_to_date_files and not stale_files:
if os.path.isdir(h):
- missing_dirs.append(d)
+ missing_dirs.add(d)
else:
- missing_dirs.append(posixpath.dirname(d))
+ missing_dirs.add(posixpath.dirname(d))
if delete_device_stale and all_stale_files:
self.RunShellCommand(['rm', '-f'] + all_stale_files, check_return=True)
if all_changed_files:
if missing_dirs:
- self.RunShellCommand(['mkdir', '-p'] + missing_dirs, check_return=True)
+ try:
+ self.RunShellCommand(['mkdir', '-p'] + list(missing_dirs),
+ check_return=True)
+ except device_errors.AdbShellCommandFailedError as e:
+ # TODO(crbug.com/739899): This is attempting to diagnose flaky EBUSY
+ # errors that have been popping up in single-device scenarios.
+ # Remove it once we've figured out what's causing them and how best
+ # to handle them.
+ m = _EBUSY_RE.search(e.output)
+ if m:
+ logging.error(
+ 'Hit EBUSY while attempting to make missing directories.')
+ logging.error('lsof output:')
+ # Don't check for return below since grep exits with a non-zero when
+ # no match is found.
+ for l in self.RunShellCommand(
+ 'lsof | grep %s' % cmd_helper.SingleQuote(m.group(1)),
+ check_return=False):
+ logging.error(' %s', l)
+ raise
self._PushFilesImpl(host_device_tuples, all_changed_files)
for func in cache_commit_funcs:
func()
@@ -1482,39 +1598,29 @@ class DeviceUtils(object):
self.adb.Push(h, d)
def _PushChangedFilesZipped(self, files, dirs):
- with tempfile.NamedTemporaryFile(suffix='.zip') as zip_file:
- zip_proc = multiprocessing.Process(
- target=DeviceUtils._CreateDeviceZip,
- args=(zip_file.name, files))
- zip_proc.start()
+ if not self._MaybeInstallCommands():
+ return False
+
+ with tempfile_ext.NamedTemporaryDirectory() as working_dir:
+ zip_path = os.path.join(working_dir, 'tmp.zip')
try:
- # While it's zipping, ensure the unzip command exists on the device.
- if not self._MaybeInstallCommands():
- zip_proc.terminate()
- return False
+ zip_utils.WriteZipFile(zip_path, files)
+ except zip_utils.ZipFailedError:
+ return False
- # Warm up NeedsSU cache while we're still zipping.
- self.NeedsSU()
- with device_temp_file.DeviceTempFile(
- self.adb, suffix='.zip') as device_temp:
- zip_proc.join()
- self.adb.Push(zip_file.name, device_temp.name)
- quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs)
- self.RunShellCommand(
- 'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs),
- shell=True, as_root=True,
- env={'PATH': '%s:$PATH' % install_commands.BIN_DIR},
- check_return=True)
- finally:
- if zip_proc.is_alive():
- zip_proc.terminate()
- return True
+ self.NeedsSU()
+ with device_temp_file.DeviceTempFile(
+ self.adb, suffix='.zip') as device_temp:
+ self.adb.Push(zip_path, device_temp.name)
- @staticmethod
- def _CreateDeviceZip(zip_path, host_device_tuples):
- with zipfile.ZipFile(zip_path, 'w') as zip_file:
- for host_path, device_path in host_device_tuples:
- zip_utils.WriteToZipFile(zip_file, host_path, device_path)
+ quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs)
+ self.RunShellCommand(
+ 'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs),
+ shell=True, as_root=True,
+ env={'PATH': '%s:$PATH' % install_commands.BIN_DIR},
+ check_return=True)
+
+ return True
# TODO(nednguyen): remove this and migrate the callsite to PathExists().
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -1561,7 +1667,7 @@ class DeviceUtils(object):
@decorators.WithTimeoutAndRetriesFromInstance()
def RemovePath(self, device_path, force=False, recursive=False,
- as_root=False, timeout=None, retries=None):
+ as_root=False, rename=False, timeout=None, retries=None):
"""Removes the given path(s) from the device.
Args:
@@ -1571,21 +1677,33 @@ class DeviceUtils(object):
recursive: Whether to remove any directories in the path(s) recursively.
as_root: Whether root permissions should be use to remove the given
path(s).
+ rename: Whether to rename the path(s) before removing to help avoid
+ filesystem errors. See https://stackoverflow.com/questions/11539657
timeout: timeout in seconds
retries: number of retries
"""
+ def _RenamePath(path):
+ random_suffix = hex(random.randint(2 ** 12, 2 ** 16 - 1))[2:]
+ dest = '%s-%s' % (path, random_suffix)
+ try:
+ self.RunShellCommand(
+ ['mv', path, dest], as_root=as_root, check_return=True)
+ return dest
+ except device_errors.AdbShellCommandFailedError:
+ # If it couldn't be moved, just try rm'ing the original path instead.
+ return path
args = ['rm']
if force:
args.append('-f')
if recursive:
args.append('-r')
if isinstance(device_path, basestring):
- args.append(device_path)
+ args.append(device_path if not rename else _RenamePath(device_path))
else:
- args.extend(device_path)
+ args.extend(
+ device_path if not rename else [_RenamePath(p) for p in device_path])
self.RunShellCommand(args, as_root=as_root, check_return=True)
-
@decorators.WithTimeoutAndRetriesFromInstance()
def PullFile(self, device_path, host_path, timeout=None, retries=None):
"""Pull a file from the device.
@@ -1724,7 +1842,14 @@ class DeviceUtils(object):
m = _LONG_LS_OUTPUT_RE.match(line)
if m:
if m.group('filename') not in ['.', '..']:
- entries.append(m.groupdict())
+ item = m.groupdict()
+ # A change in toybox is causing recent Android versions to escape
+ # spaces in file names. Here we just unquote those spaces. If we
+ # later find more essoteric characters in file names, a more careful
+ # unquoting mechanism may be needed. But hopefully not.
+ # See: https://goo.gl/JAebZj
+ item['filename'] = item['filename'].replace('\\ ', ' ')
+ entries.append(item)
else:
logger.info('Skipping: %s', line)
@@ -2145,10 +2270,74 @@ class DeviceUtils(object):
"""
return self.GetProp('ro.product.cpu.abi', cache=True)
+ def _GetPsOutput(self, pattern):
+ """Runs |ps| command on the device and returns its output,
+
+ This private method abstracts away differences between Android verions for
+ calling |ps|, and implements support for filtering the output by a given
+ |pattern|, but does not do any output parsing.
+ """
+ try:
+ ps_cmd = 'ps'
+ # ps behavior was changed in Android above N, http://crbug.com/686716
+ if (self.build_version_sdk >= version_codes.NOUGAT_MR1
+ and self.build_id[0] > 'N'):
+ ps_cmd = 'ps -e'
+ if pattern:
+ return self._RunPipedShellCommand(
+ '%s | grep -F %s' % (ps_cmd, cmd_helper.SingleQuote(pattern)))
+ else:
+ return self.RunShellCommand(
+ ps_cmd.split(), check_return=True, large_output=True)
+ except device_errors.AdbShellCommandFailedError as e:
+ if e.status and isinstance(e.status, list) and not e.status[0]:
+ # If ps succeeded but grep failed, there were no processes with the
+ # given name.
+ return []
+ else:
+ raise
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def ListProcesses(self, process_name=None, timeout=None, retries=None):
+ """Returns a list of tuples with info about processes on the device.
+
+ This essentially parses the output of the |ps| command into convenient
+ ProcessInfo tuples.
+
+ Args:
+ process_name: A string used to filter the returned processes. If given,
+ only processes whose name have this value as a substring
+ will be returned.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A list of ProcessInfo tuples with |name|, |pid|, and |ppid| fields.
+ """
+ process_name = process_name or ''
+ processes = []
+ for line in self._GetPsOutput(process_name):
+ row = line.split()
+ try:
+ row = {k: row[i] for k, i in _PS_COLUMNS.iteritems()}
+ if row['pid'] == 'PID' or process_name not in row['name']:
+ # Skip over header and non-matching processes.
+ continue
+ row['pid'] = int(row['pid'])
+ row['ppid'] = int(row['ppid'])
+ except StandardError: # e.g. IndexError, TypeError, ValueError.
+ logging.warning('failed to parse ps line: %r', line)
+ continue
+ processes.append(ProcessInfo(**row))
+ return processes
+
+ # TODO(#4103): Remove after migrating clients to ListProcesses.
@decorators.WithTimeoutAndRetriesFromInstance()
def GetPids(self, process_name=None, timeout=None, retries=None):
"""Returns the PIDs of processes containing the given name as substring.
+ DEPRECATED
+
Note that the |process_name| is often the package name.
Args:
@@ -2166,38 +2355,13 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
procs_pids = collections.defaultdict(list)
- try:
- ps_cmd = 'ps'
- # ps behavior was changed in Android above N, http://crbug.com/686716
- if (self.build_version_sdk >= version_codes.NOUGAT_MR1
- and self.build_id[0] > 'N'):
- ps_cmd = 'ps -e'
- if process_name:
- ps_output = self._RunPipedShellCommand(
- '%s | grep -F %s' % (ps_cmd, cmd_helper.SingleQuote(process_name)))
- else:
- ps_output = self.RunShellCommand(
- ps_cmd.split(), check_return=True, large_output=True)
- except device_errors.AdbShellCommandFailedError as e:
- if e.status and isinstance(e.status, list) and not e.status[0]:
- # If ps succeeded but grep failed, there were no processes with the
- # given name.
- return procs_pids
- else:
- raise
-
- process_name = process_name or ''
- for line in ps_output:
- try:
- ps_data = line.split()
- pid, process = ps_data[1], ps_data[-1]
- if process_name in process and pid != 'PID':
- procs_pids[process].append(pid)
- except IndexError:
- pass
+ for p in self.ListProcesses(process_name):
+ procs_pids[p.name].append(str(p.pid))
return procs_pids
- def GetApplicationPids(self, process_name, at_most_one=False, **kwargs):
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def GetApplicationPids(self, process_name, at_most_one=False,
+ timeout=None, retries=None):
"""Returns the PID or PIDs of a given process name.
Note that the |process_name|, often the package name, must match exactly.
@@ -2219,11 +2383,13 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
- pids = self.GetPids(process_name, **kwargs).get(process_name, [])
+ pids = [p.pid for p in self.ListProcesses(process_name)
+ if p.name == process_name]
if at_most_one:
if len(pids) > 1:
raise device_errors.CommandFailedError(
- 'Expected a single process but found PIDs: %s.' % ', '.join(pids),
+ 'Expected a single PID for %r but found: %r.' % (
+ process_name, pids),
device_serial=str(self))
return pids[0] if pids else None
else:
@@ -2300,37 +2466,6 @@ class DeviceUtils(object):
return host_path
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetMemoryUsageForPid(self, pid, timeout=None, retries=None):
- """Gets the memory usage for the given PID.
-
- Args:
- pid: PID of the process.
- timeout: timeout in seconds
- retries: number of retries
-
- Returns:
- A dict containing memory usage statistics for the PID. May include:
- Size, Rss, Pss, Shared_Clean, Shared_Dirty, Private_Clean,
- Private_Dirty, VmHWM
-
- Raises:
- CommandTimeoutError on timeout.
- """
- result = collections.defaultdict(int)
-
- try:
- result.update(self._GetMemoryUsageForPidFromSmaps(pid))
- except device_errors.CommandFailedError:
- logger.exception('Error getting memory usage from smaps')
-
- try:
- result.update(self._GetMemoryUsageForPidFromStatus(pid))
- except device_errors.CommandFailedError:
- logger.exception('Error getting memory usage from status')
-
- return result
-
- @decorators.WithTimeoutAndRetriesFromInstance()
def DismissCrashDialogIfNeeded(self, timeout=None, retries=None):
"""Dismiss the error/ANR dialog if present.
@@ -2362,31 +2497,6 @@ class DeviceUtils(object):
logger.error('Still showing a %s dialog for %s', *match.groups())
return package
- def _GetMemoryUsageForPidFromSmaps(self, pid):
- SMAPS_COLUMNS = (
- 'Size', 'Rss', 'Pss', 'Shared_Clean', 'Shared_Dirty', 'Private_Clean',
- 'Private_Dirty')
-
- showmap_out = self._RunPipedShellCommand(
- 'showmap %d | grep TOTAL' % int(pid), as_root=True)
-
- split_totals = showmap_out[-1].split()
- if (not split_totals
- or len(split_totals) != 9
- or split_totals[-1] != 'TOTAL'):
- raise device_errors.CommandFailedError(
- 'Invalid output from showmap: %s' % '\n'.join(showmap_out))
-
- return dict(itertools.izip(SMAPS_COLUMNS, (int(n) for n in split_totals)))
-
- def _GetMemoryUsageForPidFromStatus(self, pid):
- for line in self.ReadFile(
- '/proc/%s/status' % str(pid), as_root=True).splitlines():
- if line.startswith('VmHWM:'):
- return {'VmHWM': int(line.split()[1])}
- raise device_errors.CommandFailedError(
- 'Could not find memory peak value for pid %s', str(pid))
-
def GetLogcatMonitor(self, *args, **kwargs):
"""Returns a new LogcatMonitor associated with this device.
@@ -2504,7 +2614,8 @@ class DeviceUtils(object):
return parallelizer.SyncParallelizer(devices)
@classmethod
- def HealthyDevices(cls, blacklist=None, device_arg='default', **kwargs):
+ def HealthyDevices(cls, blacklist=None, device_arg='default', retry=True,
+ **kwargs):
"""Returns a list of DeviceUtils instances.
Returns a list of DeviceUtils instances that are attached, not blacklisted,
@@ -2526,6 +2637,8 @@ class DeviceUtils(object):
blacklisted.
['A', 'B', ...] -> Returns instances for the subset that is not
blacklisted.
+ retry: If true, will attempt to restart adb server and query it again if
+ no devices are found.
A device serial, or a list of device serials (optional).
Returns:
@@ -2561,19 +2674,30 @@ class DeviceUtils(object):
return True
return False
- if device_arg:
- devices = [cls(x, **kwargs) for x in device_arg if not blacklisted(x)]
- else:
- devices = []
- for adb in adb_wrapper.AdbWrapper.Devices():
- if not blacklisted(adb.GetDeviceSerial()):
- devices.append(cls(_CreateAdbWrapper(adb), **kwargs))
+ def _get_devices():
+ if device_arg:
+ devices = [cls(x, **kwargs) for x in device_arg if not blacklisted(x)]
+ else:
+ devices = []
+ for adb in adb_wrapper.AdbWrapper.Devices():
+ if not blacklisted(adb.GetDeviceSerial()):
+ devices.append(cls(_CreateAdbWrapper(adb), **kwargs))
- if len(devices) == 0 and not allow_no_devices:
- raise device_errors.NoDevicesError()
- if len(devices) > 1 and not select_multiple:
- raise device_errors.MultipleDevicesError(devices)
- return sorted(devices)
+ if len(devices) == 0 and not allow_no_devices:
+ raise device_errors.NoDevicesError()
+ if len(devices) > 1 and not select_multiple:
+ raise device_errors.MultipleDevicesError(devices)
+ return sorted(devices)
+
+ try:
+ return _get_devices()
+ except device_errors.NoDevicesError:
+ if not retry:
+ raise
+ logger.warning(
+ 'No devices found. Will try again after restarting adb server.')
+ RestartServer()
+ return _get_devices()
@decorators.WithTimeoutAndRetriesFromInstance()
def RestartAdbd(self, timeout=None, retries=None):
@@ -2590,19 +2714,49 @@ class DeviceUtils(object):
# the permission model.
if not permissions or self.build_version_sdk < version_codes.MARSHMALLOW:
return
- logger.info('Setting permissions for %s.', package)
- permissions = [p for p in permissions if p not in _PERMISSIONS_BLACKLIST]
+
+ permissions = set(
+ p for p in permissions if not _PERMISSIONS_BLACKLIST_RE.match(p))
+
if ('android.permission.WRITE_EXTERNAL_STORAGE' in permissions
and 'android.permission.READ_EXTERNAL_STORAGE' not in permissions):
- permissions.append('android.permission.READ_EXTERNAL_STORAGE')
- cmd = '&&'.join('pm grant %s %s' % (package, p) for p in permissions)
- if cmd:
- output = self.RunShellCommand(cmd, shell=True, check_return=True)
- if output:
- logger.warning('Possible problem when granting permissions. Blacklist '
- 'may need to be updated.')
- for line in output:
- logger.warning(' %s', line)
+ permissions.add('android.permission.READ_EXTERNAL_STORAGE')
+
+ script = ';'.join([
+ 'p={package}',
+ 'for q in {permissions}',
+ 'do pm grant "$p" "$q"',
+ 'echo "{sep}$q{sep}$?{sep}"',
+ 'done'
+ ]).format(
+ package=cmd_helper.SingleQuote(package),
+ permissions=' '.join(
+ cmd_helper.SingleQuote(p) for p in sorted(permissions)),
+ sep=_SHELL_OUTPUT_SEPARATOR)
+
+ logger.info('Setting permissions for %s.', package)
+ res = self.RunShellCommand(
+ script, shell=True, raw_output=True, large_output=True,
+ check_return=True)
+ res = res.split(_SHELL_OUTPUT_SEPARATOR)
+ failures = [
+ (permission, output.strip())
+ for permission, status, output in zip(res[1::3], res[2::3], res[0::3])
+ if int(status)]
+
+ if failures:
+ logger.warning(
+ 'Failed to grant some permissions. Blacklist may need to be updated?')
+ for permission, output in failures:
+ # Try to grab the relevant error message from the output.
+ m = _PERMISSIONS_EXCEPTION_RE.search(output)
+ if m:
+ error_msg = m.group(0)
+ elif len(output) > 200:
+ error_msg = repr(output[:200]) + ' (truncated)'
+ else:
+ error_msg = repr(output)
+ logger.warning('- %s: %s', permission, error_msg)
@decorators.WithTimeoutAndRetriesFromInstance()
def IsScreenOn(self, timeout=None, retries=None):