blob: 05445aaa2adbbd4100fa98579fab75e7923e83cd [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This module wraps Android's adb tool.
This is a thin wrapper around the adb interface. Any additional complexity
should be delegated to a higher level (ex. DeviceUtils).
"""
import errno
import logging
import os
from pylib import cmd_helper
from pylib.utils import reraiser_thread
from pylib.utils import timeout_retry
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 2
class BaseError(Exception):
"""Base exception for all device and command errors."""
pass
class CommandFailedError(BaseError):
"""Exception for command failures."""
def __init__(self, cmd, msg, device=None):
super(CommandFailedError, self).__init__(
(('device %s: ' % device) if device else '') +
'adb command \'%s\' failed with message: \'%s\'' % (' '.join(cmd), msg))
class CommandTimeoutError(BaseError):
"""Exception for command timeouts."""
pass
def _VerifyLocalFileExists(path):
"""Verifies a local file exists.
Args:
path: Path to the local file.
Raises:
IOError: If the file doesn't exist.
"""
if not os.path.exists(path):
raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), path)
class AdbWrapper(object):
"""A wrapper around a local Android Debug Bridge executable."""
def __init__(self, device_serial):
"""Initializes the AdbWrapper.
Args:
device_serial: The device serial number as a string.
"""
self._device_serial = str(device_serial)
@classmethod
def _AdbCmd(cls, arg_list, timeout, retries, check_error=True):
"""Runs an adb command with a timeout and retries.
Args:
arg_list: A list of arguments to adb.
timeout: Timeout in seconds.
retries: Number of retries.
check_error: Check that the command doesn't return an error message. This
does NOT check the return code of shell commands.
Returns:
The output of the command.
"""
cmd = ['adb'] + arg_list
# This method runs inside the timeout/retries.
def RunCmd():
exit_code, output = cmd_helper.GetCmdStatusAndOutput(cmd)
if exit_code != 0:
raise CommandFailedError(
cmd, 'returned non-zero exit code %s, output: %s' %
(exit_code, output))
# This catches some errors, including when the device drops offline;
# unfortunately adb is very inconsistent with error reporting so many
# command failures present differently.
if check_error and output[:len('error:')] == 'error:':
raise CommandFailedError(arg_list, output)
return output
try:
return timeout_retry.Run(RunCmd, timeout, retries)
except reraiser_thread.TimeoutError as e:
raise CommandTimeoutError(str(e))
def _DeviceAdbCmd(self, arg_list, timeout, retries, check_error=True):
"""Runs an adb command on the device associated with this object.
Args:
arg_list: A list of arguments to adb.
timeout: Timeout in seconds.
retries: Number of retries.
check_error: Check that the command doesn't return an error message. This
does NOT check the return code of shell commands.
Returns:
The output of the command.
"""
return self._AdbCmd(
['-s', self._device_serial] + arg_list, timeout, retries,
check_error=check_error)
def __eq__(self, other):
"""Consider instances equal if they refer to the same device.
Args:
other: The instance to compare equality with.
Returns:
True if the instances are considered equal, false otherwise.
"""
return self._device_serial == str(other)
def __str__(self):
"""The string representation of an instance.
Returns:
The device serial number as a string.
"""
return self._device_serial
def __repr__(self):
return '%s(\'%s\')' % (self.__class__.__name__, self)
# TODO(craigdh): Determine the filter criteria that should be supported.
@classmethod
def GetDevices(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Get the list of active attached devices.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Yields:
AdbWrapper instances.
"""
output = cls._AdbCmd(['devices'], timeout, retries)
lines = [line.split() for line in output.split('\n')]
return [AdbWrapper(line[0]) for line in lines
if len(line) == 2 and line[1] == 'device']
def GetDeviceSerial(self):
"""Gets the device serial number associated with this object.
Returns:
Device serial number as a string.
"""
return self._device_serial
def Push(self, local, remote, timeout=60*5, retries=_DEFAULT_RETRIES):
"""Pushes a file from the host to the device.
Args:
local: Path on the host filesystem.
remote: Path on the device filesystem.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
_VerifyLocalFileExists(local)
self._DeviceAdbCmd(['push', local, remote], timeout, retries)
def Pull(self, remote, local, timeout=60*5, retries=_DEFAULT_RETRIES):
"""Pulls a file from the device to the host.
Args:
remote: Path on the device filesystem.
local: Path on the host filesystem.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
self._DeviceAdbCmd(['pull', remote, local], timeout, retries)
_VerifyLocalFileExists(local)
def Shell(self, command, expect_rc=None, timeout=_DEFAULT_TIMEOUT,
retries=_DEFAULT_RETRIES):
"""Runs a shell command on the device.
Args:
command: The shell command to run.
expect_rc: (optional) If set checks that the command's return code matches
this value.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Returns:
The output of the shell command as a string.
Raises:
CommandFailedError: If the return code doesn't match |expect_rc|.
"""
if expect_rc is None:
actual_command = command
else:
actual_command = '%s; echo $?;' % command
output = self._DeviceAdbCmd(
['shell', actual_command], timeout, retries, check_error=False)
if expect_rc is not None:
output_end = output.rstrip().rfind('\n') + 1
rc = output[output_end:].strip()
output = output[:output_end]
if int(rc) != expect_rc:
raise CommandFailedError(
['shell', command],
'shell command exited with code: %s' % rc,
self._device_serial)
return output
def Logcat(self, filter_spec=None, timeout=_DEFAULT_TIMEOUT,
retries=_DEFAULT_RETRIES):
"""Get the logcat output.
Args:
filter_spec: (optional) Spec to filter the logcat.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Returns:
logcat output as a string.
"""
cmd = ['logcat']
if filter_spec is not None:
cmd.append(filter_spec)
return self._DeviceAdbCmd(cmd, timeout, retries, check_error=False)
def Forward(self, local, remote, timeout=_DEFAULT_TIMEOUT,
retries=_DEFAULT_RETRIES):
"""Forward socket connections from the local socket to the remote socket.
Sockets are specified by one of:
tcp:<port>
localabstract:<unix domain socket name>
localreserved:<unix domain socket name>
localfilesystem:<unix domain socket name>
dev:<character device name>
jdwp:<process pid> (remote only)
Args:
local: The host socket.
remote: The device socket.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
self._DeviceAdbCmd(['forward', str(local), str(remote)], timeout, retries)
def JDWP(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""List of PIDs of processes hosting a JDWP transport.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Returns:
A list of PIDs as strings.
"""
return [a.strip() for a in
self._DeviceAdbCmd(['jdwp'], timeout, retries).split('\n')]
def Install(self, apk_path, forward_lock=False, reinstall=False,
sd_card=False, timeout=60*2, retries=_DEFAULT_RETRIES):
"""Install an apk on the device.
Args:
apk_path: Host path to the APK file.
forward_lock: (optional) If set forward-locks the app.
reinstall: (optional) If set reinstalls the app, keeping its data.
sd_card: (optional) If set installs on the SD card.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
_VerifyLocalFileExists(apk_path)
cmd = ['install']
if forward_lock:
cmd.append('-l')
if reinstall:
cmd.append('-r')
if sd_card:
cmd.append('-s')
cmd.append(apk_path)
output = self._DeviceAdbCmd(cmd, timeout, retries)
if 'Success' not in output:
raise CommandFailedError(cmd, output)
def Uninstall(self, package, keep_data=False, timeout=_DEFAULT_TIMEOUT,
retries=_DEFAULT_RETRIES):
"""Remove the app |package| from the device.
Args:
package: The package to uninstall.
keep_data: (optional) If set keep the data and cache directories.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
cmd = ['uninstall']
if keep_data:
cmd.append('-k')
cmd.append(package)
output = self._DeviceAdbCmd(cmd, timeout, retries)
if 'Failure' in output:
raise CommandFailedError(cmd, output)
def Backup(self, path, packages=None, apk=False, shared=False,
nosystem=True, include_all=False, timeout=_DEFAULT_TIMEOUT,
retries=_DEFAULT_RETRIES):
"""Write an archive of the device's data to |path|.
Args:
path: Local path to store the backup file.
packages: List of to packages to be backed up.
apk: (optional) If set include the .apk files in the archive.
shared: (optional) If set buckup the device's SD card.
nosystem: (optional) If set exclude system applications.
include_all: (optional) If set back up all installed applications and
|packages| is optional.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
cmd = ['backup', path]
if apk:
cmd.append('-apk')
if shared:
cmd.append('-shared')
if nosystem:
cmd.append('-nosystem')
if include_all:
cmd.append('-all')
if packages:
cmd.extend(packages)
assert bool(packages) ^ bool(include_all), (
'Provide \'packages\' or set \'include_all\' but not both.')
ret = self._DeviceAdbCmd(cmd, timeout, retries)
_VerifyLocalFileExists(path)
return ret
def Restore(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Restore device contents from the backup archive.
Args:
path: Host path to the backup archive.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
_VerifyLocalFileExists(path)
self._DeviceAdbCmd(['restore'] + [path], timeout, retries)
def WaitForDevice(self, timeout=60*5, retries=_DEFAULT_RETRIES):
"""Block until the device is online.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
self._DeviceAdbCmd(['wait-for-device'], timeout, retries)
def GetState(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Get device state.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Returns:
One of 'offline', 'bootloader', or 'device'.
"""
return self._DeviceAdbCmd(['get-state'], timeout, retries).strip()
def GetDevPath(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Gets the device path.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
Returns:
The device path (e.g. usb:3-4)
"""
return self._DeviceAdbCmd(['get-devpath'], timeout, retries)
def Remount(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Remounts the /system partition on the device read-write."""
self._DeviceAdbCmd(['remount'], timeout, retries)
def Reboot(self, to_bootloader=False, timeout=60*5,
retries=_DEFAULT_RETRIES):
"""Reboots the device.
Args:
to_bootloader: (optional) If set reboots to the bootloader.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
if to_bootloader:
cmd = ['reboot-bootloader']
else:
cmd = ['reboot']
self._DeviceAdbCmd(cmd, timeout, retries)
def Root(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
"""Restarts the adbd daemon with root permissions, if possible.
Args:
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
"""
output = self._DeviceAdbCmd(['root'], timeout, retries)
if 'cannot' in output:
raise CommandFailedError(['root'], output)