diff options
author | Chris Craik <ccraik@google.com> | 2017-04-03 15:54:29 -0700 |
---|---|---|
committer | Chris Craik <ccraik@google.com> | 2017-04-03 16:02:14 -0700 |
commit | c088b23ae5d8cab6878e43cc37b7b5d141f6c6d7 (patch) | |
tree | df2eac86f23cb2dab056b1972c16a6222e005a89 /catapult/devil | |
parent | b6ff195e4c30012fa6a7b3fe13a7938dc1c007ea (diff) | |
parent | 9ede47ea07acc34a4fe73d2ef5ba128fe1511483 (diff) | |
download | chromium-trace-c088b23ae5d8cab6878e43cc37b7b5d141f6c6d7.tar.gz |
resolve merge conflicts of 9ede47ea to oc-dev-plus-aosp
Test: external/chromium-trace/systrace.py
Change-Id: I06f10b563d774ebcd6ebee75eb3622d0d71795c2
Diffstat (limited to 'catapult/devil')
38 files changed, 3324 insertions, 397 deletions
diff --git a/catapult/devil/README.md b/catapult/devil/README.md new file mode 100644 index 00000000..852ac378 --- /dev/null +++ b/catapult/devil/README.md @@ -0,0 +1,37 @@ +<!-- Copyright 2015 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. +--> +## devil + +😈 + +devil is a library used by the Chromium developers to interact with Android +devices. It currently supports SDK level 16 and above. + +## Interfaces + +devil provides python APIs: + - [`devil.android.adb_wrapper`](docs/adb_wrapper.md) provides a thin wrapper + around the adb binary. Most functions and methods have direct analogues on + the adb command-line. + - [`devil.android.device_utils`](docs/device_utils.md) provides higher-level + functionality built on top of `adb_wrapper`. **This is the primary + mechanism through which chromium's scripts interact with devices.** + +## Utilities + +devil also provides command-line utilities: + - [`devil/utils/markdown.py`](docs/markdown.md) generated markdown + documentation for python modules. + +## Constraints and Caveats + +devil is used with python 2.7. Its compatibility with python 3 has not been +tested, and neither achieving nor maintaining said compatibility is currently +a priority. + +## Contributing + +Please see the [contributor's guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md). + diff --git a/catapult/devil/bin/generate_md_docs b/catapult/devil/bin/generate_md_docs new file mode 100755 index 00000000..634e14a5 --- /dev/null +++ b/catapult/devil/bin/generate_md_docs @@ -0,0 +1,45 @@ +#!/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 + +_DEVIL_PATH = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..')) +_DEVIL_URL = ( + 'https://github.com/catapult-project/catapult/blob/master/devil/') + +sys.path.append(_DEVIL_PATH) +from devil.utils import cmd_helper + +_FILES_TO_DOC = { + 'devil/android/sdk/adb_wrapper.py': 'docs/adb_wrapper.md', + 'devil/android/device_utils.py': 'docs/device_utils.md', + 'devil/utils/markdown.py': 'docs/markdown.md', +} + +_MARKDOWN_SCRIPT = os.path.join(_DEVIL_PATH, 'devil', 'utils', 'markdown.py') + +def main(): + failed = False + for k, v in _FILES_TO_DOC.iteritems(): + module_path = os.path.join(_DEVIL_PATH, k) + module_link = _DEVIL_URL + k + doc_path = os.path.join(_DEVIL_PATH, v) + + status, stdout = cmd_helper.GetCmdStatusAndOutput( + [sys.executable, _MARKDOWN_SCRIPT, module_path, + '--module-link', module_link]) + if status: + logging.error('Failed to update doc for %s' % module_path) + failed = True + else: + with open(doc_path, 'w') as doc_file: + doc_file.write(stdout) + + return 1 if failed else 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/catapult/devil/devil/README.md b/catapult/devil/devil/README.md deleted file mode 100644 index b3eb5d0f..00000000 --- a/catapult/devil/devil/README.md +++ /dev/null @@ -1,17 +0,0 @@ -<!-- Copyright 2015 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. ---> -devil -===== - -devil is a library used by the Chromium developers to interact with Android -devices. It currently supports SDK level 16 and above. - -😈 - -Contributing -============ - -Please see the [contributor's guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md). - diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py index 61eeda06..1a9b8c55 100644 --- a/catapult/devil/devil/android/apk_helper.py +++ b/catapult/devil/devil/android/apk_helper.py @@ -4,8 +4,10 @@ """Module containing utilities for apk packages.""" +import itertools import re +from devil import base_error from devil.android.sdk import aapt @@ -54,16 +56,22 @@ def _ParseManifestFromApk(apk_path): m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:]) if m: - if not m.group(1) in node: - node[m.group(1)] = {} - node_stack += [node[m.group(1)]] + manifest_key = m.group(1) + if manifest_key in node: + node[manifest_key] += [{}] + else: + node[manifest_key] = [{}] + node_stack += [node[manifest_key][-1]] continue m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:]) if m: - if not m.group(1) in node: - node[m.group(1)] = [] - node[m.group(1)].append(m.group(2) or m.group(3)) + manifest_key = m.group(1) + if manifest_key in node: + raise base_error.BaseError( + "A single attribute should have one key and one value") + else: + node[manifest_key] = m.group(2) or m.group(3) continue return parsed_manifest @@ -84,8 +92,8 @@ class ApkHelper(object): manifest_info = self._GetManifest() try: activity = ( - manifest_info['manifest']['application']['activity'] - ['android:name'][0]) + manifest_info['manifest'][0]['application'][0]['activity'][0] + ['android:name']) except KeyError: return None if '.' not in activity: @@ -97,24 +105,34 @@ class ApkHelper(object): def GetInstrumentationName( self, default='android.test.InstrumentationTestRunner'): """Returns the name of the Instrumentation in the apk.""" - manifest_info = self._GetManifest() + all_instrumentations = self.GetAllInstrumentations(default=default) + if len(all_instrumentations) != 1: + raise base_error.BaseError( + 'There is more than one instrumentation. Expected one.') + else: + return all_instrumentations[0]['android:name'] + + def GetAllInstrumentations( + self, default='android.test.InstrumentationTestRunner'): + """Returns a list of all Instrumentations in the apk.""" try: - return manifest_info['manifest']['instrumentation']['android:name'][0] + return self._GetManifest()['manifest'][0]['instrumentation'] except KeyError: - return default + return [{'android:name': default}] def GetPackageName(self): """Returns the package name of the apk.""" manifest_info = self._GetManifest() try: - return manifest_info['manifest']['package'][0] + return manifest_info['manifest'][0]['package'] except KeyError: raise Exception('Failed to determine package name of %s' % self._apk_path) def GetPermissions(self): manifest_info = self._GetManifest() try: - return manifest_info['manifest']['uses-permission']['android:name'] + return [p['android:name'] for + p in manifest_info['manifest'][0]['uses-permission']] except KeyError: return [] @@ -122,7 +140,7 @@ class ApkHelper(object): """Returns the name of the split of the apk.""" manifest_info = self._GetManifest() try: - return manifest_info['manifest']['split'][0] + return manifest_info['manifest'][0]['split'] except KeyError: return None @@ -130,8 +148,12 @@ class ApkHelper(object): """Returns whether any services exist that use isolatedProcess=true.""" manifest_info = self._GetManifest() try: - services = manifest_info['manifest']['application']['service'] - return any(int(v, 0) for v in services['android:isolatedProcess']) + applications = manifest_info['manifest'][0].get('application', []) + services = itertools.chain( + *(application.get('service', []) for application in applications)) + return any( + int(s.get('android:isolatedProcess', '0'), 0) + for s in services) except KeyError: return False diff --git a/catapult/devil/devil/android/apk_helper_test.py b/catapult/devil/devil/android/apk_helper_test.py new file mode 100755 index 00000000..f7d077dd --- /dev/null +++ b/catapult/devil/devil/android/apk_helper_test.py @@ -0,0 +1,169 @@ +#! /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. + +from devil import base_error +from devil import devil_env +from devil.android import apk_helper +from devil.utils import mock_calls + +with devil_env.SysPath(devil_env.PYMOCK_PATH): + import mock # pylint: disable=import-error + + +_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.abc" (Raw: "org.chromium.abc") + A: split="random_split" (Raw: "random_split") + E: uses-permission (line=2) + A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET") + E: uses-permission (line=3) + A: android:name(0x01010003)="android.permission.READ_EXTERNAL_STORAGE" (Raw: "android.permission.READ_EXTERNAL_STORAGE") + E: uses-permission (line=4) + A: android:name(0x01010003)="android.permission.ACCESS_FINE_LOCATION" (Raw: "android.permission.ACCESS_FINE_LOCATION") + E: application (line=5) + E: activity (line=6) + A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName") + A: android:exported(0x01010010)=(type 0x12)0xffffffff + E: service (line=7) + A: android:name(0x01010001)="org.chromium.RandomService" (Raw: "org.chromium.RandomService") + A: android:isolatedProcess(0x01010888)=(type 0x12)0xffffffff + E: instrumentation (line=8) + A: android:label(0x01010001)="abc" (Raw: "abc") + A: android:name(0x01010003)="org.chromium.RandomJUnit4TestRunner" (Raw: "org.chromium.RandomJUnit4TestRunner") + A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge") + A: junit4=(type 0x12)0xffffffff (Raw: "true") + E: instrumentation (line=9) + A: android:label(0x01010001)="abc" (Raw: "abc") + A: android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner") + A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge") +""" + +_NO_ISOLATED_SERVICES = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.abc" (Raw: "org.chromium.abc") + E: application (line=5) + E: activity (line=6) + A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName") + A: android:exported(0x01010010)=(type 0x12)0xffffffff + E: service (line=7) + A: android:name(0x01010001)="org.chromium.RandomService" (Raw: "org.chromium.RandomService") +""" + +_NO_SERVICES = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.abc" (Raw: "org.chromium.abc") + E: application (line=5) + E: activity (line=6) + A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName") + A: android:exported(0x01010010)=(type 0x12)0xffffffff +""" + +_NO_APPLICATION = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.abc" (Raw: "org.chromium.abc") +""" + +_SINGLE_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.xyz" (Raw: "org.chromium.xyz") + E: instrumentation (line=8) + A: android:label(0x01010001)="xyz" (Raw: "xyz") + A: android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner") + A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge") +""" + +_SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android + E: manifest (line=1) + A: package="org.chromium.xyz" (Raw: "org.chromium.xyz") + E: instrumentation (line=8) + A: android:label(0x01010001)="xyz" (Raw: "xyz") + A: android:name(0x01010003)="org.chromium.RandomJ4TestRunner" (Raw: "org.chromium.RandomJ4TestRunner") + A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge") + A: junit4=(type 0x12)0xffffffff (Raw: "true") +""" + + +def _MockAaptDump(manifest_dump): + return mock.patch( + 'devil.android.sdk.aapt.Dump', + mock.Mock(side_effect=None, return_value=manifest_dump.split('\n'))) + +class ApkHelperTest(mock_calls.TestCase): + + def testGetInstrumentationName(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + with self.assertRaises(base_error.BaseError): + helper.GetInstrumentationName() + + def testGetActivityName(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + self.assertEquals( + helper.GetActivityName(), 'org.chromium.ActivityName') + + def testGetAllInstrumentations(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + all_instrumentations = helper.GetAllInstrumentations() + self.assertEquals(len(all_instrumentations), 2) + self.assertEquals(all_instrumentations[0]['android:name'], + 'org.chromium.RandomJUnit4TestRunner') + self.assertEquals(all_instrumentations[1]['android:name'], + 'org.chromium.RandomTestRunner') + + def testGetPackageName(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + self.assertEquals(helper.GetPackageName(), 'org.chromium.abc') + + def testGetPermssions(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + all_permissions = helper.GetPermissions() + self.assertEquals(len(all_permissions), 3) + self.assertTrue('android.permission.INTERNET' in all_permissions) + self.assertTrue( + 'android.permission.READ_EXTERNAL_STORAGE' in all_permissions) + self.assertTrue( + 'android.permission.ACCESS_FINE_LOCATION' in all_permissions) + + def testGetSplitName(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + self.assertEquals(helper.GetSplitName(), 'random_split') + + def testHasIsolatedProcesses_noApplication(self): + with _MockAaptDump(_NO_APPLICATION): + helper = apk_helper.ApkHelper("") + self.assertFalse(helper.HasIsolatedProcesses()) + + def testHasIsolatedProcesses_noServices(self): + with _MockAaptDump(_NO_SERVICES): + helper = apk_helper.ApkHelper("") + self.assertFalse(helper.HasIsolatedProcesses()) + + def testHasIsolatedProcesses_oneNotIsolatedProcess(self): + with _MockAaptDump(_NO_ISOLATED_SERVICES): + helper = apk_helper.ApkHelper("") + self.assertFalse(helper.HasIsolatedProcesses()) + + def testHasIsolatedProcesses_oneIsolatedProcess(self): + with _MockAaptDump(_MANIFEST_DUMP): + helper = apk_helper.ApkHelper("") + self.assertTrue(helper.HasIsolatedProcesses()) + + def testGetSingleInstrumentationName(self): + with _MockAaptDump(_SINGLE_INSTRUMENTATION_MANIFEST_DUMP): + 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("") + self.assertEquals('org.chromium.RandomJ4TestRunner', + helper.GetInstrumentationName()) + diff --git a/catapult/devil/devil/android/constants/chrome.py b/catapult/devil/devil/android/constants/chrome.py index 006764f2..dca04bdc 100644 --- a/catapult/devil/devil/android/constants/chrome.py +++ b/catapult/devil/devil/android/constants/chrome.py @@ -12,46 +12,46 @@ PACKAGE_INFO = { 'chrome_document': PackageInfo( 'com.google.android.apps.chrome.document', 'com.google.android.apps.chrome.document.ChromeLauncherActivity', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome': PackageInfo( 'com.google.android.apps.chrome', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome_beta': PackageInfo( 'com.chrome.beta', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome_stable': PackageInfo( 'com.android.chrome', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome_dev': PackageInfo( 'com.chrome.dev', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome_canary': PackageInfo( 'com.chrome.canary', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chrome_work': PackageInfo( 'com.chrome.work', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'chromium': PackageInfo( 'org.chromium.chrome', 'com.google.android.apps.chrome.Main', - '/data/local/chrome-command-line', + 'chrome-command-line', 'chrome_devtools_remote'), 'content_shell': PackageInfo( 'org.chromium.content_shell_apk', '.ContentShellActivity', - '/data/local/tmp/content-shell-command-line', + 'content-shell-command-line', 'content_shell_devtools_remote'), } diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py index 3d8e2034..7ba1b514 100644 --- a/catapult/devil/devil/android/device_utils.py +++ b/catapult/devil/devil/android/device_utils.py @@ -76,13 +76,22 @@ _PERMISSIONS_BLACKLIST = [ 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS', 'android.permission.ACCESS_MOCK_LOCATION', 'android.permission.ACCESS_NETWORK_STATE', + 'android.permission.ACCESS_NOTIFICATION_POLICY', 'android.permission.ACCESS_WIFI_STATE', 'android.permission.AUTHENTICATE_ACCOUNTS', 'android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN', + 'android.permission.BROADCAST_STICKY', + 'android.permission.CHANGE_NETWORK_STATE', + 'android.permission.CHANGE_WIFI_MULTICAST_STATE', + 'android.permission.CHANGE_WIFI_STATE', 'android.permission.DISABLE_KEYGUARD', 'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION', + 'android.permission.EXPAND_STATUS_BAR', + 'android.permission.GET_PACKAGE_SIZE', + 'android.permission.INSTALL_SHORTCUT', 'android.permission.INTERNET', + 'android.permission.KILL_BACKGROUND_PROCESSES', 'android.permission.MANAGE_ACCOUNTS', 'android.permission.MODIFY_AUDIO_SETTINGS', 'android.permission.NFC', @@ -90,8 +99,16 @@ _PERMISSIONS_BLACKLIST = [ 'android.permission.READ_SYNC_STATS', 'android.permission.RECEIVE_BOOT_COMPLETED', 'android.permission.RECORD_VIDEO', + 'android.permission.REORDER_TASKS', + 'android.permission.REQUEST_INSTALL_PACKAGES', 'android.permission.RUN_INSTRUMENTATION', + 'android.permission.SET_ALARM', + 'android.permission.SET_TIME_ZONE', + 'android.permission.SET_WALLPAPER', + 'android.permission.SET_WALLPAPER_HINTS', + 'android.permission.TRANSMIT_IR', 'android.permission.USE_CREDENTIALS', + 'android.permission.USE_FINGERPRINT', 'android.permission.VIBRATE', 'android.permission.WAKE_LOCK', 'android.permission.WRITE_SYNC_SETTINGS', @@ -150,6 +167,17 @@ _FILE_MODE_SPECIAL = [ ('s', stat.S_ISGID), ('t', stat.S_ISVTX), ] +_SELINUX_MODE = { + 'enforcing': True, + 'permissive': False, + 'disabled': None +} +# Some devices require different logic for checking if root is necessary +_SPECIAL_ROOT_DEVICE_LIST = [ + 'marlin', + 'sailfish', +] + @decorators.WithExplicitTimeoutAndRetries( _DEFAULT_TIMEOUT, _DEFAULT_RETRIES) @@ -367,6 +395,8 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ try: + if self.product_name in _SPECIAL_ROOT_DEVICE_LIST: + return self.GetProp('service.adb.root') == '1' self.RunShellCommand(['ls', '/root'], check_return=True) return True except device_errors.AdbCommandFailedError: @@ -390,9 +420,14 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ if 'needs_su' not in self._cache: + cmd = '%s && ! ls /root' % self._Su('ls /root') + if self.product_name in _SPECIAL_ROOT_DEVICE_LIST: + if self.HasRoot(): + self._cache['needs_su'] = False + return False + cmd = 'which which && which su' try: - self.RunShellCommand( - '%s && ! ls /root' % self._Su('ls /root'), check_return=True, + self.RunShellCommand(cmd, shell=True, check_return=True, timeout=self._default_timeout if timeout is DEFAULT else timeout, retries=self._default_retries if retries is DEFAULT else retries) self._cache['needs_su'] = True @@ -400,6 +435,7 @@ class DeviceUtils(object): self._cache['needs_su'] = False return self._cache['needs_su'] + def _Su(self, command): if self.build_version_sdk >= version_codes.MARSHMALLOW: return 'su 0 %s' % command @@ -799,24 +835,27 @@ class DeviceUtils(object): device_serial=self.serial) @decorators.WithTimeoutAndRetriesFromInstance() - def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None, - run_as=None, as_root=False, single_line=False, + def RunShellCommand(self, cmd, shell=False, check_return=False, cwd=None, + env=None, run_as=None, as_root=False, single_line=False, large_output=False, raw_output=False, timeout=None, retries=None): """Run an ADB shell command. - The command to run |cmd| should be a sequence of program arguments or else - 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. @@ -825,8 +864,9 @@ class DeviceUtils(object): 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. @@ -906,7 +946,13 @@ class DeviceUtils(object): else: raise - if not isinstance(cmd, basestring): + if isinstance(cmd, basestring): + if not shell: + logging.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.') + else: cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd) if env: env = ' '.join(env_quote(k, v) for k, v in env.iteritems()) @@ -941,7 +987,7 @@ class DeviceUtils(object): PIPESTATUS_LEADER = 'PIPESTATUS: ' script += '; echo "%s${PIPESTATUS[@]}"' % PIPESTATUS_LEADER - kwargs['check_return'] = True + kwargs.update(shell=True, check_return=True) output = self.RunShellCommand(script, **kwargs) pipestatus_line = output[-1] @@ -1073,7 +1119,7 @@ class DeviceUtils(object): package = component.split('/')[0] shell_snippet = 'p=%s;%s' % (package, cmd_helper.ShrinkToSnippet(cmd, 'p', package)) - return self.RunShellCommand(shell_snippet, check_return=True, + return self.RunShellCommand(shell_snippet, shell=True, check_return=True, large_output=True) @decorators.WithTimeoutAndRetriesFromInstance() @@ -1145,7 +1191,7 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ cmd = 'p=%s;if [[ "$(ps)" = *$p* ]]; then am force-stop $p; fi' - self.RunShellCommand(cmd % package, check_return=True) + self.RunShellCommand(cmd % package, shell=True, check_return=True) @decorators.WithTimeoutAndRetriesFromInstance() def ClearApplicationState( @@ -1451,7 +1497,7 @@ class DeviceUtils(object): quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs) self.RunShellCommand( 'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs), - as_root=True, + shell=True, as_root=True, env={'PATH': '%s:$PATH' % install_commands.BIN_DIR}, check_return=True) finally: @@ -1496,8 +1542,11 @@ class DeviceUtils(object): paths = device_paths if isinstance(paths, basestring): paths = (paths,) - condition = ' -a '.join('-e %s' % cmd_helper.SingleQuote(p) for p in paths) - cmd = 'test %s' % condition + if not paths: + return True + cmd = ['test', '-e', paths[0]] + for p in paths[1:]: + cmd.extend(['-a', '-e', p]) try: self.RunShellCommand(cmd, as_root=as_root, check_return=True, timeout=timeout, retries=retries) @@ -1603,7 +1652,7 @@ class DeviceUtils(object): cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % ( cmd_helper.SingleQuote(device_path), cmd_helper.SingleQuote(device_temp.name)) - self.RunShellCommand(cmd, as_root=True, check_return=True) + self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True) return self._ReadFileWithPull(device_temp.name) else: return self._ReadFileWithPull(device_path) @@ -1641,7 +1690,7 @@ class DeviceUtils(object): # a shell command rather than pushing a file. cmd = 'echo -n %s > %s' % (cmd_helper.SingleQuote(contents), cmd_helper.SingleQuote(device_path)) - self.RunShellCommand(cmd, as_root=as_root, check_return=True) + self.RunShellCommand(cmd, shell=True, as_root=as_root, check_return=True) elif as_root and self.NeedsSU(): # Adb does not allow to "push with su", so we first push to a temp file # on a safe location, and then copy it to the desired location with su. @@ -1993,7 +2042,8 @@ class DeviceUtils(object): 'echo "%s">$c &&' % token + 'getprop' ) - output = self.RunShellCommand(cmd, check_return=True, large_output=True) + output = self.RunShellCommand( + cmd, shell=True, check_return=True, large_output=True) # Error-checking for this existing is done in GetExternalStoragePath(). self._cache['external_storage'] = output[0] self._cache['prev_token'] = output[1] @@ -2091,13 +2141,14 @@ class DeviceUtils(object): return self.GetProp('ro.product.cpu.abi', cache=True) @decorators.WithTimeoutAndRetriesFromInstance() - def GetPids(self, process_name, timeout=None, retries=None): - """Returns the PIDs of processes with the given name. + def GetPids(self, process_name=None, timeout=None, retries=None): + """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 @@ -2111,8 +2162,17 @@ class DeviceUtils(object): """ procs_pids = collections.defaultdict(list) try: - ps_output = self._RunPipedShellCommand( - 'ps | grep -F %s' % cmd_helper.SingleQuote(process_name)) + 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 @@ -2121,16 +2181,91 @@ class DeviceUtils(object): else: raise + process_name = process_name or '' for line in ps_output: try: ps_data = line.split() - if process_name in ps_data[-1]: - pid, process = ps_data[1], ps_data[-1] + pid, process = ps_data[1], ps_data[-1] + if process_name in process and pid != 'PID': procs_pids[process].append(pid) except IndexError: pass return procs_pids + def GetApplicationPids(self, process_name, at_most_one=False, **kwargs): + """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. + """ + pids = self.GetPids(process_name, **kwargs).get(process_name, []) + if at_most_one: + if len(pids) > 1: + raise device_errors.CommandFailedError( + 'Expected a single process but found PIDs: %s.' % ', '.join(pids), + device_serial=str(self)) + return pids[0] if pids else None + else: + return pids + + @decorators.WithTimeoutAndRetriesFromInstance() + def GetEnforce(self, timeout=None, retries=None): + """Get the current mode of SELinux. + + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True (enforcing), False (permissive), or None (disabled). + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + output = self.RunShellCommand( + ['getenforce'], check_return=True, single_line=True).lower() + if output not in _SELINUX_MODE: + raise device_errors.CommandFailedError( + 'Unexpected getenforce output: %s' % output) + return _SELINUX_MODE[output] + + @decorators.WithTimeoutAndRetriesFromInstance() + def SetEnforce(self, enabled, timeout=None, retries=None): + """Modify the mode SELinux is running in. + + Args: + enabled: a boolean indicating whether to put SELinux in encorcing mode + (if True), or permissive mode (otherwise). + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + self.RunShellCommand( + ['setenforce', '1' if int(enabled) else '0'], as_root=True, + check_return=True) + @decorators.WithTimeoutAndRetriesFromInstance() def TakeScreenshot(self, host_path=None, timeout=None, retries=None): """Takes a screenshot of the device. @@ -2440,7 +2575,8 @@ class DeviceUtils(object): logger.info('Restarting adbd on device.') with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: self.WriteFile(script.name, _RESTART_ADBD_SCRIPT) - self.RunShellCommand(['source', script.name], as_root=True) + self.RunShellCommand( + ['source', script.name], check_return=True, as_root=True) self.adb.WaitForDevice() @decorators.WithTimeoutAndRetriesFromInstance() @@ -2456,7 +2592,7 @@ class DeviceUtils(object): 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, check_return=True) + output = self.RunShellCommand(cmd, shell=True, check_return=True) if output: logger.warning('Possible problem when granting permissions. Blacklist ' 'may need to be updated.') @@ -2509,5 +2645,5 @@ class DeviceUtils(object): if screen_test(): logger.info('Screen already in expected state.') return - self.RunShellCommand('input keyevent 26') + self.SendKeyEvent(keyevent.KEYCODE_POWER) timeout_retry.WaitFor(screen_test, wait_period=1) diff --git a/catapult/devil/devil/android/device_utils_devicetest.py b/catapult/devil/devil/android/device_utils_devicetest.py index 54ab7dbf..e69cc909 100755 --- a/catapult/devil/devil/android/device_utils_devicetest.py +++ b/catapult/devil/devil/android/device_utils_devicetest.py @@ -9,6 +9,7 @@ The test will invoke real devices """ import os +import posixpath import sys import tempfile import unittest @@ -87,12 +88,12 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase): device_file_path = "%s/%s" % (_DEVICE_DIR, file_name) self.adb.Push(host_file_path, device_file_path) self.device.PushChangedFiles([(host_file_path, device_file_path)]) - result = self.device.RunShellCommand(['cat', device_file_path], - single_line=True) + result = self.device.RunShellCommand( + ['cat', device_file_path], check_return=True, single_line=True) self.assertEqual(_OLD_CONTENTS, result) cmd_helper.RunCmd(['rm', host_file_path]) - self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR]) + self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True) def testPushChangedFiles_singleFileChange(self): (host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS) @@ -102,12 +103,12 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase): with open(host_file_path, 'w') as f: f.write(_NEW_CONTENTS) self.device.PushChangedFiles([(host_file_path, device_file_path)]) - result = self.device.RunShellCommand(['cat', device_file_path], - single_line=True) + result = self.device.RunShellCommand( + ['cat', device_file_path], check_return=True, single_line=True) self.assertEqual(_NEW_CONTENTS, result) cmd_helper.RunCmd(['rm', host_file_path]) - self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR]) + self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True) def testDeleteFiles(self): host_tmp_dir = tempfile.mkdtemp() @@ -120,11 +121,11 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase): cmd_helper.RunCmd(['rm', host_file_path]) self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)], delete_device_stale=True) - result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True) - self.assertEqual('', result) + filenames = self.device.ListDirectory(_DEVICE_DIR) + self.assertEqual([], filenames) cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir]) - self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR]) + self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True) def testPushAndDeleteFiles_noSubDir(self): host_tmp_dir = tempfile.mkdtemp() @@ -144,14 +145,15 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase): self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)], delete_device_stale=True) - result = self.device.RunShellCommand(['cat', device_file_path1], - single_line=True) + result = self.device.RunShellCommand( + ['cat', device_file_path1], check_return=True, single_line=True) self.assertEqual(_NEW_CONTENTS, result) - result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True) - self.assertEqual(file_name1, result) - self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR]) + filenames = self.device.ListDirectory(_DEVICE_DIR) + self.assertEqual([file_name1], filenames) + cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir]) + self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True) def testPushAndDeleteFiles_SubDir(self): host_tmp_dir = tempfile.mkdtemp() @@ -187,31 +189,31 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase): self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)], delete_device_stale=True) - result = self.device.RunShellCommand(['cat', device_file_path1], - single_line=True) + result = self.device.RunShellCommand( + ['cat', device_file_path1], check_return=True, single_line=True) self.assertEqual(_NEW_CONTENTS, result) - result = self.device.RunShellCommand(['ls', _DEVICE_DIR]) - self.assertIn(file_name1, result) - self.assertIn(_SUB_DIR1, result) - self.assertIn(_SUB_DIR, result) - self.assertEqual(3, len(result)) + filenames = self.device.ListDirectory(_DEVICE_DIR) + self.assertIn(file_name1, filenames) + self.assertIn(_SUB_DIR1, filenames) + self.assertIn(_SUB_DIR, filenames) + self.assertEqual(3, len(filenames)) - result = self.device.RunShellCommand(['cat', device_file_path3], - single_line=True) + result = self.device.RunShellCommand( + ['cat', device_file_path3], check_return=True, single_line=True) self.assertEqual(_OLD_CONTENTS, result) - result = self.device.RunShellCommand(["ls", "%s/%s/%s" - % (_DEVICE_DIR, _SUB_DIR, _SUB_DIR2)], - single_line=True) - self.assertEqual('', result) + filenames = self.device.ListDirectory( + posixpath.join(_DEVICE_DIR, _SUB_DIR, _SUB_DIR2)) + self.assertEqual([], filenames) - self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR]) cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir]) + self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True) def testRestartAdbd(self): def get_adbd_pid(): - ps_output = self.device.RunShellCommand(['ps']) + # 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] diff --git a/catapult/devil/devil/android/device_utils_test.py b/catapult/devil/devil/android/device_utils_test.py index a77d1202..24902096 100755 --- a/catapult/devil/devil/android/device_utils_test.py +++ b/catapult/devil/devil/android/device_utils_test.py @@ -22,6 +22,7 @@ from devil.android import device_signal from devil.android import device_utils from devil.android.sdk import adb_wrapper from devil.android.sdk import intent +from devil.android.sdk import keyevent from devil.android.sdk import version_codes from devil.utils import cmd_helper from devil.utils import mock_calls @@ -194,7 +195,8 @@ class DeviceUtilsTest(mock_calls.TestCase): props = props or [] ret = [sdcard, 'TOKEN'] + props return (self.call.device.RunShellCommand( - AnyStringWith('getprop'), check_return=True, large_output=True), ret) + AnyStringWith('getprop'), + shell=True, check_return=True, large_output=True), ret) class DeviceUtilsEqTest(DeviceUtilsTest): @@ -282,11 +284,30 @@ class DeviceUtilsIsOnlineTest(DeviceUtilsTest): class DeviceUtilsHasRootTest(DeviceUtilsTest): def testHasRoot_true(self): - with self.assertCall(self.call.adb.Shell('ls /root'), 'foo\n'): + with self.patch_call(self.call.device.product_name, + return_value='notasailfish'), ( + self.assertCall(self.call.adb.Shell('ls /root'), 'foo\n')): + self.assertTrue(self.device.HasRoot()) + + def testhasRootSpecial_true(self): + with self.patch_call(self.call.device.product_name, + return_value='sailfish'), ( + self.assertCall(self.call.adb.Shell('getprop service.adb.root'), + '1\n')): self.assertTrue(self.device.HasRoot()) def testHasRoot_false(self): - with self.assertCall(self.call.adb.Shell('ls /root'), self.ShellError()): + with self.patch_call(self.call.device.product_name, + return_value='notasailfish'), ( + self.assertCall(self.call.adb.Shell('ls /root'), + self.ShellError())): + self.assertFalse(self.device.HasRoot()) + + def testHasRootSpecial_false(self): + with self.patch_call(self.call.device.product_name, + return_value='sailfish'), ( + self.assertCall(self.call.adb.Shell('getprop service.adb.root'), + '\n')): self.assertFalse(self.device.HasRoot()) @@ -849,38 +870,47 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): def testRunShellCommand_commandAsList(self): with self.assertCall(self.call.adb.Shell('pm list packages'), ''): - self.device.RunShellCommand(['pm', 'list', 'packages']) + self.device.RunShellCommand( + ['pm', 'list', 'packages'], check_return=True) def testRunShellCommand_commandAsListQuoted(self): with self.assertCall(self.call.adb.Shell("echo 'hello world' '$10'"), ''): - self.device.RunShellCommand(['echo', 'hello world', '$10']) + self.device.RunShellCommand( + ['echo', 'hello world', '$10'], check_return=True) def testRunShellCommand_commandAsString(self): with self.assertCall(self.call.adb.Shell('echo "$VAR"'), ''): - self.device.RunShellCommand('echo "$VAR"') + self.device.RunShellCommand( + 'echo "$VAR"', shell=True, check_return=True) def testNewRunShellImpl_withEnv(self): with self.assertCall( self.call.adb.Shell('VAR=some_string echo "$VAR"'), ''): - self.device.RunShellCommand('echo "$VAR"', env={'VAR': 'some_string'}) + self.device.RunShellCommand( + 'echo "$VAR"', shell=True, check_return=True, + env={'VAR': 'some_string'}) def testNewRunShellImpl_withEnvQuoted(self): with self.assertCall( self.call.adb.Shell('PATH="$PATH:/other/path" run_this'), ''): - self.device.RunShellCommand('run_this', env={'PATH': '$PATH:/other/path'}) + self.device.RunShellCommand( + ['run_this'], check_return=True, env={'PATH': '$PATH:/other/path'}) def testNewRunShellImpl_withEnv_failure(self): with self.assertRaises(KeyError): - self.device.RunShellCommand('some_cmd', env={'INVALID NAME': 'value'}) + self.device.RunShellCommand( + ['some_cmd'], check_return=True, env={'INVALID NAME': 'value'}) def testNewRunShellImpl_withCwd(self): with self.assertCall(self.call.adb.Shell('cd /some/test/path && ls'), ''): - self.device.RunShellCommand('ls', cwd='/some/test/path') + self.device.RunShellCommand( + ['ls'], check_return=True, cwd='/some/test/path') def testNewRunShellImpl_withCwdQuoted(self): with self.assertCall( self.call.adb.Shell("cd '/some test/path with/spaces' && ls"), ''): - self.device.RunShellCommand('ls', cwd='/some test/path with/spaces') + self.device.RunShellCommand( + ['ls'], check_return=True, cwd='/some test/path with/spaces') def testRunShellCommand_withHugeCmd(self): payload = 'hi! ' * 1024 @@ -890,8 +920,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')), self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd), (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')): - self.assertEquals([payload], - self.device.RunShellCommand(['echo', payload])) + self.assertEquals( + [payload], + self.device.RunShellCommand(['echo', payload], check_return=True)) def testRunShellCommand_withHugeCmdAndSu(self): payload = 'hi! ' * 1024 @@ -906,7 +937,8 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')): self.assertEquals( [payload], - self.device.RunShellCommand(['echo', payload], as_root=True)) + self.device.RunShellCommand( + ['echo', payload], check_return=True, as_root=True)) def testRunShellCommand_withSu(self): expected_cmd_without_su = "sh -c 'setprop service.adb.root 0'" @@ -915,7 +947,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): (self.call.device.NeedsSU(), True), (self.call.device._Su(expected_cmd_without_su), expected_cmd), (self.call.adb.Shell(expected_cmd), '')): - self.device.RunShellCommand('setprop service.adb.root 0', as_root=True) + self.device.RunShellCommand( + ['setprop', 'service.adb.root', '0'], + check_return=True, as_root=True) def testRunShellCommand_withRunAs(self): expected_cmd_without_run_as = "sh -c 'mkdir -p files'" @@ -924,7 +958,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): with self.assertCall(self.call.adb.Shell(expected_cmd), ''): self.device.RunShellCommand( ['mkdir', '-p', 'files'], - run_as='org.devil.test_package') + check_return=True, run_as='org.devil.test_package') def testRunShellCommand_withRunAsAndSu(self): expected_cmd_with_nothing = "sh -c 'mkdir -p files'" @@ -939,72 +973,86 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): (self.call.adb.Shell(expected_cmd), '')): self.device.RunShellCommand( ['mkdir', '-p', 'files'], - run_as='org.devil.test_package', + check_return=True, run_as='org.devil.test_package', as_root=True) def testRunShellCommand_manyLines(self): cmd = 'ls /some/path' with self.assertCall(self.call.adb.Shell(cmd), 'file1\nfile2\nfile3\n'): - self.assertEquals(['file1', 'file2', 'file3'], - self.device.RunShellCommand(cmd)) + self.assertEquals( + ['file1', 'file2', 'file3'], + self.device.RunShellCommand(cmd.split(), check_return=True)) def testRunShellCommand_manyLinesRawOutput(self): cmd = 'ls /some/path' with self.assertCall(self.call.adb.Shell(cmd), '\rfile1\nfile2\r\nfile3\n'): - self.assertEquals('\rfile1\nfile2\r\nfile3\n', - self.device.RunShellCommand(cmd, raw_output=True)) + self.assertEquals( + '\rfile1\nfile2\r\nfile3\n', + self.device.RunShellCommand( + cmd.split(), check_return=True, raw_output=True)) def testRunShellCommand_singleLine_success(self): cmd = 'echo $VALUE' with self.assertCall(self.call.adb.Shell(cmd), 'some value\n'): - self.assertEquals('some value', - self.device.RunShellCommand(cmd, single_line=True)) + self.assertEquals( + 'some value', + self.device.RunShellCommand( + cmd, shell=True, check_return=True, single_line=True)) def testRunShellCommand_singleLine_successEmptyLine(self): cmd = 'echo $VALUE' with self.assertCall(self.call.adb.Shell(cmd), '\n'): - self.assertEquals('', - self.device.RunShellCommand(cmd, single_line=True)) + self.assertEquals( + '', + self.device.RunShellCommand( + cmd, shell=True, check_return=True, single_line=True)) def testRunShellCommand_singleLine_successWithoutEndLine(self): cmd = 'echo -n $VALUE' with self.assertCall(self.call.adb.Shell(cmd), 'some value'): - self.assertEquals('some value', - self.device.RunShellCommand(cmd, single_line=True)) + self.assertEquals( + 'some value', + self.device.RunShellCommand( + cmd, shell=True, check_return=True, single_line=True)) def testRunShellCommand_singleLine_successNoOutput(self): cmd = 'echo -n $VALUE' with self.assertCall(self.call.adb.Shell(cmd), ''): - self.assertEquals('', - self.device.RunShellCommand(cmd, single_line=True)) + self.assertEquals( + '', + self.device.RunShellCommand( + cmd, shell=True, check_return=True, single_line=True)) def testRunShellCommand_singleLine_failTooManyLines(self): cmd = 'echo $VALUE' with self.assertCall(self.call.adb.Shell(cmd), 'some value\nanother value\n'): with self.assertRaises(device_errors.CommandFailedError): - self.device.RunShellCommand(cmd, single_line=True) + self.device.RunShellCommand( + cmd, shell=True, check_return=True, single_line=True) def testRunShellCommand_checkReturn_success(self): cmd = 'echo $ANDROID_DATA' output = '/data\n' with self.assertCall(self.call.adb.Shell(cmd), output): - self.assertEquals([output.rstrip()], - self.device.RunShellCommand(cmd, check_return=True)) + self.assertEquals( + [output.rstrip()], + self.device.RunShellCommand(cmd, shell=True, check_return=True)) def testRunShellCommand_checkReturn_failure(self): cmd = 'ls /root' output = 'opendir failed, Permission denied\n' with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)): with self.assertRaises(device_errors.AdbCommandFailedError): - self.device.RunShellCommand(cmd, check_return=True) + self.device.RunShellCommand(cmd.split(), check_return=True) def testRunShellCommand_checkReturn_disabled(self): cmd = 'ls /root' output = 'opendir failed, Permission denied\n' with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)): - self.assertEquals([output.rstrip()], - self.device.RunShellCommand(cmd, check_return=False)) + self.assertEquals( + [output.rstrip()], + self.device.RunShellCommand(cmd.split(), check_return=False)) def testRunShellCommand_largeOutput_enabled(self): cmd = 'echo $VALUE' @@ -1019,13 +1067,13 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): self.assertEquals( ['something'], self.device.RunShellCommand( - cmd, large_output=True, check_return=True)) + cmd, shell=True, large_output=True, check_return=True)) def testRunShellCommand_largeOutput_disabledNoTrigger(self): cmd = 'something' with self.assertCall(self.call.adb.Shell(cmd), self.ShellError('')): with self.assertRaises(device_errors.AdbCommandFailedError): - self.device.RunShellCommand(cmd, check_return=True) + self.device.RunShellCommand([cmd], check_return=True) def testRunShellCommand_largeOutput_disabledTrigger(self): cmd = 'echo $VALUE' @@ -1038,8 +1086,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest): (self.call.adb.Shell(cmd_redirect)), (self.call.device.ReadFile(mock.ANY, force_pull=True), 'something')): - self.assertEquals(['something'], - self.device.RunShellCommand(cmd, check_return=True)) + self.assertEquals( + ['something'], + self.device.RunShellCommand(cmd, shell=True, check_return=True)) class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest): @@ -1048,7 +1097,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest): with self.assertCall( self.call.device.RunShellCommand( 'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"', - check_return=True), + shell=True, check_return=True), ['This line contains foo', 'PIPESTATUS: 0 0']): self.assertEquals(['This line contains foo'], self.device._RunPipedShellCommand('ps | grep foo')) @@ -1057,7 +1106,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest): with self.assertCall( self.call.device.RunShellCommand( 'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"', - check_return=True), + shell=True, check_return=True), ['PIPESTATUS: 1 0']): with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec: self.device._RunPipedShellCommand('ps | grep foo') @@ -1067,7 +1116,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest): with self.assertCall( self.call.device.RunShellCommand( 'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"', - check_return=True), + shell=True, check_return=True), ['PIPESTATUS: 0 1']): with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec: self.device._RunPipedShellCommand('ps | grep foo') @@ -1077,7 +1126,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest): with self.assertCall( self.call.device.RunShellCommand( 'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"', - check_return=True), + shell=True, check_return=True), ['foo.bar'] * 256 + ['foo.ba']): with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec: self.device._RunPipedShellCommand('ps | grep foo') @@ -1336,7 +1385,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest): with self.assertCalls( self.call.device.RunShellCommand( 'p=test.package;am instrument "$p"/.TestInstrumentation', - check_return=True, large_output=True)): + shell=True, check_return=True, large_output=True)): self.device.StartInstrumentation( 'test.package/.TestInstrumentation', finish=False, raw=False, extras=None) @@ -1345,7 +1394,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest): with self.assertCalls( (self.call.device.RunShellCommand( 'p=test.package;am instrument -w "$p"/.TestInstrumentation', - check_return=True, large_output=True), + shell=True, check_return=True, large_output=True), ['OK (1 test)'])): output = self.device.StartInstrumentation( 'test.package/.TestInstrumentation', @@ -1356,7 +1405,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest): with self.assertCalls( self.call.device.RunShellCommand( 'p=test.package;am instrument -r "$p"/.TestInstrumentation', - check_return=True, large_output=True)): + shell=True, check_return=True, large_output=True)): self.device.StartInstrumentation( 'test.package/.TestInstrumentation', finish=False, raw=True, extras=None) @@ -1366,7 +1415,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest): self.call.device.RunShellCommand( 'p=test.package;am instrument -e "$p".foo Foo -e bar \'Val \'"$p" ' '"$p"/.TestInstrumentation', - check_return=True, large_output=True)): + shell=True, check_return=True, large_output=True)): self.device.StartInstrumentation( 'test.package/.TestInstrumentation', finish=False, raw=False, extras={'test.package.foo': 'Foo', @@ -1611,7 +1660,7 @@ class DeviceUtilsPushChangedFilesZippedTest(DeviceUtilsTest): '/test/temp/file/tmp.zip', '/test/sdcard/foo123.zip'), self.call.device.RunShellCommand( 'unzip /test/sdcard/foo123.zip&&chmod -R 777 /test/dir', - as_root=True, + shell=True, as_root=True, env={'PATH': '/data/local/tmp/bin:$PATH'}, check_return=True)): self.assertTrue(self.device._PushChangedFilesZipped(test_files, @@ -1632,7 +1681,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest): def testPathExists_pathExists(self): with self.assertCall( self.call.device.RunShellCommand( - "test -e '/path/file exists'", + ['test', '-e', '/path/file exists'], as_root=False, check_return=True, timeout=10, retries=0), []): self.assertTrue(self.device.PathExists('/path/file exists')) @@ -1640,7 +1689,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest): def testPathExists_multiplePathExists(self): with self.assertCall( self.call.device.RunShellCommand( - "test -e '/path 1' -a -e /path2", + ['test', '-e', '/path 1', '-a', '-e', '/path2'], as_root=False, check_return=True, timeout=10, retries=0), []): self.assertTrue(self.device.PathExists(('/path 1', '/path2'))) @@ -1648,7 +1697,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest): def testPathExists_pathDoesntExist(self): with self.assertCall( self.call.device.RunShellCommand( - "test -e /path/file.not.exists", + ['test', '-e', '/path/file.not.exists'], as_root=False, check_return=True, timeout=10, retries=0), self.ShellError()): self.assertFalse(self.device.PathExists('/path/file.not.exists')) @@ -1656,7 +1705,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest): def testPathExists_asRoot(self): with self.assertCall( self.call.device.RunShellCommand( - "test -e /root/path/exists", + ['test', '-e', '/root/path/exists'], as_root=True, check_return=True, timeout=10, retries=0), self.ShellError()): self.assertFalse( @@ -1665,7 +1714,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest): def testFileExists_pathDoesntExist(self): with self.assertCall( self.call.device.RunShellCommand( - "test -e /path/file.not.exists", + ['test', '-e', '/path/file.not.exists'], as_root=False, check_return=True, timeout=10, retries=0), self.ShellError()): self.assertFalse(self.device.FileExists('/path/file.not.exists')) @@ -1829,7 +1878,7 @@ class DeviceUtilsReadFileTest(DeviceUtilsTest): self.call.device.RunShellCommand( 'SRC=/this/big/file/can.be.read.with.su DEST=/sdcard/tmp/on.device;' 'cp "$SRC" "$DEST" && chmod 666 "$DEST"', - as_root=True, check_return=True), + shell=True, as_root=True, check_return=True), (self.call.device._ReadFileWithPull('/sdcard/tmp/on.device'), contents)): self.assertEqual( @@ -2154,7 +2203,8 @@ class DeviceUtilsEnsureCacheInitializedTest(DeviceUtilsTest): self.assertIsNone(self.device._cache['token']) with self.assertCall( self.call.device.RunShellCommand( - AnyStringWith('getprop'), check_return=True, large_output=True), + AnyStringWith('getprop'), + shell=True, check_return=True, large_output=True), ['/sdcard', 'TOKEN']): self.device._EnsureCacheInitialized() self.assertIsNotNone(self.device._cache['token']) @@ -2163,7 +2213,8 @@ class DeviceUtilsEnsureCacheInitializedTest(DeviceUtilsTest): self.assertIsNone(self.device._cache['token']) with self.assertCall( self.call.device.RunShellCommand( - AnyStringWith('getprop'), check_return=True, large_output=True), + AnyStringWith('getprop'), + shell=True, check_return=True, large_output=True), self.TimeoutError()): with self.assertRaises(device_errors.CommandTimeoutError): self.device._EnsureCacheInitialized() @@ -2232,57 +2283,202 @@ class DeviceUtilsSetPropTest(DeviceUtilsTest): class DeviceUtilsGetPidsTest(DeviceUtilsTest): + def setUp(self): + super(DeviceUtilsGetPidsTest, 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 1236 100 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): + 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, + return_value='ZZZ99Z'): + with self.assertCall( + self.call.device._RunPipedShellCommand( + 'ps -e | grep -F example.process'), []): + self.device.GetPids('example.process') def testGetPids_noMatches(self): - with self.assertCall( - self.call.device._RunPipedShellCommand('ps | grep -F does.not.match'), - []): - self.assertEqual({}, self.device.GetPids('does.not.match')) + 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')) def testGetPids_oneMatch(self): - with self.assertCall( - self.call.device._RunPipedShellCommand('ps | grep -F one.match'), - ['user 1001 100 1024 1024 ffffffff 00000000 one.match']): - self.assertEqual( - {'one.match': ['1001']}, - self.device.GetPids('one.match')) + 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')) def testGetPids_multipleMatches(self): - with self.assertCall( - self.call.device._RunPipedShellCommand('ps | grep -F match'), - ['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']): - self.assertEqual( - {'one.match': ['1001'], - 'two.match': ['1002'], - 'three.match': ['1003']}, - self.device.GetPids('match')) - - def testGetPids_exactMatch(self): - with self.assertCall( - self.call.device._RunPipedShellCommand('ps | grep -F exact.match'), - ['user 1000 100 1024 1024 ffffffff 00000000 not.exact.match', - 'user 1234 100 1024 1024 ffffffff 00000000 exact.match']): - self.assertEqual( - {'not.exact.match': ['1000'], 'exact.match': ['1234']}, - self.device.GetPids('exact.match')) + 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')) def testGetPids_quotable(self): - with self.assertCall( - self.call.device._RunPipedShellCommand("ps | grep -F 'my$process'"), - ['user 1234 100 1024 1024 ffffffff 00000000 my$process']): - self.assertEqual( - {'my$process': ['1234']}, self.device.GetPids('my$process')) + 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')) def testGetPids_multipleInstances(self): - with self.assertCall( - self.call.device._RunPipedShellCommand('ps | grep -F foo'), - ['user 1000 100 1024 1024 ffffffff 00000000 foo', - 'user 1234 100 1024 1024 ffffffff 00000000 foo']): - self.assertEqual( - {'foo': ['1000', '1234']}, - self.device.GetPids('foo')) + 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 foo'), + self._grepOutput('foo')): + self.assertEqual( + {'foo': ['1000', '1236']}, + self.device.GetPids('foo')) + + def testGetPids_allProcesses(self): + with self.patch_call(self.call.device.build_version_sdk, + return_value=version_codes.LOLLIPOP): + with self.assertCall( + self.call.device.RunShellCommand( + ['ps'], check_return=True, large_output=True), + self.sample_output): + self.assertEqual( + {'one.match': ['1001'], + 'two.match': ['1002'], + 'three.match': ['1003'], + 'my$process': ['1234'], + 'foo': ['1000', '1236']}, + self.device.GetPids()) + + def testGetApplicationPids_notFound(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')): + # No PIDs found, process name should be exact match. + self.assertEqual([], self.device.GetApplicationPids('match')) + + def testGetApplicationPids_foundOne(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(['1001'], self.device.GetApplicationPids('one.match')) + + def testGetApplicationPids_foundMany(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 foo'), + self._grepOutput('foo')): + self.assertEqual( + ['1000', '1236'], + self.device.GetApplicationPids('foo')) + + def testGetApplicationPids_atMostOneNotFound(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')): + # No PIDs found, process name should be exact match. + self.assertEqual( + None, + self.device.GetApplicationPids('match', at_most_one=True)) + + def testGetApplicationPids_atMostOneFound(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( + '1001', + self.device.GetApplicationPids('one.match', at_most_one=True)) + + def testGetApplicationPids_atMostOneFoundTooMany(self): + with self.patch_call(self.call.device.build_version_sdk, + return_value=version_codes.LOLLIPOP): + with self.assertRaises(device_errors.CommandFailedError): + with self.assertCall( + self.call.device._RunPipedShellCommand('ps | grep -F foo'), + self._grepOutput('foo')): + self.device.GetApplicationPids('foo', at_most_one=True) + + +class DeviceUtilsGetSetEnforce(DeviceUtilsTest): + + def testGetEnforce_Enforcing(self): + with self.assertCall(self.call.adb.Shell('getenforce'), 'Enforcing'): + self.assertEqual(True, self.device.GetEnforce()) + + def testGetEnforce_Permissive(self): + with self.assertCall(self.call.adb.Shell('getenforce'), 'Permissive'): + self.assertEqual(False, self.device.GetEnforce()) + + def testGetEnforce_Disabled(self): + with self.assertCall(self.call.adb.Shell('getenforce'), 'Disabled'): + self.assertEqual(None, self.device.GetEnforce()) + + def testSetEnforce_Enforcing(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 1'), '')): + self.device.SetEnforce(enabled=True) + + def testSetEnforce_Permissive(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 0'), '')): + self.device.SetEnforce(enabled=False) + + def testSetEnforce_EnforcingWithInt(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 1'), '')): + self.device.SetEnforce(enabled=1) + + def testSetEnforce_PermissiveWithInt(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 0'), '')): + self.device.SetEnforce(enabled=0) + + def testSetEnforce_EnforcingWithStr(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 1'), '')): + self.device.SetEnforce(enabled='1') + + def testSetEnforce_PermissiveWithStr(self): + with self.assertCalls( + (self.call.device.NeedsSU(), False), + (self.call.adb.Shell('setenforce 0'), '')): + self.device.SetEnforce(enabled='0') # Not recommended but it works! class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest): @@ -2539,7 +2735,7 @@ class DeviceUtilsRestartAdbdTest(DeviceUtilsTest): self.adb, suffix='.sh'), MockTempFile(mock_temp_file)), self.call.device.WriteFile(mock.ANY, mock.ANY), (self.call.device.RunShellCommand( - ['source', mock_temp_file], as_root=True)), + ['source', mock_temp_file], check_return=True, as_root=True)), self.call.adb.WaitForDevice()): self.device.RestartAdbd() @@ -2560,7 +2756,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest): return_value=version_codes.MARSHMALLOW): with self.assertCalls( (self.call.device.RunShellCommand( - permissions_cmd, check_return=True), [])): + permissions_cmd, shell=True, check_return=True), [])): self.device.GrantPermissions('package', ['p1']) def testGrantPermissions_multiple(self): @@ -2569,7 +2765,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest): return_value=version_codes.MARSHMALLOW): with self.assertCalls( (self.call.device.RunShellCommand( - permissions_cmd, check_return=True), [])): + permissions_cmd, shell=True, check_return=True), [])): self.device.GrantPermissions('package', ['p1', 'p2']) def testGrantPermissions_WriteExtrnalStorage(self): @@ -2580,7 +2776,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest): return_value=version_codes.MARSHMALLOW): with self.assertCalls( (self.call.device.RunShellCommand( - permissions_cmd, check_return=True), [])): + permissions_cmd, shell=True, check_return=True), [])): self.device.GrantPermissions( 'package', ['android.permission.WRITE_EXTERNAL_STORAGE']) @@ -2652,7 +2848,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest): def testSetScreen_on(self): with self.assertCalls( (self.call.device.IsScreenOn(), False), - (self.call.device.RunShellCommand('input keyevent 26'), []), + (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None), (self.call.device.IsScreenOn(), True)): self.device.SetScreen(True) @@ -2660,7 +2856,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest): def testSetScreen_off(self): with self.assertCalls( (self.call.device.IsScreenOn(), True), - (self.call.device.RunShellCommand('input keyevent 26'), []), + (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None), (self.call.device.IsScreenOn(), False)): self.device.SetScreen(False) @@ -2668,7 +2864,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest): def testSetScreen_slow(self): with self.assertCalls( (self.call.device.IsScreenOn(), True), - (self.call.device.RunShellCommand('input keyevent 26'), []), + (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None), (self.call.device.IsScreenOn(), True), (self.call.device.IsScreenOn(), True), (self.call.device.IsScreenOn(), False)): diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py index 2c8cc3c2..b2ee8b16 100644 --- a/catapult/devil/devil/android/flag_changer.py +++ b/catapult/devil/devil/android/flag_changer.py @@ -2,10 +2,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import contextlib import logging import posixpath import re +from devil.android.sdk import version_codes + + logger = logging.getLogger(__name__) @@ -16,6 +20,42 @@ _QUOTES = '"\'' # Either a single or a double quote. _ESCAPE = '\\' # A backslash. +@contextlib.contextmanager +def CustomCommandLineFlags(device, cmdline_name, flags): + """Context manager to change Chrome's command line temporarily. + + Example: + + with flag_changer.TemporaryCommandLineFlags(device, name, flags): + # Launching Chrome will use the provided flags. + + # Previous set of flags on the device is now restored. + + Args: + device: A DeviceUtils instance. + 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) + try: + changer = FlagChanger(device, cmdline_name) + try: + changer.ReplaceFlags(flags) + yield + finally: + changer.Restore() + finally: + if needs_permissive: + device.SetEnforce(enabled=True) + + class FlagChanger(object): """Changes the flags Chrome runs with. @@ -33,21 +73,13 @@ class FlagChanger(object): """ self._device = device - unused_dir, basename = posixpath.split(cmdline_file) - self._cmdline_path = posixpath.join(_CMDLINE_DIR, basename) + if posixpath.sep in cmdline_file: + raise ValueError( + 'cmdline_file should be a file name only, do not include path' + ' separators in: %s' % cmdline_file) + self._cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file) - # TODO(catapult:#3112): Make this fail instead of warn after all clients - # have been switched. - if unused_dir: - logging.warning( - 'cmdline_file argument of %s() should be a file name only (not a' - ' full path).', type(self).__name__) - if cmdline_file != self._cmdline_path: - logging.warning( - 'Client supplied %r, but %r will be used instead.', - cmdline_file, self._cmdline_path) - - cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, basename) + cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file) if self._device.PathExists(cmdline_path_legacy): logging.warning( 'Removing legacy command line file %r.', cmdline_path_legacy) @@ -168,7 +200,7 @@ class FlagChanger(object): if command_line is not None: self._device.WriteFile(self._cmdline_path, command_line) else: - self._device.RunShellCommand('rm ' + self._cmdline_path) + self._device.RemovePath(self._cmdline_path, force=True) current_flags = self.GetCurrentFlags() logger.info('Flags now set on the device: %s', current_flags) @@ -262,14 +294,7 @@ def _QuoteFlag(flag): if value is None: return key - else: - # TODO(catapult:#3112): Remove this check when all clients comply. - if value[0] in _QUOTES and value[0] == value[-1]: - logging.warning( - 'Flag %s appears to be quoted, so will be passed as-is.', flag) - logging.warning( - 'Note: this behavior will be changed in the future. ' - 'Clients should pass values unquoted to prevent double-quoting.') - elif _RE_NEEDS_QUOTING.search(value): - value = '"%s"' % value.replace('"', r'\"') - return '='.join([key, value]) + + if _RE_NEEDS_QUOTING.search(value): + value = '"%s"' % value.replace('"', r'\"') + return '='.join([key, value]) diff --git a/catapult/devil/devil/android/flag_changer_devicetest.py b/catapult/devil/devil/android/flag_changer_devicetest.py index f5d19d60..b75504b5 100644 --- a/catapult/devil/devil/android/flag_changer_devicetest.py +++ b/catapult/devil/devil/android/flag_changer_devicetest.py @@ -39,10 +39,11 @@ class FlagChangerTest(device_test_case.DeviceTestCase): flag_changer._CMDLINE_DIR_LEGACY, _CMDLINE_FILE) def tearDown(self): + super(FlagChangerTest, self).tearDown() self.device.RemovePath( [self.cmdline_path, self.cmdline_path_legacy], force=True, as_root=True) - def testFlagChanger(self): + def testFlagChanger_restoreFlags(self): if not self.device.HasRoot(): self.skipTest('Test needs a rooted device') @@ -72,6 +73,16 @@ class FlagChangerTest(device_test_case.DeviceTestCase): changer.Restore(), ['--some', '--old', '--flags']) + def testFlagChanger_removeFlags(self): + self.device.RemovePath(self.cmdline_path, force=True) + self.assertFalse(self.device.PathExists(self.cmdline_path)) + + with flag_changer.CustomCommandLineFlags( + self.device, _CMDLINE_FILE, ['--some', '--flags']): + self.assertTrue(self.device.PathExists(self.cmdline_path)) + + self.assertFalse(self.device.PathExists(self.cmdline_path)) + if __name__ == '__main__': unittest.main() diff --git a/catapult/devil/devil/android/flag_changer_test.py b/catapult/devil/devil/android/flag_changer_test.py index f692bd62..5342cf44 100755 --- a/catapult/devil/devil/android/flag_changer_test.py +++ b/catapult/devil/devil/android/flag_changer_test.py @@ -52,6 +52,10 @@ class FlagChangerTest(unittest.TestCase): self.cmdline_path) self.assertFalse(self.device.PathExists(self.cmdline_path_legacy)) + def testFlagChanger_mustBeFileName(self): + with self.assertRaises(ValueError): + flag_changer.FlagChanger(self.device, '/data/local/chrome-command-line') + class ParseSerializeFlagsTest(unittest.TestCase): def _testQuoteFlag(self, flag, expected_quoted_flag): @@ -82,18 +86,17 @@ class ParseSerializeFlagsTest(unittest.TestCase): "--key=this is 'fine' too", '''--key="this is 'fine' too"''') def testQuoteFlag_withQuotedValue4(self): - with self.assertRaises(AssertionError): - # TODO(catapult:#3112) This test is broken in the current implementation; - # flags that appear to be quoted are left as-is and, thus, do not - # survive the round-trip. - self._testQuoteFlag( - "--key='I really want to keep these quotes'", - '''--key="'I really want to keep these quotes'"''') + self._testQuoteFlag( + "--key='I really want to keep these quotes'", + '''--key="'I really want to keep these quotes'"''') def testQuoteFlag_withQuotedValue5(self): self._testQuoteFlag( "--this is a strange=flag", '"--this is a strange=flag"') + def testQuoteFlag_withEmptyValue(self): + self._testQuoteFlag('--some-flag=', '--some-flag=') + def _testParseCmdLine(self, command_line, expected_flags): # Start with a command line, check that flags are parsed as expected. # pylint: disable=protected-access diff --git a/catapult/devil/devil/android/forwarder.py b/catapult/devil/devil/android/forwarder.py index b5a2bf14..244f555a 100644 --- a/catapult/devil/devil/android/forwarder.py +++ b/catapult/devil/devil/android/forwarder.py @@ -13,6 +13,7 @@ from devil import base_error from devil import devil_env from devil.android import device_errors from devil.android.constants import file_system +from devil.android.sdk import adb_wrapper from devil.android.valgrind_tools import base_tool from devil.utils import cmd_helper @@ -130,7 +131,7 @@ class Forwarder(object): device_serial = str(device) map_arg_lists = [ - ['--adb=' + devil_env.config.FetchPath('adb'), + ['--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=' + device_serial, '--map', str(device_port), str(host_port)] for device_port, host_port in port_pairs] @@ -203,7 +204,7 @@ class Forwarder(object): instance = Forwarder._GetInstanceLocked(None) unmap_all_cmd = [ instance._host_forwarder_path, - '--adb=%s' % devil_env.config.FetchPath('adb'), + '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=%s' % device.serial, '--unmap-all' ] @@ -307,7 +308,7 @@ class Forwarder(object): unmap_cmd = [ instance._host_forwarder_path, - '--adb=%s' % devil_env.config.FetchPath('adb'), + '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=%s' % serial, '--unmap', str(device_port) ] @@ -387,7 +388,10 @@ class Forwarder(object): forwarder_device_path_on_host, forwarder_device_path_on_device)]) - cmd = '%s %s' % (tool.GetUtilWrapper(), Forwarder._DEVICE_FORWARDER_PATH) + cmd = [Forwarder._DEVICE_FORWARDER_PATH] + wrapper = tool.GetUtilWrapper() + if wrapper: + cmd.insert(0, wrapper) device.RunShellCommand( cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER}, check_return=True) @@ -451,8 +455,10 @@ class Forwarder(object): if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH): return - cmd = '%s %s --kill-server' % (tool.GetUtilWrapper(), - Forwarder._DEVICE_FORWARDER_PATH) + cmd = [Forwarder._DEVICE_FORWARDER_PATH, '--kill-server'] + wrapper = tool.GetUtilWrapper() + if wrapper: + cmd.insert(0, wrapper) device.RunShellCommand( cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER}, check_return=True) diff --git a/catapult/devil/devil/android/install_commands.py b/catapult/devil/devil/android/install_commands.py index 5a06bf3f..c8da8696 100644 --- a/catapult/devil/devil/android/install_commands.py +++ b/catapult/devil/devil/android/install_commands.py @@ -42,7 +42,8 @@ def InstallCommands(device): '%s not found. Please build chromium_commands.' % chromium_commands_jar_path) - device.RunShellCommand(['mkdir', BIN_DIR, _FRAMEWORK_DIR]) + device.RunShellCommand( + ['mkdir', '-p', BIN_DIR, _FRAMEWORK_DIR], check_return=True) for command, main_class in _COMMANDS.iteritems(): shell_command = _SHELL_COMMAND_FORMAT % ( file_system.TEST_EXECUTABLE_DIR, main_class) @@ -54,4 +55,3 @@ def InstallCommands(device): device.adb.Push( chromium_commands_jar_path, '%s/chromium_commands.jar' % _FRAMEWORK_DIR) - diff --git a/catapult/devil/devil/android/logcat_monitor.py b/catapult/devil/devil/android/logcat_monitor.py index c99fda9f..0aece87d 100644 --- a/catapult/devil/devil/android/logcat_monitor.py +++ b/catapult/devil/devil/android/logcat_monitor.py @@ -23,7 +23,8 @@ logger = logging.getLogger(__name__) class LogcatMonitor(object): - _RECORD_THREAD_JOIN_WAIT = 2.0 + _RECORD_ITER_TIMEOUT = 2.0 + _RECORD_THREAD_JOIN_WAIT = 5.0 _WAIT_TIME = 0.2 _THREADTIME_RE_FORMAT = ( r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +' @@ -164,10 +165,16 @@ class LogcatMonitor(object): # Write the log with line buffering so the consumer sees each individual # line. for data in self._adb.Logcat(filter_specs=self._filter_specs, - logcat_format='threadtime'): + logcat_format='threadtime', + iter_timeout=self._RECORD_ITER_TIMEOUT): + if self._stop_recording_event.isSet(): + return + + if data is None: + # Logcat can yield None if the iter_timeout is hit. + continue + with self._record_file_lock: - if self._stop_recording_event.isSet(): - return if self._record_file and not self._record_file.closed: self._record_file.write(data + '\n') diff --git a/catapult/devil/devil/android/md5sum.py b/catapult/devil/devil/android/md5sum.py index 52706461..6dece9e8 100644 --- a/catapult/devil/devil/android/md5sum.py +++ b/catapult/devil/devil/android/md5sum.py @@ -89,7 +89,7 @@ def CalculateDeviceMd5Sums(paths, device): # Note: ":" is equivalent to "true". md5sum_script += ';:' try: - out = device.RunShellCommand(md5sum_script, check_return=True) + out = device.RunShellCommand(md5sum_script, shell=True, check_return=True) except device_errors.AdbShellCommandFailedError as e: # Push the binary only if it is found to not exist # (faster than checking up-front). @@ -103,10 +103,10 @@ def CalculateDeviceMd5Sums(paths, device): device.adb.Push(md5sum_dist_path, MD5SUM_DEVICE_LIB_PATH) else: mkdir_cmd = 'a=%s;[[ -e $a ]] || mkdir $a' % MD5SUM_DEVICE_LIB_PATH - device.RunShellCommand(mkdir_cmd, check_return=True) + device.RunShellCommand(mkdir_cmd, shell=True, check_return=True) device.adb.Push(md5sum_dist_bin_path, MD5SUM_DEVICE_BIN_PATH) - out = device.RunShellCommand(md5sum_script, check_return=True) + out = device.RunShellCommand(md5sum_script, shell=True, check_return=True) else: raise diff --git a/catapult/devil/devil/android/perf/cache_control.py b/catapult/devil/devil/android/perf/cache_control.py index 7bd0a4e7..27782b50 100644 --- a/catapult/devil/devil/android/perf/cache_control.py +++ b/catapult/devil/devil/android/perf/cache_control.py @@ -11,6 +11,5 @@ class CacheControl(object): def DropRamCaches(self): """Drops the filesystem ram caches for performance testing.""" - self._device.RunShellCommand('sync', as_root=True) + self._device.RunShellCommand(['sync'], check_return=True, as_root=True) self._device.WriteFile(CacheControl._DROP_CACHES, '3', as_root=True) - diff --git a/catapult/devil/devil/android/perf/perf_control.py b/catapult/devil/devil/android/perf/perf_control.py index d52d64e6..06a5db61 100644 --- a/catapult/devil/devil/android/perf/perf_control.py +++ b/catapult/devil/devil/android/perf/perf_control.py @@ -111,7 +111,7 @@ class PerfControl(object): 'done' ]) output = self._device.RunShellCommand( - script, cwd=self._CPU_PATH, check_return=True, as_root=True) + script, cwd=self._CPU_PATH, check_return=True, as_root=True, shell=True) output = '\n'.join(output).split('%~%') return zip(self._cpu_files, output[0::2], (int(c) for c in output[1::2])) diff --git a/catapult/devil/devil/android/perf/surface_stats_collector.py b/catapult/devil/devil/android/perf/surface_stats_collector.py index 49372ad2..25079f31 100644 --- a/catapult/devil/devil/android/perf/surface_stats_collector.py +++ b/catapult/devil/devil/android/perf/surface_stats_collector.py @@ -105,15 +105,17 @@ class SurfaceStatsCollector(object): # The command returns nothing if it is supported, otherwise returns many # lines of result just like 'dumpsys SurfaceFlinger'. results = self._device.RunShellCommand( - 'dumpsys SurfaceFlinger --latency-clear SurfaceView') + ['dumpsys', 'SurfaceFlinger', '--latency-clear', 'SurfaceView'], + check_return=True) return not len(results) def GetSurfaceFlingerPid(self): - results = self._device.RunShellCommand('ps | grep surfaceflinger') - if not results: + pids_dict = self._device.GetPids('surfaceflinger') + if not pids_dict: raise Exception('Unable to get surface flinger process id') - pid = results[0].split()[1] - return pid + # 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. @@ -156,7 +158,8 @@ class SurfaceStatsCollector(object): # the activity's main window are not updated when the main web content is # composited into a SurfaceView. results = self._device.RunShellCommand( - 'dumpsys SurfaceFlinger --latency SurfaceView') + ['dumpsys', 'SurfaceFlinger', '--latency', 'SurfaceView'], + check_return=True) if not len(results): return (None, None) diff --git a/catapult/devil/devil/android/perf/thermal_throttle.py b/catapult/devil/devil/android/perf/thermal_throttle.py index 5a0454ec..546a92e0 100644 --- a/catapult/devil/devil/android/perf/thermal_throttle.py +++ b/catapult/devil/devil/android/perf/thermal_throttle.py @@ -124,7 +124,8 @@ class ThermalThrottle(object): serial_number, temperature, degree_symbol) # Print temperature of battery, to give a system temperature - dumpsys_log = self._device.RunShellCommand('dumpsys battery') + dumpsys_log = self._device.RunShellCommand( + ['dumpsys', 'battery'], check_return=True) for line in dumpsys_log: if 'temperature' in line: btemp = float([s for s in line.split() if s.isdigit()][0]) / 10.0 diff --git a/catapult/devil/devil/android/sdk/adb_wrapper.py b/catapult/devil/devil/android/sdk/adb_wrapper.py index 8654d269..7f6b8d95 100644 --- a/catapult/devil/devil/android/sdk/adb_wrapper.py +++ b/catapult/devil/devil/android/sdk/adb_wrapper.py @@ -38,8 +38,8 @@ DEFAULT_RETRIES = 2 _ADB_VERSION_RE = re.compile(r'Android Debug Bridge version (\d+\.\d+\.\d+)') _EMULATOR_RE = re.compile(r'^emulator-[0-9]+$') _READY_STATE = 'device' -_VERITY_DISABLE_RE = re.compile('Verity (already)? disabled') -_VERITY_ENABLE_RE = re.compile('Verity (already)? enabled') +_VERITY_DISABLE_RE = re.compile(r'Verity (already )?disabled') +_VERITY_ENABLE_RE = re.compile(r'Verity (already )?enabled') def VerifyLocalFileExists(path): @@ -281,18 +281,21 @@ class AdbWrapper(object): device_serial=self._device_serial, check_error=check_error) - def _IterRunDeviceAdbCmd(self, args, timeout): + def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout): """Runs an adb command and returns an iterator over its output lines. Args: args: A list of arguments to adb. - timeout: Timeout in seconds. + iter_timeout: Timeout for each iteration in seconds. + timeout: Timeout for the entire command in seconds. Yields: The output of the command line by line. """ return cmd_helper.IterCmdOutputLines( - self._BuildAdbCmd(args, self._device_serial), timeout=timeout) + self._BuildAdbCmd(args, self._device_serial), + iter_timeout=iter_timeout, + timeout=timeout) def __eq__(self, other): """Consider instances equal if they refer to the same device. @@ -453,7 +456,9 @@ class AdbWrapper(object): VerifyLocalFileExists(local) except IOError: raise device_errors.AdbCommandFailedError( - cmd, 'File not found on host: %s' % local, device_serial=str(self)) + cmd, + 'File pulled from the device did not arrive on the host: %s' % local, + device_serial=str(self)) def Shell(self, command, expect_status=0, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES): @@ -548,8 +553,8 @@ class AdbWrapper(object): device_serial=self._device_serial) def Logcat(self, clear=False, dump=False, filter_specs=None, - logcat_format=None, ring_buffer=None, timeout=None, - retries=DEFAULT_RETRIES): + logcat_format=None, ring_buffer=None, iter_timeout=None, + timeout=None, retries=DEFAULT_RETRIES): """Get an iterable over the logcat output. Args: @@ -562,6 +567,9 @@ class AdbWrapper(object): ring_buffer: If set, a list of alternate ring buffers to request. Options include "main", "system", "radio", "events", "crash" or "all". The default is equivalent to ["main", "system", "crash"]. + iter_timeout: If set and neither clear nor dump is set, the number of + seconds to wait between iterations. If no line is found before the + given number of seconds elapses, the iterable will yield None. timeout: (optional) If set, timeout per try in seconds. If clear or dump is set, defaults to DEFAULT_TIMEOUT. retries: (optional) If clear or dump is set, the number of retries to @@ -587,7 +595,7 @@ class AdbWrapper(object): cmd.extend(filter_specs) if use_iter: - return self._IterRunDeviceAdbCmd(cmd, timeout) + return self._IterRunDeviceAdbCmd(cmd, iter_timeout, timeout) else: timeout = timeout if timeout is not None else DEFAULT_TIMEOUT return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines() @@ -745,7 +753,7 @@ class AdbWrapper(object): cmd.append('-k') cmd.append(package) output = self._RunDeviceAdbCmd(cmd, timeout, retries) - if 'Failure' in output: + if 'Failure' in output or 'Exception' in output: raise device_errors.AdbCommandFailedError( cmd, output, device_serial=self._device_serial) @@ -886,14 +894,14 @@ class AdbWrapper(object): def DisableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES): """Disable Marshmallow's Verity security feature""" output = self._RunDeviceAdbCmd(['disable-verity'], timeout, retries) - if output and _VERITY_DISABLE_RE.search(output): + if output and not _VERITY_DISABLE_RE.search(output): raise device_errors.AdbCommandFailedError( ['disable-verity'], output, device_serial=self._device_serial) def EnableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES): """Enable Marshmallow's Verity security feature""" output = self._RunDeviceAdbCmd(['enable-verity'], timeout, retries) - if output and _VERITY_ENABLE_RE.search(output): + if output and not _VERITY_ENABLE_RE.search(output): raise device_errors.AdbCommandFailedError( ['enable-verity'], output, device_serial=self._device_serial) diff --git a/catapult/devil/devil/android/sdk/adb_wrapper_test.py b/catapult/devil/devil/android/sdk/adb_wrapper_test.py new file mode 100755 index 00000000..ef086612 --- /dev/null +++ b/catapult/devil/devil/android/sdk/adb_wrapper_test.py @@ -0,0 +1,59 @@ +#!/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. + +""" +Unit tests for some APIs with conditional logic in adb_wrapper.py +""" + +import unittest + +from devil import devil_env +from devil.android import device_errors +from devil.android.sdk import adb_wrapper + +with devil_env.SysPath(devil_env.PYMOCK_PATH): + import mock # pylint: disable=import-error + + +class AdbWrapperTest(unittest.TestCase): + def setUp(self): + self.adb = adb_wrapper.AdbWrapper('ABC12345678') + + def _MockRunDeviceAdbCmd(self, return_value): + return mock.patch.object( + self.adb, + '_RunDeviceAdbCmd', + mock.Mock(side_effect=None, return_value=return_value)) + + def testDisableVerityWhenDisabled(self): + with self._MockRunDeviceAdbCmd('Verity already disabled on /system'): + self.adb.DisableVerity() + + def testDisableVerityWhenEnabled(self): + with self._MockRunDeviceAdbCmd( + 'Verity disabled on /system\nNow reboot your device for settings to ' + 'take effect'): + self.adb.DisableVerity() + + def testEnableVerityWhenEnabled(self): + with self._MockRunDeviceAdbCmd('Verity already enabled on /system'): + self.adb.EnableVerity() + + def testEnableVerityWhenDisabled(self): + with self._MockRunDeviceAdbCmd( + 'Verity enabled on /system\nNow reboot your device for settings to ' + 'take effect'): + self.adb.EnableVerity() + + def testFailEnableVerity(self): + with self._MockRunDeviceAdbCmd('error: closed'): + self.assertRaises( + device_errors.AdbCommandFailedError, self.adb.EnableVerity) + + def testFailDisableVerity(self): + with self._MockRunDeviceAdbCmd('error: closed'): + self.assertRaises( + device_errors.AdbCommandFailedError, self.adb.DisableVerity) + diff --git a/catapult/devil/devil/android/sdk/keyevent.py b/catapult/devil/devil/android/sdk/keyevent.py index 40f9416c..657dc963 100644 --- a/catapult/devil/devil/android/sdk/keyevent.py +++ b/catapult/devil/devil/android/sdk/keyevent.py @@ -22,6 +22,8 @@ KEYCODE_9 = 16 KEYCODE_DPAD_RIGHT = 22 +KEYCODE_POWER = 26 + KEYCODE_A = 29 KEYCODE_B = 30 KEYCODE_C = 31 diff --git a/catapult/devil/devil/android/sdk/shared_prefs.py b/catapult/devil/devil/android/sdk/shared_prefs.py index 58370bc0..2fa2e6a1 100644 --- a/catapult/devil/devil/android/sdk/shared_prefs.py +++ b/catapult/devil/devil/android/sdk/shared_prefs.py @@ -11,6 +11,8 @@ See e.g.: import logging import posixpath +from devil.android import device_errors +from devil.android.sdk import version_codes from xml.etree import ElementTree logger = logging.getLogger(__name__) @@ -270,6 +272,19 @@ class SharedPrefs(object): ['mkdir', '-p', posixpath.dirname(self.path)], as_root=True, check_return=True) self._device.WriteFile(self.path, str(self), as_root=True) + # Creating the directory/file can cause issues with SELinux if they did + # not already exist. As a workaround, apply the package's security context + # to the shared_prefs directory, which mimics the behavior of a file + # created by the app itself + if self._device.build_version_sdk >= version_codes.MARSHMALLOW: + security_context = self._GetSecurityContext(self.package) + if security_context == None: + raise device_errors.CommandFailedError( + 'Failed to get security context for %s' % self.package) + self._device.RunShellCommand( + ['chcon', '-R', security_context, + '/data/data/%s/shared_prefs' % self.package], + as_root=True, check_return=True) self._device.KillAll(self.package, exact=True, as_root=True, quiet=True) self._changed = False @@ -391,3 +406,15 @@ class SharedPrefs(object): pref.set(value) self._changed = True logger.info('Setting property: %s', pref) + + def _GetSecurityContext(self, package): + for line in self._device.RunShellCommand(['ls', '-Z', '/data/data/'], + as_root=True, check_return=True): + split_line = line.split() + # ls -Z output differs between Android versions, but the package is + # always last and the context always starts with "u:object" + if split_line[-1] == package: + for column in split_line: + if column.startswith('u:object'): + return column + return None diff --git a/catapult/devil/devil/android/sdk/shared_prefs_test.py b/catapult/devil/devil/android/sdk/shared_prefs_test.py index ff3b9a13..4c31c569 100755 --- a/catapult/devil/devil/android/sdk/shared_prefs_test.py +++ b/catapult/devil/devil/android/sdk/shared_prefs_test.py @@ -13,6 +13,7 @@ import unittest from devil import devil_env from devil.android import device_utils from devil.android.sdk import shared_prefs +from devil.android.sdk import version_codes with devil_env.SysPath(devil_env.PYMOCK_PATH): import mock # pylint: disable=import-error @@ -97,6 +98,8 @@ class SharedPrefsTest(unittest.TestCase): self.assertTrue(prefs.changed) def testCommit(self): + type(self.device).build_version_sdk = mock.PropertyMock( + return_value=version_codes.LOLLIPOP_MR1) prefs = shared_prefs.SharedPrefs( self.device, 'com.some.package', 'other_prefs.xml') self.assertFalse(self.device.FileExists(prefs.path)) # file does not exist @@ -131,6 +134,8 @@ class SharedPrefsTest(unittest.TestCase): self.assertEquals(self.device.WriteFile.call_args_list, []) # did not write def testAsContextManager_readAndWrite(self): + type(self.device).build_version_sdk = mock.PropertyMock( + return_value=version_codes.LOLLIPOP_MR1) with shared_prefs.SharedPrefs( self.device, 'com.some.package', 'prefs.xml') as prefs: prefs.SetBoolean('featureEnabled', True) diff --git a/catapult/devil/devil/android/sdk/version_codes.py b/catapult/devil/devil/android/sdk/version_codes.py index 095b59cc..3f03cbac 100644 --- a/catapult/devil/devil/android/sdk/version_codes.py +++ b/catapult/devil/devil/android/sdk/version_codes.py @@ -16,4 +16,5 @@ LOLLIPOP = 21 LOLLIPOP_MR1 = 22 MARSHMALLOW = 23 NOUGAT = 24 +NOUGAT_MR1 = 25 diff --git a/catapult/devil/devil/android/settings.py b/catapult/devil/devil/android/settings.py index 427592f2..886b2661 100644 --- a/catapult/devil/devil/android/settings.py +++ b/catapult/devil/devil/android/settings.py @@ -141,7 +141,8 @@ class ContentSettings(dict): # Example row: # 'Row: 0 _id=13, name=logging_id2, value=-1fccbaa546705b05' for row in self._device.RunShellCommand( - 'content query --uri content://%s' % self._table, as_root=True): + ['content', 'query', '--uri', 'content://%s' % self._table], + check_return=True, as_root=True): fields = row.split(', ') key = None value = None @@ -159,33 +160,29 @@ class ContentSettings(dict): def __getitem__(self, key): return self._device.RunShellCommand( - 'content query --uri content://%s --where "name=\'%s\'" ' - '--projection value' % (self._table, key), as_root=True).strip() + ['content', 'query', '--uri', 'content://%s' % self._table, + '--where', "name='%s'" % key], + check_return=True, as_root=True).strip() def __setitem__(self, key, value): if key in self: self._device.RunShellCommand( - 'content update --uri content://%s ' - '--bind value:%s:%s --where "name=\'%s\'"' % ( - self._table, - self._GetTypeBinding(value), value, key), - as_root=True) + ['content', 'update', '--uri', 'content://%s' % self._table, + '--bind', 'value:%s:%s' % (self._GetTypeBinding(value), value), + '--where', "name='%s'" % key], + check_return=True, as_root=True) else: self._device.RunShellCommand( - 'content insert --uri content://%s ' - '--bind name:%s:%s --bind value:%s:%s' % ( - self._table, - self._GetTypeBinding(key), key, - self._GetTypeBinding(value), value), - as_root=True) + ['content', 'insert', '--uri', 'content://%s' % self._table, + '--bind', 'name:%s:%s' % (self._GetTypeBinding(key), key), + '--bind', 'value:%s:%s' % (self._GetTypeBinding(value), value)], + check_return=True, as_root=True) def __delitem__(self, key): self._device.RunShellCommand( - 'content delete --uri content://%s ' - '--bind name:%s:%s' % ( - self._table, - self._GetTypeBinding(key), key), - as_root=True) + ['content', 'delete', '--uri', 'content://%s' % self._table, + '--bind', 'name:%s:%s' % (self._GetTypeBinding(key), key)], + check_return=True, as_root=True) def ConfigureContentSettings(device, desired_settings): @@ -270,8 +267,7 @@ commit transaction;""" % { 'columns': ', '.join(columns), 'values': ', '.join(["'%s'" % value for value in values]) } - output_msg = device.RunShellCommand('sqlite3 %s "%s"' % (db, cmd), - as_root=True) + output_msg = device.RunShellCommand( + ['sqlite3', db, cmd], check_return=True, as_root=True) if output_msg: logger.info(' '.join(output_msg)) - diff --git a/catapult/devil/devil/android/tools/device_monitor.py b/catapult/devil/devil/android/tools/device_monitor.py index 6139c770..49214a92 100755 --- a/catapult/devil/devil/android/tools/device_monitor.py +++ b/catapult/devil/devil/android/tools/device_monitor.py @@ -16,6 +16,7 @@ import logging import logging.handlers import os import re +import socket import sys import time @@ -42,8 +43,15 @@ CPU_TEMP_SENSORS = [ ] DEVICE_FILE_VERSION = 1 -DEVICE_FILE = os.path.join(os.path.expanduser('~'), - 'android_device_status.json') +# TODO(bpastene): Remove the old file once sysmon has been updated to read the +# new status file. +DEVICE_FILES = [ + os.path.join(os.path.expanduser('~'), 'android_device_status.json'), + os.path.join( + os.path.expanduser('~'), '.android', + '%s__android_device_status.json' % socket.gethostname().split('.')[0] + ), +] MEM_INFO_REGEX = re.compile(r'.*?\:\s*(\d+)\s*kB') # ex: 'MemTotal: 185735 kB' @@ -116,7 +124,8 @@ def get_device_status(device): # Process try: - lines = device.RunShellCommand('ps', check_return=True) + # 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: logging.exception('Unable to count process list.') @@ -128,7 +137,7 @@ def get_device_status(device): try: files = device.RunShellCommand( 'grep -lE "%s" /sys/class/thermal/thermal_zone*/type' % '|'.join( - CPU_TEMP_SENSORS), check_return=True) + CPU_TEMP_SENSORS), shell=True, check_return=True) except device_errors.AdbShellCommandFailedError: logging.exception('Unable to list thermal sensors.') for f in files: @@ -211,8 +220,9 @@ def main(argv): while True: start = time.time() status_dict = get_all_status(blacklist) - with open(DEVICE_FILE, 'wb') as f: - json.dump(status_dict, f, indent=2, sort_keys=True) + for device_file in DEVICE_FILES: + with open(device_file, 'wb') as f: + json.dump(status_dict, f, indent=2, sort_keys=True) logging.info('Got status of all devices in %.2fs.', time.time() - start) time.sleep(60) diff --git a/catapult/devil/devil/android/tools/device_monitor_test.py b/catapult/devil/devil/android/tools/device_monitor_test.py index 9c597938..e39e324b 100755 --- a/catapult/devil/devil/android/tools/device_monitor_test.py +++ b/catapult/devil/devil/android/tools/device_monitor_test.py @@ -43,8 +43,15 @@ class DeviceMonitorTest(unittest.TestCase): 'ps': ['headers', 'p1', 'p2', 'p3', 'p4', 'p5'], 'grep': ['/sys/class/thermal/thermal_zone0/type'], } - self.device.RunShellCommand = mock.MagicMock( - side_effect=lambda cmd, **kwargs: self.cmd_outputs[cmd.split()[0]]) + + def mock_run_shell(cmd, **_kwargs): + args = cmd.split() if isinstance(cmd, basestring) else cmd + try: + return self.cmd_outputs[args[0]] + except KeyError: + raise device_errors.AdbShellCommandFailedError(cmd, None, None) + + self.device.RunShellCommand = mock.MagicMock(side_effect=mock_run_shell) self.battery = mock.Mock() self.battery.GetBatteryInfo = mock.MagicMock( @@ -104,14 +111,7 @@ class DeviceMonitorTest(unittest.TestCase): def test_getStatsNoPs(self, get_devices, get_battery): get_devices.return_value = [self.device] get_battery.return_value = self.battery - def _throw_on_ps(cmd): - if cmd == 'ps': - raise device_errors.AdbShellCommandFailedError(cmd, None, None) - else: - return [] - self.device.RunShellCommand = mock.MagicMock( - side_effect=lambda cmd, **kwargs: - _throw_on_ps(cmd) + self.cmd_outputs[cmd.split()[0]]) + del self.cmd_outputs['ps'] # Throw exception on run shell ps command. # Should be same status dict but without process stats. expected_status_no_ps = self.expected_status.copy() @@ -125,14 +125,7 @@ class DeviceMonitorTest(unittest.TestCase): def test_getStatsNoSensors(self, get_devices, get_battery): get_devices.return_value = [self.device] get_battery.return_value = self.battery - def _throw_on_grep(cmd): - if cmd.startswith('grep'): - raise device_errors.AdbShellCommandFailedError(cmd, None, None) - else: - return [] - self.device.RunShellCommand = mock.MagicMock( - side_effect=lambda cmd, **kwargs: - _throw_on_grep(cmd) + self.cmd_outputs[cmd.split()[0]]) + del self.cmd_outputs['grep'] # Throw exception on run shell grep command. # Should be same status dict but without temp stats. expected_status_no_temp = self.expected_status.copy() @@ -173,4 +166,3 @@ class DeviceMonitorTest(unittest.TestCase): if __name__ == '__main__': sys.exit(unittest.main()) - diff --git a/catapult/devil/devil/android/tools/provision_devices.py b/catapult/devil/devil/android/tools/provision_devices.py index df75539f..7374290c 100755 --- a/catapult/devil/devil/android/tools/provision_devices.py +++ b/catapult/devil/devil/android/tools/provision_devices.py @@ -49,7 +49,8 @@ from devil.utils import timeout_retry logger = logging.getLogger(__name__) -_SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle'] +_SYSTEM_APP_DIRECTORIES = ['/system/app/', '/system/priv-app/'] +_SYSTEM_WEBVIEW_NAMES = ['webview', 'WebViewGoogle'] _CHROME_PACKAGE_REGEX = re.compile('.*chrom.*') _TOMBSTONE_REGEX = re.compile('tombstone.*') @@ -84,10 +85,12 @@ def ProvisionDevices( output_device_blacklist=None, reboot_timeout=None, remove_system_webview=False, + system_app_remove_list=None, wipe=True): blacklist = (device_blacklist.Blacklist(blacklist_file) if blacklist_file else None) + system_app_remove_list = system_app_remove_list or [] try: devices = script_common.GetDevices(devices, blacklist) except device_errors.NoDevicesError: @@ -122,7 +125,11 @@ def ProvisionDevices( lambda d: WaitForCharge(d, min_battery_level))) if remove_system_webview: - steps.append(ProvisionStep(RemoveSystemWebView)) + system_app_remove_list.extend(_SYSTEM_WEBVIEW_NAMES) + + if system_app_remove_list: + steps.append(ProvisionStep( + lambda d: RemoveSystemApps(d, system_app_remove_list))) steps.append(ProvisionStep(SetDate)) steps.append(ProvisionStep(CheckExternalStorage)) @@ -204,8 +211,9 @@ def WipeChromeData(device): _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, chrome.PACKAGE_INFO['chrome_stable'].package) device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), - check_return=True) - device.RunShellCommand('rm -rf /data/local/tmp/*', check_return=True) + shell=True, check_return=True) + device.RunShellCommand('rm -rf /data/local/tmp/*', + shell=True, check_return=True) else: device.EnableRoot() _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, @@ -218,17 +226,18 @@ def WipeChromeData(device): _WipeFileOrDir(device, '/data/local/.config/') _WipeFileOrDir(device, '/data/local/tmp/') device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), - check_return=True) + shell=True, check_return=True) except device_errors.CommandFailedError: logger.exception('Possible failure while wiping the device. ' 'Attempting to continue.') def _UninstallIfMatch(device, pattern, app_to_keep): - installed_packages = device.RunShellCommand(['pm', 'list', 'packages']) + installed_packages = device.RunShellCommand( + ['pm', 'list', 'packages'], check_return=True) installed_system_packages = [ - pkg.split(':')[1] for pkg in device.RunShellCommand(['pm', 'list', - 'packages', '-s'])] + pkg.split(':')[1] for pkg in device.RunShellCommand( + ['pm', 'list', 'packages', '-s'], check_return=True)] for package_output in installed_packages: package = package_output.split(":")[1] if pattern.match(package) and not package == app_to_keep: @@ -343,28 +352,43 @@ def DisableSystemChrome(device): check_return=True) -def RemoveSystemWebView(device): - if any(device.PathExists(p) for p in _SYSTEM_WEBVIEW_PATHS): - logger.info('System WebView exists and needs to be removed') - if device.HasRoot(): - # Disabled Marshmallow's Verity security feature - if device.build_version_sdk >= version_codes.MARSHMALLOW: - device.adb.DisableVerity() - device.Reboot() - device.WaitUntilFullyBooted() - device.EnableRoot() - - # This is required, e.g., to replace the system webview on a device. - device.adb.Remount() - device.RunShellCommand(['stop'], check_return=True) - device.RunShellCommand(['rm', '-rf'] + _SYSTEM_WEBVIEW_PATHS, - check_return=True) - device.RunShellCommand(['start'], check_return=True) - else: - logger.warning('Cannot remove system webview from a non-rooted device') - else: - logger.info('System WebView already removed') +def _RemoveSystemApp(device, system_app): + found_paths = [] + for directory in _SYSTEM_APP_DIRECTORIES: + path = os.path.join(directory, system_app) + if device.PathExists(path): + found_paths.append(path) + if not found_paths: + logger.warning('Could not find install location for system app %s', + system_app) + device.RemovePath(found_paths, force=True, recursive=True) +def RemoveSystemApps(device, system_app_remove_list): + """Attempts to remove the provided system apps from the given device. + + Arguments: + device: The device to remove the system apps from. + system_app_remove_list: A list of app names to remove, e.g. + ['WebViewGoogle', 'GoogleVrCore'] + """ + device.EnableRoot() + if device.HasRoot(): + # 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) + for system_app in system_app_remove_list: + _RemoveSystemApp(device, system_app) + device.RunShellCommand(['start'], check_return=True) + else: + raise device_errors.CommandFailedError( + 'Failed to remove system apps from non-rooted device', str(device)) def _ConfigureLocalProperties(device, java_debug=True): @@ -435,7 +459,8 @@ def SetDate(device): get_date_command.append('+"%Y%m%d.%H%M%S"') device_time = device.RunShellCommand( - get_date_command, as_root=True, single_line=True).replace('"', '') + get_date_command, check_return=True, + as_root=True, single_line=True).replace('"', '') device_time = datetime.datetime.strptime(device_time, "%Y%m%d.%H%M%S") correct_time = datetime.datetime.strptime(strgmtime, date_format) tdelta = (correct_time - device_time).seconds @@ -456,12 +481,13 @@ def SetDate(device): _set_and_verify_date, wait_period=1, max_tries=2): raise device_errors.CommandFailedError( 'Failed to set date & time.', device_serial=str(device)) + device.EnableRoot() device.BroadcastIntent( intent.Intent(action='android.intent.action.TIME_SET')) def LogDeviceProperties(device): - props = device.RunShellCommand('getprop', check_return=True) + props = device.RunShellCommand(['getprop'], check_return=True) for prop in props: logger.info(' %s', prop) @@ -527,7 +553,8 @@ def main(raw_args): help='Disable the system chrome from devices.') parser.add_argument( '--emulators', action='store_true', - help='provision only emulators and ignore usb devices') + help='provision only emulators and ignore usb devices ' + '(this will not wipe emulators)') parser.add_argument( '--max-battery-temp', type=int, metavar='NUM', help='Wait for the battery to have this temp or lower.') @@ -544,11 +571,14 @@ def main(raw_args): ' wait after each reboot ' '(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT) parser.add_argument( + '--remove-system-apps', nargs='*', dest='system_app_remove_list', + help='the names of system apps to remove') + parser.add_argument( '--remove-system-webview', action='store_true', help='Remove the system webview from devices.') parser.add_argument( '--skip-wipe', action='store_true', default=False, - help="don't wipe device data during provisioning") + help='do not wipe device data during provisioning') parser.add_argument( '-v', '--verbose', action='count', default=1, help='Log more information.') @@ -596,7 +626,8 @@ def main(raw_args): output_device_blacklist=args.output_device_blacklist, reboot_timeout=args.reboot_timeout, remove_system_webview=args.remove_system_webview, - wipe=not args.skip_wipe) + system_app_remove_list=args.system_app_remove_list, + wipe=not args.skip_wipe and not args.emulators) except (device_errors.DeviceUnreachableError, device_errors.NoDevicesError): logging.exception('Unable to provision local devices.') return exit_codes.INFRA diff --git a/catapult/devil/devil/android/tools/video_recorder.py b/catapult/devil/devil/android/tools/video_recorder.py index 28def21a..a91e6496 100755 --- a/catapult/devil/devil/android/tools/video_recorder.py +++ b/catapult/devil/devil/android/tools/video_recorder.py @@ -107,7 +107,7 @@ class VideoRecorder(object): time.strftime('%Y%m%dT%H%M%S', time.localtime()))) host_file_name = os.path.abspath(host_file_name) self._device.PullFile(self._device_file, host_file_name) - self._device.RunShellCommand('rm -f "%s"' % self._device_file) + self._device.RemovePath(self._device_file, force=True) return host_file_name diff --git a/catapult/devil/devil/utils/cmd_helper.py b/catapult/devil/devil/utils/cmd_helper.py index 269be5bb..06c105fc 100644 --- a/catapult/devil/devil/utils/cmd_helper.py +++ b/catapult/devil/devil/utils/cmd_helper.py @@ -12,6 +12,7 @@ import signal import string import StringIO import subprocess +import sys import time # fcntl is not available on Windows. @@ -93,10 +94,15 @@ def ShrinkToSnippet(cmd_parts, var_name, var_value): def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): + # preexec_fn isn't supported on windows. + if sys.platform == 'win32': + preexec_fn = None + else: + preexec_fn = lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL) + return subprocess.Popen( args=args, cwd=cwd, stdout=stdout, stderr=stderr, - shell=shell, close_fds=True, env=env, - preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) + shell=shell, close_fds=True, env=env, preexec_fn=preexec_fn) def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): @@ -213,8 +219,30 @@ class TimeoutError(Exception): return self._output -def _IterProcessStdout(process, timeout=None, buffer_size=4096, - poll_interval=1): +def _IterProcessStdout(process, iter_timeout=None, timeout=None, + buffer_size=4096, poll_interval=1): + """Iterate over a process's stdout. + + This is intentionally not public. + + Args: + process: The process in question. + iter_timeout: An optional length of time, in seconds, to wait in + between each iteration. If no output is received in the given + time, this generator will yield None. + timeout: An optional length of time, in seconds, during which + the process must finish. If it fails to do so, a TimeoutError + will be raised. + buffer_size: The maximum number of bytes to read (and thus yield) at once. + poll_interval: The length of time to wait in calls to `select.select`. + If iter_timeout is set, the remaining length of time in the iteration + may take precedence. + Raises: + TimeoutError: if timeout is set and the process does not complete. + Yields: + basestrings of data or None. + """ + assert fcntl, 'fcntl module is required' try: # Enable non-blocking reads from the child's stdout. @@ -223,10 +251,24 @@ def _IterProcessStdout(process, timeout=None, buffer_size=4096, fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) end_time = (time.time() + timeout) if timeout else None + iter_end_time = (time.time() + iter_timeout) if iter_timeout else None + while True: if end_time and time.time() > end_time: raise TimeoutError() - read_fds, _, _ = select.select([child_fd], [], [], poll_interval) + if iter_end_time and time.time() > iter_end_time: + yield None + iter_end_time = time.time() + iter_timeout + + if iter_end_time: + iter_aware_poll_interval = min( + poll_interval, + max(0, iter_end_time - time.time())) + else: + iter_aware_poll_interval = poll_interval + + read_fds, _, _ = select.select( + [child_fd], [], [], iter_aware_poll_interval) if child_fd in read_fds: data = os.read(child_fd, buffer_size) if not data: @@ -283,20 +325,21 @@ def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False, return process.returncode, str_output -def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False, - check_status=True): +def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None, + shell=False, check_status=True): """Executes a subprocess and continuously yields lines from its output. Args: args: List of arguments to the program, the program to execute is the first element. + iter_timeout: Timeout for each iteration, in seconds. + timeout: Timeout for the entire command, in seconds. 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. Must be True if args is a string and False if args is a sequence. check_status: A boolean indicating whether to check the exit status of the process after all output has been read. - Yields: The output of the subprocess, line by line. @@ -307,14 +350,44 @@ def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False, cmd = _ValidateAndLogCommand(args, cwd, shell) process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + return _IterCmdOutputLines( + process, cmd, iter_timeout=iter_timeout, timeout=timeout, + check_status=check_status) + +def _IterCmdOutputLines(process, cmd, iter_timeout=None, timeout=None, + check_status=True): buffer_output = '' - for data in _IterProcessStdout(process, timeout=timeout): + + iter_end = None + cur_iter_timeout = None + if iter_timeout: + iter_end = time.time() + iter_timeout + cur_iter_timeout = iter_timeout + + for data in _IterProcessStdout(process, iter_timeout=cur_iter_timeout, + timeout=timeout): + if iter_timeout: + # Check whether the current iteration has timed out. + cur_iter_timeout = iter_end - time.time() + if data is None or cur_iter_timeout < 0: + yield None + iter_end = time.time() + iter_timeout + continue + else: + assert data is not None, ( + 'Iteration received no data despite no iter_timeout being set. ' + 'cmd: %s' % cmd) + + # Construct lines to yield from raw data. buffer_output += data has_incomplete_line = buffer_output[-1] not in '\r\n' lines = buffer_output.splitlines() buffer_output = lines.pop() if has_incomplete_line else '' for line in lines: yield line + if iter_timeout: + iter_end = time.time() + iter_timeout + if buffer_output: yield buffer_output if check_status and process.returncode: diff --git a/catapult/devil/devil/utils/cmd_helper_test.py b/catapult/devil/devil/utils/cmd_helper_test.py index a04f1adf..783c4137 100755 --- a/catapult/devil/devil/utils/cmd_helper_test.py +++ b/catapult/devil/devil/utils/cmd_helper_test.py @@ -7,9 +7,14 @@ import unittest import subprocess +import time +from devil import devil_env from devil.utils import cmd_helper +with devil_env.SysPath(devil_env.PYMOCK_PATH): + import mock # pylint: disable=import-error + class CmdHelperSingleQuoteTest(unittest.TestCase): @@ -84,35 +89,173 @@ class CmdHelperShinkToSnippetTest(unittest.TestCase): cmd_helper.ShrinkToSnippet(['foo', ' barbar '], 'a', 'bar')) +_DEFAULT = 'DEFAULT' + + +class _ProcessOutputEvent(object): + + def __init__(self, select_fds=_DEFAULT, read_contents=None, ts=_DEFAULT): + self.select_fds = select_fds + self.read_contents = read_contents + self.ts = ts + + +class _MockProcess(object): + + def __init__(self, output_sequence=None, return_value=0): + + # Arbitrary. + fake_stdout_fileno = 25 + + self.mock_proc = mock.MagicMock(spec=subprocess.Popen) + self.mock_proc.stdout = mock.MagicMock() + self.mock_proc.stdout.fileno = mock.MagicMock( + return_value=fake_stdout_fileno) + self.mock_proc.returncode = None + + self._return_value = return_value + + # This links the behavior of os.read, select.select, time.time, and + # <process>.poll. The output sequence can be thought of as a list of + # return values for select.select with corresponding return values for + # the other calls at any time between that select call and the following + # one. We iterate through the sequence only on calls to select.select. + # + # os.read is a special case, though, where we only return a given chunk + # of data *once* after a given call to select. + + if not output_sequence: + output_sequence = [] + + # Use an leading element to make the iteration logic work. + initial_seq_element = _ProcessOutputEvent( + _DEFAULT, '', + output_sequence[0].ts if output_sequence else _DEFAULT) + output_sequence.insert(0, initial_seq_element) + + for o in output_sequence: + if o.select_fds == _DEFAULT: + if o.read_contents is None: + o.select_fds = [] + else: + o.select_fds = [fake_stdout_fileno] + if o.ts == _DEFAULT: + o.ts = time.time() + self._output_sequence = output_sequence + + self._output_seq_index = 0 + self._read_flags = [False] * len(output_sequence) + + def read_side_effect(*_args, **_kwargs): + if self._read_flags[self._output_seq_index]: + return None + self._read_flags[self._output_seq_index] = True + return self._output_sequence[self._output_seq_index].read_contents + + def select_side_effect(*_args, **_kwargs): + if self._output_seq_index is None: + self._output_seq_index = 0 + else: + self._output_seq_index += 1 + return (self._output_sequence[self._output_seq_index].select_fds, + None, None) + + def time_side_effect(*_args, **_kwargs): + return self._output_sequence[self._output_seq_index].ts + + def poll_side_effect(*_args, **_kwargs): + if self._output_seq_index >= len(self._output_sequence) - 1: + self.mock_proc.returncode = self._return_value + return self.mock_proc.returncode + + mock_read = mock.MagicMock(side_effect=read_side_effect) + mock_select = mock.MagicMock(side_effect=select_side_effect) + mock_time = mock.MagicMock(side_effect=time_side_effect) + self.mock_proc.poll = mock.MagicMock(side_effect=poll_side_effect) + + # Set up but *do not start* the mocks. + self._mocks = [ + mock.patch('fcntl.fcntl'), + mock.patch('os.read', new=mock_read), + mock.patch('select.select', new=mock_select), + mock.patch('time.time', new=mock_time), + ] + + def __enter__(self): + for m in self._mocks: + m.__enter__() + return self.mock_proc + + def __exit__(self, exc_type, exc_val, exc_tb): + for m in reversed(self._mocks): + m.__exit__(exc_type, exc_val, exc_tb) + + class CmdHelperIterCmdOutputLinesTest(unittest.TestCase): """Test IterCmdOutputLines with some calls to the unix 'seq' command.""" + # This calls _IterCmdOutputLines rather than IterCmdOutputLines s.t. it + # can mock the process. + # pylint: disable=protected-access + + _SIMPLE_OUTPUT_SEQUENCE = [ + _ProcessOutputEvent(read_contents='1\n2\n'), + ] + def testIterCmdOutputLines_success(self): - for num, line in enumerate( - cmd_helper.IterCmdOutputLines(['seq', '10']), 1): - self.assertEquals(num, int(line)) + with _MockProcess( + output_sequence=self._SIMPLE_OUTPUT_SEQUENCE) as mock_proc: + for num, line in enumerate( + cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1): + self.assertEquals(num, int(line)) def testIterCmdOutputLines_exitStatusFail(self): with self.assertRaises(subprocess.CalledProcessError): - for num, line in enumerate( - cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True), 1): - self.assertEquals(num, int(line)) - # after reading all the output we get an exit status of 1 + with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE, + return_value=1) as mock_proc: + for num, line in enumerate( + cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1): + self.assertEquals(num, int(line)) + # after reading all the output we get an exit status of 1 def testIterCmdOutputLines_exitStatusIgnored(self): - for num, line in enumerate( - cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True, - check_status=False), 1): - self.assertEquals(num, int(line)) + with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE, + return_value=1) as mock_proc: + for num, line in enumerate( + cmd_helper._IterCmdOutputLines( + mock_proc, 'mock_proc', check_status=False), + 1): + self.assertEquals(num, int(line)) def testIterCmdOutputLines_exitStatusSkipped(self): - for num, line in enumerate( - cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True), 1): - self.assertEquals(num, int(line)) - # no exception will be raised because we don't attempt to read past - # the end of the output and, thus, the status never gets checked - if num == 10: - break + with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE, + return_value=1) as mock_proc: + for num, line in enumerate( + cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1): + self.assertEquals(num, int(line)) + # no exception will be raised because we don't attempt to read past + # the end of the output and, thus, the status never gets checked + if num == 2: + break + + def testIterCmdOutputLines_delay(self): + output_sequence = [ + _ProcessOutputEvent(read_contents='1\n2\n', ts=1), + _ProcessOutputEvent(read_contents=None, ts=2), + _ProcessOutputEvent(read_contents='Awake', ts=10), + ] + with _MockProcess(output_sequence=output_sequence) as mock_proc: + for num, line in enumerate( + cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc', + iter_timeout=5), 1): + if num <= 2: + self.assertEquals(num, int(line)) + elif num == 3: + self.assertEquals(None, line) + elif num == 4: + self.assertEquals('Awake', line) + else: + self.fail() if __name__ == '__main__': diff --git a/catapult/devil/devil/utils/markdown.py b/catapult/devil/devil/utils/markdown.py index cb2dc2bd..54e7ed56 100755 --- a/catapult/devil/devil/utils/markdown.py +++ b/catapult/devil/devil/utils/markdown.py @@ -1,8 +1,69 @@ +#! /usr/bin/env python # Copyright 2016 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 argparse +import imp +import os +import re +import sys +import textwrap +import types + +# A markdown code block template: https://goo.gl/9EsyRi +_CODE_BLOCK_FORMAT = '''```{language} +{code} +``` +''' + +_DEVIL_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..')) + + +def md_bold(raw_text): + """Returns markdown-formatted bold text.""" + return '**%s**' % md_escape(raw_text, characters='*') + + +def md_code(raw_text, language): + """Returns a markdown-formatted code block in the given language.""" + return _CODE_BLOCK_FORMAT.format( + language=language or '', + code=md_escape(raw_text, characters='`')) + + +def md_escape(raw_text, characters='*_'): + """Escapes * and _.""" + def escape_char(m): + return '\\%s' % m.group(0) + pattern = '[%s]' % re.escape(characters) + return re.sub(pattern, escape_char, raw_text) + + +def md_heading(raw_text, level): + """Returns markdown-formatted heading.""" + adjusted_level = min(max(level, 0), 6) + return '%s%s%s' % ( + '#' * adjusted_level, ' ' if adjusted_level > 0 else '', raw_text) + + +def md_inline_code(raw_text): + """Returns markdown-formatted inline code.""" + return '`%s`' % md_escape(raw_text, characters='`') + + +def md_italic(raw_text): + """Returns markdown-formatted italic text.""" + return '*%s*' % md_escape(raw_text, characters='*') + + +def md_link(link_text, link_target): + """returns a markdown-formatted link.""" + return '[%s](%s)' % ( + md_escape(link_text, characters=']'), + md_escape(link_target, characters=')')) + class MarkdownHelpFormatter(argparse.HelpFormatter): """A really bare-bones argparse help formatter that generates valid markdown. @@ -25,28 +86,25 @@ class MarkdownHelpFormatter(argparse.HelpFormatter): def _format_usage(self, usage, actions, groups, prefix): usage_text = super(MarkdownHelpFormatter, self)._format_usage( usage, actions, groups, prefix) - return '\n```\n%s\n```\n\n' % usage_text + return md_code(usage_text, language=None) #override def format_help(self): - self._root_section.heading = '# %s' % self._prog + self._root_section.heading = md_heading(self._prog, level=1) return super(MarkdownHelpFormatter, self).format_help() #override def start_section(self, heading): - super(MarkdownHelpFormatter, self).start_section('## **%s**' % heading) + super(MarkdownHelpFormatter, self).start_section( + md_heading(heading, level=2)) #override def _format_action(self, action): lines = [] action_header = self._format_action_invocation(action) - lines.append('### **%s** ' % action_header) + lines.append(md_heading(action_header, level=3)) if action.help: - lines.append('') - lines.append('```') - help_text = self._expand_help(action) - lines.extend(self._split_lines(help_text, 80)) - lines.append('```') + lines.append(md_code(self._expand_help(action), language=None)) lines.extend(['', '']) return '\n'.join(lines) @@ -69,6 +127,194 @@ class MarkdownHelpAction(argparse.Action): def add_md_help_argument(parser): + """Adds --md-help to the given argparse.ArgumentParser. + + Running a script with --md-help will print the help text for that script + as valid markdown. + + Args: + parser: The ArgumentParser to which --md-help should be added. + """ parser.add_argument('--md-help', action=MarkdownHelpAction, help='print Markdown-formatted help text and exit.') + +def load_module_from_path(module_path): + """Load a module given only the path name. + + Also loads package modules as necessary. + + Args: + module_path: An absolute path to a python module. + Returns: + The module object for the given path. + """ + module_names = [os.path.splitext(os.path.basename(module_path))[0]] + d = os.path.dirname(module_path) + + while os.path.exists(os.path.join(d, '__init__.py')): + module_names.append(os.path.basename(d)) + d = os.path.dirname(d) + + d = [d] + + module = None + full_module_name = '' + for package_name in reversed(module_names): + if module: + d = module.__path__ + full_module_name += '.' + r = imp.find_module(package_name, d) + full_module_name += package_name + module = imp.load_module(full_module_name, *r) + return module + + +def md_module(module_obj, module_path=None, module_link=None): + """Write markdown documentation for a class. + + Documents public classes and functions. + + Args: + class_obj: a types.TypeType object for the class that should be + documented. + Returns: + A list of markdown-formatted lines. + """ + def should_doc(name): + return (type(module_obj.__dict__[name]) != types.ModuleType + and not name.startswith('_')) + + stuff_to_doc = sorted( + obj for name, obj in module_obj.__dict__.iteritems() + if should_doc(name)) + + classes_to_doc = [] + functions_to_doc = [] + + for s in stuff_to_doc: + if type(s) == types.TypeType: + classes_to_doc.append(s) + elif type(s) == types.FunctionType: + functions_to_doc.append(s) + + command = ['devil/utils/markdown.py'] + if module_link: + command.extend(['--module-link', module_link]) + if module_path: + command.append(os.path.relpath(module_path, _DEVIL_ROOT)) + + heading_text = module_obj.__name__ + if module_link: + heading_text = md_link(heading_text, module_link) + + content = [ + md_heading(heading_text, level=1), + '', + md_italic('This page was autogenerated by %s' + % md_inline_code(' '.join(command))), + '', + ] + + for c in classes_to_doc: + content += md_class(c) + for f in functions_to_doc: + content += md_function(f) + + print '\n'.join(content) + + return 0 + + +def md_class(class_obj): + """Write markdown documentation for a class. + + Documents public methods. Does not currently document subclasses. + + Args: + class_obj: a types.TypeType object for the class that should be + documented. + Returns: + A list of markdown-formatted lines. + """ + content = [md_heading(md_escape(class_obj.__name__), level=2)] + content.append('') + if class_obj.__doc__: + content.extend(md_docstring(class_obj.__doc__)) + + def should_doc(name, obj): + return (type(obj) == types.FunctionType + and (name.startswith('__') or not name.startswith('_'))) + + methods_to_doc = sorted( + obj for name, obj in class_obj.__dict__.iteritems() + if should_doc(name, obj)) + + for m in methods_to_doc: + content.extend(md_function(m, class_obj=class_obj)) + + return content + + +def md_docstring(docstring): + """Write a markdown-formatted docstring. + + Returns: + A list of markdown-formatted lines. + """ + content = [] + lines = textwrap.dedent(docstring).splitlines() + content.append(md_escape(lines[0])) + lines = lines[1:] + while lines and (not lines[0] or lines[0].isspace()): + lines = lines[1:] + + if not all(l.isspace() for l in lines): + content.append(md_code('\n'.join(lines), language=None)) + content.append('') + return content + + +def md_function(func_obj, class_obj=None): + """Write markdown documentation for a function. + + Args: + func_obj: a types.FunctionType object for the function that should be + documented. + Returns: + A list of markdown-formatted lines. + """ + if class_obj: + heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__) + else: + heading_text = func_obj.__name__ + content = [md_heading(md_escape(heading_text), level=3)] + content.append('') + + if func_obj.__doc__: + content.extend(md_docstring(func_obj.__doc__)) + + return content + + +def main(raw_args): + """Write markdown documentation for the module at the provided path. + + Args: + raw_args: the raw command-line args. Usually sys.argv[1:]. + Returns: + An integer exit code. 0 for success, non-zero for failure. + """ + parser = argparse.ArgumentParser() + parser.add_argument('--module-link') + parser.add_argument('module_path', type=os.path.realpath) + args = parser.parse_args(raw_args) + + return md_module( + load_module_from_path(args.module_path), + module_link=args.module_link) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + diff --git a/catapult/devil/devil/utils/markdown_test.py b/catapult/devil/devil/utils/markdown_test.py new file mode 100755 index 00000000..323776ca --- /dev/null +++ b/catapult/devil/devil/utils/markdown_test.py @@ -0,0 +1,121 @@ +#! /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 textwrap +import unittest + +if __name__ == '__main__': + sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from devil.utils import markdown + + +class MarkdownTest(unittest.TestCase): + + def testBold(self): + raw = 'foo' + self.assertEquals('**foo**', markdown.md_bold(raw)) + + def testBoldContainsStars(self): + raw = '*foo*' + self.assertEquals('**\\*foo\\***', markdown.md_bold(raw)) + + def testCode(self): + raw = textwrap.dedent("""\ + class MarkdownTest(unittest.TestCase): + def testCode(self): + pass""") + + expected = textwrap.dedent("""\ + ```python + class MarkdownTest(unittest.TestCase): + def testCode(self): + pass + ``` + """) + actual = markdown.md_code(raw, language='python') + self.assertEquals(expected, actual) + + def testCodeContainsTicks(self): + raw = textwrap.dedent("""\ + This is sample markdown. + ```c + // This is a sample code block. + int main(int argc, char** argv) { + return 0; + } + ```""") + + expected = textwrap.dedent("""\ + ``` + This is sample markdown. + \\`\\`\\`c + // This is a sample code block. + int main(int argc, char** argv) { + return 0; + } + \\`\\`\\` + ``` + """) + actual = markdown.md_code(raw, language=None) + self.assertEquals(expected, actual) + + def testEscape(self): + raw = 'text_with_underscores *and stars*' + expected = 'text\\_with\\_underscores \\*and stars\\*' + actual = markdown.md_escape(raw) + self.assertEquals(expected, actual) + + def testHeading1(self): + raw = 'Heading 1' + self.assertEquals('# Heading 1', markdown.md_heading(raw, level=1)) + + def testHeading5(self): + raw = 'Heading 5' + self.assertEquals('##### Heading 5', markdown.md_heading(raw, level=5)) + + def testHeading10(self): + raw = 'Heading 10' + self.assertEquals('###### Heading 10', markdown.md_heading(raw, level=10)) + + def testInlineCode(self): + raw = 'devil.utils.markdown_test' + self.assertEquals( + '`devil.utils.markdown_test`', markdown.md_inline_code(raw)) + + def testInlineCodeContainsTicks(self): + raw = 'this contains `backticks`' + self.assertEquals( + '`this contains \\`backticks\\``', markdown.md_inline_code(raw)) + + def testItalic(self): + raw = 'bar' + self.assertEquals('*bar*', markdown.md_italic(raw)) + + def testItalicContainsStars(self): + raw = '*bar*' + self.assertEquals('*\\*bar\\**', markdown.md_italic(raw)) + + def testLink(self): + link_text = 'Devil home' + link_target = ( + 'https://github.com/catapult-project/catapult/tree/master/devil') + expected = ( + '[Devil home]' + '(https://github.com/catapult-project/catapult/tree/master/devil)') + self.assertEquals(expected, markdown.md_link(link_text, link_target)) + + def testLinkTextContainsBracket(self): + link_text = 'foo [] bar' + link_target = 'https://www.google.com' + expected = '[foo [\\] bar](https://www.google.com)' + self.assertEquals(expected, markdown.md_link(link_text, link_target)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/catapult/devil/docs/adb_wrapper.md b/catapult/devil/docs/adb_wrapper.md new file mode 100644 index 00000000..a8dc3b05 --- /dev/null +++ b/catapult/devil/docs/adb_wrapper.md @@ -0,0 +1,388 @@ +# [devil.android.sdk.adb_wrapper](https://github.com/catapult-project/catapult/blob/master/devil/devil/android/sdk/adb_wrapper.py) + +*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/android/sdk/adb_wrapper.py`* + +## DeviceStat + +DeviceStat(st\_mode, st\_size, st\_time) +### DeviceStat.\_\_repr\_\_ + +Return a nicely formatted representation string +### DeviceStat.\_\_getnewargs\_\_ + +Return self as a plain tuple. Used by copy and pickle. +### DeviceStat.\_\_getstate\_\_ + +Exclude the OrderedDict from pickling +## AdbWrapper + +A wrapper around a local Android Debug Bridge executable. +### AdbWrapper.GetDeviceSerial + +Gets the device serial number associated with this object. +``` + Returns: + Device serial number as a string. +``` + + +### AdbWrapper.Push + +Pushes a file from the host to the device. +``` + Args: + local: Path on the host filesystem. + remote: Path on the device filesystem. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Pull + +Pulls a file from the device to the host. +``` + Args: + remote: Path on the device filesystem. + local: Path on the host filesystem. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Shell + +Runs a shell command on the device. +``` + Args: + command: A string with the shell command to run. + expect_status: (optional) Check that the command's exit status matches + this value. Default is 0. If set to None the test is skipped. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + The output of the shell command as a string. + + Raises: + device_errors.AdbCommandFailedError: If the exit status doesn't match + |expect_status|. +``` + + +### AdbWrapper.IterShell + +Runs a shell command and returns an iterator over its output lines. +``` + Args: + command: A string with the shell command to run. + timeout: Timeout in seconds. + + Yields: + The output of the command line by line. +``` + + +### AdbWrapper.Ls + +List the contents of a directory on the device. +``` + Args: + path: Path on the device filesystem. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + A list of pairs (filename, stat) for each file found in the directory, + where the stat object has the properties: st_mode, st_size, and st_time. + + Raises: + AdbCommandFailedError if |path| does not specify a valid and accessible + directory in the device, or the output of "adb ls" command is less + than four columns +``` + + +### AdbWrapper.Logcat + +Get an iterable over the logcat output. +``` + Args: + clear: If true, clear the logcat. + dump: If true, dump the current logcat contents. + filter_specs: If set, a list of specs to filter the logcat. + logcat_format: If set, the format in which the logcat should be output. + Options include "brief", "process", "tag", "thread", "raw", "time", + "threadtime", and "long" + ring_buffer: If set, a list of alternate ring buffers to request. + Options include "main", "system", "radio", "events", "crash" or "all". + The default is equivalent to ["main", "system", "crash"]. + iter_timeout: If set and neither clear nor dump is set, the number of + seconds to wait between iterations. If no line is found before the + given number of seconds elapses, the iterable will yield None. + timeout: (optional) If set, timeout per try in seconds. If clear or dump + is set, defaults to DEFAULT_TIMEOUT. + retries: (optional) If clear or dump is set, the number of retries to + attempt. Otherwise, does nothing. + + Yields: + logcat output line by line. +``` + + +### AdbWrapper.Forward + +Forward socket connections from the local socket to the remote socket. +``` + Sockets are specified by one of: + tcp:<port> + localabstract:<unix domain socket name> + localreserved:<unix domain socket name> + localfilesystem:<unix domain socket name> + dev:<character device name> + jdwp:<process pid> (remote only) + + Args: + local: The host socket. + remote: The device socket. + allow_rebind: A boolean indicating whether adb may rebind a local socket; + otherwise, the default, an exception is raised if the local socket is + already being forwarded. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.ForwardRemove + +Remove a forward socket connection. +``` + Args: + local: The host socket. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.ForwardList + +List all currently forwarded socket connections. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + Returns: + The output of adb forward --list as a string. +``` + + +### AdbWrapper.JDWP + +List of PIDs of processes hosting a JDWP transport. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + A list of PIDs as strings. +``` + + +### AdbWrapper.Install + +Install an apk on the device. +``` + Args: + apk_path: Host path to the APK file. + forward_lock: (optional) If set forward-locks the app. + allow_downgrade: (optional) If set, allows for downgrades. + reinstall: (optional) If set reinstalls the app, keeping its data. + sd_card: (optional) If set installs on the SD card. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.InstallMultiple + +Install an apk with splits on the device. +``` + Args: + apk_paths: Host path to the APK file. + forward_lock: (optional) If set forward-locks the app. + reinstall: (optional) If set reinstalls the app, keeping its data. + sd_card: (optional) If set installs on the SD card. + allow_downgrade: (optional) Allow versionCode downgrade. + partial: (optional) Package ID if apk_paths doesn't include all .apks. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Uninstall + +Remove the app |package| from the device. +``` + Args: + package: The package to uninstall. + keep_data: (optional) If set keep the data and cache directories. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Backup + +Write an archive of the device's data to |path|. +``` + Args: + path: Local path to store the backup file. + packages: List of to packages to be backed up. + apk: (optional) If set include the .apk files in the archive. + shared: (optional) If set buckup the device's SD card. + nosystem: (optional) If set exclude system applications. + include_all: (optional) If set back up all installed applications and + |packages| is optional. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Restore + +Restore device contents from the backup archive. +``` + Args: + path: Host path to the backup archive. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.WaitForDevice + +Block until the device is online. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.GetState + +Get device state. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + One of 'offline', 'bootloader', or 'device'. +``` + + +### AdbWrapper.GetDevPath + +Gets the device path. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + The device path (e.g. usb:3-4) +``` + + +### AdbWrapper.Remount + +Remounts the /system partition on the device read-write. +### AdbWrapper.Reboot + +Reboots the device. +``` + Args: + to_bootloader: (optional) If set reboots to the bootloader. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Root + +Restarts the adbd daemon with root permissions, if possible. +``` + Args: + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. +``` + + +### AdbWrapper.Emu + +Runs an emulator console command. +``` + See http://developer.android.com/tools/devices/emulator.html#console + + Args: + cmd: The command to run on the emulator console. + timeout: (optional) Timeout per try in seconds. + retries: (optional) Number of retries to attempt. + + Returns: + The output of the emulator console command. +``` + + +### AdbWrapper.DisableVerity + +Disable Marshmallow's Verity security feature +### AdbWrapper.EnableVerity + +Enable Marshmallow's Verity security feature +### AdbWrapper.\_\_init\_\_ + +Initializes the AdbWrapper. +``` + Args: + device_serial: The device serial number as a string. +``` + + +### AdbWrapper.\_\_eq\_\_ + +Consider instances equal if they refer to the same device. +``` + Args: + other: The instance to compare equality with. + + Returns: + True if the instances are considered equal, false otherwise. +``` + + +### AdbWrapper.\_\_str\_\_ + +The string representation of an instance. +``` + Returns: + The device serial number as a string. +``` + + +### AdbWrapper.\_\_repr\_\_ + +### VerifyLocalFileExists + +Verifies a local file exists. +``` + Args: + path: Path to the local file. + + Raises: + IOError: If the file doesn't exist. +``` + + diff --git a/catapult/devil/docs/device_utils.md b/catapult/devil/docs/device_utils.md new file mode 100644 index 00000000..b281b266 --- /dev/null +++ b/catapult/devil/docs/device_utils.md @@ -0,0 +1,1041 @@ +# [devil.android.device_utils](https://github.com/catapult-project/catapult/blob/master/devil/devil/android/device_utils.py) + +*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/android/device_utils.py`* + +## DeviceUtils + +### DeviceUtils.\_\_init\_\_ + +DeviceUtils constructor. +``` + Args: + device: Either a device serial, an existing AdbWrapper instance, or an + an existing AndroidCommands instance. + enable_device_files_cache: For PushChangedFiles(), cache checksums of + pushed files rather than recomputing them on a subsequent call. + default_timeout: An integer containing the default number of seconds to + wait for an operation to complete if no explicit value is provided. + default_retries: An integer containing the default number or times an + operation should be retried on failure if no explicit value is provided. +``` + + +### DeviceUtils.\_\_eq\_\_ + +Checks whether |other| refers to the same device as |self|. +``` + Args: + other: The object to compare to. This can be a basestring, an instance + of adb_wrapper.AdbWrapper, or an instance of DeviceUtils. + Returns: + Whether |other| refers to the same device as |self|. +``` + + +### DeviceUtils.\_\_lt\_\_ + +Compares two instances of DeviceUtils. +``` + This merely compares their serial numbers. + + Args: + other: The instance of DeviceUtils to compare to. + Returns: + Whether |self| is less than |other|. +``` + + +### DeviceUtils.\_\_str\_\_ + +Returns the device serial. +### DeviceUtils.NeedsSU + +Checks whether 'su' is needed to access protected resources. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True if 'su' is available on the device and is needed to to access + protected resources; False otherwise if either 'su' is not available + (e.g. because the device has a user build), or not needed (because adbd + already has root privileges). + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.IsOnline + +Checks whether the device is online. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True if the device is online, False otherwise. + + Raises: + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.HasRoot + +Checks whether or not adbd has root privileges. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True if adbd has root privileges, False otherwise. + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.EnableRoot + +Restarts adbd with root privileges. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if root could not be enabled. + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.IsUserBuild + +Checks whether or not the device is running a user build. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True if the device is running a user build, False otherwise (i.e. if + it's running a userdebug build). + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.GetExternalStoragePath + +Get the device's path to its SD card. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + The device's path to its SD card. + + Raises: + CommandFailedError if the external storage path could not be determined. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.GetApplicationPaths + +Get the paths of the installed apks on the device for the given package. +``` + Args: + package: Name of the package. + + Returns: + List of paths to the apks on the device for the given package. +``` + + +### DeviceUtils.GetApplicationVersion + +Get the version name of a package installed on the device. +``` + Args: + package: Name of the package. + + Returns: + A string with the version name or None if the package is not found + on the device. +``` + + +### DeviceUtils.GetApplicationDataDirectory + +Get the data directory on the device for the given package. +``` + Args: + package: Name of the package. + + Returns: + The package's data directory. + Raises: + CommandFailedError if the package's data directory can't be found, + whether because it's not installed or otherwise. +``` + + +### DeviceUtils.WaitUntilFullyBooted + +Wait for the device to fully boot. +``` + This means waiting for the device to boot, the package manager to be + available, and the SD card to be ready. It can optionally mean waiting + for wifi to come up, too. + + Args: + wifi: A boolean indicating if we should wait for wifi to come up or not. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError if one of the component waits times out. + DeviceUnreachableError if the device becomes unresponsive. +``` + + +### DeviceUtils.Reboot + +Reboot the device. +``` + Args: + block: A boolean indicating if we should wait for the reboot to complete. + wifi: A boolean indicating if we should wait for wifi to be enabled after + the reboot. The option has no effect unless |block| is also True. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.Install + +Install an APK. +``` + Noop if an identical APK is already installed. + + Args: + apk: An ApkHelper instance or string containing the path to the APK. + allow_downgrade: A boolean indicating if we should allow downgrades. + reinstall: A boolean indicating if we should keep any existing app data. + permissions: Set of permissions to set. If not set, finds permissions with + apk helper. To set no permissions, pass []. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if the installation fails. + CommandTimeoutError if the installation times out. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.InstallSplitApk + +Install a split APK. +``` + Noop if all of the APK splits are already installed. + + Args: + base_apk: An ApkHelper instance or string containing the path to the base + APK. + split_apks: A list of strings of paths of all of the APK splits. + allow_downgrade: A boolean indicating if we should allow downgrades. + reinstall: A boolean indicating if we should keep any existing app data. + allow_cached_props: Whether to use cached values for device properties. + permissions: Set of permissions to set. If not set, finds permissions with + apk helper. To set no permissions, pass []. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if the installation fails. + CommandTimeoutError if the installation times out. + DeviceUnreachableError on missing device. + DeviceVersionError if device SDK is less than Android L. +``` + + +### DeviceUtils.Uninstall + +Remove the app |package\_name| from the device. +``` + This is a no-op if the app is not already installed. + + Args: + package_name: The package to uninstall. + keep_data: (optional) Whether to keep the data and cache directories. + timeout: Timeout in seconds. + retries: Number of retries. + + Raises: + CommandFailedError if the uninstallation fails. + CommandTimeoutError if the uninstallation times out. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.RunShellCommand + +Run an ADB shell command. +``` + The command to run |cmd| should be a sequence of program arguments or else + a single string. + + 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. + + When |cmd| is given as a string, it will be interpreted and run by the + shell on the device. + + This behaviour is consistent with that of command runners in cmd_helper as + well as Python's own subprocess.Popen. + + TODO(perezju) Change the default of |check_return| to True when callers + 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. + 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. + env: The environment variables with which the command should be run. + run_as: A string containing the package as which the command should be + run. + as_root: A boolean indicating whether the shell command should be run + with root privileges. + single_line: A boolean indicating if only a single line of output is + expected. + large_output: Uses a work-around for large shell command output. Without + this large output will be truncated. + raw_output: Whether to only return the raw output + (no splitting into lines). + timeout: timeout in seconds + retries: number of retries + + Returns: + If single_line is False, the output of the command as a list of lines, + otherwise, a string with the unique line of output emmited by the command + (with the optional newline at the end stripped). + + Raises: + AdbCommandFailedError if check_return is True and the exit code of + the command run on the device is non-zero. + CommandFailedError if single_line is True but the output contains two or + more lines. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.KillAll + +Kill all processes with the given name on the device. +``` + Args: + process_name: A string containing the name of the process to kill. + exact: A boolean indicating whether to kill all processes matching + the string |process_name| exactly, or all of those which contain + |process_name| as a substring. Defaults to False. + signum: An integer containing the signal number to send to kill. Defaults + to SIGKILL (9). + as_root: A boolean indicating whether the kill should be executed with + root privileges. + blocking: A boolean indicating whether we should wait until all processes + with the given |process_name| are dead. + quiet: A boolean indicating whether to ignore the fact that no processes + to kill were found. + timeout: timeout in seconds + retries: number of retries + + Returns: + The number of processes attempted to kill. + + Raises: + CommandFailedError if no process was killed and |quiet| is False. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.StartActivity + +Start package's activity on the device. +``` + Args: + intent_obj: An Intent object to send. + blocking: A boolean indicating whether we should wait for the activity to + finish launching. + trace_file_name: If present, a string that both indicates that we want to + profile the activity and contains the path to which the + trace should be saved. + force_stop: A boolean indicating whether we should stop the activity + before starting it. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if the activity could not be started. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.StartInstrumentation + +### DeviceUtils.BroadcastIntent + +Send a broadcast intent. +``` + Args: + intent: An Intent to broadcast. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.GoHome + +Return to the home screen and obtain launcher focus. +``` + This command launches the home screen and attempts to obtain + launcher focus until the timeout is reached. + + Args: + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.ForceStop + +Close the application. +``` + Args: + package: A string containing the name of the package to stop. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.ClearApplicationState + +Clear all state for the given package. +``` + Args: + package: A string containing the name of the package to stop. + permissions: List of permissions to set after clearing data. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.SendKeyEvent + +Sends a keycode to the device. +``` + See the devil.android.sdk.keyevent module for suitable keycode values. + + Args: + keycode: A integer keycode to send to the device. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.PushChangedFiles + +Push files to the device, skipping files that don't need updating. +``` + When a directory is pushed, it is traversed recursively on the host and + all files in it are pushed to the device as needed. + Additionally, if delete_device_stale option is True, + files that exist on the device but don't exist on the host are deleted. + + Args: + host_device_tuples: A list of (host_path, device_path) tuples, where + |host_path| is an absolute path of a file or directory on the host + that should be minimially pushed to the device, and |device_path| is + an absolute path of the destination on the device. + timeout: timeout in seconds + retries: number of retries + delete_device_stale: option to delete stale files on device + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.FileExists + +Checks whether the given file exists on the device. +``` + Arguments are the same as PathExists. +``` + + +### DeviceUtils.PathExists + +Checks whether the given path(s) exists on the device. +``` + Args: + device_path: A string containing the absolute path to the file on the + device, or an iterable of paths to check. + as_root: Whether root permissions should be use to check for the existence + of the given path(s). + timeout: timeout in seconds + retries: number of retries + + Returns: + True if the all given paths exist on the device, False otherwise. + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.RemovePath + +Removes the given path(s) from the device. +``` + Args: + device_path: A string containing the absolute path to the file on the + device, or an iterable of paths to check. + force: Whether to remove the path(s) with force (-f). + 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). + timeout: timeout in seconds + retries: number of retries +``` + + +### DeviceUtils.PullFile + +Pull a file from the device. +``` + Args: + device_path: A string containing the absolute path of the file to pull + from the device. + host_path: A string containing the absolute path of the destination on + the host. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.ReadFile + +Reads the contents of a file from the device. +``` + Args: + device_path: A string containing the absolute path of the file to read + from the device. + as_root: A boolean indicating whether the read should be executed with + root privileges. + force_pull: A boolean indicating whether to force the operation to be + performed by pulling a file from the device. The default is, when the + contents are short, to retrieve the contents using cat instead. + timeout: timeout in seconds + retries: number of retries + + Returns: + The contents of |device_path| as a string. Contents are intepreted using + universal newlines, so the caller will see them encoded as ' +'. Also, + all lines will be terminated. + + Raises: + AdbCommandFailedError if the file can't be read. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.WriteFile + +Writes |contents| to a file on the device. +``` + Args: + device_path: A string containing the absolute path to the file to write + on the device. + contents: A string containing the data to write to the device. + as_root: A boolean indicating whether the write should be executed with + root privileges (if available). + force_push: A boolean indicating whether to force the operation to be + performed by pushing a file to the device. The default is, when the + contents are short, to pass the contents using a shell script instead. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if the file could not be written on the device. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.ListDirectory + +List all files on a device directory. +``` + Mirroring os.listdir (and most client expectations) the resulting list + does not include the special entries '.' and '..' even if they are present + in the directory. + + Args: + device_path: A string containing the path of the directory on the device + to list. + as_root: A boolean indicating whether the to use root privileges to list + the directory contents. + timeout: timeout in seconds + retries: number of retries + + Returns: + A list of filenames for all entries contained in the directory. + + Raises: + AdbCommandFailedError if |device_path| does not specify a valid and + accessible directory in the device. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.StatDirectory + +List file and stat info for all entries on a device directory. +``` + Implementation notes: this is currently implemented by parsing the output + of 'ls -a -l' on the device. Whether possible and convenient, we attempt to + make parsing strict and return values mirroring those of the standard |os| + and |stat| Python modules. + + Mirroring os.listdir (and most client expectations) the resulting list + does not include the special entries '.' and '..' even if they are present + in the directory. + + Args: + device_path: A string containing the path of the directory on the device + to list. + as_root: A boolean indicating whether the to use root privileges to list + the directory contents. + timeout: timeout in seconds + retries: number of retries + + Returns: + A list of dictionaries, each containing the following keys: + filename: A string with the file name. + st_mode: File permissions, use the stat module to interpret these. + st_nlink: Number of hard links (may be missing). + st_owner: A string with the user name of the owner. + st_group: A string with the group name of the owner. + st_rdev_pair: Device type as (major, minior) (only if inode device). + st_size: Size of file, in bytes (may be missing for non-regular files). + st_mtime: Time of most recent modification, in seconds since epoch + (although resolution is in minutes). + symbolic_link_to: If entry is a symbolic link, path where it points to; + missing otherwise. + + Raises: + AdbCommandFailedError if |device_path| does not specify a valid and + accessible directory in the device. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.StatPath + +Get the stat attributes of a file or directory on the device. +``` + Args: + device_path: A string containing the path of a file or directory from + which to get attributes. + as_root: A boolean indicating whether the to use root privileges to + access the file information. + timeout: timeout in seconds + retries: number of retries + + Returns: + A dictionary with the stat info collected; see StatDirectory for details. + + Raises: + CommandFailedError if device_path cannot be found on the device. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.FileSize + +Get the size of a file on the device. +``` + Note: This is implemented by parsing the output of the 'ls' command on + the device. On some Android versions, when passing a directory or special + file, the size is *not* reported and this function will throw an exception. + + Args: + device_path: A string containing the path of a file on the device. + as_root: A boolean indicating whether the to use root privileges to + access the file information. + timeout: timeout in seconds + retries: number of retries + + Returns: + The size of the file in bytes. + + Raises: + CommandFailedError if device_path cannot be found on the device, or + its size cannot be determited for some reason. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.GetLanguage + +Returns the language setting on the device. +``` + Args: + cache: Whether to use cached properties when available. +``` + + +### DeviceUtils.SetJavaAsserts + +Enables or disables Java asserts. +``` + Args: + enabled: A boolean indicating whether Java asserts should be enabled + or disabled. + timeout: timeout in seconds + retries: number of retries + + Returns: + True if the device-side property changed and a restart is required as a + result, False otherwise. + + Raises: + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.GetCountry + +Returns the country setting on the device. +``` + Args: + cache: Whether to use cached properties when available. +``` + + +### DeviceUtils.GetProp + +Gets a property from the device. +``` + Args: + property_name: A string containing the name of the property to get from + the device. + cache: Whether to use cached properties when available. + timeout: timeout in seconds + retries: number of retries + + Returns: + The value of the device's |property_name| property. + + Raises: + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.SetProp + +Sets a property on the device. +``` + Args: + property_name: A string containing the name of the property to set on + the device. + value: A string containing the value to set to the property on the + device. + check: A boolean indicating whether to check that the property was + successfully set on the device. + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError if check is true and the property was not correctly + set on the device (e.g. because it is not rooted). + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.GetABI + +Gets the device main ABI. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + The device's main ABI name. + + Raises: + CommandTimeoutError on timeout. +``` + + +### DeviceUtils.GetPids + +Returns the PIDs of processes with the given name. +``` + Note that the |process_name| is often the package name. + + Args: + process_name: A string containing the process name to get the PIDs for. + timeout: timeout in seconds + retries: number of retries + + Returns: + A dict mapping process name to a list of PIDs for each process that + contained the provided |process_name|. + + Raises: + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.GetEnforce + +Get the current mode of SELinux. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True (enforcing), False (permissive), or None (disabled). + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.SetEnforce + +Modify the mode SELinux is running in. +``` + Args: + enabled: a boolean indicating whether to put SELinux in encorcing mode + (if True), or permissive mode (otherwise). + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. +``` + + +### DeviceUtils.TakeScreenshot + +Takes a screenshot of the device. +``` + Args: + host_path: A string containing the path on the host to save the + screenshot to. If None, a file name in the current + directory will be generated. + timeout: timeout in seconds + retries: number of retries + + Returns: + The name of the file on the host to which the screenshot was saved. + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing 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. +``` + Returns: Name of the crashed package if a dialog is focused, + None otherwise. +``` + + +### 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. +### DeviceUtils.LoadCacheData + +Initializes the cache from data created using DumpCacheData. +``` + The cache is used only if its token matches the one found on the device. + This prevents a stale cache from being used (which can happen when sharing + devices). + + Args: + data: A previously serialized cache (string). + timeout: timeout in seconds + retries: number of retries + + Returns: + Whether the cache was loaded. +``` + + +### DeviceUtils.DumpCacheData + +Dumps the current cache state to a string. +``` + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + A serialized cache as a string. +``` + + +### DeviceUtils.RestartAdbd + +### DeviceUtils.GrantPermissions + +### DeviceUtils.IsScreenOn + +Determines if screen is on. +``` + Dumpsys input_method exposes screen on/off state. Below is an explination of + the states. + + Pre-L: + On: mScreenOn=true + Off: mScreenOn=false + L+: + On: mInteractive=true + Off: mInteractive=false + + Returns: + True if screen is on, false if it is off. + + Raises: + device_errors.CommandFailedError: If screen state cannot be found. +``` + + +### DeviceUtils.SetScreen + +Turns screen on and off. +``` + Args: + on: bool to decide state to switch to. True = on False = off. +``` + + +### GetAVDs + +Returns a list of Android Virtual Devices. +``` + Returns: + A list containing the configured AVDs. +``` + + +### RestartServer + +Restarts the adb server. +``` + Raises: + CommandFailedError if we fail to kill or restart the server. +``` + + diff --git a/catapult/devil/docs/markdown.md b/catapult/devil/docs/markdown.md new file mode 100644 index 00000000..957dba7d --- /dev/null +++ b/catapult/devil/docs/markdown.md @@ -0,0 +1,139 @@ +# [devil.utils.markdown](https://github.com/catapult-project/catapult/blob/master/devil/devil/utils/markdown.py) + +*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/utils/markdown.py`* + +## MarkdownHelpAction + +### MarkdownHelpAction.\_\_init\_\_ + +### MarkdownHelpAction.\_\_call\_\_ + +## MarkdownHelpFormatter + +A really bare-bones argparse help formatter that generates valid markdown. +``` + This will generate something like: + + usage + + # **section heading**: + + ## **--argument-one** + + \`\`\` + argument-one help text + \`\`\` + +``` + + +### MarkdownHelpFormatter.format\_help + +### MarkdownHelpFormatter.start\_section + +### md\_bold + +Returns markdown-formatted bold text. +### md\_code + +Returns a markdown-formatted code block in the given language. +### md\_escape + +Escapes \* and \_. +### md\_heading + +Returns markdown-formatted heading. +### md\_inline\_code + +Returns markdown-formatted inline code. +### md\_italic + +Returns markdown-formatted italic text. +### md\_link + +returns a markdown-formatted link. +### add\_md\_help\_argument + +Adds --md-help to the given argparse.ArgumentParser. +``` + Running a script with --md-help will print the help text for that script + as valid markdown. + + Args: + parser: The ArgumentParser to which --md-help should be added. +``` + + +### load\_module\_from\_path + +Load a module given only the path name. +``` + Also loads package modules as necessary. + + Args: + module_path: An absolute path to a python module. + Returns: + The module object for the given path. +``` + + +### md\_module + +Write markdown documentation for a class. +``` + Documents public classes and functions. + + Args: + class_obj: a types.TypeType object for the class that should be + documented. + Returns: + A list of markdown-formatted lines. +``` + + +### md\_class + +Write markdown documentation for a class. +``` + Documents public methods. Does not currently document subclasses. + + Args: + class_obj: a types.TypeType object for the class that should be + documented. + Returns: + A list of markdown-formatted lines. +``` + + +### md\_docstring + +Write a markdown-formatted docstring. +``` + Returns: + A list of markdown-formatted lines. +``` + + +### md\_function + +Write markdown documentation for a function. +``` + Args: + func_obj: a types.FunctionType object for the function that should be + documented. + Returns: + A list of markdown-formatted lines. +``` + + +### main + +Write markdown documentation for the module at the provided path. +``` + Args: + raw_args: the raw command-line args. Usually sys.argv[1:]. + Returns: + An integer exit code. 0 for success, non-zero for failure. +``` + + |