diff options
author | Wei Wang <wvw@google.com> | 2018-02-26 14:02:53 -0800 |
---|---|---|
committer | Wei Wang <wvw@google.com> | 2018-02-26 14:13:51 -0800 |
commit | b2cf025c7d5cebd43084f38c6c7ff9cc17da428a (patch) | |
tree | 06e286adf2a464b39cf69d9ff9c91cad60d79772 /catapult/devil | |
parent | 3e601f2c29e63f5151aa982790deea52645bc6ea (diff) | |
download | chromium-trace-b2cf025c7d5cebd43084f38c6c7ff9cc17da428a.tar.gz |
Update to latest catapult (3fe65c60)android-9.0.0_r47android-9.0.0_r46android-9.0.0_r45android-9.0.0_r44android-9.0.0_r43android-9.0.0_r42android-9.0.0_r41android-9.0.0_r40android-9.0.0_r39android-9.0.0_r38android-9.0.0_r37android-9.0.0_r36android-9.0.0_r35android-9.0.0_r34android-9.0.0_r33android-9.0.0_r32android-9.0.0_r31android-9.0.0_r30android-9.0.0_r22android-9.0.0_r21android-9.0.0_r20android-9.0.0_r19android-9.0.0_r16android-9.0.0_r12android-9.0.0_r11pie-qpr3-s1-releasepie-qpr3-releasepie-qpr3-b-releasepie-qpr2-releasepie-qpr1-s3-releasepie-qpr1-s2-releasepie-qpr1-s1-releasepie-qpr1-releasepie-dr1-releasepie-dr1-devpie-devpie-b4s4-releasepie-b4s4-dev
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')
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. |