diff options
Diffstat (limited to 'catapult/devil/devil/android/device_utils.py')
-rw-r--r-- | catapult/devil/devil/android/device_utils.py | 532 |
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): |