1#!/usr/bin/env python 2# Copyright 2017 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""A script to replace a system app while running a command.""" 6 7import argparse 8import contextlib 9import logging 10import os 11import posixpath 12import re 13import sys 14 15if __name__ == '__main__': 16 sys.path.append( 17 os.path.abspath( 18 os.path.join(os.path.dirname(__file__), '..', '..', '..'))) 19 20from devil.android import apk_helper 21from devil.android import decorators 22from devil.android import device_errors 23from devil.android import device_temp_file 24from devil.android.sdk import version_codes 25from devil.android.sdk import adb_wrapper 26from devil.android.tools import script_common 27from devil.utils import cmd_helper 28from devil.utils import parallelizer 29from devil.utils import run_tests_helper 30 31logger = logging.getLogger(__name__) 32 33# Some system apps aren't actually installed in the /system/ directory, so 34# special case them here with the correct install location. 35SPECIAL_SYSTEM_APP_LOCATIONS = { 36 # Older versions of ArCore were installed in /data/app/ regardless of 37 # whether they were system apps or not. Newer versions install in /system/ 38 # if they are system apps, and in /data/app/ if they aren't. Some newer 39 # devices/OSes install in /product/app/ for system apps, as well. 40 'com.google.ar.core': ['/data/app/', '/system/', '/product/app/'], 41 # On older versions of VrCore, the system app version is installed in 42 # /system/ like normal. However, at some point, this moved to /data/. 43 # So, we have to handle both cases. Like ArCore, this means we'll end up 44 # removing even non-system versions due to this, but it doesn't cause any 45 # issues. 46 'com.google.vr.core': ['/data/app/', '/system/'], 47} 48 49# Gets app path and package name pm list packages -f output. 50_PM_LIST_PACKAGE_PATH_RE = re.compile(r'^\s*package:(\S+)=(\S+)\s*$') 51 52 53def RemoveSystemApps(device, package_names): 54 """Removes the given system apps. 55 56 Args: 57 device: (device_utils.DeviceUtils) the device for which the given 58 system app should be removed. 59 package_name: (iterable of strs) the names of the packages to remove. 60 """ 61 system_package_paths = _FindSystemPackagePaths(device, package_names) 62 if system_package_paths: 63 with EnableSystemAppModification(device): 64 device.RemovePath(system_package_paths, force=True, recursive=True) 65 66 67@contextlib.contextmanager 68def ReplaceSystemApp(device, 69 package_name, 70 replacement_apk, 71 install_timeout=None): 72 """A context manager that replaces the given system app while in scope. 73 74 Args: 75 device: (device_utils.DeviceUtils) the device for which the given 76 system app should be replaced. 77 package_name: (str) the name of the package to replace. 78 replacement_apk: (str) the path to the APK to use as a replacement. 79 """ 80 storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb) 81 relocate_app = _RelocateApp(device, package_name, storage_dir.name) 82 install_app = _TemporarilyInstallApp(device, replacement_apk, install_timeout) 83 with storage_dir, relocate_app, install_app: 84 yield 85 86 87def _FindSystemPackagePaths(device, system_package_list): 88 """Finds all system paths for the given packages.""" 89 found_paths = [] 90 for system_package in system_package_list: 91 paths = _GetApplicationPaths(device, system_package) 92 p = _GetSystemPath(system_package, paths) 93 if p: 94 found_paths.append(p) 95 return found_paths 96 97 98# Find all application paths, even those flagged as uninstalled, as these 99# would still block another package with the same name from installation 100# if they differ in signing keys. 101# TODO(aluo): Move this into device_utils.py 102def _GetApplicationPaths(device, package): 103 paths = [] 104 lines = device.RunShellCommand( 105 ['pm', 'list', 'packages', '-f', '-u', package], check_return=True) 106 for line in lines: 107 match = re.match(_PM_LIST_PACKAGE_PATH_RE, line) 108 if match: 109 path = match.group(1) 110 package_name = match.group(2) 111 if package_name == package: 112 paths.append(path) 113 return paths 114 115 116def _GetSystemPath(package, paths): 117 for p in paths: 118 app_locations = SPECIAL_SYSTEM_APP_LOCATIONS.get(package, 119 ['/system/', '/product/']) 120 for location in app_locations: 121 if p.startswith(location): 122 return p 123 return None 124 125 126_MODIFICATION_TIMEOUT = 300 127_MODIFICATION_RETRIES = 2 128_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps' 129 130 131def _ShouldRetryModification(exc): 132 try: 133 if isinstance(exc, device_errors.CommandTimeoutError): 134 logger.info('Restarting the adb server') 135 adb_wrapper.RestartServer() 136 return True 137 except Exception: # pylint: disable=broad-except 138 logger.exception(('Caught an exception when deciding' 139 ' to retry system modification')) 140 return False 141 142 143# timeout and retries are both required by the decorator, but neither 144# are used within the body of the function. 145# pylint: disable=unused-argument 146 147 148@decorators.WithTimeoutAndConditionalRetries(_ShouldRetryModification) 149def _SetUpSystemAppModification(device, timeout=None, retries=None): 150 # Ensure that the device is online & available before proceeding to 151 # handle the case where something fails in the middle of set up and 152 # triggers a retry. 153 device.WaitUntilFullyBooted() 154 155 # All calls that could potentially need root should run with as_root=True, but 156 # it looks like some parts of Telemetry work as-is by implicitly assuming that 157 # root is already granted if it's necessary. The reboot can mess with this, so 158 # as a workaround, check whether we're starting with root already, and if so, 159 # restore the device to that state at the end. 160 should_restore_root = device.HasRoot() 161 device.EnableRoot() 162 if not device.HasRoot(): 163 raise device_errors.CommandFailedError( 164 'Failed to enable modification of system apps on non-rooted device', 165 str(device)) 166 167 try: 168 # Disable Marshmallow's Verity security feature 169 if device.build_version_sdk >= version_codes.MARSHMALLOW: 170 logger.info('Disabling Verity on %s', device.serial) 171 device.adb.DisableVerity() 172 device.Reboot() 173 device.WaitUntilFullyBooted() 174 device.EnableRoot() 175 176 device.adb.Remount() 177 device.RunShellCommand(['stop'], check_return=True) 178 device.SetProp(_ENABLE_MODIFICATION_PROP, '1') 179 except device_errors.CommandFailedError: 180 if device.adb.is_emulator: 181 # Point the user to documentation, since there's a good chance they can 182 # workaround this on an emulator. 183 docs_url = ('https://chromium.googlesource.com/chromium/src/+/' 184 'HEAD/docs/android_emulator.md#writable-system-partition') 185 logger.error( 186 'Did you start the emulator with "-writable-system?"\n' 187 'See %s\n', docs_url) 188 raise 189 190 return should_restore_root 191 192 193@decorators.WithTimeoutAndConditionalRetries(_ShouldRetryModification) 194def _TearDownSystemAppModification(device, 195 should_restore_root, 196 timeout=None, 197 retries=None): 198 try: 199 # The function may be re-entered after the the device loses root 200 # privilege. For instance if the adb server is restarted before 201 # re-entering the function then the device may lose root privilege. 202 # Therefore we need to do a sanity check for root privilege 203 # on the device and then re-enable root privilege if the device 204 # does not have it. 205 if not device.HasRoot(): 206 logger.warning('Need to re-enable root.') 207 device.EnableRoot() 208 209 if not device.HasRoot(): 210 raise device_errors.CommandFailedError( 211 ('Failed to tear down modification of ' 212 'system apps on non-rooted device.'), 213 str(device)) 214 215 device.SetProp(_ENABLE_MODIFICATION_PROP, '0') 216 device.Reboot() 217 device.WaitUntilFullyBooted() 218 if should_restore_root: 219 device.EnableRoot() 220 except device_errors.CommandTimeoutError: 221 logger.error('Timed out while tearing down system app modification.') 222 logger.error(' device state: %s', device.adb.GetState()) 223 raise 224 225 226# pylint: enable=unused-argument 227 228 229@contextlib.contextmanager 230def EnableSystemAppModification(device): 231 """A context manager that allows system apps to be modified while in scope. 232 233 Args: 234 device: (device_utils.DeviceUtils) the device 235 """ 236 if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1': 237 yield 238 return 239 240 should_restore_root = _SetUpSystemAppModification( 241 device, timeout=_MODIFICATION_TIMEOUT, retries=_MODIFICATION_RETRIES) 242 try: 243 yield 244 finally: 245 _TearDownSystemAppModification( 246 device, 247 should_restore_root, 248 timeout=_MODIFICATION_TIMEOUT, 249 retries=_MODIFICATION_RETRIES) 250 251 252@contextlib.contextmanager 253def _RelocateApp(device, package_name, relocate_to): 254 """A context manager that relocates an app while in scope.""" 255 relocation_map = {} 256 system_package_paths = _FindSystemPackagePaths(device, [package_name]) 257 if system_package_paths: 258 relocation_map = { 259 p: posixpath.join(relocate_to, posixpath.relpath(p, '/')) 260 for p in system_package_paths 261 } 262 relocation_dirs = [ 263 posixpath.dirname(d) for _, d in relocation_map.iteritems() 264 ] 265 device.RunShellCommand(['mkdir', '-p'] + relocation_dirs, check_return=True) 266 _MoveApp(device, relocation_map) 267 else: 268 logger.info('No system package "%s"', package_name) 269 270 try: 271 yield 272 finally: 273 _MoveApp(device, {v: k for k, v in relocation_map.iteritems()}) 274 275 276@contextlib.contextmanager 277def _TemporarilyInstallApp(device, apk, install_timeout=None): 278 """A context manager that installs an app while in scope.""" 279 if install_timeout is None: 280 device.Install(apk, reinstall=True) 281 else: 282 device.Install(apk, reinstall=True, timeout=install_timeout) 283 284 try: 285 yield 286 finally: 287 device.Uninstall(apk_helper.GetPackageName(apk)) 288 289 290def _MoveApp(device, relocation_map): 291 """Moves an app according to the provided relocation map. 292 293 Args: 294 device: (device_utils.DeviceUtils) 295 relocation_map: (dict) A dict that maps src to dest 296 """ 297 movements = ['mv %s %s' % (k, v) for k, v in relocation_map.iteritems()] 298 cmd = ' && '.join(movements) 299 with EnableSystemAppModification(device): 300 device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True) 301 302 303def main(raw_args): 304 parser = argparse.ArgumentParser() 305 subparsers = parser.add_subparsers() 306 307 def add_common_arguments(p): 308 script_common.AddDeviceArguments(p) 309 script_common.AddEnvironmentArguments(p) 310 p.add_argument( 311 '-v', 312 '--verbose', 313 action='count', 314 default=0, 315 help='Print more information.') 316 p.add_argument('command', nargs='*') 317 318 @contextlib.contextmanager 319 def remove_system_app(device, args): 320 RemoveSystemApps(device, args.packages) 321 yield 322 323 remove_parser = subparsers.add_parser('remove') 324 remove_parser.add_argument( 325 '--package', 326 dest='packages', 327 nargs='*', 328 required=True, 329 help='The system package(s) to remove.') 330 add_common_arguments(remove_parser) 331 remove_parser.set_defaults(func=remove_system_app) 332 333 @contextlib.contextmanager 334 def replace_system_app(device, args): 335 with ReplaceSystemApp(device, args.package, args.replace_with): 336 yield 337 338 replace_parser = subparsers.add_parser('replace') 339 replace_parser.add_argument( 340 '--package', required=True, help='The system package to replace.') 341 replace_parser.add_argument( 342 '--replace-with', 343 metavar='APK', 344 required=True, 345 help='The APK with which the existing system app should be replaced.') 346 add_common_arguments(replace_parser) 347 replace_parser.set_defaults(func=replace_system_app) 348 349 args = parser.parse_args(raw_args) 350 351 run_tests_helper.SetLogLevel(args.verbose) 352 script_common.InitializeEnvironment(args) 353 354 devices = script_common.GetDevices(args.devices, args.denylist_file) 355 parallel_devices = parallelizer.SyncParallelizer( 356 [args.func(d, args) for d in devices]) 357 with parallel_devices: 358 if args.command: 359 return cmd_helper.Call(args.command) 360 return 0 361 362 363if __name__ == '__main__': 364 sys.exit(main(sys.argv[1:])) 365