| # 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 collections |
| import errno |
| import logging |
| import os |
| |
| from pylib import cmd_helper |
| from pylib import constants |
| from pylib.device import decorators |
| from pylib.device import device_errors |
| from pylib.utils import timeout_retry |
| |
| |
| _DEFAULT_TIMEOUT = 30 |
| _DEFAULT_RETRIES = 2 |
| |
| |
| 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) |
| |
| |
| DeviceStat = collections.namedtuple('DeviceStat', |
| ['st_mode', 'st_size', 'st_time']) |
| |
| |
| 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. |
| """ |
| if not device_serial: |
| raise ValueError('A device serial must be specified') |
| self._device_serial = str(device_serial) |
| |
| # pylint: disable=unused-argument |
| @classmethod |
| def _BuildAdbCmd(cls, args, device_serial, cpu_affinity=None): |
| if cpu_affinity is not None: |
| cmd = ['taskset', '-c', str(cpu_affinity)] |
| else: |
| cmd = [] |
| cmd.append(constants.GetAdbPath()) |
| if device_serial is not None: |
| cmd.extend(['-s', device_serial]) |
| cmd.extend(args) |
| return cmd |
| # pylint: enable=unused-argument |
| |
| # pylint: disable=unused-argument |
| @classmethod |
| @decorators.WithTimeoutAndRetries |
| def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None, |
| check_error=True, cpu_affinity=None): |
| status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( |
| cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity), |
| timeout_retry.CurrentTimeoutThread().GetRemainingTime()) |
| if status != 0: |
| raise device_errors.AdbCommandFailedError( |
| args, output, status, device_serial) |
| # 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.startswith('error:'): |
| raise device_errors.AdbCommandFailedError(args, output) |
| return output |
| # pylint: enable=unused-argument |
| |
| def _RunDeviceAdbCmd(self, args, timeout, retries, check_error=True): |
| """Runs an adb command on the device associated with this object. |
| |
| Args: |
| args: 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 exit status of shell commands. |
| |
| Returns: |
| The output of the command. |
| """ |
| return self._RunAdbCmd(args, timeout=timeout, retries=retries, |
| device_serial=self._device_serial, |
| check_error=check_error) |
| |
| def _IterRunDeviceAdbCmd(self, args, timeout): |
| """Runs an adb command and returns an iterator over its output lines. |
| |
| Args: |
| args: A list of arguments to adb. |
| timeout: Timeout in seconds. |
| |
| Yields: |
| The output of the command line by line. |
| """ |
| return cmd_helper.IterCmdOutputLines( |
| self._BuildAdbCmd(args, self._device_serial), timeout=timeout) |
| |
| 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) |
| |
| # pylint: disable=unused-argument |
| @classmethod |
| def IsServerOnline(cls): |
| status, output = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']) |
| output = [int(x) for x in output.split()] |
| logging.info('PIDs for adb found: %r', output) |
| return status == 0 |
| # pylint: enable=unused-argument |
| |
| @classmethod |
| def KillServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES): |
| cls._RunAdbCmd(['kill-server'], timeout=timeout, retries=retries) |
| |
| @classmethod |
| def StartServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES): |
| # CPU affinity is used to reduce adb instability http://crbug.com/268450 |
| cls._RunAdbCmd(['start-server'], timeout=timeout, retries=retries, |
| cpu_affinity=0) |
| |
| # 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._RunAdbCmd(['devices'], timeout=timeout, retries=retries) |
| lines = [line.split() for line in output.splitlines()] |
| 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._RunDeviceAdbCmd(['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. |
| """ |
| cmd = ['pull', remote, local] |
| self._RunDeviceAdbCmd(cmd, timeout, retries) |
| try: |
| _VerifyLocalFileExists(local) |
| except IOError: |
| raise device_errors.AdbCommandFailedError( |
| cmd, 'File not found on host: %s' % local, device_serial=str(self)) |
| |
| def Shell(self, command, expect_status=0, timeout=_DEFAULT_TIMEOUT, |
| retries=_DEFAULT_RETRIES): |
| """Runs a shell command on the device. |
| |
| Args: |
| command: A string with the shell command to run. |
| expect_status: (optional) Check that the command's exit status matches |
| this value. Default is 0. If set to None the test is skipped. |
| 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: |
| device_errors.AdbCommandFailedError: If the exit status doesn't match |
| |expect_status|. |
| """ |
| if expect_status is None: |
| args = ['shell', command] |
| else: |
| args = ['shell', '%s; echo %%$?;' % command.rstrip()] |
| output = self._RunDeviceAdbCmd(args, timeout, retries, check_error=False) |
| if expect_status is not None: |
| output_end = output.rfind('%') |
| if output_end < 0: |
| # causes the status string to become empty and raise a ValueError |
| output_end = len(output) |
| |
| try: |
| status = int(output[output_end+1:]) |
| except ValueError: |
| logging.warning('exit status of shell command %r missing.', command) |
| raise device_errors.AdbShellCommandFailedError( |
| command, output, status=None, device_serial=self._device_serial) |
| output = output[:output_end] |
| if status != expect_status: |
| raise device_errors.AdbShellCommandFailedError( |
| command, output, status=status, device_serial=self._device_serial) |
| return output |
| |
| def IterShell(self, command, timeout): |
| """Runs a shell command and returns an iterator over its output lines. |
| |
| Args: |
| command: A string with the shell command to run. |
| timeout: Timeout in seconds. |
| |
| Yields: |
| The output of the command line by line. |
| """ |
| args = ['shell', command] |
| return cmd_helper.IterCmdOutputLines( |
| self._BuildAdbCmd(args, self._device_serial), timeout=timeout) |
| |
| def Ls(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES): |
| """List the contents of a directory on the device. |
| |
| Args: |
| path: Path on the device filesystem. |
| timeout: (optional) Timeout per try in seconds. |
| retries: (optional) Number of retries to attempt. |
| |
| Returns: |
| A list of pairs (filename, stat) for each file found in the directory, |
| where the stat object has the properties: st_mode, st_size, and st_time. |
| |
| Raises: |
| AdbCommandFailedError if |path| does not specify a valid and accessible |
| directory in the device. |
| """ |
| def ParseLine(line): |
| cols = line.split(None, 3) |
| filename = cols.pop() |
| stat = DeviceStat(*[int(num, base=16) for num in cols]) |
| return (filename, stat) |
| |
| cmd = ['ls', path] |
| lines = self._RunDeviceAdbCmd( |
| cmd, timeout=timeout, retries=retries).splitlines() |
| if lines: |
| return [ParseLine(line) for line in lines] |
| else: |
| raise device_errors.AdbCommandFailedError( |
| cmd, 'path does not specify an accessible directory in the device', |
| device_serial=self._device_serial) |
| |
| def Logcat(self, clear=False, dump=False, filter_specs=None, |
| logcat_format=None, timeout=None, retries=_DEFAULT_RETRIES): |
| """Get an iterable over the logcat output. |
| |
| Args: |
| clear: If true, clear the logcat. |
| dump: If true, dump the current logcat contents. |
| filter_specs: If set, a list of specs to filter the logcat. |
| logcat_format: If set, the format in which the logcat should be output. |
| Options include "brief", "process", "tag", "thread", "raw", "time", |
| "threadtime", and "long" |
| timeout: (optional) If set, timeout per try in seconds. If clear or dump |
| is set, defaults to _DEFAULT_TIMEOUT. |
| retries: (optional) If clear or dump is set, the number of retries to |
| attempt. Otherwise, does nothing. |
| |
| Yields: |
| logcat output line by line. |
| """ |
| cmd = ['logcat'] |
| use_iter = True |
| if clear: |
| cmd.append('-c') |
| use_iter = False |
| if dump: |
| cmd.append('-d') |
| use_iter = False |
| if logcat_format: |
| cmd.extend(['-v', logcat_format]) |
| if filter_specs: |
| cmd.extend(filter_specs) |
| |
| if use_iter: |
| return self._IterRunDeviceAdbCmd(cmd, timeout) |
| else: |
| timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT |
| return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines() |
| |
| 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._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(cmd, timeout, retries) |
| if 'Success' not in output: |
| raise device_errors.AdbCommandFailedError( |
| cmd, output, device_serial=self._device_serial) |
| |
| 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._RunDeviceAdbCmd(cmd, timeout, retries) |
| if 'Failure' in output: |
| raise device_errors.AdbCommandFailedError( |
| cmd, output, device_serial=self._device_serial) |
| |
| 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._RunDeviceAdbCmd(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._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(['get-devpath'], timeout, retries) |
| |
| def Remount(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES): |
| """Remounts the /system partition on the device read-write.""" |
| self._RunDeviceAdbCmd(['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._RunDeviceAdbCmd(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._RunDeviceAdbCmd(['root'], timeout, retries) |
| if 'cannot' in output: |
| raise device_errors.AdbCommandFailedError( |
| ['root'], output, device_serial=self._device_serial) |