# 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: localabstract: localreserved: localfilesystem: dev: jdwp: (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)