diff options
Diffstat (limited to 'catapult/devil/devil/android/tools/system_app.py')
-rwxr-xr-x | catapult/devil/devil/android/tools/system_app.py | 147 |
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: |