mirror of
https://github.com/ARM-software/workload-automation.git
synced 2025-01-19 12:24:32 +00:00
242df842bc
Standard string representation of a subprocess.CalledProcessError does not include the output of the command, so it was not previsouly included in the resulting DeviceError. This commit ensures that the output is propagated, regardless of whether it came from stdout or stderr of the underlying process.
432 lines
15 KiB
Python
432 lines
15 KiB
Python
# Copyright 2013-2015 ARM Limited
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
|
|
"""
|
|
Utility functions for working with Android devices through adb.
|
|
|
|
"""
|
|
# pylint: disable=E1103
|
|
import os
|
|
import time
|
|
import subprocess
|
|
import logging
|
|
import re
|
|
|
|
from wlauto.exceptions import DeviceError, ConfigError, HostError, WAError
|
|
from wlauto.utils.misc import (check_output, escape_single_quotes,
|
|
escape_double_quotes, get_null,
|
|
CalledProcessErrorWithStderr)
|
|
|
|
|
|
MAX_TRIES = 5
|
|
|
|
logger = logging.getLogger('android')
|
|
|
|
# See:
|
|
# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
|
|
ANDROID_VERSION_MAP = {
|
|
22: 'LOLLIPOP_MR1',
|
|
21: 'LOLLIPOP',
|
|
20: 'KITKAT_WATCH',
|
|
19: 'KITKAT',
|
|
18: 'JELLY_BEAN_MR2',
|
|
17: 'JELLY_BEAN_MR1',
|
|
16: 'JELLY_BEAN',
|
|
15: 'ICE_CREAM_SANDWICH_MR1',
|
|
14: 'ICE_CREAM_SANDWICH',
|
|
13: 'HONEYCOMB_MR2',
|
|
12: 'HONEYCOMB_MR1',
|
|
11: 'HONEYCOMB',
|
|
10: 'GINGERBREAD_MR1',
|
|
9: 'GINGERBREAD',
|
|
8: 'FROYO',
|
|
7: 'ECLAIR_MR1',
|
|
6: 'ECLAIR_0_1',
|
|
5: 'ECLAIR',
|
|
4: 'DONUT',
|
|
3: 'CUPCAKE',
|
|
2: 'BASE_1_1',
|
|
1: 'BASE',
|
|
}
|
|
|
|
# See:
|
|
# http://developer.android.com/guide/topics/security/normal-permissions.html
|
|
ANDROID_NORMAL_PERMISSIONS = [
|
|
'ACCESS_LOCATION_EXTRA_COMMANDS',
|
|
'ACCESS_NETWORK_STATE',
|
|
'ACCESS_NOTIFICATION_POLICY',
|
|
'ACCESS_WIFI_STATE',
|
|
'BLUETOOTH',
|
|
'BLUETOOTH_ADMIN',
|
|
'BROADCAST_STICKY',
|
|
'CHANGE_NETWORK_STATE',
|
|
'CHANGE_WIFI_MULTICAST_STATE',
|
|
'CHANGE_WIFI_STATE',
|
|
'DISABLE_KEYGUARD',
|
|
'EXPAND_STATUS_BAR',
|
|
'GET_PACKAGE_SIZE',
|
|
'INTERNET',
|
|
'KILL_BACKGROUND_PROCESSES',
|
|
'MODIFY_AUDIO_SETTINGS',
|
|
'NFC',
|
|
'READ_SYNC_SETTINGS',
|
|
'READ_SYNC_STATS',
|
|
'RECEIVE_BOOT_COMPLETED',
|
|
'REORDER_TASKS',
|
|
'REQUEST_INSTALL_PACKAGES',
|
|
'SET_TIME_ZONE',
|
|
'SET_WALLPAPER',
|
|
'SET_WALLPAPER_HINTS',
|
|
'TRANSMIT_IR',
|
|
'USE_FINGERPRINT',
|
|
'VIBRATE',
|
|
'WAKE_LOCK',
|
|
'WRITE_SYNC_SETTINGS',
|
|
'SET_ALARM',
|
|
'INSTALL_SHORTCUT',
|
|
'UNINSTALL_SHORTCUT',
|
|
]
|
|
|
|
# TODO: these are set to their actual values near the bottom of the file. There
|
|
# is some HACKery involved to ensure that ANDROID_HOME does not need to be set
|
|
# or adb added to path for root when installing as root, and the whole
|
|
# implemenationt is kinda clunky and messier than I'd like. The only file that
|
|
# rivals this one in levels of mess is bootstrap.py (for very much the same
|
|
# reasons). There must be a neater way to ensure that enviromental dependencies
|
|
# are met when they are needed, and are not imposed when they are not.
|
|
android_home = None
|
|
platform_tools = None
|
|
adb = None
|
|
aapt = None
|
|
fastboot = None
|
|
|
|
|
|
class _AndroidEnvironment(object):
|
|
|
|
def __init__(self):
|
|
self.android_home = None
|
|
self.platform_tools = None
|
|
self.adb = None
|
|
self.aapt = None
|
|
self.fastboot = None
|
|
|
|
|
|
class AndroidProperties(object):
|
|
|
|
def __init__(self, text):
|
|
self._properties = {}
|
|
self.parse(text)
|
|
|
|
def parse(self, text):
|
|
self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text))
|
|
|
|
def __iter__(self):
|
|
return iter(self._properties)
|
|
|
|
def __getattr__(self, name):
|
|
return self._properties.get(name)
|
|
|
|
__getitem__ = __getattr__
|
|
|
|
|
|
class ApkInfo(object):
|
|
|
|
version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'")
|
|
name_regex = re.compile(r"name='(?P<name>[^']+)'")
|
|
|
|
def __init__(self, path=None):
|
|
self.path = path
|
|
self.package = None
|
|
self.activity = None
|
|
self.label = None
|
|
self.version_name = None
|
|
self.version_code = None
|
|
self.parse(path)
|
|
|
|
def parse(self, apk_path):
|
|
_check_env()
|
|
command = [aapt, 'dump', 'badging', apk_path]
|
|
logger.debug(' '.join(command))
|
|
output = subprocess.check_output(command)
|
|
for line in output.split('\n'):
|
|
if line.startswith('application-label:'):
|
|
self.label = line.split(':')[1].strip().replace('\'', '')
|
|
elif line.startswith('package:'):
|
|
match = self.version_regex.search(line)
|
|
if match:
|
|
self.package = match.group('name')
|
|
self.version_code = match.group('vcode')
|
|
self.version_name = match.group('vname')
|
|
elif line.startswith('launchable-activity:'):
|
|
match = self.name_regex.search(line)
|
|
self.activity = match.group('name')
|
|
else:
|
|
pass # not interested
|
|
|
|
|
|
def fastboot_command(command, timeout=None):
|
|
_check_env()
|
|
full_command = "fastboot {}".format(command)
|
|
logger.debug(full_command)
|
|
output, _ = check_output(full_command, timeout, shell=True)
|
|
return output
|
|
|
|
|
|
def fastboot_flash_partition(partition, path_to_image):
|
|
command = 'flash {} {}'.format(partition, path_to_image)
|
|
fastboot_command(command)
|
|
|
|
|
|
def adb_get_device():
|
|
"""
|
|
Returns the serial number of a connected android device.
|
|
|
|
If there are more than one device connected to the machine, or it could not
|
|
find any device connected, :class:`wlauto.exceptions.ConfigError` is raised.
|
|
"""
|
|
_check_env()
|
|
# TODO this is a hacky way to issue a adb command to all listed devices
|
|
|
|
# The output of calling adb devices consists of a heading line then
|
|
# a list of the devices sperated by new line
|
|
# The last line is a blank new line. in otherwords, if there is a device found
|
|
# then the output length is 2 + (1 for each device)
|
|
output = adb_command('0', "devices").splitlines() # pylint: disable=E1103
|
|
output_length = len(output)
|
|
if output_length == 3:
|
|
# output[1] is the 2nd line in the output which has the device name
|
|
# Splitting the line by '\t' gives a list of two indexes, which has
|
|
# device serial in 0 number and device type in 1.
|
|
return output[1].split('\t')[0]
|
|
elif output_length > 3:
|
|
raise ConfigError('Number of discovered devices is {}, it should be 1'.format(output_length - 2))
|
|
else:
|
|
raise ConfigError('No device is connected and available')
|
|
|
|
|
|
def adb_connect(device, timeout=None):
|
|
_check_env()
|
|
command = "adb connect " + device
|
|
if ":" in device:
|
|
port = device.split(':')[-1]
|
|
logger.debug(command)
|
|
|
|
output, _ = check_output(command, shell=True, timeout=timeout)
|
|
logger.debug(output)
|
|
#### due to a rare adb bug sometimes an extra :5555 is appended to the IP address
|
|
if output.find('{}:{}'.format(port, port)) != -1:
|
|
logger.debug('ADB BUG with extra port')
|
|
command = "adb connect " + device.replace(':{}'.format(port), '')
|
|
|
|
tries = 0
|
|
output = None
|
|
while not poll_for_file(device, "/proc/cpuinfo"):
|
|
logger.debug("adb connect failed, retrying now...")
|
|
tries += 1
|
|
if tries > MAX_TRIES:
|
|
raise DeviceError('Cannot connect to adb server on the device.')
|
|
logger.debug(command)
|
|
output, _ = check_output(command, shell=True, timeout=timeout)
|
|
time.sleep(10)
|
|
|
|
if tries and output.find('connected to') == -1:
|
|
raise DeviceError('Could not connect to {}'.format(device))
|
|
|
|
|
|
def adb_disconnect(device):
|
|
_check_env()
|
|
if ":5555" in device:
|
|
command = "adb disconnect " + device
|
|
logger.debug(command)
|
|
retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
|
|
if retval:
|
|
raise DeviceError('"{}" returned {}'.format(command, retval))
|
|
|
|
|
|
def poll_for_file(device, dfile):
|
|
_check_env()
|
|
device_string = '-s {}'.format(device) if device else ''
|
|
command = "adb " + device_string + " shell \" if [ -f " + dfile + " ] ; then true ; else false ; fi\" "
|
|
logger.debug(command)
|
|
result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
|
|
return not bool(result)
|
|
|
|
am_start_error = re.compile(r"Error: Activity class {[\w|.|/]*} does not exist")
|
|
|
|
|
|
def adb_shell(device, command, timeout=None, check_exit_code=False, as_root=False): # NOQA
|
|
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
|
_check_env()
|
|
if as_root:
|
|
command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
|
|
device_string = '-s {}'.format(device) if device else ''
|
|
full_command = 'adb {} shell "{}"'.format(device_string, escape_double_quotes(command))
|
|
logger.debug(full_command)
|
|
if check_exit_code:
|
|
actual_command = "adb {} shell '({}); echo \"\n$?\"'".format(device_string, escape_single_quotes(command))
|
|
try:
|
|
raw_output, error = check_output(actual_command, timeout, shell=True)
|
|
except CalledProcessErrorWithStderr as e:
|
|
raw_output = e.output
|
|
error = e.error
|
|
exit_code = e.returncode
|
|
if exit_code == 1:
|
|
logger.debug("Exit code 1 could be either the return code of the command or mean ADB failed")
|
|
|
|
if raw_output:
|
|
if raw_output.endswith('\r\n'):
|
|
newline = '\r\n'
|
|
elif raw_output.endswith('\n'):
|
|
newline = '\n'
|
|
else:
|
|
raise WAError("Unknown new line separator in: {}".format(raw_output))
|
|
|
|
try:
|
|
output, exit_code, _ = raw_output.rsplit(newline, 2)
|
|
except ValueError:
|
|
exit_code, _ = raw_output.rsplit(newline, 1)
|
|
output = ''
|
|
else: # raw_output is empty
|
|
exit_code = '969696' # just because
|
|
output = ''
|
|
|
|
exit_code = exit_code.strip()
|
|
if exit_code.isdigit():
|
|
if int(exit_code):
|
|
message = 'Got exit code {}\nfrom: {}\nSTDOUT: {}\nSTDERR: {}'.format(exit_code, full_command,
|
|
output, error)
|
|
raise DeviceError(message)
|
|
elif am_start_error.findall(output):
|
|
message = 'Could not start activity; got the following:'
|
|
message += '\n{}'.format(am_start_error.findall(output)[0])
|
|
raise DeviceError(message)
|
|
else: # not all digits
|
|
if am_start_error.findall(output):
|
|
message = 'Could not start activity; got the following:'
|
|
message += '\n{}'.format(am_start_error.findall(output)[0])
|
|
raise DeviceError(message)
|
|
else:
|
|
raise DeviceError('adb has returned early; did not get an exit code. Was kill-server invoked?')
|
|
else: # do not check exit code
|
|
try:
|
|
output, _ = check_output(full_command, timeout, shell=True)
|
|
except CalledProcessErrorWithStderr as e:
|
|
output = e.output
|
|
exit_code = e.returncode
|
|
if e.returncode == 1:
|
|
logger.debug("Got Exit code 1, could be either the return code of the command or mean ADB failed")
|
|
return output
|
|
|
|
|
|
def adb_background_shell(device, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
|
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
|
|
_check_env()
|
|
if as_root:
|
|
command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
|
|
device_string = '-s {}'.format(device) if device else ''
|
|
full_command = 'adb {} shell "{}"'.format(device_string, escape_double_quotes(command))
|
|
logger.debug(full_command)
|
|
return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
|
|
|
|
|
|
class AdbDevice(object):
|
|
|
|
def __init__(self, name, status):
|
|
self.name = name
|
|
self.status = status
|
|
|
|
def __cmp__(self, other):
|
|
if isinstance(other, AdbDevice):
|
|
return cmp(self.name, other.name)
|
|
else:
|
|
return cmp(self.name, other)
|
|
|
|
|
|
def adb_list_devices():
|
|
_check_env()
|
|
output = adb_command(None, 'devices')
|
|
devices = []
|
|
for line in output.splitlines():
|
|
parts = [p.strip() for p in line.split()]
|
|
if len(parts) == 2:
|
|
devices.append(AdbDevice(*parts))
|
|
return devices
|
|
|
|
|
|
def adb_command(device, command, timeout=None):
|
|
_check_env()
|
|
device_string = '-s {}'.format(device) if device else ''
|
|
full_command = "adb {} {}".format(device_string, command)
|
|
logger.debug(full_command)
|
|
output, _ = check_output(full_command, timeout, shell=True)
|
|
return output
|
|
|
|
|
|
# Messy environment initialisation stuff...
|
|
|
|
|
|
def _initialize_with_android_home(env):
|
|
logger.debug('Using ANDROID_HOME from the environment.')
|
|
env.android_home = android_home
|
|
env.platform_tools = os.path.join(android_home, 'platform-tools')
|
|
os.environ['PATH'] += os.pathsep + env.platform_tools
|
|
_init_common(env)
|
|
return env
|
|
|
|
|
|
def _initialize_without_android_home(env):
|
|
if os.name == 'nt':
|
|
raise HostError('Please set ANDROID_HOME to point to the location of the Android SDK.')
|
|
# Assuming Unix in what follows.
|
|
if subprocess.call('adb version >{}'.format(get_null()), shell=True):
|
|
raise HostError('ANDROID_HOME is not set and adb is not in PATH. Have you installed Android SDK?')
|
|
logger.debug('Discovering ANDROID_HOME from adb path.')
|
|
env.platform_tools = os.path.dirname(subprocess.check_output('which adb', shell=True))
|
|
env.android_home = os.path.dirname(env.platform_tools)
|
|
_init_common(env)
|
|
return env
|
|
|
|
|
|
def _init_common(env):
|
|
logger.debug('ANDROID_HOME: {}'.format(env.android_home))
|
|
build_tools_directory = os.path.join(env.android_home, 'build-tools')
|
|
if not os.path.isdir(build_tools_directory):
|
|
msg = 'ANDROID_HOME ({}) does not appear to have valid Android SDK install (cannot find build-tools)'
|
|
raise HostError(msg.format(env.android_home))
|
|
versions = os.listdir(build_tools_directory)
|
|
for version in reversed(sorted(versions)):
|
|
aapt_path = os.path.join(build_tools_directory, version, 'aapt')
|
|
if os.path.isfile(aapt_path):
|
|
logger.debug('Using aapt for version {}'.format(version))
|
|
env.aapt = aapt_path
|
|
break
|
|
else:
|
|
raise HostError('aapt not found. Please make sure at least one Android platform is installed.')
|
|
|
|
|
|
def _check_env():
|
|
global android_home, platform_tools, adb, aapt # pylint: disable=W0603
|
|
if not android_home:
|
|
android_home = os.getenv('ANDROID_HOME')
|
|
if android_home:
|
|
_env = _initialize_with_android_home(_AndroidEnvironment())
|
|
else:
|
|
_env = _initialize_without_android_home(_AndroidEnvironment())
|
|
android_home = _env.android_home
|
|
platform_tools = _env.platform_tools
|
|
adb = _env.adb
|
|
aapt = _env.aapt
|