aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/tools/system_app.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/tools/system_app.py')
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app.py147
1 files changed, 106 insertions, 41 deletions
diff --git a/catapult/devil/devil/android/tools/system_app.py b/catapult/devil/devil/android/tools/system_app.py
index 8629ae68..c2f058e0 100755
--- a/catapult/devil/devil/android/tools/system_app.py
+++ b/catapult/devil/devil/android/tools/system_app.py
@@ -2,7 +2,6 @@
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-
"""A script to replace a system app while running a command."""
import argparse
@@ -13,14 +12,13 @@ import posixpath
import re
import sys
-
if __name__ == '__main__':
sys.path.append(
- os.path.abspath(os.path.join(os.path.dirname(__file__),
- '..', '..', '..')))
-
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', '..', '..')))
from devil.android import apk_helper
+from devil.android import decorators
from devil.android import device_errors
from devil.android import device_temp_file
from devil.android.sdk import version_codes
@@ -31,20 +29,25 @@ from devil.utils import run_tests_helper
logger = logging.getLogger(__name__)
-
# Some system apps aren't actually installed in the /system/ directory, so
# special case them here with the correct install location.
SPECIAL_SYSTEM_APP_LOCATIONS = {
- # This also gets installed in /data/app when not a system app, so this script
- # will remove either version. This doesn't appear to cause any issues, but
- # will cause a few unnecessary reboots if this is the only package getting
- # removed and it's already not a system app.
- 'com.google.ar.core': '/data/app/',
+ # Older versions of ArCore were installed in /data/app/ regardless of
+ # whether they were system apps or not. Newer versions install in /system/
+ # if they are system apps, and in /data/app/ if they aren't.
+ 'com.google.ar.core': ['/data/app/', '/system/'],
+ # On older versions of VrCore, the system app version is installed in
+ # /system/ like normal. However, at some point, this moved to /data/.
+ # So, we have to handle both cases. Like ArCore, this means we'll end up
+ # removing even non-system versions due to this, but it doesn't cause any
+ # issues.
+ 'com.google.vr.core': ['/data/app/', '/system/'],
}
# Gets app path and package name pm list packages -f output.
_PM_LIST_PACKAGE_PATH_RE = re.compile(r'^\s*package:(\S+)=(\S+)\s*$')
+
def RemoveSystemApps(device, package_names):
"""Removes the given system apps.
@@ -60,7 +63,9 @@ def RemoveSystemApps(device, package_names):
@contextlib.contextmanager
-def ReplaceSystemApp(device, package_name, replacement_apk,
+def ReplaceSystemApp(device,
+ package_name,
+ replacement_apk,
install_timeout=None):
"""A context manager that replaces the given system app while in scope.
@@ -94,8 +99,8 @@ def _FindSystemPackagePaths(device, system_package_list):
# TODO(aluo): Move this into device_utils.py
def _GetApplicationPaths(device, package):
paths = []
- lines = device.RunShellCommand(['pm', 'list', 'packages', '-f', '-u',
- package], check_return=True)
+ lines = device.RunShellCommand(
+ ['pm', 'list', 'packages', '-f', '-u', package], check_return=True)
for line in lines:
match = re.match(_PM_LIST_PACKAGE_PATH_RE, line)
if match:
@@ -108,24 +113,34 @@ def _GetApplicationPaths(device, package):
def _GetSystemPath(package, paths):
for p in paths:
- if p.startswith(SPECIAL_SYSTEM_APP_LOCATIONS.get(package, '/system/')):
- return p
+ app_locations = SPECIAL_SYSTEM_APP_LOCATIONS.get(package,
+ ['/system/', '/product/'])
+ for location in app_locations:
+ if p.startswith(location):
+ return p
return None
+_MODIFICATION_TIMEOUT = 300
+_MODIFICATION_RETRIES = 2
_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps'
-@contextlib.contextmanager
-def EnableSystemAppModification(device):
- """A context manager that allows system apps to be modified while in scope.
+def _ShouldRetryModification(exc):
+ return not isinstance(exc, device_errors.CommandTimeoutError)
- Args:
- device: (device_utils.DeviceUtils) the device
- """
- if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1':
- yield
- return
+
+# timeout and retries are both required by the decorator, but neither
+# are used within the body of the function.
+# pylint: disable=unused-argument
+
+
+@decorators.WithTimeoutAndConditionalRetries(_ShouldRetryModification)
+def _SetUpSystemAppModification(device, timeout=None, retries=None):
+ # Ensure that the device is online & available before proceeding to
+ # handle the case where something fails in the middle of set up and
+ # triggers a retry.
+ device.WaitUntilFullyBooted()
# All calls that could potentially need root should run with as_root=True, but
# it looks like some parts of Telemetry work as-is by implicitly assuming that
@@ -151,13 +166,61 @@ def EnableSystemAppModification(device):
device.adb.Remount()
device.RunShellCommand(['stop'], check_return=True)
device.SetProp(_ENABLE_MODIFICATION_PROP, '1')
- yield
- finally:
+ except device_errors.CommandFailedError:
+ if device.adb.is_emulator:
+ # Point the user to documentation, since there's a good chance they can
+ # workaround this on an emulator.
+ docs_url = ('https://chromium.googlesource.com/chromium/src/+/'
+ 'master/docs/android_emulator.md#writable-system-partition')
+ logger.error(
+ 'Did you start the emulator with "-writable-system?"\n'
+ 'See %s\n', docs_url)
+ raise
+
+ return should_restore_root
+
+
+@decorators.WithTimeoutAndConditionalRetries(_ShouldRetryModification)
+def _TearDownSystemAppModification(device,
+ should_restore_root,
+ timeout=None,
+ retries=None):
+ try:
device.SetProp(_ENABLE_MODIFICATION_PROP, '0')
device.Reboot()
device.WaitUntilFullyBooted()
if should_restore_root:
device.EnableRoot()
+ except device_errors.CommandTimeoutError:
+ logger.error('Timed out while tearing down system app modification.')
+ logger.error(' device state: %s', device.adb.GetState())
+ raise
+
+
+# pylint: enable=unused-argument
+
+
+@contextlib.contextmanager
+def EnableSystemAppModification(device):
+ """A context manager that allows system apps to be modified while in scope.
+
+ Args:
+ device: (device_utils.DeviceUtils) the device
+ """
+ if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1':
+ yield
+ return
+
+ should_restore_root = _SetUpSystemAppModification(
+ device, timeout=_MODIFICATION_TIMEOUT, retries=_MODIFICATION_RETRIES)
+ try:
+ yield
+ finally:
+ _TearDownSystemAppModification(
+ device,
+ should_restore_root,
+ timeout=_MODIFICATION_TIMEOUT,
+ retries=_MODIFICATION_RETRIES)
@contextlib.contextmanager
@@ -171,11 +234,9 @@ def _RelocateApp(device, package_name, relocate_to):
for p in system_package_paths
}
relocation_dirs = [
- posixpath.dirname(d)
- for _, d in relocation_map.iteritems()
+ posixpath.dirname(d) for _, d in relocation_map.iteritems()
]
- device.RunShellCommand(['mkdir', '-p'] + relocation_dirs,
- check_return=True)
+ device.RunShellCommand(['mkdir', '-p'] + relocation_dirs, check_return=True)
_MoveApp(device, relocation_map)
else:
logger.info('No system package "%s"', package_name)
@@ -207,10 +268,7 @@ def _MoveApp(device, relocation_map):
device: (device_utils.DeviceUtils)
relocation_map: (dict) A dict that maps src to dest
"""
- movements = [
- 'mv %s %s' % (k, v)
- for k, v in relocation_map.iteritems()
- ]
+ movements = ['mv %s %s' % (k, v) for k, v in relocation_map.iteritems()]
cmd = ' && '.join(movements)
with EnableSystemAppModification(device):
device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True)
@@ -224,7 +282,10 @@ def main(raw_args):
script_common.AddDeviceArguments(p)
script_common.AddEnvironmentArguments(p)
p.add_argument(
- '-v', '--verbose', action='count', default=0,
+ '-v',
+ '--verbose',
+ action='count',
+ default=0,
help='Print more information.')
p.add_argument('command', nargs='*')
@@ -235,7 +296,10 @@ def main(raw_args):
remove_parser = subparsers.add_parser('remove')
remove_parser.add_argument(
- '--package', dest='packages', nargs='*', required=True,
+ '--package',
+ dest='packages',
+ nargs='*',
+ required=True,
help='The system package(s) to remove.')
add_common_arguments(remove_parser)
remove_parser.set_defaults(func=remove_system_app)
@@ -247,10 +311,11 @@ def main(raw_args):
replace_parser = subparsers.add_parser('replace')
replace_parser.add_argument(
- '--package', required=True,
- help='The system package to replace.')
+ '--package', required=True, help='The system package to replace.')
replace_parser.add_argument(
- '--replace-with', metavar='APK', required=True,
+ '--replace-with',
+ metavar='APK',
+ required=True,
help='The APK with which the existing system app should be replaced.')
add_common_arguments(replace_parser)
replace_parser.set_defaults(func=replace_system_app)
@@ -260,7 +325,7 @@ def main(raw_args):
run_tests_helper.SetLogLevel(args.verbose)
script_common.InitializeEnvironment(args)
- devices = script_common.GetDevices(args.devices, args.blacklist_file)
+ devices = script_common.GetDevices(args.devices, args.denylist_file)
parallel_devices = parallelizer.SyncParallelizer(
[args.func(d, args) for d in devices])
with parallel_devices: