aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil
diff options
context:
space:
mode:
authorWei Wang <wvw@google.com>2018-02-26 14:02:53 -0800
committerWei Wang <wvw@google.com>2018-02-26 14:13:51 -0800
commitb2cf025c7d5cebd43084f38c6c7ff9cc17da428a (patch)
tree06e286adf2a464b39cf69d9ff9c91cad60d79772 /catapult/devil
parent3e601f2c29e63f5151aa982790deea52645bc6ea (diff)
downloadchromium-trace-b2cf025c7d5cebd43084f38c6c7ff9cc17da428a.tar.gz
Notable changes: Add clk_set_rate support Add clock state support Bug: 73775767 Bug: 73795364 Test: ./systrace.py Change-Id: Iafb25ba9750f0e4cea6c8278788d8837e4a8776a
Diffstat (limited to 'catapult/devil')
-rw-r--r--catapult/devil/devil/android/apk_helper.py122
-rwxr-xr-xcatapult/devil/devil/android/apk_helper_test.py82
-rw-r--r--catapult/devil/devil/android/battery_utils.py2
-rwxr-xr-xcatapult/devil/devil/android/battery_utils_test.py9
-rw-r--r--catapult/devil/devil/android/constants/chrome.py5
-rw-r--r--catapult/devil/devil/android/constants/webapk.py6
-rw-r--r--catapult/devil/devil/android/crash_handler.py43
-rwxr-xr-xcatapult/devil/devil/android/crash_handler_devicetest.py72
-rw-r--r--catapult/devil/devil/android/device_temp_file.py72
-rw-r--r--catapult/devil/devil/android/device_utils.py532
-rwxr-xr-xcatapult/devil/devil/android/device_utils_devicetest.py42
-rwxr-xr-xcatapult/devil/devil/android/device_utils_test.py350
-rw-r--r--catapult/devil/devil/android/flag_changer.py45
-rw-r--r--catapult/devil/devil/android/forwarder.py15
-rw-r--r--catapult/devil/devil/android/logcat_monitor.py21
-rw-r--r--catapult/devil/devil/android/perf/surface_stats_collector.py9
-rw-r--r--catapult/devil/devil/android/sdk/adb_wrapper.py31
-rwxr-xr-xcatapult/devil/devil/android/sdk/adb_wrapper_test.py15
-rw-r--r--catapult/devil/devil/android/sdk/fastboot.py54
-rw-r--r--catapult/devil/devil/android/sdk/version_codes.py3
-rw-r--r--catapult/devil/devil/android/settings.py4
-rwxr-xr-xcatapult/devil/devil/android/tools/adb_run_shell_cmd.py22
-rwxr-xr-xcatapult/devil/devil/android/tools/cpufreq.py22
-rwxr-xr-xcatapult/devil/devil/android/tools/device_monitor.py26
-rwxr-xr-xcatapult/devil/devil/android/tools/device_monitor_test.py11
-rwxr-xr-xcatapult/devil/devil/android/tools/device_recovery.py43
-rwxr-xr-xcatapult/devil/devil/android/tools/device_status.py42
-rwxr-xr-xcatapult/devil/devil/android/tools/flash_device.py11
-rwxr-xr-xcatapult/devil/devil/android/tools/keyboard.py13
-rwxr-xr-xcatapult/devil/devil/android/tools/provision_devices.py28
-rwxr-xr-xcatapult/devil/devil/android/tools/screenshot.py13
-rw-r--r--catapult/devil/devil/android/tools/script_common.py29
-rwxr-xr-xcatapult/devil/devil/android/tools/script_common_test.py45
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app.py218
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app_devicetest.py98
-rw-r--r--catapult/devil/devil/android/tools/system_app_test.py69
-rw-r--r--catapult/devil/devil/android/tools/unlock_bootloader.py145
-rwxr-xr-xcatapult/devil/devil/android/tools/video_recorder.py2
-rwxr-xr-xcatapult/devil/devil/android/tools/wait_for_devices.py10
-rw-r--r--catapult/devil/devil/devil_dependencies.json10
-rw-r--r--catapult/devil/devil/utils/cmd_helper.py48
-rwxr-xr-xcatapult/devil/devil/utils/cmd_helper_test.py7
-rwxr-xr-xcatapult/devil/devil/utils/find_usb_devices.py32
-rw-r--r--catapult/devil/devil/utils/host_utils.py13
-rw-r--r--catapult/devil/devil/utils/logging_common.py50
-rw-r--r--catapult/devil/devil/utils/parallelizer.py31
-rw-r--r--catapult/devil/devil/utils/parallelizer_test.py32
-rw-r--r--catapult/devil/devil/utils/run_tests_helper.py59
-rw-r--r--catapult/devil/devil/utils/timeout_retry.py12
-rw-r--r--catapult/devil/devil/utils/usb_hubs.py5
-rw-r--r--catapult/devil/devil/utils/watchdog_timer.py4
-rw-r--r--catapult/devil/devil/utils/zip_utils.py75
-rw-r--r--catapult/devil/devil/utils/zip_utils_test.py43
-rw-r--r--catapult/devil/docs/device_utils.md123
-rw-r--r--catapult/devil/docs/persistent_device_list.md28
55 files changed, 2213 insertions, 740 deletions
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py
index 1a9b8c55..8acb41e6 100644
--- a/catapult/devil/devil/android/apk_helper.py
+++ b/catapult/devil/devil/android/apk_helper.py
@@ -4,7 +4,6 @@
"""Module containing utilities for apk packages."""
-import itertools
import re
from devil import base_error
@@ -36,6 +35,15 @@ def ToHelper(path_or_helper):
return path_or_helper
+# To parse the manifest, the function uses a node stack where at each level of
+# the stack it keeps the currently in focus node at that level (of indentation
+# in the xmltree output, ie. depth in the tree). The height of the stack is
+# determinded by line indentation. When indentation is increased so is the stack
+# (by pushing a new empty node on to the stack). When indentation is decreased
+# the top of the stack is popped (sometimes multiple times, until indentation
+# matches the height of the stack). Each line parsed (either an attribute or an
+# element) is added to the node at the top of the stack (after the stack has
+# been popped/pushed due to indentation).
def _ParseManifestFromApk(apk_path):
aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml')
@@ -43,17 +51,35 @@ def _ParseManifestFromApk(apk_path):
node_stack = [parsed_manifest]
indent = ' '
- for line in aapt_output[1:]:
+ if aapt_output[0].startswith('N'):
+ # if the first line is a namespace then the root manifest is indented, and
+ # we need to add a dummy namespace node, then skip the first line (we dont
+ # care about namespaces).
+ node_stack.insert(0, {})
+ output_to_parse = aapt_output[1:]
+ else:
+ output_to_parse = aapt_output
+
+ for line in output_to_parse:
if len(line) == 0:
continue
+ # If namespaces are stripped, aapt still outputs the full url to the
+ # namespace and appends it to the attribute names.
+ line = line.replace('http://schemas.android.com/apk/res/android:', 'android:')
+
indent_depth = 0
while line[(len(indent) * indent_depth):].startswith(indent):
indent_depth += 1
- node_stack = node_stack[:indent_depth]
+ # Pop the stack until the height of the stack is the same is the depth of
+ # the current line within the tree.
+ node_stack = node_stack[:indent_depth + 1]
node = node_stack[-1]
+ # Element nodes are a list of python dicts while attributes are just a dict.
+ # This is because multiple elements, at the same depth of tree and the same
+ # name, are all added to the same list keyed under the element name.
m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:])
if m:
manifest_key = m.group(1)
@@ -77,6 +103,44 @@ def _ParseManifestFromApk(apk_path):
return parsed_manifest
+def _ParseNumericKey(obj, key, default=0):
+ val = obj.get(key)
+ if val is None:
+ return default
+ return int(val, 0)
+
+
+class _ExportedActivity(object):
+ def __init__(self, name):
+ self.name = name
+ self.actions = set()
+ self.categories = set()
+ self.schemes = set()
+
+
+def _IterateExportedActivities(manifest_info):
+ app_node = manifest_info['manifest'][0]['application'][0]
+ activities = app_node.get('activity', []) + app_node.get('activity-alias', [])
+ for activity_node in activities:
+ # Presence of intent filters make an activity exported by default.
+ has_intent_filter = 'intent-filter' in activity_node
+ if not _ParseNumericKey(
+ activity_node, 'android:exported', default=has_intent_filter):
+ continue
+
+ activity = _ExportedActivity(activity_node.get('android:name'))
+ # Merge all intent-filters into a single set because there is not
+ # currently a need to keep them separate.
+ for intent_filter in activity_node.get('intent-filter', []):
+ for action in intent_filter.get('action', []):
+ activity.actions.add(action.get('android:name'))
+ for category in intent_filter.get('category', []):
+ activity.categories.add(category.get('android:name'))
+ for data in intent_filter.get('data', []):
+ activity.schemes.add(data.get('android:scheme'))
+ yield activity
+
+
class ApkHelper(object):
def __init__(self, path):
@@ -88,19 +152,22 @@ class ApkHelper(object):
return self._apk_path
def GetActivityName(self):
- """Returns the name of the Activity in the apk."""
+ """Returns the name of the first launcher Activity in the apk."""
manifest_info = self._GetManifest()
- try:
- activity = (
- manifest_info['manifest'][0]['application'][0]['activity'][0]
- ['android:name'])
- except KeyError:
- return None
- if '.' not in activity:
- activity = '%s.%s' % (self.GetPackageName(), activity)
- elif activity.startswith('.'):
- activity = '%s%s' % (self.GetPackageName(), activity)
- return activity
+ for activity in _IterateExportedActivities(manifest_info):
+ if ('android.intent.action.MAIN' in activity.actions and
+ 'android.intent.category.LAUNCHER' in activity.categories):
+ return self._ResolveName(activity.name)
+ return None
+
+ def GetViewActivityName(self):
+ """Returns name of the first action=View Activity that can handle http."""
+ manifest_info = self._GetManifest()
+ for activity in _IterateExportedActivities(manifest_info):
+ if ('android.intent.action.VIEW' in activity.actions and
+ 'http' in activity.schemes):
+ return self._ResolveName(activity.name)
+ return None
def GetInstrumentationName(
self, default='android.test.InstrumentationTestRunner'):
@@ -110,7 +177,7 @@ class ApkHelper(object):
raise base_error.BaseError(
'There is more than one instrumentation. Expected one.')
else:
- return all_instrumentations[0]['android:name']
+ return self._ResolveName(all_instrumentations[0]['android:name'])
def GetAllInstrumentations(
self, default='android.test.InstrumentationTestRunner'):
@@ -148,17 +215,30 @@ class ApkHelper(object):
"""Returns whether any services exist that use isolatedProcess=true."""
manifest_info = self._GetManifest()
try:
- applications = manifest_info['manifest'][0].get('application', [])
- services = itertools.chain(
- *(application.get('service', []) for application in applications))
+ application = manifest_info['manifest'][0]['application'][0]
+ services = application['service']
return any(
- int(s.get('android:isolatedProcess', '0'), 0)
- for s in services)
+ _ParseNumericKey(s, 'android:isolatedProcess') for s in services)
except KeyError:
return False
+ def GetAllMetadata(self):
+ """Returns a list meta-data tags as (name, value) tuples."""
+ manifest_info = self._GetManifest()
+ try:
+ application = manifest_info['manifest'][0]['application'][0]
+ metadata = application['meta-data']
+ return [(x.get('android:name'), x.get('android:value')) for x in metadata]
+ except KeyError:
+ return []
+
def _GetManifest(self):
if not self._manifest:
self._manifest = _ParseManifestFromApk(self._apk_path)
return self._manifest
+ def _ResolveName(self, name):
+ name = name.lstrip('.')
+ if '.' not in name:
+ return '%s.%s' % (self.GetPackageName(), name)
+ return name
diff --git a/catapult/devil/devil/android/apk_helper_test.py b/catapult/devil/devil/android/apk_helper_test.py
index f7d077dd..12137db0 100755
--- a/catapult/devil/devil/android/apk_helper_test.py
+++ b/catapult/devil/devil/android/apk_helper_test.py
@@ -3,6 +3,8 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import unittest
+
from devil import base_error
from devil import devil_env
from devil.android import apk_helper
@@ -29,6 +31,31 @@ _MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: service (line=7)
A: android:name(0x01010001)="org.chromium.RandomService" (Raw: "org.chromium.RandomService")
A: android:isolatedProcess(0x01010888)=(type 0x12)0xffffffff
+ E: activity (line=173)
+ A: android:name(0x01010003)=".MainActivity" (Raw: ".MainActivity")
+ E: intent-filter (line=177)
+ E: action (line=178)
+ A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+ E: category (line=180)
+ A: android:name(0x01010003)="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")
+ E: category (line=181)
+ A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
+ E: activity-alias (line=173)
+ A: android:name(0x01010003)="org.chromium.ViewActivity" (Raw: "org.chromium.ViewActivity")
+ A: android:targetActivity(0x01010202)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName")
+ E: intent-filter (line=191)
+ E: action (line=192)
+ A: android:name(0x01010003)="android.intent.action.VIEW" (Raw: "android.intent.action.VIEW")
+ E: data (line=198)
+ A: android:scheme(0x01010027)="http" (Raw: "http")
+ E: data (line=199)
+ A: android:scheme(0x01010027)="https" (Raw: "https")
+ E: meta-data (line=43)
+ A: android:name(0x01010003)="name1" (Raw: "name1")
+ A: android:value(0x01010024)="value1" (Raw: "value1")
+ E: meta-data (line=43)
+ A: android:name(0x01010003)="name2" (Raw: "name2")
+ A: android:value(0x01010024)="value2" (Raw: "value2")
E: instrumentation (line=8)
A: android:label(0x01010001)="abc" (Raw: "abc")
A: android:name(0x01010003)="org.chromium.RandomJUnit4TestRunner" (Raw: "org.chromium.RandomJUnit4TestRunner")
@@ -84,6 +111,14 @@ _SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.
A: junit4=(type 0x12)0xffffffff (Raw: "true")
"""
+_NO_NAMESPACE_MANIFEST_DUMP = """E: manifest (line=1)
+ A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
+ E: instrumentation (line=8)
+ A: http://schemas.android.com/apk/res/android:label(0x01010001)="xyz" (Raw: "xyz")
+ A: http://schemas.android.com/apk/res/android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
+ A: http://schemas.android.com/apk/res/android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
+"""
+
def _MockAaptDump(manifest_dump):
return mock.patch(
@@ -94,19 +129,25 @@ class ApkHelperTest(mock_calls.TestCase):
def testGetInstrumentationName(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
with self.assertRaises(base_error.BaseError):
helper.GetInstrumentationName()
def testGetActivityName(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals(
+ helper.GetActivityName(), 'org.chromium.abc.MainActivity')
+
+ def testGetViewActivityName(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
self.assertEquals(
- helper.GetActivityName(), 'org.chromium.ActivityName')
+ helper.GetViewActivityName(), 'org.chromium.ViewActivity')
def testGetAllInstrumentations(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
all_instrumentations = helper.GetAllInstrumentations()
self.assertEquals(len(all_instrumentations), 2)
self.assertEquals(all_instrumentations[0]['android:name'],
@@ -116,12 +157,12 @@ class ApkHelperTest(mock_calls.TestCase):
def testGetPackageName(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertEquals(helper.GetPackageName(), 'org.chromium.abc')
def testGetPermssions(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
all_permissions = helper.GetPermissions()
self.assertEquals(len(all_permissions), 3)
self.assertTrue('android.permission.INTERNET' in all_permissions)
@@ -132,38 +173,53 @@ class ApkHelperTest(mock_calls.TestCase):
def testGetSplitName(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertEquals(helper.GetSplitName(), 'random_split')
def testHasIsolatedProcesses_noApplication(self):
with _MockAaptDump(_NO_APPLICATION):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_noServices(self):
with _MockAaptDump(_NO_SERVICES):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_oneNotIsolatedProcess(self):
with _MockAaptDump(_NO_ISOLATED_SERVICES):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_oneIsolatedProcess(self):
with _MockAaptDump(_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertTrue(helper.HasIsolatedProcesses())
def testGetSingleInstrumentationName(self):
with _MockAaptDump(_SINGLE_INSTRUMENTATION_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertEquals('org.chromium.RandomTestRunner',
helper.GetInstrumentationName())
def testGetSingleJUnit4InstrumentationName(self):
with _MockAaptDump(_SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP):
- helper = apk_helper.ApkHelper("")
+ helper = apk_helper.ApkHelper('')
self.assertEquals('org.chromium.RandomJ4TestRunner',
helper.GetInstrumentationName())
+ def testGetAllMetadata(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals([('name1', 'value1'), ('name2', 'value2')],
+ helper.GetAllMetadata())
+
+ def testGetSingleInstrumentationName_strippedNamespaces(self):
+ with _MockAaptDump(_NO_NAMESPACE_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('org.chromium.RandomTestRunner',
+ helper.GetInstrumentationName())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/battery_utils.py b/catapult/devil/devil/android/battery_utils.py
index 068c187c..a8a08a96 100644
--- a/catapult/devil/devil/android/battery_utils.py
+++ b/catapult/devil/devil/android/battery_utils.py
@@ -614,7 +614,7 @@ class BatteryUtils(object):
return self.GetCharging() == enabled
self._device.RunShellCommand(
- command, check_return=True, as_root=True, large_output=True)
+ command, shell=True, check_return=True, as_root=True, large_output=True)
timeout_retry.WaitFor(verify_charging, wait_period=1)
@contextlib.contextmanager
diff --git a/catapult/devil/devil/android/battery_utils_test.py b/catapult/devil/devil/android/battery_utils_test.py
index beaba3b0..feccf79e 100755
--- a/catapult/devil/devil/android/battery_utils_test.py
+++ b/catapult/devil/devil/android/battery_utils_test.py
@@ -109,7 +109,8 @@ class BatteryUtilsSetChargingTest(BatteryUtilsTest):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
- mock.ANY, check_return=True, as_root=True, large_output=True), []),
+ mock.ANY, shell=True, check_return=True, as_root=True,
+ large_output=True), []),
(self.call.battery.GetCharging(), False),
(self.call.battery.GetCharging(), True)):
self.battery._HardwareSetCharging(True)
@@ -118,7 +119,8 @@ class BatteryUtilsSetChargingTest(BatteryUtilsTest):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
- mock.ANY, check_return=True, as_root=True, large_output=True), []),
+ mock.ANY, shell=True, check_return=True, as_root=True,
+ large_output=True), []),
(self.call.battery.GetCharging(), True)):
self.battery._HardwareSetCharging(True)
@@ -127,7 +129,8 @@ class BatteryUtilsSetChargingTest(BatteryUtilsTest):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
- mock.ANY, check_return=True, as_root=True, large_output=True), []),
+ mock.ANY, shell=True, check_return=True, as_root=True,
+ large_output=True), []),
(self.call.battery.GetCharging(), True),
(self.call.battery.GetCharging(), False)):
self.battery._HardwareSetCharging(False)
diff --git a/catapult/devil/devil/android/constants/chrome.py b/catapult/devil/devil/android/constants/chrome.py
index dca04bdc..36bd972e 100644
--- a/catapult/devil/devil/android/constants/chrome.py
+++ b/catapult/devil/devil/android/constants/chrome.py
@@ -39,11 +39,6 @@ PACKAGE_INFO = {
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
- 'chrome_work': PackageInfo(
- 'com.chrome.work',
- 'com.google.android.apps.chrome.Main',
- 'chrome-command-line',
- 'chrome_devtools_remote'),
'chromium': PackageInfo(
'org.chromium.chrome',
'com.google.android.apps.chrome.Main',
diff --git a/catapult/devil/devil/android/constants/webapk.py b/catapult/devil/devil/android/constants/webapk.py
new file mode 100644
index 00000000..5a17e724
--- /dev/null
+++ b/catapult/devil/devil/android/constants/webapk.py
@@ -0,0 +1,6 @@
+# Copyright 2017 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.
+
+WEBAPK_MAIN_ACTIVITY = 'org.chromium.webapk.shell_apk.MainActivity'
+
diff --git a/catapult/devil/devil/android/crash_handler.py b/catapult/devil/devil/android/crash_handler.py
new file mode 100644
index 00000000..7cfabcfb
--- /dev/null
+++ b/catapult/devil/devil/android/crash_handler.py
@@ -0,0 +1,43 @@
+# Copyright 2017 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.
+
+
+import logging
+
+from devil import base_error
+from devil.android import device_errors
+
+logger = logging.getLogger(__name__)
+
+
+def RetryOnSystemCrash(f, device, retries=3):
+ """Retries the given function on a device crash.
+
+ If the provided function fails with a DeviceUnreachableError, this will wait
+ for the device to come back online, then retry the function.
+
+ Note that this uses the same retry scheme as timeout_retry.Run.
+
+ Args:
+ f: a unary callable that takes an instance of device_utils.DeviceUtils.
+ device: an instance of device_utils.DeviceUtils.
+ retries: the number of retries.
+ Returns:
+ Whatever f returns.
+ """
+ num_try = 1
+ while True:
+ try:
+ return f(device)
+ except device_errors.DeviceUnreachableError:
+ if num_try > retries:
+ logger.error('%d consecutive device crashes. No longer retrying.',
+ num_try)
+ raise
+ try:
+ logger.warning('Device is unreachable. Waiting for recovery...')
+ device.WaitUntilFullyBooted()
+ except base_error.BaseError:
+ logger.exception('Device never recovered. X(')
+ num_try += 1
diff --git a/catapult/devil/devil/android/crash_handler_devicetest.py b/catapult/devil/devil/android/crash_handler_devicetest.py
new file mode 100755
index 00000000..6365104d
--- /dev/null
+++ b/catapult/devil/devil/android/crash_handler_devicetest.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', )))
+
+from devil.android import crash_handler
+from devil.android import device_errors
+from devil.android import device_utils
+from devil.android import device_temp_file
+from devil.android import device_test_case
+from devil.utils import cmd_helper
+from devil.utils import reraiser_thread
+from devil.utils import timeout_retry
+
+
+class DeviceCrashTest(device_test_case.DeviceTestCase):
+
+ def setUp(self):
+ super(DeviceCrashTest, self).setUp()
+ self.device = device_utils.DeviceUtils(self.serial)
+
+ def testCrashDuringCommand(self):
+ self.device.EnableRoot()
+ with device_temp_file.DeviceTempFile(self.device.adb) as trigger_file:
+
+ trigger_text = 'hello world'
+
+ def victim():
+ trigger_cmd = 'echo -n %s > %s; sleep 20' % (
+ cmd_helper.SingleQuote(trigger_text),
+ cmd_helper.SingleQuote(trigger_file.name))
+ crash_handler.RetryOnSystemCrash(
+ lambda d: d.RunShellCommand(
+ trigger_cmd, shell=True, check_return=True, retries=1,
+ as_root=True, timeout=180),
+ device=self.device)
+ self.assertEquals(
+ trigger_text,
+ self.device.ReadFile(trigger_file.name, retries=0).strip())
+ return True
+
+ def crasher():
+ def ready_to_crash():
+ try:
+ return trigger_text == self.device.ReadFile(
+ trigger_file.name, retries=0).strip()
+ except device_errors.CommandFailedError:
+ return False
+
+ timeout_retry.WaitFor(ready_to_crash, wait_period=2, max_tries=10)
+ if not ready_to_crash():
+ return False
+ self.device.adb.Shell(
+ 'echo c > /proc/sysrq-trigger',
+ expect_status=None, timeout=60, retries=0)
+ return True
+
+ self.assertEquals([True, True],
+ reraiser_thread.RunAsync([crasher, victim]))
+
+
+if __name__ == '__main__':
+ device_test_case.PrepareDevices()
+ unittest.main()
diff --git a/catapult/devil/devil/android/device_temp_file.py b/catapult/devil/devil/android/device_temp_file.py
index 4d0c7adb..74cc5099 100644
--- a/catapult/devil/devil/android/device_temp_file.py
+++ b/catapult/devil/devil/android/device_temp_file.py
@@ -6,15 +6,28 @@
# pylint: disable=W0622
+import logging
import posixpath
import random
import threading
+from devil import base_error
from devil.android import device_errors
from devil.utils import cmd_helper
+logger = logging.getLogger(__name__)
+
+
+def _GenerateName(prefix, suffix, dir):
+ random_hex = hex(random.randint(0, 2 ** 52))[2:]
+ return posixpath.join(dir, '%s-%s%s' % (prefix, random_hex, suffix))
+
class DeviceTempFile(object):
+ """A named temporary file on a device.
+
+ Behaves like tempfile.NamedTemporaryFile.
+ """
def __init__(self, adb, suffix='', prefix='temp_file', dir='/data/local/tmp'):
"""Find an unused temporary file path on the device.
@@ -23,9 +36,10 @@ class DeviceTempFile(object):
Args:
adb: An instance of AdbWrapper
- suffix: The suffix of the name of the temp file.
- prefix: The prefix of the name of the temp file.
- dir: The directory on the device where to place the temp file.
+ suffix: The suffix of the name of the temporary file.
+ prefix: The prefix of the name of the temporary file.
+ dir: The directory on the device in which the temporary file should be
+ placed.
Raises:
ValueError if any of suffix, prefix, or dir are None.
"""
@@ -36,8 +50,7 @@ class DeviceTempFile(object):
self._adb = adb
# Python's random module use 52-bit numbers according to its docs.
- random_hex = hex(random.randint(0, 2 ** 52))[2:]
- self.name = posixpath.join(dir, '%s-%s%s' % (prefix, random_hex, suffix))
+ self.name = _GenerateName(prefix, suffix, dir)
self.name_quoted = cmd_helper.SingleQuote(self.name)
def close(self):
@@ -46,9 +59,11 @@ class DeviceTempFile(object):
def delete_temporary_file():
try:
self._adb.Shell('rm -f %s' % self.name_quoted, expect_status=None)
- except device_errors.AdbCommandFailedError:
- # file does not exist on Android version without 'rm -f' support (ICS)
- pass
+ except base_error.BaseError as e:
+ # We don't really care, and stack traces clog up the log.
+ # Log a warning and move on.
+ logger.warning('Failed to delete temporary file %s: %s',
+ self.name, str(e))
# It shouldn't matter when the temp file gets deleted, so do so
# asynchronously.
@@ -61,3 +76,44 @@ class DeviceTempFile(object):
def __exit__(self, type, value, traceback):
self.close()
+
+
+class NamedDeviceTemporaryDirectory(object):
+ """A named temporary directory on a device."""
+
+ def __init__(self, adb, suffix='', prefix='tmp', dir='/data/local/tmp'):
+ """Find an unused temporary directory path on the device. The directory is
+ not created until it is used with a 'with' statement.
+
+ When this object is closed, the directory will be deleted on the device.
+
+ Args:
+ adb: An instance of AdbWrapper
+ suffix: The suffix of the name of the temporary directory.
+ prefix: The prefix of the name of the temporary directory.
+ dir: The directory on the device where to place the temporary directory.
+ Raises:
+ ValueError if any of suffix, prefix, or dir are None.
+ """
+ self._adb = adb
+ self.name = _GenerateName(prefix, suffix, dir)
+ self.name_quoted = cmd_helper.SingleQuote(self.name)
+
+ def close(self):
+ """Deletes the temporary directory from the device."""
+ def delete_temporary_dir():
+ try:
+ self._adb.Shell('rm -rf %s' % self.name, expect_status=None)
+ except device_errors.AdbCommandFailedError:
+ pass
+
+ threading.Thread(
+ target=delete_temporary_dir,
+ name='delete_temporary_dir(%s)' % self._adb.GetDeviceSerial()).start()
+
+ def __enter__(self):
+ self._adb.Shell('mkdir -p %s' % self.name)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.close()
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):
diff --git a/catapult/devil/devil/android/device_utils_devicetest.py b/catapult/devil/devil/android/device_utils_devicetest.py
index 932e2782..173094b9 100755
--- a/catapult/devil/devil/android/device_utils_devicetest.py
+++ b/catapult/devil/devil/android/device_utils_devicetest.py
@@ -212,12 +212,10 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
def testRestartAdbd(self):
def get_adbd_pid():
- # TODO(catapult:#3215): Migrate to device.GetPids().
- ps_output = self.device.RunShellCommand(['ps'], check_return=True)
- for ps_line in ps_output:
- if 'adbd' in ps_line:
- return ps_line.split()[1]
- self.fail('Unable to find adbd')
+ try:
+ return next(p.pid for p in self.device.ListProcesses('adbd'))
+ except StopIteration:
+ self.fail('Unable to find adbd')
old_adbd_pid = get_adbd_pid()
self.device.RestartAdbd()
@@ -234,5 +232,37 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
self.assertEquals(self.device.GetProp('service.adb.root'), '1')
+class PsOutputCompatibilityTests(device_test_case.DeviceTestCase):
+
+ def setUp(self):
+ super(PsOutputCompatibilityTests, self).setUp()
+ self.adb = adb_wrapper.AdbWrapper(self.serial)
+ self.adb.WaitForDevice()
+ self.device = device_utils.DeviceUtils(self.adb, default_retries=0)
+
+ def testPsOutoutCompatibility(self):
+ # pylint: disable=protected-access
+ lines = self.device._GetPsOutput(None)
+
+ # Check column names at each index match expected values.
+ header = lines[0].split()
+ for column, idx in device_utils._PS_COLUMNS.iteritems():
+ column = column.upper()
+ self.assertEqual(
+ header[idx], column,
+ 'Expected column %s at index %d but found %s\nsource: %r' % (
+ column, idx, header[idx], lines[0]))
+
+ # Check pid and ppid are numeric values.
+ for line in lines[1:]:
+ row = line.split()
+ row = {k: row[i] for k, i in device_utils._PS_COLUMNS.iteritems()}
+ for key in ('pid', 'ppid'):
+ self.assertTrue(
+ row[key].isdigit(),
+ 'Expected numeric %s value but found %r\nsource: %r' % (
+ key, row[key], line))
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/catapult/devil/devil/android/device_utils_test.py b/catapult/devil/devil/android/device_utils_test.py
index ebd3c628..b5660ac4 100755
--- a/catapult/devil/devil/android/device_utils_test.py
+++ b/catapult/devil/devil/android/device_utils_test.py
@@ -10,6 +10,7 @@ Unit tests for the contents of device_utils.py (mostly DeviceUtils).
# pylint: disable=protected-access
# pylint: disable=unused-argument
+import contextlib
import json
import logging
import os
@@ -31,6 +32,14 @@ with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
+def Process(name, pid, ppid='1'):
+ return device_utils.ProcessInfo(name=name, pid=pid, ppid=ppid)
+
+
+def Processes(*args):
+ return [Process(*arg) for arg in args]
+
+
class AnyStringWith(object):
def __init__(self, value):
self._value = value
@@ -142,6 +151,21 @@ class MockTempFile(object):
return self.file.name
+class MockLogger(mock.Mock):
+ def __init__(self, *args, **kwargs):
+ super(MockLogger, self).__init__(*args, **kwargs)
+ # TODO(perezju): Consider adding traps for error, info, etc.
+ self.warnings = []
+
+ def warning(self, message, *args):
+ self.warnings.append(message % args)
+
+
+def PatchLogger():
+ return mock.patch(
+ 'devil.android.device_utils.logger', new_callable=MockLogger)
+
+
class _PatchedFunction(object):
def __init__(self, patched=None, mocked=None):
@@ -383,7 +407,7 @@ class DeviceUtilsGetApplicationPathsInternalTest(DeviceUtilsTest):
self.assertEquals([],
self.device._GetApplicationPathsInternal('not.installed.app'))
- def testGetApplicationPathsInternal_garbageFirstLine(self):
+ def testGetApplicationPathsInternal_garbageOutputRaises(self):
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
(self.call.device.RunShellCommand(
@@ -392,6 +416,15 @@ class DeviceUtilsGetApplicationPathsInternalTest(DeviceUtilsTest):
with self.assertRaises(device_errors.CommandFailedError):
self.device._GetApplicationPathsInternal('android')
+ def testGetApplicationPathsInternal_outputWarningsIgnored(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
+ (self.call.device.RunShellCommand(
+ ['pm', 'path', 'not.installed.app'], check_return=True),
+ ['WARNING: some warning message from pm'])):
+ self.assertEquals([],
+ self.device._GetApplicationPathsInternal('not.installed.app'))
+
def testGetApplicationPathsInternal_fails(self):
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
@@ -1058,7 +1091,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_largeOutput_enabled(self):
cmd = 'echo $VALUE'
temp_file = MockTempFile('/sdcard/temp-123')
- cmd_redirect = '( %s )>%s' % (cmd, temp_file.name)
+ cmd_redirect = '( %s )>%s 2>&1' % (cmd, temp_file.name)
with self.assertCalls(
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
temp_file),
@@ -1079,7 +1112,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_largeOutput_disabledTrigger(self):
cmd = 'echo $VALUE'
temp_file = MockTempFile('/sdcard/temp-123')
- cmd_redirect = '( %s )>%s' % (cmd, temp_file.name)
+ cmd_redirect = '( %s )>%s 2>&1' % (cmd, temp_file.name)
with self.assertCalls(
(self.call.adb.Shell(cmd), self.ShellError('', None)),
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
@@ -1138,57 +1171,59 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
class DeviceUtilsKillAllTest(DeviceUtilsTest):
def testKillAll_noMatchingProcessesFailure(self):
- with self.assertCall(self.call.device.GetPids('test_process'), {}):
+ with self.assertCall(self.call.device.ListProcesses('test_process'), []):
with self.assertRaises(device_errors.CommandFailedError):
self.device.KillAll('test_process')
def testKillAll_noMatchingProcessesQuiet(self):
- with self.assertCall(self.call.device.GetPids('test_process'), {}):
+ with self.assertCall(self.call.device.ListProcesses('test_process'), []):
self.assertEqual(0, self.device.KillAll('test_process', quiet=True))
def testKillAll_nonblocking(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234'], 'some.processing.thing': ['5678']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process.thing', 5678))),
(self.call.adb.Shell('kill -9 1234 5678'), '')):
self.assertEquals(
2, self.device.KillAll('some.process', blocking=False))
def testKillAll_blocking(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234'], 'some.processing.thing': ['5678']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process.thing', 5678))),
(self.call.adb.Shell('kill -9 1234 5678'), ''),
- (self.call.device.GetPids('some.process'),
- {'some.processing.thing': ['5678']}),
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1111']})): # Other instance with different pid.
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process.thing', 5678))),
+ (self.call.device.ListProcesses('some.process'),
+ # Other instance with different pid.
+ Processes(('some.process', 111)))):
self.assertEquals(
2, self.device.KillAll('some.process', blocking=True))
def testKillAll_exactNonblocking(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234'], 'some.processing.thing': ['5678']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process.thing', 5678))),
(self.call.adb.Shell('kill -9 1234'), '')):
self.assertEquals(
1, self.device.KillAll('some.process', exact=True, blocking=False))
def testKillAll_exactBlocking(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234'], 'some.processing.thing': ['5678']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process.thing', 5678))),
(self.call.adb.Shell('kill -9 1234'), ''),
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234'], 'some.processing.thing': ['5678']}),
- (self.call.device.GetPids('some.process'),
- {'some.processing.thing': ['5678']})):
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process.thing', 5678))),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process.thing', 5678)))):
self.assertEquals(
1, self.device.KillAll('some.process', exact=True, blocking=True))
def testKillAll_root(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'), {'some.process': ['1234']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234))),
(self.call.device.NeedsSU(), True),
(self.call.device._Su("sh -c 'kill -9 1234'"),
"su -c sh -c 'kill -9 1234'"),
@@ -1198,16 +1233,16 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
def testKillAll_sigterm(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234))),
(self.call.adb.Shell('kill -15 1234'), '')):
self.assertEquals(
1, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
def testKillAll_multipleInstances(self):
with self.assertCalls(
- (self.call.device.GetPids('some.process'),
- {'some.process': ['1234', '4567']}),
+ (self.call.device.ListProcesses('some.process'),
+ Processes(('some.process', 1234), ('some.process', 4567))),
(self.call.adb.Shell('kill -15 1234 4567'), '')):
self.assertEquals(
2, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
@@ -1541,7 +1576,7 @@ class DeviceUtilsForceStopTest(DeviceUtilsTest):
def testForceStop(self):
with self.assertCalls(
- (self.call.device.GetPids('test.package'), {'test.package': [1111]}),
+ (self.call.device.GetApplicationPids('test.package'), [1111]),
(self.call.device.RunShellCommand(
['am', 'force-stop', 'test.package'],
check_return=True),
@@ -1550,7 +1585,7 @@ class DeviceUtilsForceStopTest(DeviceUtilsTest):
def testForceStop_NoProcessFound(self):
with self.assertCall(
- self.call.device.GetPids('test.package'), {}):
+ self.call.device.GetApplicationPids('test.package'), []):
self.device.ForceStop('test.package')
@@ -1640,32 +1675,28 @@ class DeviceUtilsPushChangedFilesZippedTest(DeviceUtilsTest):
def testPushChangedFilesZipped_noUnzipCommand(self):
test_files = [('/test/host/path/file1', '/test/device/path/file1')]
- mock_zip_temp = mock.mock_open()
- mock_zip_temp.return_value.name = '/test/temp/file/tmp.zip'
with self.assertCalls(
- (mock.call.tempfile.NamedTemporaryFile(suffix='.zip'), mock_zip_temp),
- (mock.call.multiprocessing.Process(
- target=device_utils.DeviceUtils._CreateDeviceZip,
- args=('/test/temp/file/tmp.zip', test_files)), mock.Mock()),
(self.call.device._MaybeInstallCommands(), False)):
self.assertFalse(self.device._PushChangedFilesZipped(test_files,
['/test/dir']))
def _testPushChangedFilesZipped_spec(self, test_files):
- mock_zip_temp = mock.mock_open()
- mock_zip_temp.return_value.name = '/test/temp/file/tmp.zip'
+ @contextlib.contextmanager
+ def mock_zip_temp_dir():
+ yield '/test/temp/dir'
+
with self.assertCalls(
- (mock.call.tempfile.NamedTemporaryFile(suffix='.zip'), mock_zip_temp),
- (mock.call.multiprocessing.Process(
- target=device_utils.DeviceUtils._CreateDeviceZip,
- args=('/test/temp/file/tmp.zip', test_files)), mock.Mock()),
(self.call.device._MaybeInstallCommands(), True),
+ (mock.call.py_utils.tempfile_ext.NamedTemporaryDirectory(),
+ mock_zip_temp_dir),
+ (mock.call.devil.utils.zip_utils.WriteZipFile(
+ '/test/temp/dir/tmp.zip', test_files)),
(self.call.device.NeedsSU(), True),
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb,
suffix='.zip'),
MockTempFile('/test/sdcard/foo123.zip')),
self.call.adb.Push(
- '/test/temp/file/tmp.zip', '/test/sdcard/foo123.zip'),
+ '/test/temp/dir/tmp.zip', '/test/sdcard/foo123.zip'),
self.call.device.RunShellCommand(
'unzip /test/sdcard/foo123.zip&&chmod -R 777 /test/dir',
shell=True, as_root=True,
@@ -1980,6 +2011,8 @@ class DeviceUtilsStatDirectoryTest(DeviceUtilsTest):
'drwxr-xr-x 6 root root 1970-01-01 00:00 some_dir',
'-rw-r--r-- 1 root root 723 1971-01-01 07:04 some_file',
'-rw-r----- 1 root root 327 2009-02-13 23:30 My Music File',
+ # Some Android versions escape spaces in file names
+ '-rw-rw-rw- 1 root root 0 2018-01-11 13:35 Local\\ State',
# Older Android versions do not print st_nlink
'lrwxrwxrwx root root 1970-01-01 00:00 lnk -> /some/path',
'srwxrwx--- system system 2016-05-31 17:25 a_socket1',
@@ -1991,8 +2024,8 @@ class DeviceUtilsStatDirectoryTest(DeviceUtilsTest):
]
FILENAMES = [
- 'some_dir', 'some_file', 'My Music File', 'lnk', 'a_socket1',
- 'tmp', 'my_cmd', 'random', 'block_dev', 'silly']
+ 'some_dir', 'some_file', 'My Music File', 'Local State', 'lnk',
+ 'a_socket1', 'tmp', 'my_cmd', 'random', 'block_dev', 'silly']
def getStatEntries(self, path_given='/', path_listed='/'):
with self.assertCall(
@@ -2290,23 +2323,23 @@ class DeviceUtilsSetPropTest(DeviceUtilsTest):
self.device.SetProp('test.property', 'new_value', check=True)
-class DeviceUtilsGetPidsTest(DeviceUtilsTest):
+class DeviceUtilsListProcessesTest(DeviceUtilsTest):
def setUp(self):
- super(DeviceUtilsGetPidsTest, self).setUp()
+ super(DeviceUtilsListProcessesTest, self).setUp()
self.sample_output = [
'USER PID PPID VSIZE RSS WCHAN PC NAME',
'user 1001 100 1024 1024 ffffffff 00000000 one.match',
'user 1002 100 1024 1024 ffffffff 00000000 two.match',
- 'user 1003 100 1024 1024 ffffffff 00000000 three.match',
- 'user 1234 100 1024 1024 ffffffff 00000000 my$process',
- 'user 1000 100 1024 1024 ffffffff 00000000 foo',
+ 'user 1003 101 1024 1024 ffffffff 00000000 three.match',
+ 'user 1234 101 1024 1024 ffffffff 00000000 my$process',
'user 1236 100 1024 1024 ffffffff 00000000 foo',
+ 'user 1578 1236 1024 1024 ffffffff 00000000 foo',
]
def _grepOutput(self, substring):
return [line for line in self.sample_output if substring in line]
- def testGetPids_sdkGreaterThanNougatMR1(self):
+ def testListProcesses_sdkGreaterThanNougatMR1(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=(version_codes.NOUGAT_MR1 + 1)):
with self.patch_call(self.call.device.build_id,
@@ -2314,47 +2347,49 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
with self.assertCall(
self.call.device._RunPipedShellCommand(
'ps -e | grep -F example.process'), []):
- self.device.GetPids('example.process')
+ self.device.ListProcesses('example.process')
- def testGetPids_noMatches(self):
+ def testListProcesses_noMatches(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
with self.assertCall(
self.call.device._RunPipedShellCommand('ps | grep -F does.not.match'),
self._grepOutput('does.not.match')):
- self.assertEqual({}, self.device.GetPids('does.not.match'))
+ self.assertEqual([], self.device.ListProcesses('does.not.match'))
- def testGetPids_oneMatch(self):
+ def testListProcesses_oneMatch(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
with self.assertCall(
self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
self._grepOutput('one.match')):
self.assertEqual(
- {'one.match': ['1001']},
- self.device.GetPids('one.match'))
+ Processes(('one.match', 1001, 100)),
+ self.device.ListProcesses('one.match'))
- def testGetPids_multipleMatches(self):
+ def testListProcesses_multipleMatches(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
with self.assertCall(
self.call.device._RunPipedShellCommand('ps | grep -F match'),
self._grepOutput('match')):
self.assertEqual(
- {'one.match': ['1001'],
- 'two.match': ['1002'],
- 'three.match': ['1003']},
- self.device.GetPids('match'))
+ Processes(('one.match', 1001, 100),
+ ('two.match', 1002, 100),
+ ('three.match', 1003, 101)),
+ self.device.ListProcesses('match'))
- def testGetPids_quotable(self):
+ def testListProcesses_quotable(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
with self.assertCall(
self.call.device._RunPipedShellCommand("ps | grep -F 'my$process'"),
self._grepOutput('my$process')):
self.assertEqual(
- {'my$process': ['1234']}, self.device.GetPids('my$process'))
+ Processes(('my$process', 1234, 101)),
+ self.device.ListProcesses('my$process'))
+ # Tests for the GetPids wrapper interface.
def testGetPids_multipleInstances(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
@@ -2362,7 +2397,7 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
self.call.device._RunPipedShellCommand('ps | grep -F foo'),
self._grepOutput('foo')):
self.assertEqual(
- {'foo': ['1000', '1236']},
+ {'foo': ['1236', '1578']},
self.device.GetPids('foo'))
def testGetPids_allProcesses(self):
@@ -2377,9 +2412,10 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
'two.match': ['1002'],
'three.match': ['1003'],
'my$process': ['1234'],
- 'foo': ['1000', '1236']},
+ 'foo': ['1236', '1578']},
self.device.GetPids())
+ # Tests for the GetApplicationPids wrapper interface.
def testGetApplicationPids_notFound(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.LOLLIPOP):
@@ -2395,7 +2431,7 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
with self.assertCall(
self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
self._grepOutput('one.match')):
- self.assertEqual(['1001'], self.device.GetApplicationPids('one.match'))
+ self.assertEqual([1001], self.device.GetApplicationPids('one.match'))
def testGetApplicationPids_foundMany(self):
with self.patch_call(self.call.device.build_version_sdk,
@@ -2404,7 +2440,7 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
self.call.device._RunPipedShellCommand('ps | grep -F foo'),
self._grepOutput('foo')):
self.assertEqual(
- ['1000', '1236'],
+ [1236, 1578],
self.device.GetApplicationPids('foo'))
def testGetApplicationPids_atMostOneNotFound(self):
@@ -2425,7 +2461,7 @@ class DeviceUtilsGetPidsTest(DeviceUtilsTest):
self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
self._grepOutput('one.match')):
self.assertEqual(
- '1001',
+ 1001,
self.device.GetApplicationPids('one.match', at_most_one=True))
def testGetApplicationPids_atMostOneFoundTooMany(self):
@@ -2503,60 +2539,6 @@ class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
self.device.TakeScreenshot('/test/host/screenshot.png')
-class DeviceUtilsGetMemoryUsageForPidTest(DeviceUtilsTest):
-
- def setUp(self):
- super(DeviceUtilsGetMemoryUsageForPidTest, self).setUp()
-
- def testGetMemoryUsageForPid_validPid(self):
- with self.assertCalls(
- (self.call.device._RunPipedShellCommand(
- 'showmap 1234 | grep TOTAL', as_root=True),
- ['100 101 102 103 104 105 106 107 TOTAL']),
- (self.call.device.ReadFile('/proc/1234/status', as_root=True),
- 'VmHWM: 1024 kB\n')):
- self.assertEqual(
- {
- 'Size': 100,
- 'Rss': 101,
- 'Pss': 102,
- 'Shared_Clean': 103,
- 'Shared_Dirty': 104,
- 'Private_Clean': 105,
- 'Private_Dirty': 106,
- 'VmHWM': 1024
- },
- self.device.GetMemoryUsageForPid(1234))
-
- def testGetMemoryUsageForPid_noSmaps(self):
- with self.assertCalls(
- (self.call.device._RunPipedShellCommand(
- 'showmap 4321 | grep TOTAL', as_root=True),
- ['cannot open /proc/4321/smaps: No such file or directory']),
- (self.call.device.ReadFile('/proc/4321/status', as_root=True),
- 'VmHWM: 1024 kb\n')):
- self.assertEquals({'VmHWM': 1024}, self.device.GetMemoryUsageForPid(4321))
-
- def testGetMemoryUsageForPid_noStatus(self):
- with self.assertCalls(
- (self.call.device._RunPipedShellCommand(
- 'showmap 4321 | grep TOTAL', as_root=True),
- ['100 101 102 103 104 105 106 107 TOTAL']),
- (self.call.device.ReadFile('/proc/4321/status', as_root=True),
- self.CommandError())):
- self.assertEquals(
- {
- 'Size': 100,
- 'Rss': 101,
- 'Pss': 102,
- 'Shared_Clean': 103,
- 'Shared_Dirty': 104,
- 'Private_Clean': 105,
- 'Private_Dirty': 106,
- },
- self.device.GetMemoryUsageForPid(4321))
-
-
class DeviceUtilsDismissCrashDialogIfNeededTest(DeviceUtilsTest):
def testDismissCrashDialogIfNeeded_crashedPageckageNotFound(self):
@@ -2682,7 +2664,7 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials])):
with self.assertRaises(device_errors.NoDevicesError):
- device_utils.DeviceUtils.HealthyDevices(device_arg=None)
+ device_utils.DeviceUtils.HealthyDevices(device_arg=None, retry=False)
def testHealthyDevices_noneDeviceArg_multiple_attached_ANDROID_SERIAL(self):
try:
@@ -2721,7 +2703,17 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials])):
with self.assertRaises(device_errors.NoDevicesError):
- device_utils.DeviceUtils.HealthyDevices(device_arg=[])
+ device_utils.DeviceUtils.HealthyDevices(device_arg=[], retry=False)
+
+ def testHealthyDevices_EmptyListDeviceArg_no_attached_with_retry(self):
+ test_serials = []
+ with self.assertCalls(
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
+ [_AdbWrapperMock(s) for s in test_serials]),
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
+ [_AdbWrapperMock(s) for s in test_serials])):
+ with self.assertRaises(device_errors.NoDevicesError):
+ device_utils.DeviceUtils.HealthyDevices(device_arg=[], retry=True)
def testHealthyDevices_ListDeviceArg(self):
device_arg = ['0123456789abcdef', 'fedcba9876543210']
@@ -2749,6 +2741,26 @@ class DeviceUtilsRestartAdbdTest(DeviceUtilsTest):
class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest):
+ def _PmGrantShellCall(self, package, permissions):
+ fragment = 'p=%s;for q in %s;' % (package, ' '.join(sorted(permissions)))
+ results = []
+ for permission, result in sorted(permissions.iteritems()):
+ if result:
+ output, status = result + '\n', 1
+ else:
+ output, status = '', 0
+ results.append(
+ '{output}{sep}{permission}{sep}{status}{sep}\n'.format(
+ output=output,
+ permission=permission,
+ status=status,
+ sep=device_utils._SHELL_OUTPUT_SEPARATOR
+ ))
+ return (
+ self.call.device.RunShellCommand(
+ AnyStringWith(fragment),
+ shell=True, raw_output=True, large_output=True, check_return=True),
+ ''.join(results))
def testGrantPermissions_none(self):
self.device.GrantPermissions('package', [])
@@ -2759,40 +2771,52 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest):
self.device.GrantPermissions('package', ['p1'])
def testGrantPermissions_one(self):
- permissions_cmd = 'pm grant package p1'
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.MARSHMALLOW):
with self.assertCalls(
- (self.call.device.RunShellCommand(
- permissions_cmd, shell=True, check_return=True), [])):
+ self._PmGrantShellCall('package', {'p1': 0})):
self.device.GrantPermissions('package', ['p1'])
def testGrantPermissions_multiple(self):
- permissions_cmd = 'pm grant package p1&&pm grant package p2'
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.MARSHMALLOW):
with self.assertCalls(
- (self.call.device.RunShellCommand(
- permissions_cmd, shell=True, check_return=True), [])):
+ self._PmGrantShellCall('package', {'p1': 0, 'p2': 0})):
self.device.GrantPermissions('package', ['p1', 'p2'])
def testGrantPermissions_WriteExtrnalStorage(self):
- permissions_cmd = (
- 'pm grant package android.permission.WRITE_EXTERNAL_STORAGE&&'
- 'pm grant package android.permission.READ_EXTERNAL_STORAGE')
- with self.patch_call(self.call.device.build_version_sdk,
- return_value=version_codes.MARSHMALLOW):
- with self.assertCalls(
- (self.call.device.RunShellCommand(
- permissions_cmd, shell=True, check_return=True), [])):
- self.device.GrantPermissions(
- 'package', ['android.permission.WRITE_EXTERNAL_STORAGE'])
+ WRITE = 'android.permission.WRITE_EXTERNAL_STORAGE'
+ READ = 'android.permission.READ_EXTERNAL_STORAGE'
+ with PatchLogger() as logger:
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.MARSHMALLOW):
+ with self.assertCalls(
+ self._PmGrantShellCall('package', {READ: 0, WRITE: 0})):
+ self.device.GrantPermissions('package', [WRITE])
+ self.assertEqual(logger.warnings, [])
def testGrantPermissions_BlackList(self):
- with self.patch_call(self.call.device.build_version_sdk,
- return_value=version_codes.MARSHMALLOW):
- self.device.GrantPermissions(
- 'package', ['android.permission.ACCESS_MOCK_LOCATION'])
+ with PatchLogger() as logger:
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.MARSHMALLOW):
+ with self.assertCalls(
+ self._PmGrantShellCall('package', {'p1': 0})):
+ self.device.GrantPermissions(
+ 'package', ['p1', 'foo.permission.C2D_MESSAGE'])
+ self.assertEqual(logger.warnings, [])
+
+ def testGrantPermissions_unchangeablePermision(self):
+ error_message = (
+ 'Operation not allowed: java.lang.SecurityException: '
+ 'Permission UNCHANGEABLE is not a changeable permission type')
+ with PatchLogger() as logger:
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.MARSHMALLOW):
+ with self.assertCalls(
+ self._PmGrantShellCall('package', {'UNCHANGEABLE': error_message})):
+ self.device.GrantPermissions('package', ['UNCHANGEABLE'])
+ self.assertEqual(
+ logger.warnings, [mock.ANY, AnyStringWith('UNCHANGEABLE')])
class DeviecUtilsIsScreenOn(DeviceUtilsTest):
@@ -2903,6 +2927,46 @@ class DeviecUtilsLoadCacheData(DeviceUtilsTest):
self.assertTrue(self.device.LoadCacheData(json.dumps(data)))
+class DeviceUtilsGetIMEITest(DeviceUtilsTest):
+
+ def testSuccessfulDumpsys(self):
+ dumpsys_output = (
+ 'Phone Subscriber Info:'
+ ' Phone Type = GSM'
+ ' Device ID = 123454321')
+ with self.assertCalls(
+ (self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
+ (self.call.adb.Shell('dumpsys iphonesubinfo'), dumpsys_output)):
+ self.assertEquals(self.device.GetIMEI(), '123454321')
+
+ def testSuccessfulServiceCall(self):
+ service_output = """
+ Result: Parcel(\n'
+ 0x00000000: 00000000 0000000f 00350033 00360033 '........7.6.5.4.'
+ 0x00000010: 00360032 00370030 00300032 00300039 '3.2.1.0.1.2.3.4.'
+ 0x00000020: 00380033 00000039 '5.6.7... ')
+ """
+ with self.assertCalls(
+ (self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
+ (self.call.adb.Shell('service call iphonesubinfo 1'), service_output)):
+ self.assertEquals(self.device.GetIMEI(), '765432101234567')
+
+ def testNoIMEI(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
+ (self.call.adb.Shell('dumpsys iphonesubinfo'), 'no device id')):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.GetIMEI()
+
+ def testAdbError(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
+ (self.call.adb.Shell('service call iphonesubinfo 1'),
+ self.ShellError())):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.GetIMEI()
+
+
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py
index b2ee8b16..0055e233 100644
--- a/catapult/devil/devil/android/flag_changer.py
+++ b/catapult/devil/devil/android/flag_changer.py
@@ -36,24 +36,12 @@ def CustomCommandLineFlags(device, cmdline_name, flags):
cmdline_name: Name of the command line file where to store flags.
flags: A sequence of command line flags to set.
"""
- # On Android N and above, we need to temporarily set SELinux to permissive
- # so that Chrome is allowed to read the command line file.
- # TODO(crbug.com/699082): Remove when a solution to avoid this is implemented.
- needs_permissive = (
- device.build_version_sdk >= version_codes.NOUGAT and
- device.GetEnforce())
- if needs_permissive:
- device.SetEnforce(enabled=False)
+ changer = FlagChanger(device, cmdline_name)
try:
- changer = FlagChanger(device, cmdline_name)
- try:
- changer.ReplaceFlags(flags)
- yield
- finally:
- changer.Restore()
+ changer.ReplaceFlags(flags)
+ yield
finally:
- if needs_permissive:
- device.SetEnforce(enabled=True)
+ changer.Restore()
class FlagChanger(object):
@@ -72,6 +60,7 @@ class FlagChanger(object):
cmdline_file: Name of the command line file where to store flags.
"""
self._device = device
+ self._should_reset_enforce = False
if posixpath.sep in cmdline_file:
raise ValueError(
@@ -81,7 +70,7 @@ class FlagChanger(object):
cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file)
if self._device.PathExists(cmdline_path_legacy):
- logging.warning(
+ logger.warning(
'Removing legacy command line file %r.', cmdline_path_legacy)
self._device.RemovePath(cmdline_path_legacy, as_root=True)
@@ -121,6 +110,7 @@ class FlagChanger(object):
"""
new_flags = set(flags)
self._state_stack.append(new_flags)
+ self._SetPermissive()
return self._UpdateCommandLineFile()
def AddFlags(self, flags):
@@ -177,6 +167,25 @@ class FlagChanger(object):
new_flags.difference_update(remove)
return self.ReplaceFlags(new_flags)
+ def _SetPermissive(self):
+ """Set SELinux to permissive, if needed.
+
+ On Android N and above this is needed in order to allow Chrome to read the
+ command line file.
+
+ TODO(crbug.com/699082): Remove when a better solution exists.
+ """
+ if (self._device.build_version_sdk >= version_codes.NOUGAT and
+ self._device.GetEnforce()):
+ self._device.SetEnforce(enabled=False)
+ self._should_reset_enforce = True
+
+ def _ResetEnforce(self):
+ """Restore SELinux policy if it had been previously made permissive."""
+ if self._should_reset_enforce:
+ self._device.SetEnforce(enabled=True)
+ self._should_reset_enforce = False
+
def Restore(self):
"""Restores the flags to their state prior to the last AddFlags or
RemoveFlags call.
@@ -188,6 +197,8 @@ class FlagChanger(object):
assert len(self._state_stack) > 1, (
"Mismatch between calls to Add/RemoveFlags and Restore")
self._state_stack.pop()
+ if len(self._state_stack) == 1:
+ self._ResetEnforce()
return self._UpdateCommandLineFile()
def _UpdateCommandLineFile(self):
diff --git a/catapult/devil/devil/android/forwarder.py b/catapult/devil/devil/android/forwarder.py
index 76c56ecc..cf1fbe14 100644
--- a/catapult/devil/devil/android/forwarder.py
+++ b/catapult/devil/devil/android/forwarder.py
@@ -5,6 +5,7 @@
# pylint: disable=W0212
import fcntl
+import inspect
import logging
import os
import psutil
@@ -26,7 +27,11 @@ DYNAMIC_DEVICE_PORT = 0
def _GetProcessStartTime(pid):
- return psutil.Process(pid).create_time
+ p = psutil.Process(pid)
+ if inspect.ismethod(p.create_time):
+ return p.create_time()
+ else: # Process.create_time is a property in old versions of psutil.
+ return p.create_time
def _LogMapFailureDiagnostics(device):
@@ -160,7 +165,7 @@ class Forwarder(object):
device_errors.DeviceUnreachableError):
# We don't want the failure to kill the device forwarder to
# supersede the original failure to map.
- logging.warning(
+ logger.warning(
'Failed to kill the device forwarder after map failure: %s',
str(e))
_LogMapFailureDiagnostics(device)
@@ -345,6 +350,9 @@ class Forwarder(object):
"""
# See if the host_forwarder daemon was already initialized by a concurrent
# process or thread (in case multi-process sharding is not used).
+ # TODO(crbug.com/762005): Consider using a different implemention; relying
+ # on matching the string represantion of the process start time seems
+ # fragile.
pid_for_lock = Forwarder._GetPidForLock()
fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
with os.fdopen(fd, 'r+') as pid_file:
@@ -414,9 +422,10 @@ class Forwarder(object):
logger.info('Killing host_forwarder.')
try:
kill_cmd = [self._host_forwarder_path, '--kill-server']
- (exit_code, _o) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
+ (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
kill_cmd, Forwarder._TIMEOUT)
if exit_code != 0:
+ logger.warning('Forwarder unable to shut down:\n%s', output)
kill_cmd = ['pkill', '-9', 'host_forwarder']
(exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
kill_cmd, Forwarder._TIMEOUT)
diff --git a/catapult/devil/devil/android/logcat_monitor.py b/catapult/devil/devil/android/logcat_monitor.py
index 0aece87d..249320b7 100644
--- a/catapult/devil/devil/android/logcat_monitor.py
+++ b/catapult/devil/devil/android/logcat_monitor.py
@@ -23,14 +23,15 @@ logger = logging.getLogger(__name__)
class LogcatMonitor(object):
- _RECORD_ITER_TIMEOUT = 2.0
+ _RECORD_ITER_TIMEOUT = 0.2
_RECORD_THREAD_JOIN_WAIT = 5.0
_WAIT_TIME = 0.2
- _THREADTIME_RE_FORMAT = (
+ THREADTIME_RE_FORMAT = (
r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +'
r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
- def __init__(self, adb, clear=True, filter_specs=None, output_file=None):
+ def __init__(self, adb, clear=True, filter_specs=None, output_file=None,
+ transform_func=None):
"""Create a LogcatMonitor instance.
Args:
@@ -38,6 +39,8 @@ class LogcatMonitor(object):
clear: If True, clear the logcat when monitoring starts.
filter_specs: An optional list of '<tag>[:priority]' strings.
output_file: File path to save recorded logcat.
+ transform_func: An optional unary callable that takes and returns
+ a list of lines, possibly transforming them in the process.
"""
if isinstance(adb, adb_wrapper.AdbWrapper):
self._adb = adb
@@ -50,6 +53,7 @@ class LogcatMonitor(object):
self._record_file_lock = threading.Lock()
self._record_thread = None
self._stop_recording_event = threading.Event()
+ self._transform_func = transform_func
@property
def output_file(self):
@@ -146,7 +150,7 @@ class LogcatMonitor(object):
component = r'[^\s:]+'
# pylint: disable=protected-access
threadtime_re = re.compile(
- type(self)._THREADTIME_RE_FORMAT % (
+ type(self).THREADTIME_RE_FORMAT % (
proc_id, thread_id, log_level, component, message_regex))
with open(self._record_file.name, 'r') as f:
@@ -176,6 +180,8 @@ class LogcatMonitor(object):
with self._record_file_lock:
if self._record_file and not self._record_file.closed:
+ if self._transform_func:
+ data = '\n'.join(self._transform_func([data]))
self._record_file.write(data + '\n')
self._stop_recording_event.clear()
@@ -228,6 +234,13 @@ class LogcatMonitor(object):
self._record_file.close()
self._record_file = None
+ def close(self):
+ """An alias for Close.
+
+ Allows LogcatMonitors to be used with contextlib.closing.
+ """
+ self.Close()
+
def __enter__(self):
"""Starts the logcat monitor."""
self.Start()
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector.py b/catapult/devil/devil/android/perf/surface_stats_collector.py
index 25079f31..eab493df 100644
--- a/catapult/devil/devil/android/perf/surface_stats_collector.py
+++ b/catapult/devil/devil/android/perf/surface_stats_collector.py
@@ -110,12 +110,11 @@ class SurfaceStatsCollector(object):
return not len(results)
def GetSurfaceFlingerPid(self):
- pids_dict = self._device.GetPids('surfaceflinger')
- if not pids_dict:
+ try:
+ # Returns the first matching PID found.
+ return next(p.pid for p in self._device.ListProcesses('surfaceflinger'))
+ except StopIteration:
raise Exception('Unable to get surface flinger process id')
- # TODO(cataput:#3378): Do more strict checks in GetPids when possible.
- # For now it just returns the first pid found of some matching process.
- return pids_dict.popitem()[1][0]
def _GetSurfaceFlingerFrameData(self):
"""Returns collected SurfaceFlinger frame timing data.
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper.py b/catapult/devil/devil/android/sdk/adb_wrapper.py
index e2ca0139..5d24d470 100644
--- a/catapult/devil/devil/android/sdk/adb_wrapper.py
+++ b/catapult/devil/devil/android/sdk/adb_wrapper.py
@@ -41,6 +41,7 @@ _DEVICE_NOT_FOUND_RE = re.compile(r"error: device '(?P<serial>.+)' not found")
_READY_STATE = 'device'
_VERITY_DISABLE_RE = re.compile(r'Verity (already )?disabled')
_VERITY_ENABLE_RE = re.compile(r'Verity (already )?enabled')
+_WAITING_FOR_DEVICE_RE = re.compile(r'- waiting for device -')
def VerifyLocalFileExists(path):
@@ -56,6 +57,12 @@ def VerifyLocalFileExists(path):
raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), path)
+def _CreateAdbEnvironment():
+ adb_env = dict(os.environ)
+ adb_env['ADB_LIBUSB'] = '0'
+ return adb_env
+
+
def _FindAdb():
try:
return devil_env.config.LocalPath('adb')
@@ -113,6 +120,8 @@ def _IsExtraneousLine(line, send_cmd):
class AdbWrapper(object):
"""A wrapper around a local Android Debug Bridge executable."""
+ _ADB_ENV = _CreateAdbEnvironment()
+
_adb_path = lazy.WeakConstant(_FindAdb)
_adb_version = lazy.WeakConstant(_GetVersion)
@@ -159,10 +168,12 @@ class AdbWrapper(object):
"""Start the shell."""
if self._process is not None:
raise RuntimeError('Persistent shell already running.')
+ # pylint: disable=protected-access
self._process = subprocess.Popen(self._cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
- shell=False)
+ shell=False,
+ env=AdbWrapper._ADB_ENV)
def WaitForReady(self):
"""Wait for the shell to be ready after starting.
@@ -193,7 +204,7 @@ class AdbWrapper(object):
send_cmd = '( %s ); echo $?; exit;\n' % cmd.rstrip()
(output, _) = self._process.communicate(send_cmd)
self._process = None
- for x in output.splitlines():
+ for x in output.rstrip().splitlines():
yield x
else:
@@ -247,7 +258,8 @@ class AdbWrapper(object):
try:
status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity),
- timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime())
+ timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime(),
+ env=cls._ADB_ENV)
except OSError as e:
if e.errno in (errno.ENOENT, errno.ENOEXEC):
raise device_errors.NoAdbError(msg=str(e))
@@ -258,8 +270,11 @@ class AdbWrapper(object):
# inconsistent with error reporting so many command failures present
# differently.
if status != 0 or (check_error and output.startswith('error:')):
- m = _DEVICE_NOT_FOUND_RE.match(output)
- if m is not None and m.group('serial') == device_serial:
+ not_found_m = _DEVICE_NOT_FOUND_RE.match(output)
+ device_waiting_m = _WAITING_FOR_DEVICE_RE.match(output)
+ if (device_waiting_m is not None
+ or (not_found_m is not None and
+ not_found_m.group('serial') == device_serial)):
raise device_errors.DeviceUnreachableError(device_serial)
else:
raise device_errors.AdbCommandFailedError(
@@ -299,7 +314,8 @@ class AdbWrapper(object):
return cmd_helper.IterCmdOutputLines(
self._BuildAdbCmd(args, self._device_serial),
iter_timeout=iter_timeout,
- timeout=timeout)
+ timeout=timeout,
+ env=self._ADB_ENV)
def __eq__(self, other):
"""Consider instances equal if they refer to the same device.
@@ -517,7 +533,8 @@ class AdbWrapper(object):
"""
args = ['shell', command]
return cmd_helper.IterCmdOutputLines(
- self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
+ self._BuildAdbCmd(args, self._device_serial), timeout=timeout,
+ env=self._ADB_ENV)
def Ls(self, path, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
"""List the contents of a directory on the device.
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper_test.py b/catapult/devil/devil/android/sdk/adb_wrapper_test.py
index ef086612..07f784d0 100755
--- a/catapult/devil/devil/android/sdk/adb_wrapper_test.py
+++ b/catapult/devil/devil/android/sdk/adb_wrapper_test.py
@@ -19,7 +19,8 @@ with devil_env.SysPath(devil_env.PYMOCK_PATH):
class AdbWrapperTest(unittest.TestCase):
def setUp(self):
- self.adb = adb_wrapper.AdbWrapper('ABC12345678')
+ self.device_serial = 'ABC12345678'
+ self.adb = adb_wrapper.AdbWrapper(self.device_serial)
def _MockRunDeviceAdbCmd(self, return_value):
return mock.patch.object(
@@ -57,3 +58,15 @@ class AdbWrapperTest(unittest.TestCase):
self.assertRaises(
device_errors.AdbCommandFailedError, self.adb.DisableVerity)
+ @mock.patch('devil.utils.cmd_helper.GetCmdStatusAndOutputWithTimeout')
+ def testDeviceUnreachable(self, get_cmd_mock):
+ get_cmd_mock.return_value = (
+ 1, "error: device '%s' not found" % self.device_serial)
+ self.assertRaises(
+ device_errors.DeviceUnreachableError, self.adb.Shell, '/bin/true')
+
+ @mock.patch('devil.utils.cmd_helper.GetCmdStatusAndOutputWithTimeout')
+ def testWaitingForDevice(self, get_cmd_mock):
+ get_cmd_mock.return_value = (1, '- waiting for device - ')
+ self.assertRaises(
+ device_errors.DeviceUnreachableError, self.adb.Shell, '/bin/true')
diff --git a/catapult/devil/devil/android/sdk/fastboot.py b/catapult/devil/devil/android/sdk/fastboot.py
index d7f9f624..ae99d398 100644
--- a/catapult/devil/devil/android/sdk/fastboot.py
+++ b/catapult/devil/devil/android/sdk/fastboot.py
@@ -38,8 +38,12 @@ class Fastboot(object):
self._default_timeout = default_timeout
self._default_retries = default_retries
- def _RunFastbootCommand(self, cmd):
- """Run a command line command using the fastboot android tool.
+ def __str__(self):
+ return self._device_serial
+
+ @classmethod
+ def _RunFastbootCommand(cls, cmd):
+ """Run a generic fastboot command.
Args:
cmd: Command to run. Must be list of args, the first one being the command
@@ -51,16 +55,31 @@ class Fastboot(object):
TypeError: If cmd is not of type list.
"""
if type(cmd) == list:
- cmd = [self._fastboot_path.read(), '-s', self._device_serial] + cmd
+ cmd = [cls._fastboot_path.read()] + cmd
else:
raise TypeError(
- 'Command for _RunFastbootCommand must be a list.')
+ 'Command for _RunDeviceFastbootCommand must be a list.')
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
if int(status) != 0:
- raise device_errors.FastbootCommandFailedError(
- cmd, output, status, self._device_serial)
+ raise device_errors.FastbootCommandFailedError(cmd, output, status)
return output
+ def _RunDeviceFastbootCommand(self, cmd):
+ """Run a fastboot command on the device associated with this object.
+
+ Args:
+ cmd: Command to run. Must be list of args, the first one being the command
+
+ Returns:
+ output of command.
+
+ Raises:
+ TypeError: If cmd is not of type list.
+ """
+ if type(cmd) == list:
+ cmd = ['-s', self._device_serial] + cmd
+ return self._RunFastbootCommand(cmd)
+
@decorators.WithTimeoutAndRetriesDefaults(_FLASH_TIMEOUT, 0)
def Flash(self, partition, image, timeout=None, retries=None):
"""Flash partition with img.
@@ -69,23 +88,28 @@ class Fastboot(object):
partition: Partition to be flashed.
image: location of image to flash with.
"""
- self._RunFastbootCommand(['flash', partition, image])
+ self._RunDeviceFastbootCommand(['flash', partition, image])
- @decorators.WithTimeoutAndRetriesFromInstance()
- def Devices(self, timeout=None, retries=None):
- """Outputs list of devices in fastboot mode."""
- output = self._RunFastbootCommand(['devices'])
- return [line.split()[0] for line in output.splitlines()]
+ @classmethod
+ @decorators.WithTimeoutAndRetriesDefaults(_DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
+ def Devices(cls, timeout=None, retries=None):
+ """Outputs list of devices in fastboot mode.
+
+ Returns:
+ List of Fastboot objects, one for each device in fastboot.
+ """
+ output = cls._RunFastbootCommand(['devices'])
+ return [Fastboot(line.split()[0]) for line in output.splitlines()]
@decorators.WithTimeoutAndRetriesFromInstance()
def RebootBootloader(self, timeout=None, retries=None):
"""Reboot from fastboot, into fastboot."""
- self._RunFastbootCommand(['reboot-bootloader'])
+ self._RunDeviceFastbootCommand(['reboot-bootloader'])
@decorators.WithTimeoutAndRetriesDefaults(_FLASH_TIMEOUT, 0)
def Reboot(self, timeout=None, retries=None):
"""Reboot from fastboot to normal usage"""
- self._RunFastbootCommand(['reboot'])
+ self._RunDeviceFastbootCommand(['reboot'])
@decorators.WithTimeoutAndRetriesFromInstance()
def SetOemOffModeCharge(self, value, timeout=None, retries=None):
@@ -94,5 +118,5 @@ class Fastboot(object):
Args:
value: boolean value to set off-mode-charging on or off.
"""
- self._RunFastbootCommand(
+ self._RunDeviceFastbootCommand(
['oem', 'off-mode-charge', str(int(value))])
diff --git a/catapult/devil/devil/android/sdk/version_codes.py b/catapult/devil/devil/android/sdk/version_codes.py
index 3f03cbac..ec14359a 100644
--- a/catapult/devil/devil/android/sdk/version_codes.py
+++ b/catapult/devil/devil/android/sdk/version_codes.py
@@ -17,4 +17,5 @@ LOLLIPOP_MR1 = 22
MARSHMALLOW = 23
NOUGAT = 24
NOUGAT_MR1 = 25
-
+O = 26
+O_MR1 = 27
diff --git a/catapult/devil/devil/android/settings.py b/catapult/devil/devil/android/settings.py
index d053d2a7..1713be46 100644
--- a/catapult/devil/devil/android/settings.py
+++ b/catapult/devil/devil/android/settings.py
@@ -71,6 +71,8 @@ DETERMINISTIC_DEVICE_SETTINGS = [
('stay_on_while_plugged_in', 3),
('verifier_verify_adb_installs', 0),
+
+ ('window_animation_scale', 0),
]),
('settings/secure', [
('allowed_geolocation_origins',
@@ -100,6 +102,8 @@ DETERMINISTIC_DEVICE_SETTINGS = [
('screen_brightness_mode', 0),
('user_rotation', 0),
+
+ ('window_animation_scale', 0),
]),
]
diff --git a/catapult/devil/devil/android/tools/adb_run_shell_cmd.py b/catapult/devil/devil/android/tools/adb_run_shell_cmd.py
index 77b67e84..6edd5606 100755
--- a/catapult/devil/devil/android/tools/adb_run_shell_cmd.py
+++ b/catapult/devil/devil/android/tools/adb_run_shell_cmd.py
@@ -13,33 +13,27 @@ if __name__ == '__main__':
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil.android import device_blacklist
from devil.android import device_utils
-from devil.utils import run_tests_helper
+from devil.android.tools import script_common
+from devil.utils import logging_common
def main():
parser = argparse.ArgumentParser(
'Run an adb shell command on selected devices')
parser.add_argument('cmd', help='Adb shell command to run.', nargs="+")
- parser.add_argument('-d', '--device', action='append', dest='devices',
- default=[],
- help='Device to run cmd on. Runs on all devices if not '
- 'specified. Set multiple times for multiple devices')
- parser.add_argument('-v', '--verbose', default=0, action='count',
- help='Verbose level (multiple times for more)')
- parser.add_argument('--blacklist-file', help='Device blacklist file.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddDeviceArguments(parser)
+ script_common.AddEnvironmentArguments(parser)
parser.add_argument('--as-root', action='store_true', help='Run as root.')
parser.add_argument('--json-output',
help='File to dump json output to.')
args = parser.parse_args()
- run_tests_helper.SetLogLevel(args.verbose)
- args.blacklist_file = device_blacklist.Blacklist(
- args.blacklist_file) if args.blacklist_file else None
- devices = device_utils.DeviceUtils.HealthyDevices(
- blacklist=args.blacklist_file, device_arg=args.devices)
+ logging_common.InitializeLogging(args)
+ script_common.InitializeEnvironment(args)
+ devices = script_common.GetDevices(args.devices, args.blacklist_file)
p_out = (device_utils.DeviceUtils.parallel(devices).RunShellCommand(
args.cmd, large_output=True, as_root=args.as_root, check_return=True)
.pGet(None))
diff --git a/catapult/devil/devil/android/tools/cpufreq.py b/catapult/devil/devil/android/tools/cpufreq.py
index 97deaf04..6ce0affd 100755
--- a/catapult/devil/devil/android/tools/cpufreq.py
+++ b/catapult/devil/devil/android/tools/cpufreq.py
@@ -15,10 +15,10 @@ if __name__ == '__main__':
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import device_utils
from devil.android.perf import perf_control
-from devil.utils import run_tests_helper
+from devil.android.tools import script_common
+from devil.utils import logging_common
def SetScalingGovernor(device, args):
@@ -40,15 +40,11 @@ def ListAvailableGovernors(device, _args):
def main(raw_args):
parser = argparse.ArgumentParser()
- parser.add_argument(
- '--adb-path',
- help='ADB binary path.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddEnvironmentArguments(parser)
parser.add_argument(
'--device', dest='devices', action='append', default=[],
help='Devices for which the governor should be set. Defaults to all.')
- parser.add_argument(
- '-v', '--verbose', action='count',
- help='Log more.')
subparsers = parser.add_subparsers()
@@ -66,14 +62,8 @@ def main(raw_args):
args = parser.parse_args(raw_args)
- run_tests_helper.SetLogLevel(args.verbose)
-
- devil_dynamic_config = devil_env.EmptyConfig()
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ logging_common.InitializeLogging(args)
+ script_common.InitializeEnvironment(args)
devices = device_utils.DeviceUtils.HealthyDevices(device_arg=args.devices)
diff --git a/catapult/devil/devil/android/tools/device_monitor.py b/catapult/devil/devil/android/tools/device_monitor.py
index d0f7521c..10e0333a 100755
--- a/catapult/devil/devil/android/tools/device_monitor.py
+++ b/catapult/devil/devil/android/tools/device_monitor.py
@@ -25,11 +25,11 @@ if __name__ == '__main__':
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_utils
+from devil.android.tools import script_common
# Various names of sensors used to measure cpu temp
@@ -63,6 +63,7 @@ def get_device_status_unsafe(device):
'build.id': 'ABC12D',
'product.device': 'chickenofthesea'
},
+ 'imei': 123456789,
'mem': {
'avail': 1000000,
'total': 1234567,
@@ -118,10 +119,8 @@ def get_device_status_unsafe(device):
# Process
try:
- # TODO(catapult:#3215): Migrate to device.GetPids()
- lines = device.RunShellCommand(['ps'], check_return=True)
- status['processes'] = len(lines) - 1 # Ignore the header row.
- except device_errors.AdbShellCommandFailedError:
+ status['processes'] = len(device.ListProcesses())
+ except device_errors.AdbCommandFailedError:
logging.exception('Unable to count process list.')
# CPU Temps
@@ -149,6 +148,12 @@ def get_device_status_unsafe(device):
except (device_errors.AdbShellCommandFailedError, ValueError):
logging.exception('Unable to read /proc/uptime')
+ try:
+ status['imei'] = device.GetIMEI()
+ except device_errors.CommandFailedError:
+ logging.exception('Unable to read IMEI')
+ status['imei'] = 'unknown'
+
status['state'] = 'available'
return status
@@ -193,7 +198,7 @@ def main(argv):
parser = argparse.ArgumentParser(
description='Launches the device monitor.')
- parser.add_argument('--adb-path', help='Path to adb binary.')
+ script_common.AddEnvironmentArguments(parser)
parser.add_argument('--blacklist-file', help='Path to device blacklist file.')
args = parser.parse_args(argv)
@@ -205,14 +210,7 @@ def main(argv):
datefmt='%y%m%d %H:%M:%S')
handler.setFormatter(fmt)
logger.addHandler(handler)
-
- devil_dynamic_config = devil_env.EmptyConfig()
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
-
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file else None)
diff --git a/catapult/devil/devil/android/tools/device_monitor_test.py b/catapult/devil/devil/android/tools/device_monitor_test.py
index e39e324b..2cb0dd28 100755
--- a/catapult/devil/devil/android/tools/device_monitor_test.py
+++ b/catapult/devil/devil/android/tools/device_monitor_test.py
@@ -25,7 +25,8 @@ class DeviceMonitorTest(unittest.TestCase):
def setUp(self):
self.device = mock.Mock(spec=device_utils.DeviceUtils,
- serial='device_cereal', build_id='abc123', build_product='clownfish')
+ serial='device_cereal', build_id='abc123', build_product='clownfish',
+ GetIMEI=lambda: '123456789')
self.file_contents = {
'/proc/meminfo': """
MemTotal: 1234567 kB
@@ -39,8 +40,9 @@ class DeviceMonitorTest(unittest.TestCase):
self.device.ReadFile = mock.MagicMock(
side_effect=lambda file_name: self.file_contents[file_name])
+ self.device.ListProcesses.return_value = ['p1', 'p2', 'p3', 'p4', 'p5']
+
self.cmd_outputs = {
- 'ps': ['headers', 'p1', 'p2', 'p3', 'p4', 'p5'],
'grep': ['/sys/class/thermal/thermal_zone0/type'],
}
@@ -76,6 +78,7 @@ class DeviceMonitorTest(unittest.TestCase):
'build.id': 'abc123',
'product.device': 'clownfish',
},
+ 'imei': '123456789',
'state': 'available',
}
}
@@ -111,7 +114,9 @@ class DeviceMonitorTest(unittest.TestCase):
def test_getStatsNoPs(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
- del self.cmd_outputs['ps'] # Throw exception on run shell ps command.
+ # Throw exception when listing processes.
+ self.device.ListProcesses.side_effect = device_errors.AdbCommandFailedError(
+ ['ps'], 'something failed', 1)
# Should be same status dict but without process stats.
expected_status_no_ps = self.expected_status.copy()
diff --git a/catapult/devil/devil/android/tools/device_recovery.py b/catapult/devil/devil/android/tools/device_recovery.py
index 80c78d25..0925aaef 100755
--- a/catapult/devil/devil/android/tools/device_recovery.py
+++ b/catapult/devil/devil/android/tools/device_recovery.py
@@ -16,15 +16,16 @@ if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_utils
+from devil.android.sdk import adb_wrapper
from devil.android.tools import device_status
+from devil.android.tools import script_common
+from devil.utils import logging_common
from devil.utils import lsusb
# TODO(jbudorick): Resolve this after experimenting w/ disabling the USB reset.
from devil.utils import reset_usb # pylint: disable=unused-import
-from devil.utils import run_tests_helper
logger = logging.getLogger(__name__)
@@ -33,23 +34,27 @@ def KillAllAdb():
def get_all_adb():
for p in psutil.process_iter():
try:
- if 'adb' in p.name:
- yield p
+ # Note: p.as_dict is compatible with both older (v1 and under) as well
+ # as newer (v2 and over) versions of psutil.
+ # See: http://grodola.blogspot.com/2014/01/psutil-20-porting.html
+ pinfo = p.as_dict(attrs=['pid', 'name', 'cmdline'])
+ if 'adb' == pinfo['name']:
+ pinfo['cmdline'] = ' '.join(pinfo['cmdline'])
+ yield p, pinfo
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]:
- for p in get_all_adb():
+ for p, pinfo in get_all_adb():
try:
- logger.info('kill %d %d (%s [%s])', sig, p.pid, p.name,
- ' '.join(p.cmdline))
+ pinfo['signal'] = sig
+ logger.info('kill %(signal)s %(pid)s (%(name)s [%(cmdline)s])', pinfo)
p.send_signal(sig)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
- for p in get_all_adb():
+ for _, pinfo in get_all_adb():
try:
- logger.error('Unable to kill %d (%s [%s])', p.pid, p.name,
- ' '.join(p.cmdline))
+ logger.error('Unable to kill %(pid)s (%(name)s [%(cmdline)s])', pinfo)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
@@ -149,6 +154,8 @@ def RecoverDevices(devices, blacklist, enable_usb_reset=False):
if should_restart_adb:
KillAllAdb()
+ adb_wrapper.AdbWrapper.StartServer()
+
for serial in should_restart_usb:
try:
# TODO(crbug.com/642194): Resetting may be causing more harm
@@ -174,26 +181,18 @@ def RecoverDevices(devices, blacklist, enable_usb_reset=False):
def main():
parser = argparse.ArgumentParser()
- parser.add_argument('--adb-path',
- help='Absolute path to the adb binary to use.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddEnvironmentArguments(parser)
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
parser.add_argument('--known-devices-file', action='append', default=[],
dest='known_devices_files',
help='Path to known device lists.')
parser.add_argument('--enable-usb-reset', action='store_true',
help='Reset USB if necessary.')
- parser.add_argument('-v', '--verbose', action='count', default=1,
- help='Log more information.')
args = parser.parse_args()
- run_tests_helper.SetLogLevel(args.verbose)
-
- devil_dynamic_config = devil_env.EmptyConfig()
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ logging_common.InitializeLogging(args)
+ script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file
diff --git a/catapult/devil/devil/android/tools/device_status.py b/catapult/devil/devil/android/tools/device_status.py
index 159c6c5d..dbbf2908 100755
--- a/catapult/devil/devil/android/tools/device_status.py
+++ b/catapult/devil/devil/android/tools/device_status.py
@@ -16,16 +16,16 @@ if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_list
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
+from devil.android.tools import script_common
from devil.constants import exit_codes
+from devil.utils import logging_common
from devil.utils import lsusb
-from devil.utils import run_tests_helper
logger = logging.getLogger(__name__)
@@ -59,21 +59,6 @@ def _BatteryStatus(device, blacklist):
return battery_info
-def _IMEISlice(device):
- imei_slice = ''
- try:
- for l in device.RunShellCommand(['dumpsys', 'iphonesubinfo'],
- check_return=True, timeout=5):
- m = _RE_DEVICE_ID.match(l)
- if m:
- imei_slice = m.group(1)[-6:]
- except (device_errors.CommandFailedError,
- device_errors.DeviceUnreachableError):
- logger.exception('Failed to get IMEI slice for %s', str(device))
-
- return imei_slice
-
-
def DeviceStatus(devices, blacklist):
"""Generates status information for the given devices.
@@ -129,7 +114,11 @@ def DeviceStatus(devices, blacklist):
build_description = device.build_description
wifi_ip = device.GetProp('dhcp.wlan0.ipaddress')
battery_info = _BatteryStatus(device, blacklist)
- imei_slice = _IMEISlice(device)
+ try:
+ imei_slice = device.GetIMEI()
+ except device_errors.CommandFailedError:
+ logging.exception('Unable to fetch IMEI for %s.', str(device))
+ imei_slice = 'unknown'
if (device.product_name == 'mantaray' and
battery_info.get('AC powered', None) != 'true'):
@@ -242,8 +231,6 @@ def GetExpectedDevices(known_devices_files):
def AddArguments(parser):
parser.add_argument('--json-output',
help='Output JSON information into a specified file.')
- parser.add_argument('--adb-path',
- help='Absolute path to the adb binary to use.')
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
parser.add_argument('--known-devices-file', action='append', default=[],
dest='known_devices_files',
@@ -251,8 +238,6 @@ def AddArguments(parser):
parser.add_argument('--buildbot-path', '-b',
default='/home/chrome-bot/.adb_device_info',
help='Absolute path to buildbot file location')
- parser.add_argument('-v', '--verbose', action='count', default=1,
- help='Log more information.')
parser.add_argument('-w', '--overwrite-known-devices-files',
action='store_true',
help='If set, overwrites known devices files wiht new '
@@ -260,18 +245,13 @@ def AddArguments(parser):
def main():
parser = argparse.ArgumentParser()
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddEnvironmentArguments(parser)
AddArguments(parser)
args = parser.parse_args()
- run_tests_helper.SetLogLevel(args.verbose)
-
- devil_dynamic_config = devil_env.EmptyConfig()
-
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ logging_common.InitializeLogging(args)
+ script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file
diff --git a/catapult/devil/devil/android/tools/flash_device.py b/catapult/devil/devil/android/tools/flash_device.py
index d13c1df7..8b51c604 100755
--- a/catapult/devil/devil/android/tools/flash_device.py
+++ b/catapult/devil/devil/android/tools/flash_device.py
@@ -16,7 +16,7 @@ from devil.android import device_utils
from devil.android import fastboot_utils
from devil.android.tools import script_common
from devil.constants import exit_codes
-from devil.utils import run_tests_helper
+from devil.utils import logging_common
logger = logging.getLogger(__name__)
@@ -24,15 +24,12 @@ logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('build_path', help='Path to android build.')
- parser.add_argument('-d', '--device', dest='devices', action='append',
- help='Device(s) to flash.')
- parser.add_argument('-v', '--verbose', default=0, action='count',
- help='Verbose level (multiple times for more)')
parser.add_argument('-w', '--wipe', action='store_true',
help='If set, wipes user data')
- parser.add_argument('--blacklist-file', help='Device blacklist file.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddDeviceArguments(parser)
args = parser.parse_args()
- run_tests_helper.SetLogLevel(args.verbose)
+ logging_common.InitializeLogging(args)
if args.blacklist_file:
blacklist = device_blacklist.Blacklist(args.blacklist_file).Read()
diff --git a/catapult/devil/devil/android/tools/keyboard.py b/catapult/devil/devil/android/tools/keyboard.py
index 31daf59e..c5cb6149 100755
--- a/catapult/devil/devil/android/tools/keyboard.py
+++ b/catapult/devil/devil/android/tools/keyboard.py
@@ -19,7 +19,7 @@ if __name__ == '__main__':
from devil import base_error
from devil.android.sdk import keyevent
from devil.android.tools import script_common
-from devil.utils import run_tests_helper
+from devil.utils import logging_common
_KEY_MAPPING = {
@@ -80,12 +80,6 @@ def Keyboard(device, stream_itr):
pass
-def AddArguments(parser):
- parser.add_argument('-d', '--device', action='append', dest='devices',
- metavar='DEVICE', help='device serial')
- parser.add_argument('-v', '--verbose', action='count', help='print more')
-
-
class MultipleDevicesError(base_error.BaseError):
def __init__(self, devices):
super(MultipleDevicesError, self).__init__(
@@ -95,10 +89,11 @@ class MultipleDevicesError(base_error.BaseError):
def main(raw_args):
parser = argparse.ArgumentParser(
description="Use your keyboard as your phone's keyboard.")
- AddArguments(parser)
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddDeviceArguments(parser)
args = parser.parse_args(raw_args)
- run_tests_helper.SetLogLevel(args.verbose)
+ logging_common.InitializeLogging(args)
devices = script_common.GetDevices(args.devices, None)
if len(devices) > 1:
diff --git a/catapult/devil/devil/android/tools/provision_devices.py b/catapult/devil/devil/android/tools/provision_devices.py
index 9359f113..68aca3b9 100755
--- a/catapult/devil/devil/android/tools/provision_devices.py
+++ b/catapult/devil/devil/android/tools/provision_devices.py
@@ -30,7 +30,6 @@ if __name__ == '__main__':
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
@@ -43,7 +42,7 @@ from devil.android.sdk import keyevent
from devil.android.sdk import version_codes
from devil.android.tools import script_common
from devil.constants import exit_codes
-from devil.utils import run_tests_helper
+from devil.utils import logging_common
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
@@ -542,18 +541,13 @@ def main(raw_args):
parser = argparse.ArgumentParser(
description='Provision Android devices with settings required for bots.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddDeviceArguments(parser)
+ script_common.AddEnvironmentArguments(parser)
parser.add_argument(
'--adb-key-files', type=str, nargs='+',
help='list of adb keys to push to device')
parser.add_argument(
- '--adb-path',
- help='Absolute path to the adb binary to use.')
- parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
- parser.add_argument(
- '-d', '--device', metavar='SERIAL', action='append', dest='devices',
- help='the serial number of the device to be provisioned '
- '(the default is to provision all devices attached)')
- parser.add_argument(
'--disable-location', action='store_true',
help='disable Google location services on devices')
parser.add_argument(
@@ -604,9 +598,6 @@ def main(raw_args):
parser.add_argument(
'--skip-wipe', action='store_true', default=False,
help='do not wipe device data during provisioning')
- parser.add_argument(
- '-v', '--verbose', action='count', default=1,
- help='Log more information.')
# No-op arguments for compatibility with build/android/provision_devices.py.
# TODO(jbudorick): Remove these once all callers have stopped using them.
@@ -625,15 +616,8 @@ def main(raw_args):
args = parser.parse_args(raw_args)
- run_tests_helper.SetLogLevel(args.verbose)
-
- devil_dynamic_config = devil_env.EmptyConfig()
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
-
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ logging_common.InitializeLogging(args)
+ script_common.InitializeEnvironment(args)
try:
return ProvisionDevices(
diff --git a/catapult/devil/devil/android/tools/screenshot.py b/catapult/devil/devil/android/tools/screenshot.py
index a264c4f3..3b3335c2 100755
--- a/catapult/devil/devil/android/tools/screenshot.py
+++ b/catapult/devil/devil/android/tools/screenshot.py
@@ -15,6 +15,7 @@ if __name__ == '__main__':
os.path.dirname(__file__), '..', '..', '..')))
from devil.android import device_utils
from devil.android.tools import script_common
+from devil.utils import logging_common
logger = logging.getLogger(__name__)
@@ -22,23 +23,17 @@ logger = logging.getLogger(__name__)
def main():
# Parse options.
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('-d', '--device', dest='devices', action='append',
- help='Serial number of Android device to use.')
- parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
+ logging_common.AddLoggingArguments(parser)
+ script_common.AddDeviceArguments(parser)
parser.add_argument('-f', '--file', metavar='FILE',
help='Save result to file instead of generating a '
'timestamped file name.')
- parser.add_argument('-v', '--verbose', action='store_true',
- help='Verbose logging.')
parser.add_argument('host_file', nargs='?',
help='File to which the screenshot will be saved.')
args = parser.parse_args()
-
host_file = args.host_file or args.file
-
- if args.verbose:
- logging.getLogger().setLevel(logging.DEBUG)
+ logging_common.InitializeLogging(args)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
diff --git a/catapult/devil/devil/android/tools/script_common.py b/catapult/devil/devil/android/tools/script_common.py
index f91ad5ee..150e63fb 100644
--- a/catapult/devil/devil/android/tools/script_common.py
+++ b/catapult/devil/devil/android/tools/script_common.py
@@ -2,12 +2,41 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import os
+
+from devil import devil_env
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_utils
+def AddEnvironmentArguments(parser):
+ """Adds environment-specific arguments to the provided parser."""
+ parser.add_argument(
+ '--adb-path', type=os.path.realpath,
+ help='Path to the adb binary')
+
+
+def InitializeEnvironment(args):
+ devil_dynamic_config = devil_env.EmptyConfig()
+ if args.adb_path:
+ devil_dynamic_config['dependencies'].update(
+ devil_env.LocalConfigItem(
+ 'adb', devil_env.GetPlatform(), args.adb_path))
+
+ devil_env.config.Initialize(configs=[devil_dynamic_config])
+
+
+def AddDeviceArguments(parser):
+ """Adds device and blacklist arguments to the provided parser."""
+ parser.add_argument(
+ '-d', '--device', dest='devices', action='append',
+ help='Serial number of the Android device to use. (default: use all)')
+ parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
+
+
def GetDevices(requested_devices, blacklist_file):
+ """Gets a list of healthy devices matching the given parameters."""
if not isinstance(blacklist_file, device_blacklist.Blacklist):
blacklist_file = (device_blacklist.Blacklist(blacklist_file)
if blacklist_file
diff --git a/catapult/devil/devil/android/tools/script_common_test.py b/catapult/devil/devil/android/tools/script_common_test.py
index a2267645..3ddb1c16 100755
--- a/catapult/devil/devil/android/tools/script_common_test.py
+++ b/catapult/devil/devil/android/tools/script_common_test.py
@@ -4,7 +4,9 @@
# found in the LICENSE file.
+import argparse
import sys
+import tempfile
import unittest
from devil import devil_env
@@ -15,10 +17,13 @@ from devil.android.tools import script_common
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
+with devil_env.SysPath(devil_env.DEPENDENCY_MANAGER_PATH):
+ from dependency_manager import exceptions
-class ScriptCommonTest(unittest.TestCase):
- def testGetDevices_noSpecs(self):
+class GetDevicesTest(unittest.TestCase):
+
+ def testNoSpecs(self):
devices = [
device_utils.DeviceUtils('123'),
device_utils.DeviceUtils('456'),
@@ -29,7 +34,7 @@ class ScriptCommonTest(unittest.TestCase):
devices,
script_common.GetDevices(None, None))
- def testGetDevices_withDevices(self):
+ def testWithDevices(self):
devices = [
device_utils.DeviceUtils('123'),
device_utils.DeviceUtils('456'),
@@ -40,19 +45,49 @@ class ScriptCommonTest(unittest.TestCase):
[device_utils.DeviceUtils('456')],
script_common.GetDevices(['456'], None))
- def testGetDevices_missingDevice(self):
+ def testMissingDevice(self):
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=[device_utils.DeviceUtils('123')]):
with self.assertRaises(device_errors.DeviceUnreachableError):
script_common.GetDevices(['456'], None)
- def testGetDevices_noDevices(self):
+ def testNoDevices(self):
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=[]):
with self.assertRaises(device_errors.NoDevicesError):
script_common.GetDevices(None, None)
+class InitializeEnvironmentTest(unittest.TestCase):
+
+ def setUp(self):
+ # pylint: disable=protected-access
+ self.parser = argparse.ArgumentParser()
+ script_common.AddEnvironmentArguments(self.parser)
+ devil_env.config = devil_env._Environment()
+
+ def testNoAdb(self):
+ args = self.parser.parse_args([])
+ script_common.InitializeEnvironment(args)
+ with self.assertRaises(exceptions.NoPathFoundError):
+ devil_env.config.LocalPath('adb')
+
+ def testAdb(self):
+ with tempfile.NamedTemporaryFile() as f:
+ args = self.parser.parse_args(['--adb-path=%s' % f.name])
+ script_common.InitializeEnvironment(args)
+ self.assertEquals(
+ f.name,
+ devil_env.config.LocalPath('adb'))
+
+ def testNonExistentAdb(self):
+ with tempfile.NamedTemporaryFile() as f:
+ args = self.parser.parse_args(['--adb-path=%s' % f.name])
+ script_common.InitializeEnvironment(args)
+ with self.assertRaises(exceptions.NoPathFoundError):
+ devil_env.config.LocalPath('adb')
+
+
if __name__ == '__main__':
sys.exit(unittest.main())
diff --git a/catapult/devil/devil/android/tools/system_app.py b/catapult/devil/devil/android/tools/system_app.py
new file mode 100755
index 00000000..00ea312a
--- /dev/null
+++ b/catapult/devil/devil/android/tools/system_app.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+"""A script to replace a system app while running a command."""
+
+import argparse
+import contextlib
+import logging
+import os
+import posixpath
+import sys
+
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..', '..', '..')))
+
+
+from devil.android import apk_helper
+from devil.android import device_errors
+from devil.android import device_temp_file
+from devil.android.sdk import version_codes
+from devil.android.tools import script_common
+from devil.utils import cmd_helper
+from devil.utils import parallelizer
+from devil.utils import run_tests_helper
+
+logger = logging.getLogger(__name__)
+
+
+def RemoveSystemApps(device, package_names):
+ """Removes the given system apps.
+
+ Args:
+ device: (device_utils.DeviceUtils) the device for which the given
+ system app should be removed.
+ package_name: (iterable of strs) the names of the packages to remove.
+ """
+ system_package_paths = _FindSystemPackagePaths(device, package_names)
+ if system_package_paths:
+ with EnableSystemAppModification(device):
+ device.RemovePath(system_package_paths, force=True, recursive=True)
+
+
+@contextlib.contextmanager
+def ReplaceSystemApp(device, package_name, replacement_apk):
+ """A context manager that replaces the given system app while in scope.
+
+ Args:
+ device: (device_utils.DeviceUtils) the device for which the given
+ system app should be replaced.
+ package_name: (str) the name of the package to replace.
+ replacement_apk: (str) the path to the APK to use as a replacement.
+ """
+ storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb)
+ relocate_app = _RelocateApp(device, package_name, storage_dir.name)
+ install_app = _TemporarilyInstallApp(device, replacement_apk)
+ with storage_dir, relocate_app, install_app:
+ yield
+
+
+def _FindSystemPackagePaths(device, system_package_list):
+ """Finds all system paths for the given packages."""
+ found_paths = []
+ for system_package in system_package_list:
+ found_paths.extend(device.GetApplicationPaths(system_package))
+ return [p for p in found_paths if p.startswith('/system/')]
+
+
+_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps'
+
+
+@contextlib.contextmanager
+def EnableSystemAppModification(device):
+ """A context manager that allows system apps to be modified while in scope.
+
+ Args:
+ device: (device_utils.DeviceUtils) the device
+ """
+ if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1':
+ yield
+ return
+
+ device.EnableRoot()
+ if not device.HasRoot():
+ raise device_errors.CommandFailedError(
+ 'Failed to enable modification of system apps on non-rooted device',
+ str(device))
+
+ try:
+ # Disable Marshmallow's Verity security feature
+ if device.build_version_sdk >= version_codes.MARSHMALLOW:
+ logger.info('Disabling Verity on %s', device.serial)
+ device.adb.DisableVerity()
+ device.Reboot()
+ device.WaitUntilFullyBooted()
+ device.EnableRoot()
+
+ device.adb.Remount()
+ device.RunShellCommand(['stop'], check_return=True)
+ device.SetProp(_ENABLE_MODIFICATION_PROP, '1')
+ yield
+ finally:
+ device.SetProp(_ENABLE_MODIFICATION_PROP, '0')
+ device.Reboot()
+ device.WaitUntilFullyBooted()
+
+
+@contextlib.contextmanager
+def _RelocateApp(device, package_name, relocate_to):
+ """A context manager that relocates an app while in scope."""
+ relocation_map = {}
+ system_package_paths = _FindSystemPackagePaths(device, [package_name])
+ if system_package_paths:
+ relocation_map = {
+ p: posixpath.join(relocate_to, posixpath.relpath(p, '/'))
+ for p in system_package_paths
+ }
+ relocation_dirs = [
+ posixpath.dirname(d)
+ for _, d in relocation_map.iteritems()
+ ]
+ device.RunShellCommand(['mkdir', '-p'] + relocation_dirs,
+ check_return=True)
+ _MoveApp(device, relocation_map)
+ else:
+ logger.info('No system package "%s"', package_name)
+
+ try:
+ yield
+ finally:
+ _MoveApp(device, {v: k for k, v in relocation_map.iteritems()})
+
+
+@contextlib.contextmanager
+def _TemporarilyInstallApp(device, apk):
+ """A context manager that installs an app while in scope."""
+ device.adb.Install(apk, reinstall=True)
+ try:
+ yield
+ finally:
+ device.adb.Uninstall(apk_helper.GetPackageName(apk))
+
+
+def _MoveApp(device, relocation_map):
+ """Moves an app according to the provided relocation map.
+
+ Args:
+ device: (device_utils.DeviceUtils)
+ relocation_map: (dict) A dict that maps src to dest
+ """
+ movements = [
+ 'mv %s %s' % (k, v)
+ for k, v in relocation_map.iteritems()
+ ]
+ cmd = ' && '.join(movements)
+ with EnableSystemAppModification(device):
+ device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True)
+
+
+def main(raw_args):
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers()
+
+ def add_common_arguments(p):
+ script_common.AddDeviceArguments(p)
+ script_common.AddEnvironmentArguments(p)
+ p.add_argument(
+ '-v', '--verbose', action='count', default=0,
+ help='Print more information.')
+ p.add_argument('command', nargs='*')
+
+ @contextlib.contextmanager
+ def remove_system_app(device, args):
+ RemoveSystemApps(device, args.packages)
+ yield
+
+ remove_parser = subparsers.add_parser('remove')
+ remove_parser.add_argument(
+ '--package', dest='packages', nargs='*', required=True,
+ help='The system package(s) to remove.')
+ add_common_arguments(remove_parser)
+ remove_parser.set_defaults(func=remove_system_app)
+
+ @contextlib.contextmanager
+ def replace_system_app(device, args):
+ with ReplaceSystemApp(device, args.package, args.replace_with):
+ yield
+
+ replace_parser = subparsers.add_parser('replace')
+ replace_parser.add_argument(
+ '--package', required=True,
+ help='The system package to replace.')
+ replace_parser.add_argument(
+ '--replace-with', metavar='APK', required=True,
+ help='The APK with which the existing system app should be replaced.')
+ add_common_arguments(replace_parser)
+ replace_parser.set_defaults(func=replace_system_app)
+
+ args = parser.parse_args(raw_args)
+
+ run_tests_helper.SetLogLevel(args.verbose)
+ script_common.InitializeEnvironment(args)
+
+ devices = script_common.GetDevices(args.devices, args.blacklist_file)
+ parallel_devices = parallelizer.SyncParallelizer(
+ [args.func(d, args) for d in devices])
+ with parallel_devices:
+ if args.command:
+ return cmd_helper.Call(args.command)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/catapult/devil/devil/android/tools/system_app_devicetest.py b/catapult/devil/devil/android/tools/system_app_devicetest.py
new file mode 100755
index 00000000..0e8afdca
--- /dev/null
+++ b/catapult/devil/devil/android/tools/system_app_devicetest.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+import logging
+import os
+import posixpath
+import shutil
+import sys
+import tempfile
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(
+ os.path.dirname(__file__), '..', '..', '..')))
+
+from devil import base_error
+from devil import devil_env
+from devil.android import device_temp_file
+from devil.android import device_test_case
+from devil.android import device_utils
+from devil.android.tools import system_app
+
+logger = logging.getLogger(__name__)
+
+
+class SystemAppDeviceTest(device_test_case.DeviceTestCase):
+
+ PACKAGE = 'com.google.android.webview'
+
+ def setUp(self):
+ super(SystemAppDeviceTest, self).setUp()
+ self._device = device_utils.DeviceUtils(self.serial)
+ self._original_paths = self._device.GetApplicationPaths(self.PACKAGE)
+ self._apk_cache_dir = tempfile.mkdtemp()
+ # Host location -> device location
+ self._cached_apks = {}
+ for o in self._original_paths:
+ h = os.path.join(self._apk_cache_dir, posixpath.basename(o))
+ self._device.PullFile(o, h)
+ self._cached_apks[h] = o
+
+ def tearDown(self):
+ final_paths = self._device.GetApplicationPaths(self.PACKAGE)
+ if self._original_paths != final_paths:
+ try:
+ self._device.Uninstall(self.PACKAGE)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ with system_app.EnableSystemAppModification(self._device):
+ for cached_apk, install_path in self._cached_apks.iteritems():
+ try:
+ with device_temp_file.DeviceTempFile(self._device.adb) as tmp:
+ self._device.adb.Push(cached_apk, tmp.name)
+ self._device.RunShellCommand(
+ ['mv', tmp.name, install_path],
+ as_root=True, check_return=True)
+ except base_error.BaseError:
+ logger.warning('Failed to reinstall %s',
+ os.path.basename(cached_apk))
+
+ try:
+ shutil.rmtree(self._apk_cache_dir)
+ except IOError:
+ logger.error('Unable to remove app cache directory.')
+
+ super(SystemAppDeviceTest, self).tearDown()
+
+ def _check_preconditions(self):
+ if not self._original_paths:
+ self.skipTest('%s is not installed on %s' % (
+ self.PACKAGE, str(self._device)))
+ if not any(p.startswith('/system/') for p in self._original_paths):
+ self.skipTest('%s is not installed in a system location on %s' % (
+ self.PACKAGE, str(self._device)))
+
+ def testReplace(self):
+ self._check_preconditions()
+ replacement = devil_env.config.FetchPath(
+ 'empty_system_webview', device=self._device)
+ with system_app.ReplaceSystemApp(self._device, self.PACKAGE, replacement):
+ replaced_paths = self._device.GetApplicationPaths(self.PACKAGE)
+ self.assertNotEqual(self._original_paths, replaced_paths)
+ restored_paths = self._device.GetApplicationPaths(self.PACKAGE)
+ self.assertEqual(self._original_paths, restored_paths)
+
+ def testRemove(self):
+ self._check_preconditions()
+ system_app.RemoveSystemApps(self._device, [self.PACKAGE])
+ removed_paths = self._device.GetApplicationPaths(self.PACKAGE)
+ self.assertEqual([], removed_paths)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/catapult/devil/devil/android/tools/system_app_test.py b/catapult/devil/devil/android/tools/system_app_test.py
new file mode 100644
index 00000000..f72aa166
--- /dev/null
+++ b/catapult/devil/devil/android/tools/system_app_test.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', '..', '..')))
+
+from devil import devil_env
+from devil.android import device_utils
+from devil.android.sdk import adb_wrapper
+from devil.android.sdk import version_codes
+from devil.android.tools import system_app
+
+with devil_env.SysPath(devil_env.PYMOCK_PATH):
+ import mock
+
+
+class SystemAppTest(unittest.TestCase):
+
+ def testDoubleEnableModification(self):
+ """Ensures that system app modification logic isn't repeated.
+
+ If EnableSystemAppModification uses are nested, inner calls should
+ not need to perform any of the expensive modification logic.
+ """
+ # pylint: disable=no-self-use,protected-access
+ mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
+ type(mock_device).build_version_sdk = mock.PropertyMock(
+ return_value=version_codes.LOLLIPOP)
+
+ system_props = {}
+
+ def dict_setprop(prop_name, value):
+ system_props[prop_name] = value
+
+ def dict_getprop(prop_name):
+ return system_props.get(prop_name, '')
+
+ mock_device.SetProp.side_effect = dict_setprop
+ mock_device.GetProp.side_effect = dict_getprop
+
+ with system_app.EnableSystemAppModification(mock_device):
+ mock_device.EnableRoot.assert_called_once()
+ mock_device.GetProp.assert_called_once_with(
+ system_app._ENABLE_MODIFICATION_PROP)
+ mock_device.SetProp.assert_called_once_with(
+ system_app._ENABLE_MODIFICATION_PROP, '1')
+ mock_device.reset_mock()
+
+ with system_app.EnableSystemAppModification(mock_device):
+ mock_device.EnableRoot.assert_not_called()
+ mock_device.GetProp.assert_called_once_with(
+ system_app._ENABLE_MODIFICATION_PROP)
+ mock_device.SetProp.assert_not_called()
+ mock_device.reset_mock()
+
+ mock_device.SetProp.assert_called_once_with(
+ system_app._ENABLE_MODIFICATION_PROP, '0')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/catapult/devil/devil/android/tools/unlock_bootloader.py b/catapult/devil/devil/android/tools/unlock_bootloader.py
new file mode 100644
index 00000000..46fec9df
--- /dev/null
+++ b/catapult/devil/devil/android/tools/unlock_bootloader.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+"""A script to open the unlock bootloader on-screen prompt on all devices."""
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+import time
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..', '..', '..')))
+
+from devil import devil_env
+from devil.android import device_errors
+from devil.android.sdk import adb_wrapper
+from devil.android.sdk import fastboot
+from devil.android.tools import script_common
+from devil.utils import parallelizer
+
+
+def reboot_into_bootloader(filter_devices):
+ # Reboot all devices into bootloader if they aren't there already.
+ rebooted_devices = set()
+ for d in adb_wrapper.AdbWrapper.Devices(desired_state=None):
+ if filter_devices and str(d) not in filter_devices:
+ continue
+ state = d.GetState()
+ if state == 'device':
+ logging.info('Booting %s to bootloader.', d)
+ try:
+ d.Reboot(to_bootloader=True)
+ rebooted_devices.add(str(d))
+ except (device_errors.AdbCommandFailedError,
+ device_errors.DeviceUnreachableError):
+ logging.exception('Unable to reboot device %s', d)
+ else:
+ logging.error('Unable to reboot device %s: %s', d, state)
+
+ # Wait for the rebooted devices to show up in fastboot.
+ if rebooted_devices:
+ logging.info('Waiting for devices to reboot...')
+ timeout = 60
+ start = time.time()
+ while True:
+ time.sleep(5)
+ fastbooted_devices = set([str(d) for d in fastboot.Fastboot.Devices()])
+ if rebooted_devices <= set(fastbooted_devices):
+ logging.info('All devices in fastboot.')
+ break
+ if time.time() - start > timeout:
+ logging.error('Timed out waiting for %s to reboot.',
+ rebooted_devices - set(fastbooted_devices))
+ break
+
+
+def unlock_bootloader(d):
+ # Unlock the phones.
+ unlocking_processes = []
+ logging.info('Unlocking %s...', d)
+ # The command to unlock the bootloader could be either of the following
+ # depending on the android version and/or oem. Can't really tell which is
+ # needed, so just try both.
+ # pylint: disable=protected-access
+ cmd_old = [d._fastboot_path.read(), '-s', str(d), 'oem', 'unlock']
+ cmd_new = [d._fastboot_path.read(), '-s', str(d), 'flashing', 'unlock']
+ unlocking_processes.append(
+ subprocess.Popen(
+ cmd_old, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
+ unlocking_processes.append(
+ subprocess.Popen(
+ cmd_new, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
+
+ # Give the unlocking command time to finish and/or open the on-screen prompt.
+ logging.info('Sleeping for 5 seconds...')
+ time.sleep(5)
+
+ leftover_pids = []
+ for p in unlocking_processes:
+ p.poll()
+ rc = p.returncode
+ # If the command succesfully opened the unlock prompt on the screen, the
+ # fastboot command process will hang and wait for a response. We still
+ # need to read its stdout/stderr, so use os.read so that we don't
+ # have to wait for EOF to be written.
+ out = os.read(p.stderr.fileno(), 1024).strip().lower()
+ if not rc:
+ if out == '...' or out == '< waiting for device >':
+ logging.info('Device %s is waiting for confirmation.', d)
+ else:
+ logging.error(
+ 'Device %s is hanging, but not waiting for confirmation: %s',
+ d, out)
+ leftover_pids.append(p.pid)
+ else:
+ if 'unknown command' in out:
+ # Of the two unlocking commands, this was likely the wrong one.
+ continue
+ elif 'already unlocked' in out:
+ logging.info('Device %s already unlocked.', d)
+ elif 'unlock is not allowed' in out:
+ logging.error("Device %s is oem locked. Can't unlock bootloader.", d)
+ else:
+ logging.error('Device %s in unknown state: "%s"', d, out)
+ break
+
+ if leftover_pids:
+ logging.warning('Processes %s left over after unlocking.', leftover_pids)
+
+ return 0
+
+
+def main():
+ logging.getLogger().setLevel(logging.INFO)
+
+ parser = argparse.ArgumentParser()
+ script_common.AddDeviceArguments(parser)
+ parser.add_argument('--adb-path',
+ help='Absolute path to the adb binary to use.')
+ args = parser.parse_args()
+
+ devil_dynamic_config = devil_env.EmptyConfig()
+ if args.adb_path:
+ devil_dynamic_config['dependencies'].update(
+ devil_env.LocalConfigItem(
+ 'adb', devil_env.GetPlatform(), args.adb_path))
+ devil_env.config.Initialize(configs=[devil_dynamic_config])
+
+ reboot_into_bootloader(args.devices)
+ devices = [
+ d for d in fastboot.Fastboot.Devices() if not args.devices or
+ str(d) in args.devices]
+ parallel_devices = parallelizer.Parallelizer(devices)
+ parallel_devices.pMap(unlock_bootloader).pGet(None)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/catapult/devil/devil/android/tools/video_recorder.py b/catapult/devil/devil/android/tools/video_recorder.py
index a91e6496..08432640 100755
--- a/catapult/devil/devil/android/tools/video_recorder.py
+++ b/catapult/devil/devil/android/tools/video_recorder.py
@@ -124,7 +124,7 @@ def main():
help='Verbose logging.')
parser.add_argument('-b', '--bitrate', default=4, type=float,
help='Bitrate in megabits/s, from 0.1 to 100 mbps, '
- '%default mbps by default.')
+ '%(default)d mbps by default.')
parser.add_argument('-r', '--rotate', action='store_true',
help='Rotate video by 90 degrees.')
parser.add_argument('-s', '--size', metavar='WIDTHxHEIGHT',
diff --git a/catapult/devil/devil/android/tools/wait_for_devices.py b/catapult/devil/devil/android/tools/wait_for_devices.py
index 4bde2cd4..bc733355 100755
--- a/catapult/devil/devil/android/tools/wait_for_devices.py
+++ b/catapult/devil/devil/android/tools/wait_for_devices.py
@@ -14,8 +14,8 @@ if __name__ == '__main__':
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
-from devil import devil_env
from devil.android import device_utils
+from devil.android.tools import script_common
from devil.utils import run_tests_helper
@@ -31,13 +31,7 @@ def main(raw_args):
args = parser.parse_args(raw_args)
run_tests_helper.SetLogLevel(args.verbose)
-
- devil_dynamic_config = devil_env.EmptyConfig()
- if args.adb_path:
- devil_dynamic_config['dependencies'].update(
- devil_env.LocalConfigItem(
- 'adb', devil_env.GetPlatform(), args.adb_path))
- devil_env.config.Initialize(configs=[devil_dynamic_config])
+ script_common.InitializeEnvironment(args)
devices = device_utils.DeviceUtils.HealthyDevices(
device_arg=args.device_serials)
diff --git a/catapult/devil/devil/devil_dependencies.json b/catapult/devil/devil/devil_dependencies.json
index bed6fe10..6884a36b 100644
--- a/catapult/devil/devil/devil_dependencies.json
+++ b/catapult/devil/devil/devil_dependencies.json
@@ -51,6 +51,16 @@
}
}
},
+ "empty_system_webview": {
+ "cloud_storage_base_folder": "binary_dependencies",
+ "cloud_storage_bucket": "chromium-telemetry",
+ "file_info": {
+ "android_armeabi-v7a": {
+ "cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
+ "download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
+ }
+ }
+ },
"fastboot": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
diff --git a/catapult/devil/devil/utils/cmd_helper.py b/catapult/devil/devil/utils/cmd_helper.py
index 0b6ccd9b..b477c700 100644
--- a/catapult/devil/devil/utils/cmd_helper.py
+++ b/catapult/devil/devil/utils/cmd_helper.py
@@ -125,7 +125,7 @@ def RunCmd(args, cwd=None):
return Call(args, cwd=cwd)
-def GetCmdOutput(args, cwd=None, shell=False):
+def GetCmdOutput(args, cwd=None, shell=False, env=None):
"""Open a subprocess to execute a program and returns its output.
Args:
@@ -134,12 +134,14 @@ def GetCmdOutput(args, cwd=None, shell=False):
cwd: If not None, the subprocess's current directory will be changed to
|cwd| before it's executed.
shell: Whether to execute args as a shell command.
+ env: If not None, a mapping that defines environment variables for the
+ subprocess.
Returns:
Captures and returns the command's stdout.
Prints the command's stderr to logger (which defaults to stdout).
"""
- (_, output) = GetCmdStatusAndOutput(args, cwd, shell)
+ (_, output) = GetCmdStatusAndOutput(args, cwd, shell, env)
return output
@@ -159,7 +161,7 @@ def _ValidateAndLogCommand(args, cwd, shell):
return args
-def GetCmdStatusAndOutput(args, cwd=None, shell=False):
+def GetCmdStatusAndOutput(args, cwd=None, shell=False, env=None):
"""Executes a subprocess and returns its exit code and output.
Args:
@@ -169,12 +171,14 @@ def GetCmdStatusAndOutput(args, cwd=None, shell=False):
|cwd| before it's executed.
shell: Whether to execute args as a shell command. Must be True if args
is a string and False if args is a sequence.
+ env: If not None, a mapping that defines environment variables for the
+ subprocess.
Returns:
- The 2-tuple (exit code, output).
+ The 2-tuple (exit code, stdout).
"""
status, stdout, stderr = GetCmdStatusOutputAndError(
- args, cwd=cwd, shell=shell)
+ args, cwd=cwd, shell=shell, env=env)
if stderr:
logger.critical('STDERR: %s', stderr)
@@ -183,7 +187,7 @@ def GetCmdStatusAndOutput(args, cwd=None, shell=False):
return (status, stdout)
-def GetCmdStatusOutputAndError(args, cwd=None, shell=False):
+def GetCmdStatusOutputAndError(args, cwd=None, shell=False, env=None):
"""Executes a subprocess and returns its exit code, output, and errors.
Args:
@@ -193,13 +197,15 @@ def GetCmdStatusOutputAndError(args, cwd=None, shell=False):
|cwd| before it's executed.
shell: Whether to execute args as a shell command. Must be True if args
is a string and False if args is a sequence.
+ env: If not None, a mapping that defines environment variables for the
+ subprocess.
Returns:
- The 2-tuple (exit code, output).
+ The 3-tuple (exit code, stdout, stderr).
"""
_ValidateAndLogCommand(args, cwd, shell)
pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- shell=shell, cwd=cwd)
+ shell=shell, cwd=cwd, env=env)
stdout, stderr = pipe.communicate()
return (pipe.returncode, stdout, stderr)
@@ -251,7 +257,19 @@ def _IterProcessStdoutFcntl(
if not data:
break
yield data
+
if process.poll() is not None:
+ # If process is closed, keep checking for output data (because of timing
+ # issues).
+ while True:
+ read_fds, _, _ = select.select(
+ [child_fd], [], [], iter_aware_poll_interval)
+ if child_fd in read_fds:
+ data = os.read(child_fd, buffer_size)
+ if data:
+ yield data
+ continue
+ break
break
finally:
try:
@@ -345,7 +363,7 @@ Yields:
def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
- logfile=None):
+ logfile=None, env=None):
"""Executes a subprocess with a timeout.
Args:
@@ -358,6 +376,8 @@ def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
is a string and False if args is a sequence.
logfile: Optional file-like object that will receive output from the
command as it is running.
+ env: If not None, a mapping that defines environment variables for the
+ subprocess.
Returns:
The 2-tuple (exit code, output).
@@ -367,7 +387,7 @@ def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
_ValidateAndLogCommand(args, cwd, shell)
output = StringIO.StringIO()
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ stderr=subprocess.STDOUT, env=env)
try:
for data in _IterProcessStdout(process, timeout=timeout):
if logfile:
@@ -383,7 +403,7 @@ def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None,
- shell=False, check_status=True):
+ shell=False, env=None, check_status=True):
"""Executes a subprocess and continuously yields lines from its output.
Args:
@@ -395,6 +415,8 @@ def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None,
|cwd| before it's executed.
shell: Whether to execute args as a shell command. Must be True if args
is a string and False if args is a sequence.
+ env: If not None, a mapping that defines environment variables for the
+ subprocess.
check_status: A boolean indicating whether to check the exit status of the
process after all output has been read.
Yields:
@@ -405,8 +427,8 @@ def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None,
non-zero exit status.
"""
cmd = _ValidateAndLogCommand(args, cwd, shell)
- process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = Popen(args, cwd=cwd, shell=shell, env=env,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return _IterCmdOutputLines(
process, cmd, iter_timeout=iter_timeout, timeout=timeout,
check_status=check_status)
diff --git a/catapult/devil/devil/utils/cmd_helper_test.py b/catapult/devil/devil/utils/cmd_helper_test.py
index b7fc8ee3..6a8e8813 100755
--- a/catapult/devil/devil/utils/cmd_helper_test.py
+++ b/catapult/devil/devil/utils/cmd_helper_test.py
@@ -158,8 +158,11 @@ class _MockProcess(object):
self._output_seq_index = 0
else:
self._output_seq_index += 1
- return (self._output_sequence[self._output_seq_index].select_fds,
- None, None)
+ if self._output_seq_index < len(self._output_sequence):
+ return (self._output_sequence[self._output_seq_index].select_fds,
+ None, None)
+ else:
+ return([], None, None)
def time_side_effect(*_args, **_kwargs):
return self._output_sequence[self._output_seq_index].ts
diff --git a/catapult/devil/devil/utils/find_usb_devices.py b/catapult/devil/devil/utils/find_usb_devices.py
index 28a2eb86..74b888dd 100755
--- a/catapult/devil/devil/utils/find_usb_devices.py
+++ b/catapult/devil/devil/utils/find_usb_devices.py
@@ -4,6 +4,7 @@
# found in the LICENSE file.
import argparse
+import logging
import os
import re
import sys
@@ -17,6 +18,8 @@ from devil.utils import cmd_helper
from devil.utils import usb_hubs
from devil.utils import lsusb
+logger = logging.getLogger(__name__)
+
# Note: In the documentation below, "virtual port" refers to the port number
# as observed by the system (e.g. by usb-devices) and "physical port" refers
# to the physical numerical label on the physical port e.g. on a USB hub.
@@ -189,9 +192,9 @@ class USBDeviceNode(USBNode):
#override
def Display(self, port_chain='', info=False):
- print '%s Device %d (%s)' % (port_chain, self.device_num, self.desc)
+ logger.info('%s Device %d (%s)', port_chain, self.device_num, self.desc)
if info:
- print self.info
+ logger.info('%s', self.info)
for (port, device) in self._port_to_node.iteritems():
device.Display('%s%d:' % (port_chain, port), info=info)
@@ -235,7 +238,7 @@ class USBBusNode(USBNode):
#override
def Display(self, port_chain='', info=False):
- print "=== %s ===" % self.desc
+ logger.info('=== %s ===', self.desc)
for (port, device) in self._port_to_node.iteritems():
device.Display('%s%d:' % (port_chain, port), info=info)
@@ -483,29 +486,34 @@ def GetBusDeviceToTTYMap():
def TestUSBTopologyScript():
"""Test display and hub identification."""
+ # The following makes logger.info behave pretty much like print
+ # during this test script.
+ logging.basicConfig(format='%(message)s', stream=sys.stdout)
+ logger.setLevel(logging.INFO)
+
# Identification criteria for Plugable 7-Port Hub
- print '==== USB TOPOLOGY SCRIPT TEST ===='
+ logger.info('==== USB TOPOLOGY SCRIPT TEST ====')
+ logger.info('')
# Display devices
- print '==== DEVICE DISPLAY ===='
+ logger.info('==== DEVICE DISPLAY ====')
device_trees = GetBusNumberToDeviceTreeMap()
for device_tree in device_trees.values():
device_tree.Display()
- print
+ logger.info('')
# Display TTY information about devices plugged into hubs.
- print '==== TTY INFORMATION ===='
+ logger.info('==== TTY INFORMATION ====')
for port_map in GetAllPhysicalPortToTTYMaps(
usb_hubs.ALL_HUBS, device_tree_map=device_trees):
- print port_map
- print
+ logger.info('%s', port_map)
+ logger.info('')
# Display serial number information about devices plugged into hubs.
- print '==== SERIAL NUMBER INFORMATION ===='
+ logger.info('==== SERIAL NUMBER INFORMATION ====')
for port_map in GetAllPhysicalPortToSerialMaps(
usb_hubs.ALL_HUBS, device_tree_map=device_trees):
- print port_map
-
+ logger.info('%s', port_map)
return 0
diff --git a/catapult/devil/devil/utils/host_utils.py b/catapult/devil/devil/utils/host_utils.py
index 580721f1..6c337cf7 100644
--- a/catapult/devil/devil/utils/host_utils.py
+++ b/catapult/devil/devil/utils/host_utils.py
@@ -2,15 +2,22 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import logging
import os
def GetRecursiveDiskUsage(path):
"""Returns the disk usage in bytes of |path|. Similar to `du -sb |path|`."""
- running_size = os.path.getsize(path)
+ def get_size(filepath):
+ try:
+ return os.path.getsize(filepath)
+ except OSError:
+ logging.warning('File or directory no longer found: %s', filepath)
+ return 0
+
+ running_size = get_size(path)
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
- running_size += sum([os.path.getsize(os.path.join(root, f))
+ running_size += sum([get_size(os.path.join(root, f))
for f in files + dirs])
return running_size
-
diff --git a/catapult/devil/devil/utils/logging_common.py b/catapult/devil/devil/utils/logging_common.py
new file mode 100644
index 00000000..5aea3c64
--- /dev/null
+++ b/catapult/devil/devil/utils/logging_common.py
@@ -0,0 +1,50 @@
+# Copyright 2017 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.
+
+import logging
+import sys
+import time
+
+
+def AddLoggingArguments(parser):
+ parser.add_argument(
+ '-v', '--verbose', action='count', default=0,
+ help='Log more. Use multiple times for even more logging.')
+
+
+def InitializeLogging(args, handler=None):
+ if args.verbose == 0:
+ log_level = logging.WARNING
+ elif args.verbose == 1:
+ log_level = logging.INFO
+ else:
+ log_level = logging.DEBUG
+ logger = logging.getLogger()
+ logger.setLevel(log_level)
+ if not handler:
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setFormatter(CustomFormatter())
+ logger.addHandler(handler)
+
+
+class CustomFormatter(logging.Formatter):
+ """Custom log formatter."""
+
+ # override
+ def __init__(self, fmt='%(threadName)-4s %(message)s'):
+ # Can't use super() because in older Python versions logging.Formatter does
+ # not inherit from object.
+ logging.Formatter.__init__(self, fmt=fmt)
+ self._creation_time = time.time()
+
+ # override
+ def format(self, record):
+ # Can't use super() because in older Python versions logging.Formatter does
+ # not inherit from object.
+ msg = logging.Formatter.format(self, record)
+ if 'MainThread' in msg[:19]:
+ msg = msg.replace('MainThread', 'Main', 1)
+ timediff = time.time() - self._creation_time
+ return '%s %8.3fs %s' % (record.levelname[0], timediff, msg)
+
diff --git a/catapult/devil/devil/utils/parallelizer.py b/catapult/devil/devil/utils/parallelizer.py
index 35995251..678066c7 100644
--- a/catapult/devil/devil/utils/parallelizer.py
+++ b/catapult/devil/devil/utils/parallelizer.py
@@ -118,7 +118,7 @@ class Parallelizer(object):
o, args=args, kwargs=kwargs,
name='%s.%s' % (str(d), o.__name__))
for d, o in zip(self._orig_objs, self._objs)])
- r._objs.StartAll() # pylint: disable=W0212
+ r._objs.StartAll()
return r
def pFinish(self, timeout):
@@ -174,7 +174,7 @@ class Parallelizer(object):
f, args=tuple([o] + list(args)), kwargs=kwargs,
name='%s(%s)' % (f.__name__, d))
for d, o in zip(self._orig_objs, self._objs)])
- r._objs.StartAll() # pylint: disable=W0212
+ r._objs.StartAll()
return r
def _assertNoShadow(self, attr_name):
@@ -200,6 +200,33 @@ class Parallelizer(object):
class SyncParallelizer(Parallelizer):
"""A Parallelizer that blocks on function calls."""
+ def __enter__(self):
+ """Emulate entering the context of |self|.
+
+ Note that this call is synchronous.
+
+ Returns:
+ A Parallelizer emulating the value returned from entering into the
+ context of |self|.
+ """
+ r = type(self)(self._orig_objs)
+ r._objs = [o.__enter__ for o in r._objs]
+ return r.__call__()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Emulate exiting the context of |self|.
+
+ Note that this call is synchronous.
+
+ Args:
+ exc_type: the exception type.
+ exc_val: the exception value.
+ exc_tb: the exception traceback.
+ """
+ r = type(self)(self._orig_objs)
+ r._objs = [o.__exit__ for o in r._objs]
+ r.__call__(exc_type, exc_val, exc_tb)
+
# override
def __call__(self, *args, **kwargs):
"""Emulate calling |self| with |args| and |kwargs|.
diff --git a/catapult/devil/devil/utils/parallelizer_test.py b/catapult/devil/devil/utils/parallelizer_test.py
index 32ff7ec5..acbb986e 100644
--- a/catapult/devil/devil/utils/parallelizer_test.py
+++ b/catapult/devil/devil/utils/parallelizer_test.py
@@ -1,17 +1,24 @@
+#! /usr/bin/env python
# Copyright 2014 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.
"""Unit tests for the contents of parallelizer.py."""
-# pylint: disable=W0212
-# pylint: disable=W0613
+# pylint: disable=protected-access
+# pylint: disable=unused-argument
+import contextlib
import os
import tempfile
import time
+import sys
import unittest
+if __name__ == '__main__':
+ sys.path.append(os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', '..')))
+
from devil.utils import parallelizer
@@ -157,6 +164,27 @@ class ParallelizerTest(unittest.TestCase):
self.assertEquals(range(9, 19), results)
+class SyncParallelizerTest(unittest.TestCase):
+
+ def testContextManager(self):
+ in_context = [False for i in xrange(10)]
+
+ @contextlib.contextmanager
+ def enter_into_context(i):
+ in_context[i] = True
+ try:
+ yield
+ finally:
+ in_context[i] = False
+
+ parallelized_context = parallelizer.SyncParallelizer(
+ [enter_into_context(i) for i in xrange(10)])
+
+ with parallelized_context:
+ self.assertTrue(all(in_context))
+ self.assertFalse(any(in_context))
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/utils/run_tests_helper.py b/catapult/devil/devil/utils/run_tests_helper.py
index 7df2da65..7f71b65c 100644
--- a/catapult/devil/devil/utils/run_tests_helper.py
+++ b/catapult/devil/devil/utils/run_tests_helper.py
@@ -4,41 +4,26 @@
"""Helper functions common to native, java and host-driven test runners."""
+import collections
import logging
-import sys
-import time
-
-
-class CustomFormatter(logging.Formatter):
- """Custom log formatter."""
-
- # override
- def __init__(self, fmt='%(threadName)-4s %(message)s'):
- # Can't use super() because in older Python versions logging.Formatter does
- # not inherit from object.
- logging.Formatter.__init__(self, fmt=fmt)
- self._creation_time = time.time()
-
- # override
- def format(self, record):
- # Can't use super() because in older Python versions logging.Formatter does
- # not inherit from object.
- msg = logging.Formatter.format(self, record)
- if 'MainThread' in msg[:19]:
- msg = msg.replace('MainThread', 'Main', 1)
- timediff = time.time() - self._creation_time
- return '%s %8.3fs %s' % (record.levelname[0], timediff, msg)
-
-
-def SetLogLevel(verbose_count):
- """Sets log level as |verbose_count|."""
- log_level = logging.WARNING # Default.
- if verbose_count == 1:
- log_level = logging.INFO
- elif verbose_count >= 2:
- log_level = logging.DEBUG
- logger = logging.getLogger()
- logger.setLevel(log_level)
- custom_handler = logging.StreamHandler(sys.stdout)
- custom_handler.setFormatter(CustomFormatter())
- logging.getLogger().addHandler(custom_handler)
+
+from devil.utils import logging_common
+
+
+CustomFormatter = logging_common.CustomFormatter
+
+
+_WrappedLoggingArgs = collections.namedtuple(
+ '_WrappedLoggingArgs', ['verbose'])
+
+
+def SetLogLevel(verbose_count, add_handler=True):
+ """Sets log level as |verbose_count|.
+
+ Args:
+ verbose_count: Verbosity level.
+ add_handler: If true, adds a handler with |CustomFormatter|.
+ """
+ logging_common.InitializeLogging(
+ _WrappedLoggingArgs(verbose_count),
+ handler=None if add_handler else logging.NullHandler())
diff --git a/catapult/devil/devil/utils/timeout_retry.py b/catapult/devil/devil/utils/timeout_retry.py
index d2304629..2327b6bf 100644
--- a/catapult/devil/devil/utils/timeout_retry.py
+++ b/catapult/devil/devil/utils/timeout_retry.py
@@ -28,7 +28,7 @@ class TimeoutRetryThreadGroup(reraiser_thread.ReraiserThreadGroup):
def GetElapsedTime(self):
return self._watcher.GetElapsed()
- def GetRemainingTime(self, required=0, msg=None):
+ def GetRemainingTime(self, required=0, suffix=None):
"""Get the remaining time before the thread times out.
Useful to send as the |timeout| parameter of async IO operations.
@@ -48,11 +48,9 @@ class TimeoutRetryThreadGroup(reraiser_thread.ReraiserThreadGroup):
"""
remaining = self._watcher.GetRemaining()
if remaining is not None and remaining < required:
- if msg is None:
- msg = 'Timeout expired'
- if remaining > 0:
- msg += (', wait of %.1f secs required but only %.1f secs left'
- % (required, remaining))
+ msg = 'Timeout of %.1f secs expired' % self._watcher.GetTimeout()
+ if suffix:
+ msg += suffix
raise reraiser_thread.TimeoutError(msg)
return remaining
@@ -110,7 +108,7 @@ def WaitFor(condition, wait_period=5, max_tries=None):
if timeout_thread_group:
# pylint: disable=no-member
timeout_thread_group.GetRemainingTime(wait_period,
- msg='Timed out waiting for %r' % condition_name)
+ suffix=' waiting for condition %r' % condition_name)
time.sleep(wait_period)
return None
diff --git a/catapult/devil/devil/utils/usb_hubs.py b/catapult/devil/devil/utils/usb_hubs.py
index b7186940..bd984c7b 100644
--- a/catapult/devil/devil/utils/usb_hubs.py
+++ b/catapult/devil/devil/utils/usb_hubs.py
@@ -149,11 +149,12 @@ def _is_via_hub(node):
The topology of this device is a 4-port hub,
with another 4-port hub connected on port 4.
"""
- if '2109:2812' not in node.desc:
+ if '2109:2812' not in node.desc and '2109:0812' not in node.desc:
return False
if not node.HasPort(4):
return False
- return '2109:2812' in node.PortToDevice(4).desc
+ return ('2109:2812' in node.PortToDevice(4).desc or
+ '2109:0812' in node.PortToDevice(4).desc)
PLUGABLE_7PORT = HubType(_is_plugable_7port_hub, PLUGABLE_7PORT_LAYOUT)
diff --git a/catapult/devil/devil/utils/watchdog_timer.py b/catapult/devil/devil/utils/watchdog_timer.py
index 2f4c4645..bff1f8cc 100644
--- a/catapult/devil/devil/utils/watchdog_timer.py
+++ b/catapult/devil/devil/utils/watchdog_timer.py
@@ -37,6 +37,10 @@ class WatchdogTimer(object):
else:
return None
+ def GetTimeout(self):
+ """Returns the timout of the watchdog."""
+ return self._timeout
+
def IsTimedOut(self):
"""Whether the watchdog has timed out.
diff --git a/catapult/devil/devil/utils/zip_utils.py b/catapult/devil/devil/utils/zip_utils.py
index eaa6a2df..e1f812b7 100644
--- a/catapult/devil/devil/utils/zip_utils.py
+++ b/catapult/devil/devil/utils/zip_utils.py
@@ -2,14 +2,34 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import argparse
+import json
import logging
import os
+import sys
import zipfile
+if __name__ == '__main__':
+ _DEVIL_ROOT_DIR = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', '..'))
+ _PY_UTILS_ROOT_DIR = os.path.abspath(
+ os.path.join(_DEVIL_ROOT_DIR, '..', 'common', 'py_utils'))
+ sys.path.extend((_DEVIL_ROOT_DIR, _PY_UTILS_ROOT_DIR))
+
+from devil import base_error
+from devil.utils import cmd_helper
+from py_utils import tempfile_ext
+
+
logger = logging.getLogger(__name__)
-def WriteToZipFile(zip_file, path, arc_path):
+class ZipFailedError(base_error.BaseError):
+ """Raised on a failure to perform a zip operation."""
+ pass
+
+
+def _WriteToZipFile(zip_file, path, arc_path):
"""Recursively write |path| to |zip_file| as |arc_path|.
zip_file: An open instance of zipfile.ZipFile.
@@ -31,3 +51,56 @@ def WriteToZipFile(zip_file, path, arc_path):
logger.debug('file: %s -> %s', path, arc_path)
zip_file.write(path, arc_path, zipfile.ZIP_DEFLATED)
+
+def _WriteZipFile(zip_path, zip_contents):
+ with zipfile.ZipFile(zip_path, 'w') as zip_file:
+ for path, arc_path in zip_contents:
+ _WriteToZipFile(zip_file, path, arc_path)
+
+
+def WriteZipFile(zip_path, zip_contents):
+ """Writes the provided contents to the given zip file.
+
+ Note that this uses python's zipfile module and is done in a separate
+ process to avoid hogging the GIL.
+
+ Args:
+ zip_path: String path to the zip file to write.
+ zip_contents: A list of (host path, archive path) tuples.
+
+ Raises:
+ ZipFailedError on failure.
+ """
+ zip_spec = {
+ 'zip_path': zip_path,
+ 'zip_contents': zip_contents,
+ }
+ with tempfile_ext.NamedTemporaryDirectory() as tmpdir:
+ json_path = os.path.join(tmpdir, 'zip_spec.json')
+ with open(json_path, 'w') as json_file:
+ json.dump(zip_spec, json_file)
+ ret, output, error = cmd_helper.GetCmdStatusOutputAndError([
+ sys.executable, os.path.abspath(__file__),
+ '--zip-spec', json_path])
+
+ if ret != 0:
+ exc_msg = ['Failed to create %s' % zip_path]
+ exc_msg.extend('stdout: %s' % l for l in output.splitlines())
+ exc_msg.extend('stderr: %s' % l for l in error.splitlines())
+ raise ZipFailedError('\n'.join(exc_msg))
+
+
+def main(raw_args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--zip-spec', required=True)
+
+ args = parser.parse_args(raw_args)
+
+ with open(args.zip_spec) as zip_spec_file:
+ zip_spec = json.load(zip_spec_file)
+
+ return _WriteZipFile(**zip_spec)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/catapult/devil/devil/utils/zip_utils_test.py b/catapult/devil/devil/utils/zip_utils_test.py
new file mode 100644
index 00000000..4564e3f1
--- /dev/null
+++ b/catapult/devil/devil/utils/zip_utils_test.py
@@ -0,0 +1,43 @@
+# Copyright 2017 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.
+
+import os
+import unittest
+import zipfile
+
+from devil.utils import zip_utils
+from py_utils import tempfile_ext
+
+
+class WriteZipFileTest(unittest.TestCase):
+
+ def testSimple(self):
+ with tempfile_ext.NamedTemporaryDirectory() as working_dir:
+ file1 = os.path.join(working_dir, 'file1.txt')
+ file2 = os.path.join(working_dir, 'file2.txt')
+
+ with open(file1, 'w') as f1:
+ f1.write('file1')
+ with open(file2, 'w') as f2:
+ f2.write('file2')
+
+ zip_tuples = [
+ (file1, 'foo/file1.txt'),
+ (file2, 'bar/file2.txt'),
+ ]
+
+ zip_path = os.path.join(working_dir, 'out.zip')
+ zip_utils.WriteZipFile(zip_path, zip_tuples)
+
+ self.assertTrue(zipfile.is_zipfile(zip_path))
+
+ actual = zipfile.ZipFile(zip_path)
+ expected_files = [
+ 'foo/file1.txt',
+ 'bar/file2.txt',
+ ]
+
+ self.assertEquals(
+ sorted(expected_files),
+ sorted(actual.namelist()))
diff --git a/catapult/devil/docs/device_utils.md b/catapult/devil/docs/device_utils.md
index b281b266..a6e89a70 100644
--- a/catapult/devil/docs/device_utils.md
+++ b/catapult/devil/docs/device_utils.md
@@ -151,6 +151,22 @@ Get the device's path to its SD card.
```
+### DeviceUtils.GetIMEI
+
+Get the device's IMEI.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The device's IMEI.
+
+ Raises:
+ AdbCommandFailedError on error
+```
+
+
### DeviceUtils.GetApplicationPaths
Get the paths of the installed apks on the device for the given package.
@@ -163,6 +179,22 @@ Get the paths of the installed apks on the device for the given package.
```
+### DeviceUtils.TakeBugReport
+
+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.
+```
+
+
### DeviceUtils.GetApplicationVersion
Get the version name of a package installed on the device.
@@ -299,18 +331,21 @@ Remove the app |package\_name| from the device.
Run an ADB shell command.
```
- The command to run |cmd| should be a sequence of program arguments or else
- a single string.
+ The command to run |cmd| should be a sequence of program arguments
+ (preferred) or a single string with a shell script to run.
When |cmd| is a sequence, it is assumed to contain the name of the command
to run followed by its arguments. In this case, arguments are passed to the
- command exactly as given, without any further processing by the shell. This
- allows to easily pass arguments containing spaces or special characters
- without having to worry about getting quoting right. Whenever possible, it
- is recomended to pass |cmd| as a sequence.
+ command exactly as given, preventing any further processing by the shell.
+ This allows callers to easily pass arguments with spaces or special
+ characters without having to worry about quoting rules. Whenever possible,
+ it is recomended to pass |cmd| as a sequence.
- When |cmd| is given as a string, it will be interpreted and run by the
- shell on the device.
+ When |cmd| is passed as a single string, |shell| should be set to True.
+ The command will be interpreted and run by the shell on the device,
+ allowing the use of shell features such as pipes, wildcards, or variables.
+ Failing to set shell=True will issue a warning, but this will be changed
+ to a hard failure in the future (see: catapult:#3242).
This behaviour is consistent with that of command runners in cmd_helper as
well as Python's own subprocess.Popen.
@@ -319,8 +354,9 @@ Run an ADB shell command.
have switched to the new behaviour.
Args:
- cmd: A string with the full command to run on the device, or a sequence
- containing the command and its arguments.
+ cmd: A sequence containing the command to run and its arguments, or a
+ string with a shell script to run (should also set shell=True).
+ shell: A boolean indicating whether shell features may be used in |cmd|.
check_return: A boolean indicating whether or not the return code should
be checked.
cwd: The device directory in which the command should be run.
@@ -553,6 +589,8 @@ Removes the given path(s) from the device.
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
```
@@ -782,6 +820,31 @@ Returns the country setting on the device.
```
+### DeviceUtils.GetApplicationPids
+
+Returns the PID or PIDs of a given process name.
+```
+ Note that the |process_name|, often the package name, must match exactly.
+
+ Args:
+ process_name: A string containing the process name to get the PIDs for.
+ at_most_one: A boolean indicating that at most one PID is expected to
+ be found.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A list of the PIDs for the named process. If at_most_one=True returns
+ the single PID found or None otherwise.
+
+ Raises:
+ CommandFailedError if at_most_one=True and more than one PID is found
+ for the named process.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
### DeviceUtils.GetProp
Gets a property from the device.
@@ -840,12 +903,13 @@ Gets the device main ABI.
### DeviceUtils.GetPids
-Returns the PIDs of processes with the given name.
+Returns the PIDs of processes containing the given name as substring.
```
Note that the |process_name| is often the package name.
Args:
process_name: A string containing the process name to get the PIDs for.
+ If missing returns PIDs for all processes.
timeout: timeout in seconds
retries: number of retries
@@ -859,6 +923,15 @@ Returns the PIDs of processes with the given name.
```
+### DeviceUtils.GetLogcatMonitor
+
+Returns a new LogcatMonitor associated with this device.
+```
+ Parameters passed to this function are passed directly to
+ |logcat_monitor.LogcatMonitor| and are documented there.
+```
+
+
### DeviceUtils.GetEnforce
Get the current mode of SELinux.
@@ -915,25 +988,6 @@ Takes a screenshot of the device.
```
-### DeviceUtils.GetMemoryUsageForPid
-
-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.
-```
-
-
### DeviceUtils.DismissCrashDialogIfNeeded
Dismiss the error/ANR dialog if present.
@@ -943,15 +997,6 @@ Dismiss the error/ANR dialog if present.
```
-### DeviceUtils.GetLogcatMonitor
-
-Returns a new LogcatMonitor associated with this device.
-```
- Parameters passed to this function are passed directly to
- |logcat_monitor.LogcatMonitor| and are documented there.
-```
-
-
### DeviceUtils.GetClientCache
Returns client cache.
diff --git a/catapult/devil/docs/persistent_device_list.md b/catapult/devil/docs/persistent_device_list.md
index d08d9a9e..626a8788 100644
--- a/catapult/devil/docs/persistent_device_list.md
+++ b/catapult/devil/docs/persistent_device_list.md
@@ -8,16 +8,16 @@
## What is it?
A persistent device list that stores all expected devices between builds. It
-is used by the perf test runner in order to properly shard tests by device
-affinity. This is important because the same performance test can yield
-meaningfully different results when run on different devices.
+is used by non-swarmed bots to detect any missing/extra devices attached to
+them.
+
+This will be no longer needed when all bots are switched over to swarming.
## Bots
-The list is usually located at one of these locations:
+The list is usually located at:
- - `/b/build/site_config/.known_devices`.
- - `~/.android`.
+ - `~/.android/known_devices.json`.
Look at recipes listed below in order to find more up to date location.
@@ -28,14 +28,8 @@ bots that upload data to the perf dashboard.
## Where it is used
-The persistent device list is used in performance test recipes via
-[api.chromium\_tests.steps.DynamicPerfTests](https://cs.chromium.org/chromium/build/scripts/slave/recipe_modules/chromium_tests/steps.py?q=DynamicPerfTests).
-For example, the [android/perf](https://cs.chromium.org/chromium/build/scripts/slave/recipes/android/perf.py) recipe uses it like this:
-
-```python
-dynamic_perf_tests = api.chromium_tests.steps.DynamicPerfTests(
- builder['perf_id'], 'android', None,
- known_devices_file=builder.get('known_devices_file', None))
-dynamic_perf_tests.run(api, None)
-```
-
+The persistent device list is used in the
+[chromium_android](https://cs.chromium.org/chromium/build/scripts/slave/recipe_modules/chromium_android/api.py?q=known_devices_file)
+recipe module, and consumed by the
+[device_status.py](https://cs.chromium.org/chromium/src/third_party/catapult/devil/devil/android/tools/device_status.py?q=\-\-known%5C-devices%5C-file)
+script among others.