# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import datetime import errno import functools import logging import os import re import signal import stat import sys import time import common from autotest_lib.client.bin import utils as client_utils from autotest_lib.client.common_lib import android_utils from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import global_config from autotest_lib.client.common_lib.cros import dev_server from autotest_lib.client.common_lib.cros import retry from autotest_lib.server import afe_utils from autotest_lib.server import autoserv_parser from autotest_lib.server import constants as server_constants from autotest_lib.server import utils from autotest_lib.server.cros import provision from autotest_lib.server.cros.dynamic_suite import tools from autotest_lib.server.cros.dynamic_suite import constants from autotest_lib.server.hosts import abstract_ssh from autotest_lib.server.hosts import adb_label from autotest_lib.server.hosts import base_label from autotest_lib.server.hosts import teststation_host CONFIG = global_config.global_config ADB_CMD = 'adb' FASTBOOT_CMD = 'fastboot' SHELL_CMD = 'shell' # Some devices have no serial, then `adb serial` has output such as: # (no serial number) device # ?????????? device DEVICE_NO_SERIAL_MSG = '(no serial number)' DEVICE_NO_SERIAL_TAG = '<NO_SERIAL>' # Regex to find an adb device. Examples: # 0146B5580B01801B device # 018e0ecb20c97a62 device # 172.22.75.141:5555 device # localhost:22 device DEVICE_FINDER_REGEX = (r'^(?P<SERIAL>([\w-]+)|((tcp:)?' + '\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}([:]5555)?)|' + '((tcp:)?localhost([:]22)?)|' + re.escape(DEVICE_NO_SERIAL_MSG) + r')[ \t]+(?:device|fastboot)') CMD_OUTPUT_PREFIX = 'ADB_CMD_OUTPUT' CMD_OUTPUT_REGEX = ('(?P<OUTPUT>[\s\S]*)%s:(?P<EXIT_CODE>\d{1,3})' % CMD_OUTPUT_PREFIX) RELEASE_FILE = 'ro.build.version.release' BOARD_FILE = 'ro.product.device' SDK_FILE = 'ro.build.version.sdk' LOGCAT_FILE_FMT = 'logcat_%s.log' TMP_DIR = '/data/local/tmp' # Regex to pull out file type, perms and symlink. Example: # lrwxrwx--- 1 6 root system 2015-09-12 19:21 blah_link -> ./blah FILE_INFO_REGEX = '^(?P<TYPE>[dl-])(?P<PERMS>[rwx-]{9})' FILE_SYMLINK_REGEX = '^.*-> (?P<SYMLINK>.+)' # List of the perm stats indexed by the order they are listed in the example # supplied above. FILE_PERMS_FLAGS = [stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR, stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH] # Default maximum number of seconds to wait for a device to be down. DEFAULT_WAIT_DOWN_TIME_SECONDS = 10 # Default maximum number of seconds to wait for a device to be up. DEFAULT_WAIT_UP_TIME_SECONDS = 300 # Maximum number of seconds to wait for a device to be up after it's wiped. WAIT_UP_AFTER_WIPE_TIME_SECONDS = 1200 # Default timeout for retrying adb/fastboot command. DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS = 10 OS_TYPE_ANDROID = 'android' OS_TYPE_BRILLO = 'brillo' # Regex to parse build name to get the detailed build information. BUILD_REGEX = ('(?P<BRANCH>([^/]+))/(?P<BUILD_TARGET>([^/]+))-' '(?P<BUILD_TYPE>([^/]+))/(?P<BUILD_ID>([^/]+))') # Regex to parse devserver url to get the detailed build information. Sample # url: http://$devserver:8080/static/branch/target/build_id DEVSERVER_URL_REGEX = '.*/%s/*' % BUILD_REGEX ANDROID_IMAGE_FILE_FMT = '%(build_target)s-img-%(build_id)s.zip' BRILLO_VENDOR_PARTITIONS_FILE_FMT = ( '%(build_target)s-vendor_partitions-%(build_id)s.zip') AUTOTEST_SERVER_PACKAGE_FILE_FMT = ( '%(build_target)s-autotest_server_package-%(build_id)s.tar.bz2') ADB_DEVICE_PREFIXES = ['product:', 'model:', 'device:'] # Command to provision a Brillo device. # os_image_dir: The full path of the directory that contains all the Android image # files (from the image zip file). # vendor_partition_dir: The full path of the directory that contains all the # Brillo vendor partitions, and provision-device script. BRILLO_PROVISION_CMD = ( 'sudo ANDROID_PROVISION_OS_PARTITIONS=%(os_image_dir)s ' 'ANDROID_PROVISION_VENDOR_PARTITIONS=%(vendor_partition_dir)s ' '%(vendor_partition_dir)s/provision-device') # Default timeout in minutes for fastboot commands. DEFAULT_FASTBOOT_RETRY_TIMEOUT_MIN = 10 # Default permissions for files/dirs copied from the device. _DEFAULT_FILE_PERMS = 0o600 _DEFAULT_DIR_PERMS = 0o700 # Constants for getprop return value for a given property. PROPERTY_VALUE_TRUE = '1' # Timeout used for retrying installing apk. After reinstall apk failed, we try # to reboot the device and try again. APK_INSTALL_TIMEOUT_MIN = 5 # The amount of time to wait for package verification to be turned off. DISABLE_PACKAGE_VERIFICATION_TIMEOUT_MIN = 1 # Directory where (non-Brillo) Android stores tombstone crash logs. ANDROID_TOMBSTONE_CRASH_LOG_DIR = '/data/tombstones' # Directory where Brillo stores crash logs for native (non-Java) crashes. BRILLO_NATIVE_CRASH_LOG_DIR = '/data/misc/crash_reporter/crash' # A specific string value to return when a timeout has occurred. TIMEOUT_MSG = 'TIMEOUT_OCCURRED' class AndroidInstallError(error.InstallError): """Generic error for Android installation related exceptions.""" class ADBHost(abstract_ssh.AbstractSSHHost): """This class represents a host running an ADB server.""" VERSION_PREFIX = provision.ANDROID_BUILD_VERSION_PREFIX _LABEL_FUNCTIONS = [] _DETECTABLE_LABELS = [] label_decorator = functools.partial(utils.add_label_detector, _LABEL_FUNCTIONS, _DETECTABLE_LABELS) _parser = autoserv_parser.autoserv_parser # Minimum build id that supports server side packaging. Older builds may # not have server side package built or with Autotest code change to support # server-side packaging. MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value( 'AUTOSERV', 'min_launch_control_build_id_support_ssp', type=int) @staticmethod def check_host(host, timeout=10): """ Check if the given host is an adb host. If SSH connectivity can't be established, check_host will try to use user 'adb' as well. If SSH connectivity still can't be established then the original SSH user is restored. @param host: An ssh host representing a device. @param timeout: The timeout for the run command. @return: True if the host device has adb. @raises AutoservRunError: If the command failed. @raises AutoservSSHTimeout: Ssh connection has timed out. """ # host object may not have user attribute if it's a LocalHost object. current_user = host.user if hasattr(host, 'user') else None try: if not (host.hostname == 'localhost' or host.verify_ssh_user_access()): host.user = 'adb' result = host.run( 'test -f %s' % server_constants.ANDROID_TESTER_FILEFLAG, timeout=timeout) except (error.GenericHostRunError, error.AutoservSSHTimeout): if current_user is not None: host.user = current_user return False return result.exit_status == 0 def _initialize(self, hostname='localhost', serials=None, adb_serial=None, fastboot_serial=None, teststation=None, *args, **dargs): """Initialize an ADB Host. This will create an ADB Host. Hostname should always refer to the test station connected to an Android DUT. This will be the DUT to test with. If there are multiple, serial must be specified or an exception will be raised. @param hostname: Hostname of the machine running ADB. @param serials: DEPRECATED (to be removed) @param adb_serial: An ADB device serial. If None, assume a single device is attached (and fail otherwise). @param fastboot_serial: A fastboot device serial. If None, defaults to the ADB serial (or assumes a single device if the latter is None). @param teststation: The teststation object ADBHost should use. """ # Sets up the is_client_install_supported field. super(ADBHost, self)._initialize(hostname=hostname, is_client_install_supported=False, *args, **dargs) self.tmp_dirs = [] self.labels = base_label.LabelRetriever(adb_label.ADB_LABELS) adb_serial = adb_serial or self._afe_host.attributes.get('serials') fastboot_serial = (fastboot_serial or self._afe_host.attributes.get('fastboot_serial')) self.adb_serial = adb_serial if adb_serial: adb_prefix = any(adb_serial.startswith(p) for p in ADB_DEVICE_PREFIXES) self.fastboot_serial = (fastboot_serial or ('tcp:%s' % adb_serial.split(':')[0] if ':' in adb_serial and not adb_prefix else adb_serial)) self._use_tcpip = ':' in adb_serial and not adb_prefix else: self.fastboot_serial = fastboot_serial or adb_serial self._use_tcpip = False self.teststation = (teststation if teststation else teststation_host.create_teststationhost( hostname=hostname, user=self.user, password=self.password, port=self.port )) msg ='Initializing ADB device on host: %s' % hostname if self.adb_serial: msg += ', ADB serial: %s' % self.adb_serial if self.fastboot_serial: msg += ', fastboot serial: %s' % self.fastboot_serial logging.debug(msg) self._os_type = None def _connect_over_tcpip_as_needed(self): """Connect to the ADB device over TCP/IP if so configured.""" if not self._use_tcpip: return logging.debug('Connecting to device over TCP/IP') self.adb_run('connect %s' % self.adb_serial) def _restart_adbd_with_root_permissions(self): """Restarts the adb daemon with root permissions.""" @retry.retry(error.GenericHostRunError, timeout_min=20/60.0, delay_sec=1) def run_adb_root(): """Run command `adb root`.""" self.adb_run('root') # adb command may flake with error "device not found". Retry the root # command to reduce the chance of flake. run_adb_root() # TODO(ralphnathan): Remove this sleep once b/19749057 is resolved. time.sleep(1) self._connect_over_tcpip_as_needed() self.adb_run('wait-for-device') def _set_tcp_port(self): """Ensure the device remains in tcp/ip mode after a reboot.""" if not self._use_tcpip: return port = self.adb_serial.split(':')[-1] self.run('setprop persist.adb.tcp.port %s' % port) def _reset_adbd_connection(self): """Resets adbd connection to the device after a reboot/initialization""" self._connect_over_tcpip_as_needed() self._restart_adbd_with_root_permissions() self._set_tcp_port() # pylint: disable=missing-docstring def adb_run(self, command, **kwargs): """Runs an adb command. This command will launch on the test station. Refer to _device_run method for docstring for parameters. """ return self._device_run(ADB_CMD, command, **kwargs) # pylint: disable=missing-docstring def fastboot_run(self, command, **kwargs): """Runs an fastboot command. This command will launch on the test station. Refer to _device_run method for docstring for parameters. """ return self._device_run(FASTBOOT_CMD, command, **kwargs) # pylint: disable=missing-docstring @retry.retry(error.GenericHostRunError, timeout_min=DEFAULT_FASTBOOT_RETRY_TIMEOUT_MIN) def _fastboot_run_with_retry(self, command, **kwargs): """Runs an fastboot command with retry. This command will launch on the test station. Refer to _device_run method for docstring for parameters. """ return self.fastboot_run(command, **kwargs) def _log_adb_pid(self): """Log the pid of adb server. adb's server is known to have bugs and randomly restart. BY logging the server's pid it will allow us to better debug random adb failures. """ adb_pid = self.teststation.run('pgrep -f "adb.*server"') logging.debug('ADB Server PID: %s', adb_pid.stdout) def _device_run(self, function, command, shell=False, timeout=3600, ignore_status=False, ignore_timeout=False, stdout=utils.TEE_TO_LOGS, stderr=utils.TEE_TO_LOGS, connect_timeout=30, options='', stdin=None, verbose=True, require_sudo=False, args=()): """Runs a command named `function` on the test station. This command will launch on the test station. @param command: Command to run. @param shell: If true the command runs in the adb shell otherwise if False it will be passed directly to adb. For example reboot with shell=False will call 'adb reboot'. This option only applies to function adb. @param timeout: Time limit in seconds before attempting to kill the running process. The run() function will take a few seconds longer than 'timeout' to complete if it has to kill the process. @param ignore_status: Do not raise an exception, no matter what the exit code of the command is. @param ignore_timeout: Bool True if command timeouts should be ignored. Will return None on command timeout. @param stdout: Redirect stdout. @param stderr: Redirect stderr. @param connect_timeout: Connection timeout (in seconds) @param options: String with additional ssh command options @param stdin: Stdin to pass (a string) to the executed command @param require_sudo: True to require sudo to run the command. Default is False. @param args: Sequence of strings to pass as arguments to command by quoting them in " and escaping their contents if necessary. @returns a CMDResult object. """ if function == ADB_CMD: serial = self.adb_serial elif function == FASTBOOT_CMD: serial = self.fastboot_serial else: raise NotImplementedError('Mode %s is not supported' % function) if function != ADB_CMD and shell: raise error.CmdError('shell option is only applicable to `adb`.') client_side_cmd = 'timeout --signal=%d %d %s' % (signal.SIGKILL, timeout + 1, function) cmd = '%s%s ' % ('sudo -n ' if require_sudo else '', client_side_cmd) if serial: cmd += '-s %s ' % serial if shell: cmd += '%s ' % SHELL_CMD cmd += command self._log_adb_pid() if verbose: logging.debug('Command: %s', cmd) return self.teststation.run(cmd, timeout=timeout, ignore_status=ignore_status, ignore_timeout=ignore_timeout, stdout_tee=stdout, stderr_tee=stderr, options=options, stdin=stdin, connect_timeout=connect_timeout, args=args) def _run_output_with_retry(self, cmd): """Call run_output method for the given command with retry. adb command can be flaky some time, and the command may fail or return empty string. It may take several retries until a value can be returned. @param cmd: The command to run. @return: Return value from the command after retry. """ try: return client_utils.poll_for_condition( lambda: self.run_output(cmd, ignore_status=True), timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, sleep_interval=0.5, desc='Get return value for command `%s`' % cmd) except client_utils.TimeoutError: return '' def get_device_aliases(self): """Get all aliases for this device.""" product = self.get_product_name() return android_utils.AndroidAliases.get_product_aliases(product) def get_product_name(self): """Get the product name of the device, eg., shamu, bat""" return self.run_output('getprop %s' % BOARD_FILE) def get_board_name(self): """Get the name of the board, e.g., shamu, bat_land etc. """ product = self.get_product_name() return android_utils.AndroidAliases.get_board_name(product) @label_decorator() def get_board(self): """Determine the correct board label for the device. @returns a string representing this device's board. """ board = self.get_board_name() board_os = self.get_os_type() return constants.BOARD_PREFIX + '-'.join([board_os, board]) def job_start(self): """Overload of parent which intentionally doesn't log certain files. The parent implementation attempts to log certain Linux files, such as /var/log, which do not exist on Android, thus there is no call to the parent's job_start(). The sync call is made so that logcat logs can be approximately matched to server logs. """ # Try resetting the ADB daemon on the device, however if we are # creating the host to do a repair job, the device maybe inaccesible # via ADB. try: self._reset_adbd_connection() except error.GenericHostRunError as e: logging.error('Unable to reset the device adb daemon connection: ' '%s.', e) if self.is_up(): self._sync_time() self._enable_native_crash_logging() def run(self, command, timeout=3600, ignore_status=False, ignore_timeout=False, stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, connect_timeout=30, options='', stdin=None, verbose=True, args=()): """Run a command on the adb device. The command given will be ran directly on the adb device; for example 'ls' will be ran as: 'abd shell ls' @param command: The command line string. @param timeout: Time limit in seconds before attempting to kill the running process. The run() function will take a few seconds longer than 'timeout' to complete if it has to kill the process. @param ignore_status: Do not raise an exception, no matter what the exit code of the command is. @param ignore_timeout: Bool True if command timeouts should be ignored. Will return None on command timeout. @param stdout_tee: Redirect stdout. @param stderr_tee: Redirect stderr. @param connect_timeout: Connection timeout (in seconds). @param options: String with additional ssh command options. @param stdin: Stdin to pass (a string) to the executed command @param args: Sequence of strings to pass as arguments to command by quoting them in " and escaping their contents if necessary. @returns A CMDResult object or None if the call timed out and ignore_timeout is True. @raises AutoservRunError: If the command failed. @raises AutoservSSHTimeout: Ssh connection has timed out. """ command = ('"%s; echo %s:\$?"' % (utils.sh_escape(command), CMD_OUTPUT_PREFIX)) def _run(): """Run the command and try to parse the exit code. """ result = self.adb_run( command, shell=True, timeout=timeout, ignore_status=ignore_status, ignore_timeout=ignore_timeout, stdout=stdout_tee, stderr=stderr_tee, connect_timeout=connect_timeout, options=options, stdin=stdin, verbose=verbose, args=args) if not result: # In case of timeouts. Set the return to a specific string # value. That way the caller of poll_for_condition knows # a timeout occurs and should return None. Return None here will # lead to the command to be retried. return TIMEOUT_MSG parse_output = re.match(CMD_OUTPUT_REGEX, result.stdout) if not parse_output and not ignore_status: logging.error('Failed to parse the exit code for command: `%s`.' ' result: `%s`', command, result.stdout) return None elif parse_output: result.stdout = parse_output.group('OUTPUT') result.exit_status = int(parse_output.group('EXIT_CODE')) if result.exit_status != 0 and not ignore_status: raise error.AutoservRunError(command, result) return result result = client_utils.poll_for_condition( lambda: _run(), timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, sleep_interval=0.5, desc='Run command `%s`' % command) return None if result == TIMEOUT_MSG else result def check_boot_to_adb_complete(self, exception_type=error.TimeoutException): """Check if the device has finished booting and accessible by adb. @param exception_type: Type of exception to raise. Default is set to error.TimeoutException for retry. @raise exception_type: If the device has not finished booting yet, raise an exception of type `exception_type`. """ bootcomplete = self._run_output_with_retry('getprop dev.bootcomplete') if bootcomplete != PROPERTY_VALUE_TRUE: raise exception_type('dev.bootcomplete is %s.' % bootcomplete) if self.get_os_type() == OS_TYPE_ANDROID: boot_completed = self._run_output_with_retry( 'getprop sys.boot_completed') if boot_completed != PROPERTY_VALUE_TRUE: raise exception_type('sys.boot_completed is %s.' % boot_completed) def wait_up(self, timeout=DEFAULT_WAIT_UP_TIME_SECONDS, command=ADB_CMD): """Wait until the remote host is up or the timeout expires. Overrides wait_down from AbstractSSHHost. @param timeout: Time limit in seconds before returning even if the host is not up. @param command: The command used to test if a device is up, i.e., accessible by the given command. Default is set to `adb`. @returns True if the host was found to be up before the timeout expires, False otherwise. """ @retry.retry(error.TimeoutException, timeout_min=timeout/60.0, delay_sec=1) def _wait_up(): if not self.is_up(command=command): raise error.TimeoutException('Device is still down.') if command == ADB_CMD: self.check_boot_to_adb_complete() return True try: _wait_up() logging.debug('Host %s is now up, and can be accessed by %s.', self.hostname, command) return True except error.TimeoutException: logging.debug('Host %s is still down after waiting %d seconds', self.hostname, timeout) return False def wait_down(self, timeout=DEFAULT_WAIT_DOWN_TIME_SECONDS, warning_timer=None, old_boot_id=None, command=ADB_CMD, boot_id=None): """Wait till the host goes down. Return when the host is down (not accessible via the command) OR when the device's boot_id changes (if a boot_id was provided). Overrides wait_down from AbstractSSHHost. @param timeout: Time in seconds to wait for the host to go down. @param warning_timer: Time limit in seconds that will generate a warning if the host is not down yet. Currently ignored. @param old_boot_id: Not applicable for adb_host. @param command: `adb`, test if the device can be accessed by adb command, or `fastboot`, test if the device can be accessed by fastboot command. Default is set to `adb`. @param boot_id: UUID of previous boot (consider the device down when the boot_id changes from this value). Ignored if None. @returns True if the device goes down before the timeout, False otherwise. """ @retry.retry(error.TimeoutException, timeout_min=timeout/60.0, delay_sec=1) def _wait_down(): up = self.is_up(command=command) if not up: return True if boot_id: try: new_boot_id = self.get_boot_id() if new_boot_id != boot_id: return True except error.GenericHostRunError: pass raise error.TimeoutException('Device is still up.') try: _wait_down() logging.debug('Host %s is now down', self.hostname) return True except error.TimeoutException: logging.debug('Host %s is still up after waiting %d seconds', self.hostname, timeout) return False def reboot(self): """Reboot the android device via adb. @raises AutoservRebootError if reboot failed. """ # Not calling super.reboot() as we want to reboot the ADB device not # the test station we are running ADB on. boot_id = self.get_boot_id() self.adb_run('reboot', timeout=10, ignore_timeout=True) if not self.wait_down(boot_id=boot_id): raise error.AutoservRebootError( 'ADB Device is still up after reboot') if not self.wait_up(): raise error.AutoservRebootError( 'ADB Device failed to return from reboot.') self._reset_adbd_connection() def fastboot_reboot(self): """Do a fastboot reboot to go back to adb. @raises AutoservRebootError if reboot failed. """ self.fastboot_run('reboot') if not self.wait_down(command=FASTBOOT_CMD): raise error.AutoservRebootError( 'Device is still in fastboot mode after reboot') if not self.wait_up(): raise error.AutoservRebootError( 'Device failed to boot to adb after fastboot reboot.') self._reset_adbd_connection() def remount(self): """Remounts paritions on the device read-write. Specifically, the /system, /vendor (if present) and /oem (if present) partitions on the device are remounted read-write. """ self.adb_run('remount') @staticmethod def parse_device_serials(devices_output): """Return a list of parsed serials from the output. @param devices_output: Output from either an adb or fastboot command. @returns List of device serials """ devices = [] for line in devices_output.splitlines(): match = re.search(DEVICE_FINDER_REGEX, line) if match: serial = match.group('SERIAL') if serial == DEVICE_NO_SERIAL_MSG or re.match(r'^\?+$', serial): serial = DEVICE_NO_SERIAL_TAG logging.debug('Found Device: %s', serial) devices.append(serial) return devices def _get_devices(self, use_adb): """Get a list of devices currently attached to the test station. @params use_adb: True to get adb accessible devices. Set to False to get fastboot accessible devices. @returns a list of devices attached to the test station. """ if use_adb: result = self.adb_run('devices').stdout if self.adb_serial and self.adb_serial not in result: self._connect_over_tcpip_as_needed() else: result = self.fastboot_run('devices').stdout if (self.fastboot_serial and self.fastboot_serial not in result): # fastboot devices won't list the devices using TCP try: if 'product' in self.fastboot_run('getvar product', timeout=2).stderr: result += '\n%s\tfastboot' % self.fastboot_serial # The main reason we do a general Exception catch here instead # of setting ignore_timeout/status to True is because even when # the fastboot process has been nuked, it still stays around and # so bgjob wants to warn us of this and tries to read the # /proc/<pid>/stack file which then promptly returns an # 'Operation not permitted' error since we're running as moblab # and we don't have permission to read those files. except Exception: pass return self.parse_device_serials(result) def adb_devices(self): """Get a list of devices currently attached to the test station and accessible with the adb command.""" devices = self._get_devices(use_adb=True) if self.adb_serial is None and len(devices) > 1: raise error.AutoservError( 'Not given ADB serial but multiple devices detected') return devices def fastboot_devices(self): """Get a list of devices currently attached to the test station and accessible by fastboot command. """ devices = self._get_devices(use_adb=False) if self.fastboot_serial is None and len(devices) > 1: raise error.AutoservError( 'Not given fastboot serial but multiple devices detected') return devices def is_up(self, timeout=0, command=ADB_CMD): """Determine if the specified adb device is up with expected mode. @param timeout: Not currently used. @param command: `adb`, the device can be accessed by adb command, or `fastboot`, the device can be accessed by fastboot command. Default is set to `adb`. @returns True if the device is detectable by given command, False otherwise. """ if command == ADB_CMD: devices = self.adb_devices() serial = self.adb_serial # ADB has a device state, if the device is not online, no # subsequent ADB command will complete. # DUT with single device connected may not have adb_serial set. # Therefore, skip checking if serial is in the list of adb devices # if self.adb_serial is not set. if (serial and serial not in devices) or not self.is_device_ready(): logging.debug('Waiting for device to enter the ready state.') return False elif command == FASTBOOT_CMD: devices = self.fastboot_devices() serial = self.fastboot_serial else: raise NotImplementedError('Mode %s is not supported' % command) return bool(devices and (not serial or serial in devices)) def stop_loggers(self): """Inherited stop_loggers function. Calls parent function and captures logcat, since the end of the run is logically the end/stop of the logcat log. """ super(ADBHost, self).stop_loggers() # When called from atest and tools like it there will be no job. if not self.job: return # Record logcat log to a temporary file on the teststation. tmp_dir = self.teststation.get_tmp_dir() logcat_filename = LOGCAT_FILE_FMT % self.adb_serial teststation_filename = os.path.join(tmp_dir, logcat_filename) try: self.adb_run('logcat -v time -d > "%s"' % (teststation_filename), timeout=20) except (error.GenericHostRunError, error.AutoservSSHTimeout, error.CmdTimeoutError): return # Copy-back the log to the drone's results directory. results_logcat_filename = os.path.join(self.job.resultdir, logcat_filename) self.teststation.get_file(teststation_filename, results_logcat_filename) try: self.teststation.run('rm -rf %s' % tmp_dir) except (error.GenericHostRunError, error.AutoservSSHTimeout) as e: logging.warn('failed to remove dir %s: %s', tmp_dir, e) self._collect_crash_logs() def close(self): """Close the ADBHost object. Called as the test ends. Will return the device to USB mode and kill the ADB server. """ super(ADBHost, self).close() self.teststation.close() def syslog(self, message, tag='autotest'): """Logs a message to syslog on the device. @param message String message to log into syslog @param tag String tag prefix for syslog """ self.run('log -t "%s" "%s"' % (tag, message)) def get_autodir(self): """Return the directory to install autotest for client side tests.""" return '/data/autotest' def is_device_ready(self): """Return the if the device is ready for ADB commands.""" try: # Retry to avoid possible flakes. is_ready = client_utils.poll_for_condition( lambda: self.adb_run('get-state').stdout.strip() == 'device', timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, sleep_interval=1, desc='Waiting for device state to be `device`') except client_utils.TimeoutError: is_ready = False logging.debug('Device state is %sready', '' if is_ready else 'NOT ') return is_ready def verify_connectivity(self): """Verify we can connect to the device.""" if not self.is_device_ready(): raise error.AutoservHostError('device state is not in the ' '\'device\' state.') def verify_software(self): """Verify working software on an adb_host. """ # Check if adb and fastboot are present. self.teststation.run('which adb') self.teststation.run('which fastboot') self.teststation.run('which unzip') # Apply checks only for Android device. if self.get_os_type() == OS_TYPE_ANDROID: # Make sure ro.boot.hardware and ro.build.product match. hardware = self._run_output_with_retry('getprop ro.boot.hardware') product = self._run_output_with_retry('getprop ro.build.product') if hardware != product: raise error.AutoservHostError('ro.boot.hardware: %s does not ' 'match to ro.build.product: %s' % (hardware, product)) def verify_job_repo_url(self, tag=''): """Make sure job_repo_url of this host is valid. TODO (crbug.com/532223): Actually implement this method. @param tag: The tag from the server job, in the format <job_id>-<user>/<hostname>, or <hostless> for a server job. """ return def repair(self, board=None, os=None): """Attempt to get the DUT to pass `self.verify()`. @param board: Board name of the device. For host created in testbed, it does not have host labels and attributes. Therefore, the board name needs to be passed in from the testbed repair call. @param os: OS of the device. For host created in testbed, it does not have host labels and attributes. Therefore, the OS needs to be passed in from the testbed repair call. """ if self.is_up(): logging.debug('The device is up and accessible by adb. No need to ' 'repair.') return # Force to do a reinstall in repair first. The reason is that it # requires manual action to put the device into fastboot mode. # If repair tries to switch the device back to adb mode, one will # have to change it back to fastboot mode manually again. logging.debug('Verifying the device is accessible via fastboot.') self.ensure_bootloader_mode() subdir_tag = self.adb_serial if board else None if not self.job.run_test( 'provision_AndroidUpdate', host=self, value=None, force=True, repair=True, board=board, os=os, subdir_tag=subdir_tag): raise error.AutoservRepairTotalFailure( 'Unable to repair the device.') def send_file(self, source, dest, delete_dest=False, preserve_symlinks=False): """Copy files from the drone to the device. Just a note, there is the possibility the test station is localhost which makes some of these steps redundant (e.g. creating tmp dir) but that scenario will undoubtedly be a development scenario (test station is also the moblab) and not the typical live test running scenario so the redundancy I think is harmless. @param source: The file/directory on the drone to send to the device. @param dest: The destination path on the device to copy to. @param delete_dest: A flag set to choose whether or not to delete dest on the device if it exists. @param preserve_symlinks: Controls if symlinks on the source will be copied as such on the destination or transformed into the referenced file/directory. """ # If we need to preserve symlinks, let's check if the source is a # symlink itself and if so, just create it on the device. if preserve_symlinks: symlink_target = None try: symlink_target = os.readlink(source) except OSError: # Guess it's not a symlink. pass if symlink_target is not None: # Once we create the symlink, let's get out of here. self.run('ln -s %s %s' % (symlink_target, dest)) return # Stage the files on the test station. tmp_dir = self.teststation.get_tmp_dir() src_path = os.path.join(tmp_dir, os.path.basename(dest)) # Now copy the file over to the test station so you can reference the # file in the push command. self.teststation.send_file(source, src_path, preserve_symlinks=preserve_symlinks) if delete_dest: self.run('rm -rf %s' % dest) self.adb_run('push %s %s' % (src_path, dest)) # Cleanup the test station. try: self.teststation.run('rm -rf %s' % tmp_dir) except (error.GenericHostRunError, error.AutoservSSHTimeout) as e: logging.warn('failed to remove dir %s: %s', tmp_dir, e) def _get_file_info(self, dest): """Get permission and possible symlink info about file on the device. These files are on the device so we only have shell commands (via adb) to get the info we want. We'll use 'ls' to get it all. @param dest: File to get info about. @returns a dict of the file permissions and symlink. """ # Grab file info. file_info = self.run_output('ls -ld %s' % dest) symlink = None perms = 0 match = re.match(FILE_INFO_REGEX, file_info) if match: # Check if it's a symlink and grab the linked dest if it is. if match.group('TYPE') == 'l': symlink_match = re.match(FILE_SYMLINK_REGEX, file_info) if symlink_match: symlink = symlink_match.group('SYMLINK') # Set the perms. for perm, perm_flag in zip(match.group('PERMS'), FILE_PERMS_FLAGS): if perm != '-': perms |= perm_flag return {'perms': perms, 'symlink': symlink} def get_file(self, source, dest, delete_dest=False, preserve_perm=True, preserve_symlinks=False): """Copy files from the device to the drone. Just a note, there is the possibility the test station is localhost which makes some of these steps redundant (e.g. creating tmp dir) but that scenario will undoubtedly be a development scenario (test station is also the moblab) and not the typical live test running scenario so the redundancy I think is harmless. @param source: The file/directory on the device to copy back to the drone. @param dest: The destination path on the drone to copy to. @param delete_dest: A flag set to choose whether or not to delete dest on the drone if it exists. @param preserve_perm: Tells get_file() to try to preserve the sources permissions on files and dirs. @param preserve_symlinks: Try to preserve symlinks instead of transforming them into files/dirs on copy. """ # Stage the files on the test station under teststation_temp_dir. teststation_temp_dir = self.teststation.get_tmp_dir() teststation_dest = os.path.join(teststation_temp_dir, os.path.basename(source)) source_info = {} if preserve_symlinks or preserve_perm: source_info = self._get_file_info(source) # If we want to preserve symlinks, just create it here, otherwise pull # the file off the device. # # TODO(sadmac): Directories containing symlinks won't behave as # expected. if preserve_symlinks and source_info['symlink']: os.symlink(source_info['symlink'], dest) else: self.adb_run('pull %s %s' % (source, teststation_temp_dir)) # Copy over the file from the test station and clean up. self.teststation.get_file(teststation_dest, dest, delete_dest=delete_dest) try: self.teststation.run('rm -rf %s' % teststation_temp_dir) except (error.GenericHostRunError, error.AutoservSSHTimeout) as e: logging.warn('failed to remove dir %s: %s', teststation_temp_dir, e) # Source will be copied under dest if either: # 1. Source is a directory and doesn't end with /. # 2. Source is a file and dest is a directory. command = '[ -d %s ]' % source source_is_dir = self.run(command, ignore_status=True).exit_status == 0 logging.debug('%s on the device %s a directory', source, 'is' if source_is_dir else 'is not') if ((source_is_dir and not source.endswith(os.sep)) or (not source_is_dir and os.path.isdir(dest))): receive_path = os.path.join(dest, os.path.basename(source)) else: receive_path = dest if not os.path.exists(receive_path): logging.warning('Expected file %s does not exist; skipping' ' permissions copy', receive_path) return # Set the permissions of the received file/dirs. if os.path.isdir(receive_path): for root, _dirs, files in os.walk(receive_path): def process(rel_path, default_perm): info = self._get_file_info(os.path.join(source, rel_path)) if info['perms'] != 0: target = os.path.join(receive_path, rel_path) if preserve_perm: os.chmod(target, info['perms']) else: os.chmod(target, default_perm) rel_root = os.path.relpath(root, receive_path) process(rel_root, _DEFAULT_DIR_PERMS) for f in files: process(os.path.join(rel_root, f), _DEFAULT_FILE_PERMS) elif preserve_perm: os.chmod(receive_path, source_info['perms']) else: os.chmod(receive_path, _DEFAULT_FILE_PERMS) def get_release_version(self): """Get the release version from the RELEASE_FILE on the device. @returns The release string in the RELEASE_FILE. """ return self.run_output('getprop %s' % RELEASE_FILE) def get_tmp_dir(self, parent=''): """Return a suitable temporary directory on the device. We ensure this is a subdirectory of /data/local/tmp. @param parent: Parent directory of the returned tmp dir. @returns a path to the temp directory on the host. """ # TODO(kevcheng): Refactor the cleanup of tmp dir to be inherited # from the parent. if not parent.startswith(TMP_DIR): parent = os.path.join(TMP_DIR, parent.lstrip(os.path.sep)) self.run('mkdir -p %s' % parent) tmp_dir = self.run_output('mktemp -d -p %s' % parent) self.tmp_dirs.append(tmp_dir) return tmp_dir def get_platform(self): """Determine the correct platform label for this host. @returns a string representing this host's platform. """ return 'adb' def get_os_type(self): """Get the OS type of the DUT, e.g., android or brillo. """ if not self._os_type: if self.run_output('getprop ro.product.brand') == 'Brillo': self._os_type = OS_TYPE_BRILLO else: self._os_type = OS_TYPE_ANDROID return self._os_type def _forward(self, reverse, args): """Execute a forwarding command. @param reverse: Whether this is reverse forwarding (Boolean). @param args: List of command arguments. """ cmd = '%s %s' % ('reverse' if reverse else 'forward', ' '.join(args)) self.adb_run(cmd) def add_forwarding(self, src, dst, reverse=False, rebind=True): """Forward a port between the ADB host and device. Port specifications are any strings accepted as such by ADB, for example 'tcp:8080'. @param src: Port specification to forward from. @param dst: Port specification to forward to. @param reverse: Do reverse forwarding from device to host (Boolean). @param rebind: Allow rebinding an already bound port (Boolean). """ args = [] if not rebind: args.append('--no-rebind') args += [src, dst] self._forward(reverse, args) def remove_forwarding(self, src=None, reverse=False): """Removes forwarding on port. @param src: Port specification, or None to remove all forwarding. @param reverse: Whether this is reverse forwarding (Boolean). """ args = [] if src is None: args.append('--remove-all') else: args += ['--remove', src] self._forward(reverse, args) def create_ssh_tunnel(self, port, local_port): """ Forwards a port securely through a tunnel process from the server to the DUT for RPC server connection. Add a 'ADB forward' rule to forward the RPC packets from the AdbHost to the DUT. @param port: remote port on the DUT. @param local_port: local forwarding port. @return: the tunnel process. """ self.add_forwarding('tcp:%s' % port, 'tcp:%s' % port) return super(ADBHost, self).create_ssh_tunnel(port, local_port) def disconnect_ssh_tunnel(self, tunnel_proc, port): """ Disconnects a previously forwarded port from the server to the DUT for RPC server connection. Remove the previously added 'ADB forward' rule to forward the RPC packets from the AdbHost to the DUT. @param tunnel_proc: the original tunnel process returned from |create_ssh_tunnel|. @param port: remote port on the DUT. """ self.remove_forwarding('tcp:%s' % port) super(ADBHost, self).disconnect_ssh_tunnel(tunnel_proc, port) def ensure_bootloader_mode(self): """Ensure the device is in bootloader mode. @raise: error.AutoservError if the device failed to reboot into bootloader mode. """ if self.is_up(command=FASTBOOT_CMD): return self.adb_run('reboot bootloader') if not self.wait_up(command=FASTBOOT_CMD): raise error.AutoservError( 'The device failed to reboot into bootloader mode.') def ensure_adb_mode(self, timeout=DEFAULT_WAIT_UP_TIME_SECONDS): """Ensure the device is up and can be accessed by adb command. @param timeout: Time limit in seconds before returning even if the host is not up. @raise: error.AutoservError if the device failed to reboot into adb mode. """ if self.is_up(): return # Ignore timeout error to allow `fastboot reboot` to fail quietly and # check if the device is in adb mode. self.fastboot_run('reboot', timeout=timeout, ignore_timeout=True) if not self.wait_up(timeout=timeout): raise error.AutoservError( 'The device failed to reboot into adb mode.') self._reset_adbd_connection() @classmethod def get_build_info_from_build_url(cls, build_url): """Get the Android build information from the build url. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/branch/target/build_id @return: A dictionary of build information, including keys: build_target, branch, target, build_id. @raise AndroidInstallError: If failed to parse build_url. """ if not build_url: raise AndroidInstallError('Need build_url to download image files.') try: match = re.match(DEVSERVER_URL_REGEX, build_url) return {'build_target': match.group('BUILD_TARGET'), 'branch': match.group('BRANCH'), 'target': ('%s-%s' % (match.group('BUILD_TARGET'), match.group('BUILD_TYPE'))), 'build_id': match.group('BUILD_ID')} except (AttributeError, IndexError, ValueError) as e: raise AndroidInstallError( 'Failed to parse build url: %s\nError: %s' % (build_url, e)) @retry.retry(error.GenericHostRunError, timeout_min=10) def download_file(self, build_url, file, dest_dir, unzip=False, unzip_dest=None): """Download the given file from the build url. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/branch/target/build_id @param file: Name of the file to be downloaded, e.g., boot.img. @param dest_dir: Destination folder for the file to be downloaded to. @param unzip: If True, unzip the downloaded file. @param unzip_dest: Location to unzip the downloaded file to. If not provided, dest_dir is used. """ # Append the file name to the url if build_url is linked to the folder # containing the file. if not build_url.endswith('/%s' % file): src_url = os.path.join(build_url, file) else: src_url = build_url dest_file = os.path.join(dest_dir, file) try: self.teststation.run('wget -q -O "%s" "%s"' % (dest_file, src_url)) if unzip: unzip_dest = unzip_dest or dest_dir self.teststation.run('unzip "%s/%s" -x -d "%s"' % (dest_dir, file, unzip_dest)) except: # Delete the destination file if download failed. self.teststation.run('rm -f "%s"' % dest_file) raise def stage_android_image_files(self, build_url): """Download required image files from the given build_url to a local directory in the machine runs fastboot command. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/branch/target/build_id @return: Path to the directory contains image files. """ build_info = self.get_build_info_from_build_url(build_url) zipped_image_file = ANDROID_IMAGE_FILE_FMT % build_info image_dir = self.teststation.get_tmp_dir() try: self.download_file(build_url, zipped_image_file, image_dir, unzip=True) images = android_utils.AndroidImageFiles.get_standalone_images( build_info['build_target']) for image_file in images: self.download_file(build_url, image_file, image_dir) return image_dir except: self.teststation.run('rm -rf %s' % image_dir) raise def stage_brillo_image_files(self, build_url): """Download required brillo image files from the given build_url to a local directory in the machine runs fastboot command. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/branch/target/build_id @return: Path to the directory contains image files. """ build_info = self.get_build_info_from_build_url(build_url) zipped_image_file = ANDROID_IMAGE_FILE_FMT % build_info vendor_partitions_file = BRILLO_VENDOR_PARTITIONS_FILE_FMT % build_info image_dir = self.teststation.get_tmp_dir() try: self.download_file(build_url, zipped_image_file, image_dir, unzip=True) self.download_file(build_url, vendor_partitions_file, image_dir, unzip=True, unzip_dest=os.path.join(image_dir, 'vendor')) return image_dir except: self.teststation.run('rm -rf %s' % image_dir) raise def stage_build_for_install(self, build_name, os_type=None): """Stage a build on a devserver and return the build_url and devserver. @param build_name: a name like git-master/shamu-userdebug/2040953 @returns a tuple with an update URL like: http://172.22.50.122:8080/git-master/shamu-userdebug/2040953 and the devserver instance. """ os_type = os_type or self.get_os_type() logging.info('Staging build for installation: %s', build_name) devserver = dev_server.AndroidBuildServer.resolve(build_name, self.hostname) build_name = devserver.translate(build_name) branch, target, build_id = utils.parse_launch_control_build(build_name) devserver.trigger_download(target, build_id, branch, os=os_type, synchronous=False) return '%s/static/%s' % (devserver.url(), build_name), devserver def install_android(self, build_url, build_local_path=None, wipe=True, flash_all=False, disable_package_verification=True, skip_setup_wizard=True): """Install the Android DUT. Following are the steps used here to provision an android device: 1. If build_local_path is not set, download the image zip file, e.g., shamu-img-2284311.zip, unzip it. 2. Run fastboot to install following artifacts: bootloader, radio, boot, system, vendor(only if exists) Repair is not supported for Android devices yet. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/$build @param build_local_path: The path to a local folder that contains the image files needed to provision the device. Note that the folder is in the machine running adb command, rather than the drone. @param wipe: If true, userdata will be wiped before flashing. @param flash_all: If True, all img files found in img_path will be flashed. Otherwise, only boot and system are flashed. @raises AndroidInstallError if any error occurs. """ # If the build is not staged in local server yet, clean up the temp # folder used to store image files after the provision is completed. delete_build_folder = bool(not build_local_path) try: # Download image files needed for provision to a local directory. if not build_local_path: build_local_path = self.stage_android_image_files(build_url) # Device needs to be in bootloader mode for flashing. self.ensure_bootloader_mode() if wipe: self._fastboot_run_with_retry('-w') # Get all *.img file in the build_local_path. list_file_cmd = 'ls -d %s' % os.path.join(build_local_path, '*.img') image_files = self.teststation.run( list_file_cmd).stdout.strip().split() images = dict([(os.path.basename(f), f) for f in image_files]) build_info = self.get_build_info_from_build_url(build_url) board = build_info['build_target'] all_images = ( android_utils.AndroidImageFiles.get_standalone_images(board) + android_utils.AndroidImageFiles.get_zipped_images(board)) # Sort images to be flashed, bootloader needs to be the first one. bootloader = android_utils.AndroidImageFiles.BOOTLOADER sorted_images = sorted( images.items(), key=lambda pair: 0 if pair[0] == bootloader else 1) for image, image_file in sorted_images: if image not in all_images: continue logging.info('Flashing %s...', image_file) self._fastboot_run_with_retry('-S 256M flash %s %s' % (image[:-4], image_file)) if image == android_utils.AndroidImageFiles.BOOTLOADER: self.fastboot_run('reboot-bootloader') self.wait_up(command=FASTBOOT_CMD) except Exception as e: logging.error('Install Android build failed with error: %s', e) # Re-raise the exception with type of AndroidInstallError. raise AndroidInstallError, sys.exc_info()[1], sys.exc_info()[2] finally: if delete_build_folder: self.teststation.run('rm -rf %s' % build_local_path) timeout = (WAIT_UP_AFTER_WIPE_TIME_SECONDS if wipe else DEFAULT_WAIT_UP_TIME_SECONDS) self.ensure_adb_mode(timeout=timeout) if disable_package_verification: # TODO: Use a whitelist of devices to do this for rather than # doing it by default. self.disable_package_verification() if skip_setup_wizard: try: self.skip_setup_wizard() except error.GenericHostRunError: logging.error('Could not skip setup wizard.') logging.info('Successfully installed Android build staged at %s.', build_url) def install_brillo(self, build_url, build_local_path=None): """Install the Brillo DUT. Following are the steps used here to provision an android device: 1. If build_local_path is not set, download the image zip file, e.g., dragonboard-img-123456.zip, unzip it. And download the vendor partition zip file, e.g., dragonboard-vendor_partitions-123456.zip, unzip it to vendor folder. 2. Run provision_device script to install OS images and vendor partitions. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/$build @param build_local_path: The path to a local folder that contains the image files needed to provision the device. Note that the folder is in the machine running adb command, rather than the drone. @raises AndroidInstallError if any error occurs. """ # If the build is not staged in local server yet, clean up the temp # folder used to store image files after the provision is completed. delete_build_folder = bool(not build_local_path) try: # Download image files needed for provision to a local directory. if not build_local_path: build_local_path = self.stage_brillo_image_files(build_url) # Device needs to be in bootloader mode for flashing. self.ensure_bootloader_mode() # Run provision_device command to install image files and vendor # partitions. vendor_partition_dir = os.path.join(build_local_path, 'vendor') cmd = (BRILLO_PROVISION_CMD % {'os_image_dir': build_local_path, 'vendor_partition_dir': vendor_partition_dir}) if self.fastboot_serial: cmd += ' -s %s ' % self.fastboot_serial self.teststation.run(cmd) except Exception as e: logging.error('Install Brillo build failed with error: %s', e) # Re-raise the exception with type of AndroidInstallError. raise AndroidInstallError, sys.exc_info()[1], sys.exc_info()[2] finally: if delete_build_folder: self.teststation.run('rm -rf %s' % build_local_path) self.ensure_adb_mode() logging.info('Successfully installed Android build staged at %s.', build_url) @property def job_repo_url_attribute(self): """Get the host attribute name for job_repo_url, which should append the adb serial. """ return '%s_%s' % (constants.JOB_REPO_URL, self.adb_serial) def machine_install(self, build_url=None, build_local_path=None, wipe=True, flash_all=False, os_type=None): """Install the DUT. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/$build. If build_url is set to None, the code may try _parser.options.image to do the installation. If none of them is set, machine_install will fail. @param build_local_path: The path to a local directory that contains the image files needed to provision the device. @param wipe: If true, userdata will be wiped before flashing. @param flash_all: If True, all img files found in img_path will be flashed. Otherwise, only boot and system are flashed. @returns A tuple of (image_name, host_attributes). image_name is the name of image installed, e.g., git_mnc-release/shamu-userdebug/1234 host_attributes is a dictionary of (attribute, value), which can be saved to afe_host_attributes table in database. This method returns a dictionary with a single entry of `job_repo_url_[adb_serial]`: devserver_url, where devserver_url is a url to the build staged on devserver. """ os_type = os_type or self.get_os_type() if not build_url and self._parser.options.image: build_url, _ = self.stage_build_for_install( self._parser.options.image, os_type=os_type) if os_type == OS_TYPE_ANDROID: self.install_android( build_url=build_url, build_local_path=build_local_path, wipe=wipe, flash_all=flash_all) elif os_type == OS_TYPE_BRILLO: self.install_brillo( build_url=build_url, build_local_path=build_local_path) else: raise error.InstallError( 'Installation of os type %s is not supported.' % self.get_os_type()) return (build_url.split('static/')[-1], {self.job_repo_url_attribute: build_url}) def list_files_glob(self, path_glob): """Get a list of files on the device given glob pattern path. @param path_glob: The path glob that we want to return the list of files that match the glob. Relative paths will not work as expected. Supply an absolute path to get the list of files you're hoping for. @returns List of files that match the path_glob. """ # This is just in case path_glob has no path separator. base_path = os.path.dirname(path_glob) or '.' result = self.run('find %s -path \'%s\' -print' % (base_path, path_glob), ignore_status=True) if result.exit_status != 0: return [] return result.stdout.splitlines() @retry.retry(error.GenericHostRunError, timeout_min=DISABLE_PACKAGE_VERIFICATION_TIMEOUT_MIN) def disable_package_verification(self): """Disables package verification on an android device. Disables the package verificatoin manager allowing any package to be installed without checking """ logging.info('Disabling package verification on %s.', self.adb_serial) self.check_boot_to_adb_complete() self.run('am broadcast -a ' 'com.google.gservices.intent.action.GSERVICES_OVERRIDE -e ' 'global:package_verifier_enable 0') @retry.retry(error.GenericHostRunError, timeout_min=APK_INSTALL_TIMEOUT_MIN) def install_apk(self, apk, force_reinstall=True): """Install the specified apk. This will install the apk and override it if it's already installed and will also allow for downgraded apks. @param apk: The path to apk file. @param force_reinstall: True to reinstall the apk even if it's already installed. Default is set to True. @returns a CMDResult object. """ try: client_utils.poll_for_condition( lambda: self.run('pm list packages', ignore_status=True).exit_status == 0, timeout=120) client_utils.poll_for_condition( lambda: self.run('service list | grep mount', ignore_status=True).exit_status == 0, timeout=120) return self.adb_run('install %s -d %s' % ('-r' if force_reinstall else '', apk)) except error.GenericHostRunError: self.reboot() raise def uninstall_package(self, package): """Remove the specified package. @param package: Android package name. @raises GenericHostRunError: uninstall failed """ result = self.adb_run('uninstall %s' % package) if self.is_apk_installed(package): raise error.GenericHostRunError('Uninstall of "%s" failed.' % package, result) def save_info(self, results_dir, include_build_info=True): """Save info about this device. @param results_dir: The local directory to store the info in. @param include_build_info: If true this will include the build info artifact. """ if include_build_info: teststation_temp_dir = self.teststation.get_tmp_dir() job_repo_url = afe_utils.get_host_attribute( self, self.job_repo_url_attribute) build_info = ADBHost.get_build_info_from_build_url( job_repo_url) target = build_info['target'] branch = build_info['branch'] build_id = build_info['build_id'] devserver_url = dev_server.AndroidBuildServer.get_server_url( job_repo_url) ds = dev_server.AndroidBuildServer(devserver_url) ds.trigger_download(target, build_id, branch, files='BUILD_INFO', synchronous=True) pull_base_url = ds.get_pull_url(target, build_id, branch) source_path = os.path.join(teststation_temp_dir, 'BUILD_INFO') self.download_file(pull_base_url, 'BUILD_INFO', teststation_temp_dir) destination_path = os.path.join( results_dir, 'BUILD_INFO-%s' % self.adb_serial) self.teststation.get_file(source_path, destination_path) @retry.retry(error.GenericHostRunError, timeout_min=0.2) def _confirm_apk_installed(self, package_name): """Confirm if apk is already installed with the given name. `pm list packages` command is not reliable some time. The retry helps to reduce the chance of false negative. @param package_name: Name of the package, e.g., com.android.phone. @raise AutoservRunError: If the package is not found or pm list command failed for any reason. """ name = 'package:%s' % package_name self.adb_run('shell pm list packages | grep -w "%s"' % name) def is_apk_installed(self, package_name): """Check if apk is already installed with the given name. @param package_name: Name of the package, e.g., com.android.phone. @return: True if package is installed. False otherwise. """ try: self._confirm_apk_installed(package_name) return True except: return False @retry.retry(error.GenericHostRunError, timeout_min=1) def skip_setup_wizard(self): """Skip the setup wizard. Skip the starting setup wizard that normally shows up on android. """ logging.info('Skipping setup wizard on %s.', self.adb_serial) self.check_boot_to_adb_complete() self.run('am start -n com.google.android.setupwizard/' '.SetupWizardExitActivity') def get_attributes_to_clear_before_provision(self): """Get a list of attributes to be cleared before machine_install starts. """ return [self.job_repo_url_attribute] def get_labels(self): """Return a list of the labels gathered from the devices connected. @return: A list of strings that denote the labels from all the devices connected. """ return self.labels.get_labels(self) def update_labels(self): """Update the labels for this testbed.""" self.labels.update_labels(self) def stage_server_side_package(self, image=None): """Stage autotest server-side package on devserver. @param image: A build name, e.g., git_mnc_dev/shamu-eng/123 @return: A url to the autotest server-side package. @raise: error.AutoservError if fail to locate the build to test with, or fail to stage server-side package. """ # If enable_drone_in_restricted_subnet is False, do not set hostname # in devserver.resolve call, so a devserver in non-restricted subnet # is picked to stage autotest server package for drone to download. hostname = self.hostname if not utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: hostname = None if image: ds = dev_server.AndroidBuildServer.resolve(image, hostname) else: info = self.host_info_store.get() job_repo_url = afe_utils.get_host_attribute( self, self.job_repo_url_attribute) if job_repo_url: devserver_url, image = ( tools.get_devserver_build_from_package_url( job_repo_url, True)) # If enable_drone_in_restricted_subnet is True, use the # existing devserver. Otherwise, resolve a new one in # non-restricted subnet. if utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: ds = dev_server.AndroidBuildServer(devserver_url) else: ds = dev_server.AndroidBuildServer.resolve(image) elif info.build is not None: ds = dev_server.AndroidBuildServer.resolve(info.build, hostname) else: raise error.AutoservError( 'Failed to stage server-side package. The host has ' 'no job_report_url attribute or version label.') branch, target, build_id = utils.parse_launch_control_build(image) build_target, _ = utils.parse_launch_control_target(target) # For any build older than MIN_VERSION_SUPPORT_SSP, server side # packaging is not supported. try: # Some build ids may have special character before the actual # number, skip such characters. actual_build_id = build_id if build_id.startswith('P'): actual_build_id = build_id[1:] if int(actual_build_id) < self.MIN_VERSION_SUPPORT_SSP: raise error.AutoservError( 'Build %s is older than %s. Server side packaging is ' 'disabled.' % (image, self.MIN_VERSION_SUPPORT_SSP)) except ValueError: raise error.AutoservError( 'Failed to compare build id in %s with the minimum ' 'version that supports server side packaging. Server ' 'side packaging is disabled.' % image) ds.stage_artifacts(target, build_id, branch, artifacts=['autotest_server_package']) autotest_server_package_name = (AUTOTEST_SERVER_PACKAGE_FILE_FMT % {'build_target': build_target, 'build_id': build_id}) return '%s/static/%s/%s' % (ds.url(), image, autotest_server_package_name) def _sync_time(self): """Approximate synchronization of time between host and ADB device. This sets the ADB/Android device's clock to approximately the same time as the Autotest host for the purposes of comparing Android system logs such as logcat to logs from the Autotest host system. """ command = 'date ' sdk_version = int(self.run('getprop %s' % SDK_FILE).stdout) if sdk_version < 23: # Android L and earlier use this format: date -s (format). command += ('-s %s' % datetime.datetime.now().strftime('%Y%m%d.%H%M%S')) else: # Android M and later use this format: date -u (format). command += ('-u %s' % datetime.datetime.utcnow().strftime('%m%d%H%M%Y.%S')) self.run(command, timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, ignore_timeout=True) def _enable_native_crash_logging(self): """Enable native (non-Java) crash logging. """ if self.get_os_type() == OS_TYPE_ANDROID: self._enable_android_native_crash_logging() def _enable_brillo_native_crash_logging(self): """Enables native crash logging for a Brillo DUT. """ try: self.run('touch /data/misc/metrics/enabled', timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, ignore_timeout=True) # If running, crash_sender will delete crash files every hour. self.run('stop crash_sender', timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, ignore_timeout=True) except error.GenericHostRunError as e: logging.warn(e) logging.warn('Failed to enable Brillo native crash logging.') def _enable_android_native_crash_logging(self): """Enables native crash logging for an Android DUT. """ # debuggerd should be enabled by default on Android. result = self.run('pgrep debuggerd', timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS, ignore_timeout=True, ignore_status=True) if not result or result.exit_status != 0: logging.debug('Unable to confirm that debuggerd is running.') def _collect_crash_logs(self): """Copies crash log files from the DUT to the drone. """ if self.get_os_type() == OS_TYPE_BRILLO: self._collect_crash_logs_dut(BRILLO_NATIVE_CRASH_LOG_DIR) elif self.get_os_type() == OS_TYPE_ANDROID: self._collect_crash_logs_dut(ANDROID_TOMBSTONE_CRASH_LOG_DIR) def _collect_crash_logs_dut(self, log_directory): """Copies native crash logs from the Android/Brillo DUT to the drone. @param log_directory: absolute path of the directory on the DUT where log files are stored. """ files = None try: result = self.run('find %s -maxdepth 1 -type f' % log_directory, timeout=DEFAULT_COMMAND_RETRY_TIMEOUT_SECONDS) files = result.stdout.strip().split() except (error.GenericHostRunError, error.AutoservSSHTimeout, error.CmdTimeoutError): logging.debug('Unable to call find %s, unable to find crash logs', log_directory) if not files: logging.debug('There are no crash logs on the DUT.') return crash_dir = os.path.join(self.job.resultdir, 'crash') try: os.mkdir(crash_dir) except OSError as e: if e.errno != errno.EEXIST: raise e for f in files: logging.debug('DUT native crash file produced: %s', f) dest = os.path.join(crash_dir, os.path.basename(f)) # We've had cases where the crash file on the DUT has permissions # "000". Let's override permissions to make them sane for the user # collecting the crashes. self.get_file(source=f, dest=dest, preserve_perm=False)