# 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)