Index: build/android/pylib/device/device_utils.py |
diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py |
index 9f538e1b0efd9f51ffaa4c1345febe8149edccc6..46aec6fc9df0eb7b94b89628457567a692299b81 100644 |
--- a/build/android/pylib/device/device_utils.py |
+++ b/build/android/pylib/device/device_utils.py |
@@ -25,8 +25,10 @@ import zipfile |
import pylib.android_commands |
from pylib import cmd_helper |
from pylib import constants |
+from pylib import device_signal |
from pylib.device import adb_wrapper |
from pylib.device import decorators |
+from pylib.device import device_blacklist |
from pylib.device import device_errors |
from pylib.device import intent |
from pylib.device import logcat_monitor |
@@ -72,6 +74,7 @@ _CONTROL_CHARGING_COMMANDS = [ |
}, |
] |
+ |
@decorators.WithExplicitTimeoutAndRetries( |
_DEFAULT_TIMEOUT, _DEFAULT_RETRIES) |
def GetAVDs(): |
@@ -168,6 +171,7 @@ class DeviceUtils(object): |
self._default_timeout = default_timeout |
self._default_retries = default_retries |
self._cache = {} |
+ self._client_caches = {} |
assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR) |
assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR) |
@@ -406,7 +410,7 @@ class DeviceUtils(object): |
return not self.IsOnline() |
self.adb.Reboot() |
- self._cache = {} |
+ self._ClearCache() |
timeout_retry.WaitFor(device_offline, wait_period=1) |
if block: |
self.WaitUntilFullyBooted(wifi=wifi) |
@@ -446,8 +450,8 @@ class DeviceUtils(object): |
@decorators.WithTimeoutAndRetriesFromInstance() |
def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None, |
- as_root=False, single_line=False, timeout=None, |
- retries=None): |
+ as_root=False, single_line=False, large_output=False, |
+ timeout=None, retries=None): |
"""Run an ADB shell command. |
The command to run |cmd| should be a sequence of program arguments or else |
@@ -480,6 +484,8 @@ class DeviceUtils(object): |
with root privileges. |
single_line: A boolean indicating if only a single line of output is |
expected. |
+ large_output: Uses a work-around for large shell command output. Without |
+ this large output will be truncated. |
timeout: timeout in seconds |
retries: number of retries |
@@ -502,15 +508,49 @@ class DeviceUtils(object): |
# using double quotes here to allow interpolation of shell variables |
return '%s=%s' % (key, cmd_helper.DoubleQuote(value)) |
- def do_run(cmd): |
+ def run(cmd): |
+ return self.adb.Shell(cmd) |
+ |
+ def handle_check_return(cmd): |
try: |
- return self.adb.Shell(cmd) |
+ return run(cmd) |
except device_errors.AdbCommandFailedError as exc: |
if check_return: |
raise |
else: |
return exc.output |
+ def handle_large_command(cmd): |
+ if len(cmd) < self._MAX_ADB_COMMAND_LENGTH: |
+ return handle_check_return(cmd) |
+ else: |
+ with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: |
+ self._WriteFileWithPush(script.name, cmd) |
+ logging.info('Large shell command will be run from file: %s ...', |
+ cmd[:100]) |
+ return handle_check_return('sh %s' % script.name_quoted) |
+ |
+ 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) |
+ logging.info('Large output mode enabled. Will write output to device ' |
+ ' and read results from file.') |
+ handle_large_command(cmd) |
+ return self.ReadFile(large_output_file.name) |
+ else: |
+ try: |
+ return handle_large_command(cmd) |
+ except device_errors.AdbCommandFailedError as exc: |
+ if exc.status is None: |
+ logging.exception('No output found for %s', cmd) |
+ logging.warning('Attempting to run in large_output mode.') |
+ logging.warning('Use RunShellCommand(..., large_output=True) for ' |
+ 'shell commands that expect a lot of output.') |
+ return handle_large_output(cmd, True) |
+ else: |
+ raise |
+ |
if not isinstance(cmd, basestring): |
cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd) |
if env: |
@@ -521,19 +561,9 @@ class DeviceUtils(object): |
if as_root and self.NeedsSU(): |
# "su -c sh -c" allows using shell features in |cmd| |
cmd = 'su -c sh -c %s' % cmd_helper.SingleQuote(cmd) |
- if timeout is None: |
- timeout = self._default_timeout |
- if len(cmd) < self._MAX_ADB_COMMAND_LENGTH: |
- output = do_run(cmd) |
- else: |
- with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: |
- self._WriteFileWithPush(script.name, cmd) |
- logging.info('Large shell command will be run from file: %s ...', |
- cmd[:100]) |
- output = do_run('sh %s' % script.name_quoted) |
+ output = handle_large_output(cmd, large_output).splitlines() |
- output = output.splitlines() |
if single_line: |
if not output: |
return '' |
@@ -545,38 +575,70 @@ class DeviceUtils(object): |
else: |
return output |
+ def _RunPipedShellCommand(self, script, **kwargs): |
+ PIPESTATUS_LEADER = 'PIPESTATUS: ' |
+ |
+ script += '; echo "%s${PIPESTATUS[@]}"' % PIPESTATUS_LEADER |
+ kwargs['check_return'] = True |
+ output = self.RunShellCommand(script, **kwargs) |
+ pipestatus_line = output[-1] |
+ |
+ if not pipestatus_line.startswith(PIPESTATUS_LEADER): |
+ logging.error('Pipe exit statuses of shell script missing.') |
+ raise device_errors.AdbShellCommandFailedError( |
+ script, output, status=None, |
+ device_serial=self.adb.GetDeviceSerial()) |
+ |
+ output = output[:-1] |
+ statuses = [ |
+ int(s) for s in pipestatus_line[len(PIPESTATUS_LEADER):].split()] |
+ if any(statuses): |
+ raise device_errors.AdbShellCommandFailedError( |
+ script, output, status=statuses, |
+ device_serial=self.adb.GetDeviceSerial()) |
+ return output |
+ |
@decorators.WithTimeoutAndRetriesFromInstance() |
- def KillAll(self, process_name, signum=9, as_root=False, blocking=False, |
- timeout=None, retries=None): |
+ def KillAll(self, process_name, signum=device_signal.SIGKILL, as_root=False, |
+ blocking=False, quiet=False, timeout=None, retries=None): |
"""Kill all processes with the given name on the device. |
Args: |
process_name: A string containing the name of the process to kill. |
signum: An integer containing the signal number to send to kill. Defaults |
- to 9 (SIGKILL). |
+ to SIGKILL (9). |
as_root: A boolean indicating whether the kill should be executed with |
root privileges. |
blocking: A boolean indicating whether we should wait until all processes |
with the given |process_name| are dead. |
+ quiet: A boolean indicating whether to ignore the fact that no processes |
+ to kill were found. |
timeout: timeout in seconds |
retries: number of retries |
+ Returns: |
+ The number of processes attempted to kill. |
+ |
Raises: |
- CommandFailedError if no process was killed. |
+ CommandFailedError if no process was killed and |quiet| is False. |
CommandTimeoutError on timeout. |
DeviceUnreachableError on missing device. |
""" |
- pids = self._GetPidsImpl(process_name) |
+ pids = self.GetPids(process_name) |
if not pids: |
- raise device_errors.CommandFailedError( |
- 'No process "%s"' % process_name, str(self)) |
+ if quiet: |
+ return 0 |
+ else: |
+ raise device_errors.CommandFailedError( |
+ 'No process "%s"' % process_name, str(self)) |
cmd = ['kill', '-%d' % signum] + pids.values() |
self.RunShellCommand(cmd, as_root=as_root, check_return=True) |
if blocking: |
+ # TODO(perezu): use timeout_retry.WaitFor |
wait_period = 0.1 |
- while self._GetPidsImpl(process_name): |
+ while self.GetPids(process_name): |
time.sleep(wait_period) |
return len(pids) |
@@ -791,29 +853,27 @@ class DeviceUtils(object): |
if not real_device_path: |
return [(host_path, device_path)] |
- host_hash_tuples = md5sum.CalculateHostMd5Sums([real_host_path]) |
+ host_checksums = md5sum.CalculateHostMd5Sums([real_host_path]) |
device_paths_to_md5 = ( |
real_device_path if os.path.isfile(real_host_path) |
else ('%s/%s' % (real_device_path, os.path.relpath(p, real_host_path)) |
- for _, p in host_hash_tuples)) |
- device_hash_tuples = md5sum.CalculateDeviceMd5Sums( |
+ for p in host_checksums.iterkeys())) |
+ device_checksums = md5sum.CalculateDeviceMd5Sums( |
device_paths_to_md5, self) |
if os.path.isfile(host_path): |
- if (not device_hash_tuples |
- or device_hash_tuples[0].hash != host_hash_tuples[0].hash): |
+ host_checksum = host_checksums.get(real_host_path) |
+ device_checksum = device_checksums.get(real_device_path) |
+ if host_checksum != device_checksum: |
return [(host_path, device_path)] |
else: |
return [] |
else: |
- device_tuple_dict = dict((d.path, d.hash) for d in device_hash_tuples) |
to_push = [] |
- for host_hash, host_abs_path in ( |
- (h.hash, h.path) for h in host_hash_tuples): |
+ for host_abs_path, host_checksum in host_checksums.iteritems(): |
device_abs_path = '%s/%s' % ( |
real_device_path, os.path.relpath(host_abs_path, real_host_path)) |
- if (device_abs_path not in device_tuple_dict |
- or device_tuple_dict[device_abs_path] != host_hash): |
+ if (device_checksums.get(device_abs_path) != host_checksum): |
to_push.append((host_abs_path, device_abs_path)) |
return to_push |
@@ -991,7 +1051,7 @@ class DeviceUtils(object): |
else: |
logging.warning('Could not determine size of %s.', device_path) |
- if size is None or size <= self._MAX_ADB_OUTPUT_LENGTH: |
+ if 0 < size <= self._MAX_ADB_OUTPUT_LENGTH: |
return _JoinLines(self.RunShellCommand( |
['cat', device_path], as_root=as_root, check_return=True)) |
elif as_root and self.NeedsSU(): |
@@ -1326,11 +1386,19 @@ class DeviceUtils(object): |
CommandTimeoutError on timeout. |
DeviceUnreachableError on missing device. |
""" |
- return self._GetPidsImpl(process_name) |
- |
- def _GetPidsImpl(self, process_name): |
procs_pids = {} |
- for line in self.RunShellCommand('ps', check_return=True): |
+ try: |
+ ps_output = self._RunPipedShellCommand( |
+ 'ps | grep -F %s' % cmd_helper.SingleQuote(process_name)) |
+ 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 |
+ |
+ for line in ps_output: |
try: |
ps_data = line.split() |
if process_name in ps_data[-1]: |
@@ -1402,10 +1470,8 @@ class DeviceUtils(object): |
'Size', 'Rss', 'Pss', 'Shared_Clean', 'Shared_Dirty', 'Private_Clean', |
'Private_Dirty') |
- showmap_out = self.RunShellCommand( |
- ['showmap', str(pid)], as_root=True, check_return=True) |
- if not showmap_out: |
- raise device_errors.CommandFailedError('No output from showmap') |
+ showmap_out = self._RunPipedShellCommand( |
+ 'showmap %d | grep TOTAL' % int(pid), as_root=True) |
split_totals = showmap_out[-1].split() |
if (not split_totals |
@@ -1439,155 +1505,6 @@ class DeviceUtils(object): |
return logcat_monitor.LogcatMonitor(self.adb, *args, **kwargs) |
@decorators.WithTimeoutAndRetriesFromInstance() |
- def GetBatteryInfo(self, timeout=None, retries=None): |
- """Gets battery info for the device. |
- |
- Args: |
- timeout: timeout in seconds |
- retries: number of retries |
- Returns: |
- A dict containing various battery information as reported by dumpsys |
- battery. |
- """ |
- result = {} |
- # Skip the first line, which is just a header. |
- for line in self.RunShellCommand( |
- ['dumpsys', 'battery'], check_return=True)[1:]: |
- # If usb charging has been disabled, an extra line of header exists. |
- if 'UPDATES STOPPED' in line: |
- logging.warning('Dumpsys battery not receiving updates. ' |
- 'Run dumpsys battery reset if this is in error.') |
- elif ':' not in line: |
- logging.warning('Unknown line found in dumpsys battery.') |
- logging.warning(line) |
- else: |
- k, v = line.split(': ', 1) |
- result[k.strip()] = v.strip() |
- return result |
- |
- @decorators.WithTimeoutAndRetriesFromInstance() |
- def GetCharging(self, timeout=None, retries=None): |
- """Gets the charging state of the device. |
- |
- Args: |
- timeout: timeout in seconds |
- retries: number of retries |
- Returns: |
- True if the device is charging, false otherwise. |
- """ |
- battery_info = self.GetBatteryInfo() |
- for k in ('AC powered', 'USB powered', 'Wireless powered'): |
- if (k in battery_info and |
- battery_info[k].lower() in ('true', '1', 'yes')): |
- return True |
- return False |
- |
- @decorators.WithTimeoutAndRetriesFromInstance() |
- def SetCharging(self, enabled, timeout=None, retries=None): |
- """Enables or disables charging on the device. |
- |
- Args: |
- enabled: A boolean indicating whether charging should be enabled or |
- disabled. |
- timeout: timeout in seconds |
- retries: number of retries |
- """ |
- if 'charging_config' not in self._cache: |
- for c in _CONTROL_CHARGING_COMMANDS: |
- if self.FileExists(c['witness_file']): |
- self._cache['charging_config'] = c |
- break |
- else: |
- raise device_errors.CommandFailedError( |
- 'Unable to find charging commands.') |
- |
- if enabled: |
- command = self._cache['charging_config']['enable_command'] |
- else: |
- command = self._cache['charging_config']['disable_command'] |
- |
- def set_and_verify_charging(): |
- self.RunShellCommand(command, check_return=True) |
- return self.GetCharging() == enabled |
- |
- timeout_retry.WaitFor(set_and_verify_charging, wait_period=1) |
- |
- # TODO(rnephew): Make private when all use cases can use the context manager. |
- @decorators.WithTimeoutAndRetriesFromInstance() |
- def DisableBatteryUpdates(self, timeout=None, retries=None): |
- """ Resets battery data and makes device appear like it is not |
- charging so that it will collect power data since last charge. |
- |
- Args: |
- timeout: timeout in seconds |
- retries: number of retries |
- """ |
- def battery_updates_disabled(): |
- return self.GetCharging() is False |
- |
- self.RunShellCommand( |
- ['dumpsys', 'batterystats', '--reset'], check_return=True) |
- battery_data = self.RunShellCommand( |
- ['dumpsys', 'batterystats', '--charged', '--checkin'], |
- check_return=True) |
- ROW_TYPE_INDEX = 3 |
- PWI_POWER_INDEX = 5 |
- for line in battery_data: |
- l = line.split(',') |
- if (len(l) > PWI_POWER_INDEX and l[ROW_TYPE_INDEX] == 'pwi' |
- and l[PWI_POWER_INDEX] != 0): |
- raise device_errors.CommandFailedError( |
- 'Non-zero pmi value found after reset.') |
- self.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'], |
- check_return=True) |
- timeout_retry.WaitFor(battery_updates_disabled, wait_period=1) |
- |
- # TODO(rnephew): Make private when all use cases can use the context manager. |
- @decorators.WithTimeoutAndRetriesFromInstance() |
- def EnableBatteryUpdates(self, timeout=None, retries=None): |
- """ Restarts device charging so that dumpsys no longer collects power data. |
- |
- Args: |
- timeout: timeout in seconds |
- retries: number of retries |
- """ |
- def battery_updates_enabled(): |
- return self.GetCharging() is True |
- |
- self.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '1'], |
- check_return=True) |
- self.RunShellCommand(['dumpsys', 'battery', 'reset'], check_return=True) |
- timeout_retry.WaitFor(battery_updates_enabled, wait_period=1) |
- |
- @contextlib.contextmanager |
- def BatteryMeasurement(self, timeout=None, retries=None): |
- """Context manager that enables battery data collection. It makes |
- the device appear to stop charging so that dumpsys will start collecting |
- power data since last charge. Once the with block is exited, charging is |
- resumed and power data since last charge is no longer collected. |
- |
- Only for devices L and higher. |
- |
- Example usage: |
- with BatteryMeasurement(): |
- browser_actions() |
- get_power_data() # report usage within this block |
- after_measurements() # Anything that runs after power |
- # measurements are collected |
- |
- Args: |
- timeout: timeout in seconds |
- retries: number of retries |
- """ |
- if self.build_version_sdk < constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP: |
- raise device_errors.CommandFailedError('Device must be L or higher.') |
- try: |
- self.DisableBatteryUpdates(timeout=timeout, retries=retries) |
- yield |
- finally: |
- self.EnableBatteryUpdates(timeout=timeout, retries=retries) |
- |
- @decorators.WithTimeoutAndRetriesFromInstance() |
def GetDevicePieWrapper(self, timeout=None, retries=None): |
"""Gets the absolute path to the run_pie wrapper on the device. |
@@ -1622,12 +1539,24 @@ class DeviceUtils(object): |
return self._cache['run_pie'] |
+ def GetClientCache(self, client_name): |
+ """Returns client cache.""" |
+ if client_name not in self._client_caches: |
+ self._client_caches[client_name] = {} |
+ return self._client_caches[client_name] |
+ |
+ def _ClearCache(self): |
+ """Clears all caches.""" |
+ for client in self._client_caches: |
+ self._client_caches[client].clear() |
+ self._cache.clear() |
+ |
@classmethod |
def parallel(cls, devices=None, async=False): |
"""Creates a Parallelizer to operate over the provided list of devices. |
If |devices| is either |None| or an empty list, the Parallelizer will |
- operate over all attached devices. |
+ operate over all attached devices that have not been blacklisted. |
Args: |
devices: A list of either DeviceUtils instances or objects from |
@@ -1640,11 +1569,25 @@ class DeviceUtils(object): |
A Parallelizer operating over |devices|. |
""" |
if not devices: |
- devices = adb_wrapper.AdbWrapper.GetDevices() |
+ devices = cls.HealthyDevices() |
if not devices: |
raise device_errors.NoDevicesError() |
+ |
devices = [d if isinstance(d, cls) else cls(d) for d in devices] |
if async: |
return parallelizer.Parallelizer(devices) |
else: |
return parallelizer.SyncParallelizer(devices) |
+ |
+ @classmethod |
+ def HealthyDevices(cls): |
+ blacklist = device_blacklist.ReadBlacklist() |
+ def blacklisted(adb): |
+ if adb.GetDeviceSerial() in blacklist: |
+ logging.warning('Device %s is blacklisted.', adb.GetDeviceSerial()) |
+ return True |
+ return False |
+ |
+ return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices() |
+ if not blacklisted(adb)] |
+ |