#!/usr/bin/env python # # Copyright (c) 2013 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. """Provisions Android devices with settings required for bots. Usage: ./provision_devices.py [-d ] """ import argparse import datetime import json import logging import os import posixpath import re import sys import time # Import _strptime before threaded code. datetime.datetime.strptime is # threadsafe except for the initial import of the _strptime module. # See crbug.com/584730 and https://bugs.python.org/issue7980. import _strptime # pylint: disable=unused-import if __name__ == '__main__': sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))) from devil.android import battery_utils from devil.android import device_blacklist from devil.android import device_errors from devil.android import device_temp_file from devil.android import device_utils from devil.android import settings 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.android.tools import script_common from devil.constants import exit_codes from devil.utils import logging_common from devil.utils import timeout_retry logger = logging.getLogger(__name__) _SYSTEM_APP_DIRECTORIES = ['/system/app/', '/system/priv-app/'] _SYSTEM_WEBVIEW_NAMES = ['webview', 'WebViewGoogle'] _CHROME_PACKAGE_REGEX = re.compile('.*chrom.*') _TOMBSTONE_REGEX = re.compile('tombstone.*') class _DEFAULT_TIMEOUTS(object): # L can take a while to reboot after a wipe. LOLLIPOP = 600 PRE_LOLLIPOP = 180 HELP_TEXT = '{}s on L, {}s on pre-L'.format(LOLLIPOP, PRE_LOLLIPOP) class ProvisionStep(object): def __init__(self, cmd, reboot=False): self.cmd = cmd self.reboot = reboot def ProvisionDevices( devices, blacklist_file, adb_key_files=None, disable_location=False, disable_mock_location=False, disable_network=False, disable_system_chrome=False, emulators=False, enable_java_debug=False, max_battery_temp=None, min_battery_level=None, output_device_blacklist=None, reboot_timeout=None, remove_system_webview=False, system_app_remove_list=None, system_package_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 [] system_package_remove_list = system_package_remove_list or [] try: devices = script_common.GetDevices(devices, blacklist) except device_errors.NoDevicesError: logging.error('No available devices to provision.') if blacklist: logging.error('Local device blacklist: %s', blacklist.Read()) raise devices = [d for d in devices if not emulators or d.adb.is_emulator] parallel_devices = device_utils.DeviceUtils.parallel(devices) steps = [] if wipe: steps += [ProvisionStep(lambda d: Wipe(d, adb_key_files), reboot=True)] steps += [ProvisionStep( lambda d: SetProperties(d, enable_java_debug, disable_location, disable_mock_location), reboot=not emulators)] if disable_network: steps.append(ProvisionStep(DisableNetwork)) if disable_system_chrome: steps.append(ProvisionStep(DisableSystemChrome)) if max_battery_temp: steps.append(ProvisionStep( lambda d: WaitForBatteryTemperature(d, max_battery_temp))) if min_battery_level: steps.append(ProvisionStep( lambda d: WaitForCharge(d, min_battery_level))) if remove_system_webview: system_app_remove_list.extend(_SYSTEM_WEBVIEW_NAMES) if system_app_remove_list or system_package_remove_list: steps.append(ProvisionStep( lambda d: RemoveSystemApps( d, system_app_remove_list, system_package_remove_list))) steps.append(ProvisionStep(SetDate)) steps.append(ProvisionStep(CheckExternalStorage)) parallel_devices.pMap(ProvisionDevice, steps, blacklist, reboot_timeout) blacklisted_devices = blacklist.Read() if blacklist else [] if output_device_blacklist: with open(output_device_blacklist, 'w') as f: json.dump(blacklisted_devices, f) if all(d in blacklisted_devices for d in devices): raise device_errors.NoDevicesError return 0 def ProvisionDevice(device, steps, blacklist, reboot_timeout=None): try: if not reboot_timeout: if device.build_version_sdk >= version_codes.LOLLIPOP: reboot_timeout = _DEFAULT_TIMEOUTS.LOLLIPOP else: reboot_timeout = _DEFAULT_TIMEOUTS.PRE_LOLLIPOP for step in steps: try: device.WaitUntilFullyBooted(timeout=reboot_timeout, retries=0) except device_errors.CommandTimeoutError: logger.error('Device did not finish booting. Will try to reboot.') device.Reboot(timeout=reboot_timeout) step.cmd(device) if step.reboot: device.Reboot(False, retries=0) device.adb.WaitForDevice() except device_errors.CommandTimeoutError: logger.exception('Timed out waiting for device %s. Adding to blacklist.', str(device)) if blacklist: blacklist.Extend([str(device)], reason='provision_timeout') except (device_errors.CommandFailedError, device_errors.DeviceUnreachableError): logger.exception('Failed to provision device %s. Adding to blacklist.', str(device)) if blacklist: blacklist.Extend([str(device)], reason='provision_failure') def Wipe(device, adb_key_files=None): if (device.IsUserBuild() or device.build_version_sdk >= version_codes.MARSHMALLOW): WipeChromeData(device) package = 'com.google.android.gms' if device.GetApplicationPaths(package): version_name = device.GetApplicationVersion(package) logger.info('Version name for %s is %s', package, version_name) else: logger.info('Package %s is not installed', package) else: WipeDevice(device, adb_key_files) def WipeChromeData(device): """Wipes chrome specific data from device (1) uninstall any app whose name matches *chrom*, except com.android.chrome, which is the chrome stable package. Doing so also removes the corresponding dirs under /data/data/ and /data/app/ (2) remove any dir under /data/app-lib/ whose name matches *chrom* (3) remove any files under /data/tombstones/ whose name matches "tombstone*" (4) remove /data/local.prop if there is any (5) remove /data/local/chrome-command-line if there is any (6) remove anything under /data/local/.config/ if the dir exists (this is telemetry related) (7) remove anything under /data/local/tmp/ Arguments: device: the device to wipe """ try: if device.IsUserBuild(): _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX) device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), 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) _WipeUnderDirIfMatch(device, '/data/app-lib/', _CHROME_PACKAGE_REGEX) _WipeUnderDirIfMatch(device, '/data/tombstones/', _TOMBSTONE_REGEX) _WipeFileOrDir(device, '/data/local.prop') _WipeFileOrDir(device, '/data/local/chrome-command-line') _WipeFileOrDir(device, '/data/local/.config/') _WipeFileOrDir(device, '/data/local/tmp/') device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), shell=True, check_return=True) except device_errors.CommandFailedError: logger.exception('Possible failure while wiping the device. ' 'Attempting to continue.') def _UninstallIfMatch(device, pattern): 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'], check_return=True)] for package_output in installed_packages: package = package_output.split(":")[1] if pattern.match(package) and package not in installed_system_packages: device.Uninstall(package) def _WipeUnderDirIfMatch(device, path, pattern): for filename in device.ListDirectory(path): if pattern.match(filename): _WipeFileOrDir(device, posixpath.join(path, filename)) def _WipeFileOrDir(device, path): if device.PathExists(path): device.RunShellCommand(['rm', '-rf', path], check_return=True) def WipeDevice(device, adb_key_files): """Wipes data from device, keeping only the adb_keys for authorization. After wiping data on a device that has been authorized, adb can still communicate with the device, but after reboot the device will need to be re-authorized because the adb keys file is stored in /data/misc/adb/. Thus, adb_keys file is rewritten so the device does not need to be re-authorized. Arguments: device: the device to wipe """ try: device.EnableRoot() device_authorized = device.FileExists(adb_wrapper.ADB_KEYS_FILE) if device_authorized: adb_keys = device.ReadFile(adb_wrapper.ADB_KEYS_FILE, as_root=True).splitlines() device.RunShellCommand(['wipe', 'data'], as_root=True, check_return=True) device.adb.WaitForDevice() if device_authorized: adb_keys_set = set(adb_keys) for adb_key_file in adb_key_files or []: try: with open(adb_key_file, 'r') as f: adb_public_keys = f.readlines() adb_keys_set.update(adb_public_keys) except IOError: logger.warning('Unable to find adb keys file %s.', adb_key_file) _WriteAdbKeysFile(device, '\n'.join(adb_keys_set)) except device_errors.CommandFailedError: logger.exception('Possible failure while wiping the device. ' 'Attempting to continue.') def _WriteAdbKeysFile(device, adb_keys_string): dir_path = posixpath.dirname(adb_wrapper.ADB_KEYS_FILE) device.RunShellCommand(['mkdir', '-p', dir_path], as_root=True, check_return=True) device.RunShellCommand(['restorecon', dir_path], as_root=True, check_return=True) device.WriteFile(adb_wrapper.ADB_KEYS_FILE, adb_keys_string, as_root=True) device.RunShellCommand(['restorecon', adb_wrapper.ADB_KEYS_FILE], as_root=True, check_return=True) def SetProperties(device, enable_java_debug, disable_location, disable_mock_location): try: device.EnableRoot() except device_errors.CommandFailedError as e: logger.warning(str(e)) if not device.IsUserBuild(): _ConfigureLocalProperties(device, enable_java_debug) else: logger.warning('Cannot configure properties in user builds.') settings.ConfigureContentSettings( device, settings.DETERMINISTIC_DEVICE_SETTINGS) if disable_location: settings.ConfigureContentSettings( device, settings.DISABLE_LOCATION_SETTINGS) else: settings.ConfigureContentSettings( device, settings.ENABLE_LOCATION_SETTINGS) if disable_mock_location: settings.ConfigureContentSettings( device, settings.DISABLE_MOCK_LOCATION_SETTINGS) else: settings.ConfigureContentSettings( device, settings.ENABLE_MOCK_LOCATION_SETTINGS) settings.SetLockScreenSettings(device) # Some device types can momentarily disappear after setting properties. device.adb.WaitForDevice() def DisableNetwork(device): settings.ConfigureContentSettings( device, settings.NETWORK_DISABLED_SETTINGS) if device.build_version_sdk >= version_codes.MARSHMALLOW: # Ensure that NFC is also switched off. device.RunShellCommand(['svc', 'nfc', 'disable'], as_root=True, check_return=True) def DisableSystemChrome(device): # The system chrome version on the device interferes with some tests. device.RunShellCommand(['pm', 'disable', 'com.android.chrome'], as_root=True, check_return=True) def _FindSystemPackagePaths(device, system_package_list): found_paths = [] for system_package in system_package_list: found_paths.extend(device.GetApplicationPaths(system_package)) return [p for p in found_paths if p.startswith('/system/')] def _FindSystemAppPaths(device, system_app_list): found_paths = [] for system_app in system_app_list: for directory in _SYSTEM_APP_DIRECTORIES: path = os.path.join(directory, system_app) if device.PathExists(path): found_paths.append(path) return found_paths def RemoveSystemApps( device, system_app_remove_list, system_package_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'] system_package_remove_list: A list of app packages to remove, e.g. ['com.google.android.webview'] """ device.EnableRoot() if device.HasRoot(): system_app_paths = ( _FindSystemAppPaths(device, system_app_remove_list) + _FindSystemPackagePaths(device, system_package_remove_list)) if system_app_paths: # 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) device.RemovePath(system_app_paths, force=True, recursive=True) 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): """Set standard readonly testing device properties prior to reboot.""" local_props = [ 'persist.sys.usb.config=adb', 'ro.monkey=1', 'ro.test_harness=1', 'ro.audio.silent=1', 'ro.setupwizard.mode=DISABLED', ] if java_debug: local_props.append( '%s=all' % device_utils.DeviceUtils.JAVA_ASSERT_PROPERTY) local_props.append('debug.checkjni=1') try: device.WriteFile( device.LOCAL_PROPERTIES_PATH, '\n'.join(local_props), as_root=True) # Android will not respect the local props file if it is world writable. device.RunShellCommand( ['chmod', '644', device.LOCAL_PROPERTIES_PATH], as_root=True, check_return=True) except device_errors.CommandFailedError: logger.exception('Failed to configure local properties.') def FinishProvisioning(device): # The lockscreen can't be disabled on user builds, so send a keyevent # to unlock it. if device.IsUserBuild(): device.SendKeyEvent(keyevent.KEYCODE_MENU) def WaitForCharge(device, min_battery_level): battery = battery_utils.BatteryUtils(device) try: battery.ChargeDeviceToLevel(min_battery_level) except device_errors.DeviceChargingError: device.Reboot() battery.ChargeDeviceToLevel(min_battery_level) def WaitForBatteryTemperature(device, max_battery_temp): try: battery = battery_utils.BatteryUtils(device) battery.LetBatteryCoolToTemperature(max_battery_temp) except device_errors.CommandFailedError: logger.exception('Unable to let battery cool to specified temperature.') def SetDate(device): def _set_and_verify_date(): if device.build_version_sdk >= version_codes.MARSHMALLOW: date_format = '%m%d%H%M%Y.%S' set_date_command = ['date', '-u'] get_date_command = ['date', '-u'] else: date_format = '%Y%m%d.%H%M%S' set_date_command = ['date', '-s'] get_date_command = ['date'] # TODO(jbudorick): This is wrong on pre-M devices -- get/set are # dealing in local time, but we're setting based on GMT. strgmtime = time.strftime(date_format, time.gmtime()) set_date_command.append(strgmtime) device.RunShellCommand(set_date_command, as_root=True, check_return=True) get_date_command.append('+"%Y%m%d.%H%M%S"') device_time = device.RunShellCommand( 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 if tdelta <= 1: logger.info('Date/time successfully set on %s', device) return True else: logger.error('Date mismatch. Device: %s Correct: %s', device_time.isoformat(), correct_time.isoformat()) return False # Sometimes the date is not set correctly on the devices. Retry on failure. if device.IsUserBuild(): # TODO(bpastene): Figure out how to set the date & time on user builds. pass else: if not timeout_retry.WaitFor( _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() # The following intent can take a bit to complete when ran shortly after # device boot-up. device.BroadcastIntent( intent.Intent(action='android.intent.action.TIME_SET'), timeout=180) def LogDeviceProperties(device): props = device.RunShellCommand(['getprop'], check_return=True) for prop in props: logger.info(' %s', prop) def CheckExternalStorage(device): """Checks that storage is writable and if not makes it writable. Arguments: device: The device to check. """ try: with device_temp_file.DeviceTempFile( device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: device.WriteFile(f.name, 'test') except device_errors.CommandFailedError: logger.info('External storage not writable. Remounting / as RW') device.RunShellCommand(['mount', '-o', 'remount,rw', '/'], check_return=True, as_root=True) device.EnableRoot() with device_temp_file.DeviceTempFile( device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: device.WriteFile(f.name, 'test') def main(raw_args): # Recommended options on perf bots: # --disable-network # TODO(tonyg): We eventually want network on. However, currently radios # can cause perfbots to drain faster than they charge. # --min-battery-level 95 # Some perf bots run benchmarks with USB charging disabled which leads # to gradual draining of the battery. We must wait for a full charge # before starting a run in order to keep the devices online. parser = argparse.ArgumentParser( description='Provision Android devices with settings required for bots.') logging_common.AddLoggingArguments(parser) script_common.AddDeviceArguments(parser) script_common.AddEnvironmentArguments(parser) parser.add_argument( '--adb-key-files', type=str, nargs='+', help='list of adb keys to push to device') parser.add_argument( '--disable-location', action='store_true', help='disable Google location services on devices') parser.add_argument( '--disable-mock-location', action='store_true', default=False, help='Set ALLOW_MOCK_LOCATION to false') parser.add_argument( '--disable-network', action='store_true', help='disable network access on devices') parser.add_argument( '--disable-java-debug', action='store_false', dest='enable_java_debug', default=True, help='disable Java property asserts and JNI checking') parser.add_argument( '--disable-system-chrome', action='store_true', help='DEPRECATED: use --remove-system-packages com.android.google ' 'Disable the system chrome from devices.') parser.add_argument( '--emulators', action='store_true', 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.') parser.add_argument( '--min-battery-level', type=int, metavar='NUM', help='wait for the device to reach this minimum battery' ' level before trying to continue') parser.add_argument( '--output-device-blacklist', help='Json file to output the device blacklist.') parser.add_argument( '--reboot-timeout', metavar='SECS', type=int, help='when wiping the device, max number of seconds to' ' wait after each reboot ' '(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT) parser.add_argument( '--remove-system-apps', nargs='*', dest='system_app_remove_list', help='DEPRECATED: use --remove-system-packages instead. ' 'The names of system apps to remove. ') parser.add_argument( '--remove-system-packages', nargs='*', dest='system_package_remove_list', help='The names of system packages to remove.') parser.add_argument( '--remove-system-webview', action='store_true', help='DEPRECATED: use --remove-system-packages ' 'com.google.android.webview com.android.webview ' 'Remove the system webview from devices.') parser.add_argument( '--skip-wipe', action='store_true', default=False, help='do not wipe device data during provisioning') # No-op arguments for compatibility with build/android/provision_devices.py. # TODO(jbudorick): Remove these once all callers have stopped using them. parser.add_argument( '--chrome-specific-wipe', action='store_true', help=argparse.SUPPRESS) parser.add_argument( '--phase', action='append', help=argparse.SUPPRESS) parser.add_argument( '-r', '--auto-reconnect', action='store_true', help=argparse.SUPPRESS) parser.add_argument( '-t', '--target', help=argparse.SUPPRESS) args = parser.parse_args(raw_args) logging_common.InitializeLogging(args) script_common.InitializeEnvironment(args) try: return ProvisionDevices( args.devices, args.blacklist_file, adb_key_files=args.adb_key_files, disable_location=args.disable_location, disable_mock_location=args.disable_mock_location, disable_network=args.disable_network, disable_system_chrome=args.disable_system_chrome, emulators=args.emulators, enable_java_debug=args.enable_java_debug, max_battery_temp=args.max_battery_temp, min_battery_level=args.min_battery_level, output_device_blacklist=args.output_device_blacklist, reboot_timeout=args.reboot_timeout, remove_system_webview=args.remove_system_webview, system_app_remove_list=args.system_app_remove_list, system_package_remove_list=args.system_package_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 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))