[android] Adds AdbWrapper which is the base of the eventual AndroidCommands replacement.
BUG=318387 TEST=included NOTRY=True Review URL: https://codereview.chromium.org/64553012 git-svn-id: http://src.chromium.org/svn/trunk/src/build@238123 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
Родитель
47c25416ff
Коммит
c134c467a4
|
@ -0,0 +1,423 @@
|
|||
# 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)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# 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.
|
||||
|
||||
"""Tests for the AdbWrapper class."""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import adb_wrapper
|
||||
|
||||
|
||||
class TestAdbWrapper(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
devices = adb_wrapper.AdbWrapper.GetDevices()
|
||||
assert devices, 'A device must be attached'
|
||||
self._adb = devices[0]
|
||||
self._adb.WaitForDevice()
|
||||
|
||||
def _MakeTempFile(self, contents):
|
||||
"""Make a temporary file with the given contents.
|
||||
|
||||
Args:
|
||||
contents: string to write to the temporary file.
|
||||
|
||||
Returns:
|
||||
The absolute path to the file.
|
||||
"""
|
||||
fi, path = tempfile.mkstemp()
|
||||
with os.fdopen(fi, 'wb') as f:
|
||||
f.write('foo')
|
||||
return path
|
||||
|
||||
def testShell(self):
|
||||
output = self._adb.Shell('echo test', expect_rc=0)
|
||||
self.assertEqual(output.strip(), 'test')
|
||||
output = self._adb.Shell('echo test')
|
||||
self.assertEqual(output.strip(), 'test')
|
||||
self.assertRaises(adb_wrapper.CommandFailedError, self._adb.Shell,
|
||||
'echo test', expect_rc=1)
|
||||
|
||||
def testPushPull(self):
|
||||
path = self._MakeTempFile('foo')
|
||||
device_path = '/data/local/tmp/testfile.txt'
|
||||
local_tmpdir = os.path.dirname(path)
|
||||
self._adb.Push(path, device_path)
|
||||
self.assertEqual(self._adb.Shell('cat %s' % device_path), 'foo')
|
||||
self._adb.Pull(device_path, local_tmpdir)
|
||||
with open(os.path.join(local_tmpdir, 'testfile.txt'), 'r') as f:
|
||||
self.assertEqual(f.read(), 'foo')
|
||||
|
||||
def testInstall(self):
|
||||
path = self._MakeTempFile('foo')
|
||||
self.assertRaises(adb_wrapper.CommandFailedError, self._adb.Install, path)
|
||||
|
||||
def testForward(self):
|
||||
self.assertRaises(adb_wrapper.CommandFailedError, self._adb.Forward, 0, 0)
|
||||
|
||||
def testUninstall(self):
|
||||
self.assertRaises(adb_wrapper.CommandFailedError, self._adb.Uninstall,
|
||||
'some.nonexistant.package')
|
||||
|
||||
def testRebootWaitForDevice(self):
|
||||
self._adb.Reboot()
|
||||
print 'waiting for device to reboot...'
|
||||
while self._adb.GetState() == 'device':
|
||||
time.sleep(1)
|
||||
self._adb.WaitForDevice()
|
||||
self.assertEqual(self._adb.GetState(), 'device')
|
||||
print 'waiting for package manager...'
|
||||
while 'package:' not in self._adb.Shell('pm path android'):
|
||||
time.sleep(1)
|
||||
|
||||
def testRootRemount(self):
|
||||
self._adb.Root()
|
||||
while True:
|
||||
try:
|
||||
self._adb.Shell('start')
|
||||
break
|
||||
except adb_wrapper.CommandFailedError:
|
||||
time.sleep(1)
|
||||
self._adb.Remount()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Загрузка…
Ссылка в новой задаче