1#!/usr/bin/env vpython3 2# 3# Copyright 2013 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Provisions Android devices with settings required for bots. 8 9Usage: 10 ./provision_devices.py [-d <device serial number>] 11""" 12 13import argparse 14import datetime 15import json 16import logging 17import os 18import posixpath 19import re 20import subprocess 21import sys 22import time 23 24# Import _strptime before threaded code. datetime.datetime.strptime is 25# threadsafe except for the initial import of the _strptime module. 26# See crbug.com/584730 and https://bugs.python.org/issue7980. 27import _strptime # pylint: disable=unused-import 28 29import devil_chromium 30from devil.android import battery_utils 31from devil.android import device_denylist 32from devil.android import device_errors 33from devil.android import device_temp_file 34from devil.android import device_utils 35from devil.android.sdk import keyevent 36from devil.android.sdk import version_codes 37from devil.constants import exit_codes 38from devil.utils import run_tests_helper 39from devil.utils import timeout_retry 40from pylib import constants 41from pylib import device_settings 42from pylib.constants import host_paths 43 44_SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle'] 45_CHROME_PACKAGE_REGEX = re.compile('.*chrom.*') 46_TOMBSTONE_REGEX = re.compile('tombstone.*') 47 48 49class _DEFAULT_TIMEOUTS: 50 # L can take a while to reboot after a wipe. 51 LOLLIPOP = 600 52 PRE_LOLLIPOP = 180 53 54 HELP_TEXT = '{}s on L, {}s on pre-L'.format(LOLLIPOP, PRE_LOLLIPOP) 55 56 57class _PHASES: 58 WIPE = 'wipe' 59 PROPERTIES = 'properties' 60 FINISH = 'finish' 61 62 ALL = [WIPE, PROPERTIES, FINISH] 63 64 65def ProvisionDevices(args): 66 denylist = (device_denylist.Denylist(args.denylist_file) 67 if args.denylist_file else None) 68 devices = [ 69 d for d in device_utils.DeviceUtils.HealthyDevices(denylist) 70 if not args.emulators or d.is_emulator 71 ] 72 if args.device: 73 devices = [d for d in devices if d == args.device] 74 if not devices: 75 raise device_errors.DeviceUnreachableError(args.device) 76 parallel_devices = device_utils.DeviceUtils.parallel(devices) 77 if args.emulators: 78 parallel_devices.pMap(SetProperties, args) 79 else: 80 parallel_devices.pMap(ProvisionDevice, denylist, args) 81 if args.auto_reconnect: 82 _LaunchHostHeartbeat() 83 denylisted_devices = denylist.Read() if denylist else [] 84 if args.output_device_denylist: 85 with open(args.output_device_denylist, 'w') as f: 86 json.dump(denylisted_devices, f) 87 if all(d in denylisted_devices for d in devices): 88 raise device_errors.NoDevicesError 89 return 0 90 91 92def ProvisionDevice(device, denylist, options): 93 def should_run_phase(phase_name): 94 return not options.phases or phase_name in options.phases 95 96 def run_phase(phase_func, reboot_timeout, reboot=True): 97 try: 98 device.WaitUntilFullyBooted(timeout=reboot_timeout, retries=0) 99 except device_errors.CommandTimeoutError: 100 logging.error('Device did not finish booting. Will try to reboot.') 101 device.Reboot(timeout=reboot_timeout) 102 phase_func(device, options) 103 if reboot: 104 device.Reboot(False, retries=0) 105 device.adb.WaitForDevice() 106 107 try: 108 if options.reboot_timeout: 109 reboot_timeout = options.reboot_timeout 110 elif device.build_version_sdk >= version_codes.LOLLIPOP: 111 reboot_timeout = _DEFAULT_TIMEOUTS.LOLLIPOP 112 else: 113 reboot_timeout = _DEFAULT_TIMEOUTS.PRE_LOLLIPOP 114 115 if should_run_phase(_PHASES.WIPE): 116 if (options.chrome_specific_wipe or device.IsUserBuild() or 117 device.build_version_sdk >= version_codes.MARSHMALLOW): 118 run_phase(WipeChromeData, reboot_timeout) 119 else: 120 run_phase(WipeDevice, reboot_timeout) 121 122 if should_run_phase(_PHASES.PROPERTIES): 123 run_phase(SetProperties, reboot_timeout) 124 125 if should_run_phase(_PHASES.FINISH): 126 run_phase(FinishProvisioning, reboot_timeout, reboot=False) 127 128 if options.chrome_specific_wipe: 129 package = "com.google.android.gms" 130 version_name = device.GetApplicationVersion(package) 131 logging.info("Version name for %s is %s", package, version_name) 132 133 CheckExternalStorage(device) 134 135 except device_errors.CommandTimeoutError: 136 logging.exception('Timed out waiting for device %s. Adding to denylist.', 137 str(device)) 138 if denylist: 139 denylist.Extend([str(device)], reason='provision_timeout') 140 141 except (device_errors.CommandFailedError, 142 device_errors.DeviceUnreachableError): 143 logging.exception('Failed to provision device %s. Adding to denylist.', 144 str(device)) 145 if denylist: 146 denylist.Extend([str(device)], reason='provision_failure') 147 148 149def CheckExternalStorage(device): 150 """Checks that storage is writable and if not makes it writable. 151 152 Arguments: 153 device: The device to check. 154 """ 155 try: 156 with device_temp_file.DeviceTempFile( 157 device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: 158 device.WriteFile(f.name, 'test') 159 except device_errors.CommandFailedError: 160 logging.info('External storage not writable. Remounting / as RW') 161 device.RunShellCommand(['mount', '-o', 'remount,rw', '/'], 162 check_return=True, as_root=True) 163 device.EnableRoot() 164 with device_temp_file.DeviceTempFile( 165 device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: 166 device.WriteFile(f.name, 'test') 167 168def WipeChromeData(device, options): 169 """Wipes chrome specific data from device 170 171 (1) uninstall any app whose name matches *chrom*, except 172 com.android.chrome, which is the chrome stable package. Doing so also 173 removes the corresponding dirs under /data/data/ and /data/app/ 174 (2) remove any dir under /data/app-lib/ whose name matches *chrom* 175 (3) remove any files under /data/tombstones/ whose name matches "tombstone*" 176 (4) remove /data/local.prop if there is any 177 (5) remove /data/local/chrome-command-line if there is any 178 (6) remove anything under /data/local/.config/ if the dir exists 179 (this is telemetry related) 180 (7) remove anything under /data/local/tmp/ 181 182 Arguments: 183 device: the device to wipe 184 """ 185 if options.skip_wipe: 186 return 187 188 try: 189 if device.IsUserBuild(): 190 _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, 191 constants.PACKAGE_INFO['chrome_stable'].package) 192 device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), 193 check_return=True) 194 device.RunShellCommand('rm -rf /data/local/tmp/*', check_return=True) 195 else: 196 device.EnableRoot() 197 _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, 198 constants.PACKAGE_INFO['chrome_stable'].package) 199 _WipeUnderDirIfMatch(device, '/data/app-lib/', _CHROME_PACKAGE_REGEX) 200 _WipeUnderDirIfMatch(device, '/data/tombstones/', _TOMBSTONE_REGEX) 201 202 _WipeFileOrDir(device, '/data/local.prop') 203 _WipeFileOrDir(device, '/data/local/chrome-command-line') 204 _WipeFileOrDir(device, '/data/local/.config/') 205 _WipeFileOrDir(device, '/data/local/tmp/') 206 device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), 207 check_return=True) 208 except device_errors.CommandFailedError: 209 logging.exception('Possible failure while wiping the device. ' 210 'Attempting to continue.') 211 212 213def WipeDevice(device, options): 214 """Wipes data from device, keeping only the adb_keys for authorization. 215 216 After wiping data on a device that has been authorized, adb can still 217 communicate with the device, but after reboot the device will need to be 218 re-authorized because the adb keys file is stored in /data/misc/adb/. 219 Thus, adb_keys file is rewritten so the device does not need to be 220 re-authorized. 221 222 Arguments: 223 device: the device to wipe 224 """ 225 if options.skip_wipe: 226 return 227 228 try: 229 device.EnableRoot() 230 device_authorized = device.FileExists(constants.ADB_KEYS_FILE) 231 if device_authorized: 232 adb_keys = device.ReadFile(constants.ADB_KEYS_FILE, 233 as_root=True).splitlines() 234 device.RunShellCommand(['wipe', 'data'], 235 as_root=True, check_return=True) 236 device.adb.WaitForDevice() 237 238 if device_authorized: 239 adb_keys_set = set(adb_keys) 240 for adb_key_file in options.adb_key_files or []: 241 try: 242 with open(adb_key_file, 'r') as f: 243 adb_public_keys = f.readlines() 244 adb_keys_set.update(adb_public_keys) 245 except IOError: 246 logging.warning('Unable to find adb keys file %s.', adb_key_file) 247 _WriteAdbKeysFile(device, '\n'.join(adb_keys_set)) 248 except device_errors.CommandFailedError: 249 logging.exception('Possible failure while wiping the device. ' 250 'Attempting to continue.') 251 252 253def _WriteAdbKeysFile(device, adb_keys_string): 254 dir_path = posixpath.dirname(constants.ADB_KEYS_FILE) 255 device.RunShellCommand(['mkdir', '-p', dir_path], 256 as_root=True, check_return=True) 257 device.RunShellCommand(['restorecon', dir_path], 258 as_root=True, check_return=True) 259 device.WriteFile(constants.ADB_KEYS_FILE, adb_keys_string, as_root=True) 260 device.RunShellCommand(['restorecon', constants.ADB_KEYS_FILE], 261 as_root=True, check_return=True) 262 263 264def SetProperties(device, options): 265 try: 266 device.EnableRoot() 267 except device_errors.CommandFailedError as e: 268 logging.warning(str(e)) 269 270 if not device.IsUserBuild(): 271 _ConfigureLocalProperties(device, options.enable_java_debug) 272 else: 273 logging.warning('Cannot configure properties in user builds.') 274 device_settings.ConfigureContentSettings( 275 device, device_settings.DETERMINISTIC_DEVICE_SETTINGS) 276 if options.disable_location: 277 device_settings.ConfigureContentSettings( 278 device, device_settings.DISABLE_LOCATION_SETTINGS) 279 else: 280 device_settings.ConfigureContentSettings( 281 device, device_settings.ENABLE_LOCATION_SETTINGS) 282 283 if options.disable_mock_location: 284 device_settings.ConfigureContentSettings( 285 device, device_settings.DISABLE_MOCK_LOCATION_SETTINGS) 286 else: 287 device_settings.ConfigureContentSettings( 288 device, device_settings.ENABLE_MOCK_LOCATION_SETTINGS) 289 290 device_settings.SetLockScreenSettings(device) 291 if options.disable_network: 292 device_settings.ConfigureContentSettings( 293 device, device_settings.NETWORK_DISABLED_SETTINGS) 294 if device.build_version_sdk >= version_codes.MARSHMALLOW: 295 # Ensure that NFC is also switched off. 296 device.RunShellCommand(['svc', 'nfc', 'disable'], 297 as_root=True, check_return=True) 298 299 if options.disable_system_chrome: 300 # The system chrome version on the device interferes with some tests. 301 device.RunShellCommand(['pm', 'disable', 'com.android.chrome'], 302 check_return=True) 303 304 if options.remove_system_webview: 305 if any(device.PathExists(p) for p in _SYSTEM_WEBVIEW_PATHS): 306 logging.info('System WebView exists and needs to be removed') 307 if device.HasRoot(): 308 # Disabled Marshmallow's Verity security feature 309 if device.build_version_sdk >= version_codes.MARSHMALLOW: 310 device.adb.DisableVerity() 311 device.Reboot() 312 device.WaitUntilFullyBooted() 313 device.EnableRoot() 314 315 # This is required, e.g., to replace the system webview on a device. 316 device.adb.Remount() 317 device.RunShellCommand(['stop'], check_return=True) 318 device.RunShellCommand(['rm', '-rf'] + _SYSTEM_WEBVIEW_PATHS, 319 check_return=True) 320 device.RunShellCommand(['start'], check_return=True) 321 else: 322 logging.warning('Cannot remove system webview from a non-rooted device') 323 else: 324 logging.info('System WebView already removed') 325 326 # Some device types can momentarily disappear after setting properties. 327 device.adb.WaitForDevice() 328 329 330def _ConfigureLocalProperties(device, java_debug=True): 331 """Set standard readonly testing device properties prior to reboot.""" 332 local_props = [ 333 'persist.sys.usb.config=adb', 334 'ro.monkey=1', 335 'ro.test_harness=1', 336 'ro.audio.silent=1', 337 'ro.setupwizard.mode=DISABLED', 338 ] 339 if java_debug: 340 local_props.append( 341 '%s=all' % device_utils.DeviceUtils.JAVA_ASSERT_PROPERTY) 342 local_props.append('debug.checkjni=1') 343 try: 344 device.WriteFile( 345 device.LOCAL_PROPERTIES_PATH, 346 '\n'.join(local_props), as_root=True) 347 # Android will not respect the local props file if it is world writable. 348 device.RunShellCommand( 349 ['chmod', '644', device.LOCAL_PROPERTIES_PATH], 350 as_root=True, check_return=True) 351 except device_errors.CommandFailedError: 352 logging.exception('Failed to configure local properties.') 353 354 355def FinishProvisioning(device, options): 356 # The lockscreen can't be disabled on user builds, so send a keyevent 357 # to unlock it. 358 if device.IsUserBuild(): 359 device.SendKeyEvent(keyevent.KEYCODE_MENU) 360 361 if options.min_battery_level is not None: 362 battery = battery_utils.BatteryUtils(device) 363 try: 364 battery.ChargeDeviceToLevel(options.min_battery_level) 365 except device_errors.DeviceChargingError: 366 device.Reboot() 367 battery.ChargeDeviceToLevel(options.min_battery_level) 368 369 if options.max_battery_temp is not None: 370 try: 371 battery = battery_utils.BatteryUtils(device) 372 battery.LetBatteryCoolToTemperature(options.max_battery_temp) 373 except device_errors.CommandFailedError: 374 logging.exception('Unable to let battery cool to specified temperature.') 375 376 def _set_and_verify_date(): 377 if device.build_version_sdk >= version_codes.MARSHMALLOW: 378 date_format = '%m%d%H%M%Y.%S' 379 set_date_command = ['date', '-u'] 380 get_date_command = ['date', '-u'] 381 else: 382 date_format = '%Y%m%d.%H%M%S' 383 set_date_command = ['date', '-s'] 384 get_date_command = ['date'] 385 386 # TODO(jbudorick): This is wrong on pre-M devices -- get/set are 387 # dealing in local time, but we're setting based on GMT. 388 strgmtime = time.strftime(date_format, time.gmtime()) 389 set_date_command.append(strgmtime) 390 device.RunShellCommand(set_date_command, as_root=True, check_return=True) 391 392 get_date_command.append('+"%Y%m%d.%H%M%S"') 393 device_time = device.RunShellCommand( 394 get_date_command, as_root=True, single_line=True).replace('"', '') 395 device_time = datetime.datetime.strptime(device_time, "%Y%m%d.%H%M%S") 396 correct_time = datetime.datetime.strptime(strgmtime, date_format) 397 tdelta = abs(correct_time - device_time).seconds 398 if tdelta <= 1: 399 logging.info('Date/time successfully set on %s', device) 400 return True 401 logging.error('Date mismatch. Device: %s Correct: %s', 402 device_time.isoformat(), correct_time.isoformat()) 403 return False 404 405 # Sometimes the date is not set correctly on the devices. Retry on failure. 406 if device.IsUserBuild(): 407 # TODO(bpastene): Figure out how to set the date & time on user builds. 408 pass 409 else: 410 if not timeout_retry.WaitFor( 411 _set_and_verify_date, wait_period=1, max_tries=2): 412 raise device_errors.CommandFailedError( 413 'Failed to set date & time.', device_serial=str(device)) 414 415 props = device.RunShellCommand('getprop', check_return=True) 416 for prop in props: 417 logging.info(' %s', prop) 418 if options.auto_reconnect: 419 _PushAndLaunchAdbReboot(device, options.target) 420 421 422def _UninstallIfMatch(device, pattern, app_to_keep): 423 installed_packages = device.RunShellCommand(['pm', 'list', 'packages']) 424 installed_system_packages = [ 425 pkg.split(':')[1] for pkg in device.RunShellCommand(['pm', 'list', 426 'packages', '-s'])] 427 for package_output in installed_packages: 428 package = package_output.split(":")[1] 429 if pattern.match(package) and not package == app_to_keep: 430 if not device.IsUserBuild() or package not in installed_system_packages: 431 device.Uninstall(package) 432 433 434def _WipeUnderDirIfMatch(device, path, pattern): 435 for filename in device.ListDirectory(path): 436 if pattern.match(filename): 437 _WipeFileOrDir(device, posixpath.join(path, filename)) 438 439 440def _WipeFileOrDir(device, path): 441 if device.PathExists(path): 442 device.RunShellCommand(['rm', '-rf', path], check_return=True) 443 444 445def _PushAndLaunchAdbReboot(device, target): 446 """Pushes and launches the adb_reboot binary on the device. 447 448 Arguments: 449 device: The DeviceUtils instance for the device to which the adb_reboot 450 binary should be pushed. 451 target: The build target (example, Debug or Release) which helps in 452 locating the adb_reboot binary. 453 """ 454 logging.info('Will push and launch adb_reboot on %s', str(device)) 455 # Kill if adb_reboot is already running. 456 device.KillAll('adb_reboot', blocking=True, timeout=2, quiet=True) 457 # Push adb_reboot 458 logging.info(' Pushing adb_reboot ...') 459 adb_reboot = os.path.join(host_paths.DIR_SOURCE_ROOT, 460 'out/%s/adb_reboot' % target) 461 device.PushChangedFiles([(adb_reboot, '/data/local/tmp/')]) 462 # Launch adb_reboot 463 logging.info(' Launching adb_reboot ...') 464 device.RunShellCommand( 465 ['/data/local/tmp/adb_reboot'], 466 check_return=True) 467 468 469def _LaunchHostHeartbeat(): 470 # Kill if existing host_heartbeat 471 KillHostHeartbeat() 472 # Launch a new host_heartbeat 473 logging.info('Spawning host heartbeat...') 474 subprocess.Popen([os.path.join(host_paths.DIR_SOURCE_ROOT, 475 'build/android/host_heartbeat.py')]) 476 477def KillHostHeartbeat(): 478 ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE) 479 stdout, _ = ps.communicate() 480 matches = re.findall('\\n.*host_heartbeat.*', stdout) 481 for match in matches: 482 logging.info('An instance of host heart beart running... will kill') 483 pid = re.findall(r'(\S+)', match)[1] 484 subprocess.call(['kill', str(pid)]) 485 486def main(): 487 # Recommended options on perf bots: 488 # --disable-network 489 # TODO(tonyg): We eventually want network on. However, currently radios 490 # can cause perfbots to drain faster than they charge. 491 # --min-battery-level 95 492 # Some perf bots run benchmarks with USB charging disabled which leads 493 # to gradual draining of the battery. We must wait for a full charge 494 # before starting a run in order to keep the devices online. 495 496 parser = argparse.ArgumentParser( 497 description='Provision Android devices with settings required for bots.') 498 parser.add_argument('-d', '--device', metavar='SERIAL', 499 help='the serial number of the device to be provisioned' 500 ' (the default is to provision all devices attached)') 501 parser.add_argument('--adb-path', 502 help='Absolute path to the adb binary to use.') 503 parser.add_argument('--denylist-file', help='Device denylist JSON file.') 504 parser.add_argument('--phase', action='append', choices=_PHASES.ALL, 505 dest='phases', 506 help='Phases of provisioning to run. ' 507 '(If omitted, all phases will be run.)') 508 parser.add_argument('--skip-wipe', action='store_true', default=False, 509 help="don't wipe device data during provisioning") 510 parser.add_argument('--reboot-timeout', metavar='SECS', type=int, 511 help='when wiping the device, max number of seconds to' 512 ' wait after each reboot ' 513 '(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT) 514 parser.add_argument('--min-battery-level', type=int, metavar='NUM', 515 help='wait for the device to reach this minimum battery' 516 ' level before trying to continue') 517 parser.add_argument('--disable-location', action='store_true', 518 help='disable Google location services on devices') 519 parser.add_argument('--disable-mock-location', action='store_true', 520 default=False, help='Set ALLOW_MOCK_LOCATION to false') 521 parser.add_argument('--disable-network', action='store_true', 522 help='disable network access on devices') 523 parser.add_argument('--disable-java-debug', action='store_false', 524 dest='enable_java_debug', default=True, 525 help='disable Java property asserts and JNI checking') 526 parser.add_argument('--disable-system-chrome', action='store_true', 527 help='Disable the system chrome from devices.') 528 parser.add_argument('--remove-system-webview', action='store_true', 529 help='Remove the system webview from devices.') 530 parser.add_argument('-t', '--target', default='Debug', 531 help='the build target (default: %(default)s)') 532 parser.add_argument('-r', '--auto-reconnect', action='store_true', 533 help='push binary which will reboot the device on adb' 534 ' disconnections') 535 parser.add_argument('--adb-key-files', type=str, nargs='+', 536 help='list of adb keys to push to device') 537 parser.add_argument('-v', '--verbose', action='count', default=1, 538 help='Log more information.') 539 parser.add_argument('--max-battery-temp', type=int, metavar='NUM', 540 help='Wait for the battery to have this temp or lower.') 541 parser.add_argument('--output-device-denylist', 542 help='Json file to output the device denylist.') 543 parser.add_argument('--chrome-specific-wipe', action='store_true', 544 help='only wipe chrome specific data during provisioning') 545 parser.add_argument('--emulators', action='store_true', 546 help='provision only emulators and ignore usb devices') 547 args = parser.parse_args() 548 constants.SetBuildType(args.target) 549 550 run_tests_helper.SetLogLevel(args.verbose) 551 552 devil_chromium.Initialize(adb_path=args.adb_path) 553 554 try: 555 return ProvisionDevices(args) 556 except (device_errors.DeviceUnreachableError, device_errors.NoDevicesError): 557 logging.exception('Unable to provision local devices.') 558 return exit_codes.INFRA 559 560 561if __name__ == '__main__': 562 sys.exit(main()) 563