blob: 7b1c1a1697809e2e4bbefa7dbcdf6292fe857820 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2012 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 contains tools used by Android-specific buildbot scripts. """
import os
import re
import shell_utils
import shlex
import sys
import time
CPU_SCALING_MODES = ['performance', 'interactive']
DEVICE_LOOKUP = {'nexus_s': 'crespo',
'xoom': 'stingray',
'galaxy_nexus': 'toro',
'nexus_4': 'mako',
'nexus_7': 'grouper',
'nexus_10': 'manta'}
PROCESS_MONITOR_INTERVAL = 5.0 # Seconds
SKIA_RUNNING = 'running'
SKIA_RETURN_CODE_REPEATS = 10
SUBPROCESS_TIMEOUT = 30.0
def GotADB(adb):
""" Returns True iff ADB exists at the given location.
adb: string; possible path to the ADB executable.
"""
try:
shell_utils.run([adb, 'version'], echo=False)
return True
except Exception:
return False
def FindADB(hint=None):
""" Attempt to find the ADB program using the following sequence of steps.
Returns the path to ADB if it can be found, or None otherwise.
1. If a hint was provided, is it a valid path to ADB?
2. Is ADB in PATH?
3. Is there an environment variable for ADB?
4. If the ANDROID_SDK_ROOT variable is set, try to find ADB in the SDK
directory.
5. Try to find ADB in a list of common locations.
hint: string indicating a possible path to ADB.
"""
# 1. If a hint was provided, does it point to ADB?
if hint:
if os.path.basename(hint) == 'adb':
adb = hint
else:
adb = os.path.join(hint, 'adb')
if GotADB(adb):
return adb
# 2. Is 'adb' in our PATH?
adb = 'adb'
if GotADB(adb):
return adb
# 3. Is there an environment variable for ADB?
adb = os.environ.get('ADB')
if GotADB(adb):
return adb
# 4. If ANDROID_SDK_ROOT is set, try to find ADB in the SDK directory.
sdk_dir = os.environ.get('ANDROID_SDK_ROOT', '')
adb = os.path.join(sdk_dir, 'platform-tools', 'adb')
if GotADB(adb):
return adb
# 4. Try to find ADB relative to this file.
common_locations = []
os_dir = None
if sys.platform.startswith('linux'):
os_dir = 'linux'
elif sys.platform.startswith('darwin'):
os_dir = 'mac'
else:
os_dir = 'win'
common_locations.append(os.path.join('platform_tools', 'android', 'bin',
os_dir, 'adb'))
common_locations.append(os.path.join(os.environ.get('HOME', ''),
'android-sdk-%s' % os_dir))
for location in common_locations:
if GotADB(location):
return location
raise Exception('android_utils: Unable to find ADB!')
PATH_TO_ADB = FindADB(hint=os.path.join('platform_tools', 'android', 'bin',
'linux', 'adb'))
def RunADB(serial, cmd, echo=True, attempts=5, secs_between_attempts=10,
timeout=None):
""" Run 'cmd' on an Android device, using ADB. No return value; throws an
exception if the command fails more than the allotted number of attempts.
serial: string indicating the serial number of the target device
cmd: string; the command to issue on the device
attempts: number of times to attempt the command
secs_between_attempts: number of seconds to wait between attempts
timeout: optional, integer indicating the maximum elapsed time in seconds
"""
adb_cmd = [PATH_TO_ADB, '-s', serial]
adb_cmd += cmd
shell_utils.run_retry(adb_cmd, echo=echo, attempts=attempts,
secs_between_attempts=secs_between_attempts)
def ADBShell(serial, cmd, echo=True):
""" Runs 'cmd' in the ADB shell on an Android device and returns the exit
code.
serial: string indicating the serial number of the target device
cmd: string; the command to issue on the device
"""
# ADB doesn't exit with the exit code of the command we ran. It only exits
# non-zero when ADB itself encountered a problem. Therefore, we have to use
# the shell to print the exit code for the command and parse that from stdout.
adb_cmd = '%s -s %s shell "%s; echo \$?"' % (PATH_TO_ADB, serial,
' '.join(cmd))
output = shell_utils.run(adb_cmd, shell=True, echo=echo)
output_lines = output.splitlines()
try:
real_exitcode = int(output_lines[-1].rstrip())
except ValueError:
real_exitcode = -1
if real_exitcode != 0:
raise Exception('Command failed with code %s' % real_exitcode)
return '\n'.join(output_lines[:-1])
def ADBKill(serial, process, kill_app=False):
""" Kill a process running on an Android device.
serial: string indicating the serial number of the target device
process: string indicating the name of the process to kill
kill_app: bool indicating whether the process is an Android app, as opposed
to a normal executable process.
"""
if kill_app:
ADBShell(serial, ['am', 'kill', process])
else:
try:
stdout = shell_utils.run('%s -s %s shell ps | grep %s' % (PATH_TO_ADB,
serial,
process),
shell=True)
except Exception:
return
for line in stdout.split('\n'):
if line != '':
split = shlex.split(line)
if len(split) < 2:
continue
pid = split[1]
ADBShell(serial, ['kill', pid])
# Raise an exception if any Skia processes are still running.
try:
stdout = shell_utils.run('%s -s %s shell ps | grep %s' % (PATH_TO_ADB,
serial,
process),
shell=True)
except Exception:
return
if stdout:
raise Exception('There are still some skia processes running:\n%s\n'
'Maybe the device should be rebooted?' % stdout)
def GetSerial(device_type):
""" Determine the serial number of the *first* connected device with the
specified type. The ordering of 'adb devices' is not documented, and the
connected devices do not appear to be ordered by serial number. Therefore,
we have to assume that, in the case of multiple devices of the same type being
connected to one host, we cannot predict which device will be chosen.
device_type: string indicating the 'common name' for the target device
"""
if not device_type in DEVICE_LOOKUP:
raise ValueError('Unknown device: %s!' % device_type)
device_name = DEVICE_LOOKUP[device_type]
output = shell_utils.run_retry('%s devices' % PATH_TO_ADB, shell=True,
attempts=5)
print output
lines = output.split('\n')
device_ids = []
for line in lines:
# Filter garbage lines
if line != '' and not ('List of devices attached' in line) and \
line[0] != '*':
device_ids.append(line.split('\t')[0])
for device_id in device_ids:
print 'Finding type for id %s' % device_id
# Get device name
name_line = shell_utils.run_retry(
'%s -s %s shell cat /system/build.prop | grep "ro.product.device="' % (
PATH_TO_ADB, device_id), shell=True, attempts=5)
print name_line
name = name_line.split('=')[-1].rstrip()
# Just return the first attached device of the requested model.
if device_name in name:
return device_id
raise Exception('No %s device attached!' % device_name)
def SetCPUScalingMode(serial, mode):
""" Set the CPU scaling governor for the device with the given serial number
to the given mode.
serial: string indicating the serial number of the device whose scaling mode
is to be modified
mode: string indicating the desired CPU scaling mode. Acceptable values
are listed in CPU_SCALING_MODES.
"""
if mode not in CPU_SCALING_MODES:
raise ValueError('mode must be one of: %s' % CPU_SCALING_MODES)
cpu_dirs = shell_utils.run('%s -s %s shell ls /sys/devices/system/cpu' % (
PATH_TO_ADB, serial), echo=False, shell=True)
cpu_dirs_list = cpu_dirs.split('\n')
regex = re.compile('cpu\d')
for cpu_dir_from_list in cpu_dirs_list:
cpu_dir = cpu_dir_from_list.rstrip()
if regex.match(cpu_dir):
path = '/sys/devices/system/cpu/%s/cpufreq/scaling_governor' % cpu_dir
path_found = shell_utils.run('%s -s %s shell ls %s' % (
PATH_TO_ADB, serial, path),
echo=False, shell=True).rstrip()
if path_found == path:
# Unfortunately, we can't directly change the scaling_governor file over
# ADB. Instead, we write a script to do so, push it to the device, and
# run it.
old_mode = shell_utils.run('%s -s %s shell cat %s' % (
PATH_TO_ADB, serial, path),
echo=False, shell=True).rstrip()
print 'Current scaling mode for %s is: %s' % (cpu_dir, old_mode)
filename = 'skia_cpuscale.sh'
with open(filename, 'w') as script_file:
script_file.write('echo %s > %s\n' % (mode, path))
os.chmod(filename, 0777)
RunADB(serial, ['push', filename, '/system/bin'], echo=False)
RunADB(serial, ['shell', filename], echo=True)
RunADB(serial, ['shell', 'rm', '/system/bin/%s' % filename], echo=False)
os.remove(filename)
new_mode = shell_utils.run('%s -s %s shell cat %s' % (
PATH_TO_ADB, serial, path),
echo=False, shell=True).rstrip()
print 'New scaling mode for %s is: %s' % (cpu_dir, new_mode)
def IsAndroidShellRunning(serial):
""" Find the status of the Android shell for the device with the given serial
number. Returns True if the shell is running and False otherwise.
serial: string indicating the serial number of the target device.
"""
if 'Error:' in ADBShell(serial, ['pm', 'path', 'android'], echo=False):
return False
return True
def StopShell(serial, timeout=60):
""" Halt the Android runtime on the device with the given serial number.
Blocks until the shell reports that it has stopped.
serial: string indicating the serial number of the target device.
timeout: maximum allotted time, in seconds.
"""
ADBShell(serial, ['stop'])
start_time = time.time()
while IsAndroidShellRunning(serial):
time.sleep(1)
if time.time() - start_time > timeout:
raise Exception('Timeout while attempting to stop the Android runtime.')
def StartShell(serial, timeout=60):
""" Start the Android runtime on the device with the given serial number.
Blocks until the shell reports that it has started.
serial: string indicating the serial number of the target device.
timeout: maximum allotted time, in seconds.
"""
ADBShell(serial, ['start'])
start_time = time.time()
while not IsAndroidShellRunning(serial):
time.sleep(1)
if time.time() - start_time > timeout:
raise Exception('Timeout while attempting to start the Android runtime.')
def RunSkia(serial, cmd, release, device):
""" Run the given command through skia_launcher on a given device.
serial: string indicating the serial number of the target device.
cmd: list of strings; the command line to run.
release: bool; whether or not to run the app in Release mode.
device: string indicating the target device.
"""
RunADB(serial, ['logcat', '-c'])
try:
os.environ['SKIA_ANDROID_VERBOSE_SETUP'] = '1'
cmd_to_run = [os.path.join('platform_tools', 'android', 'bin',
'android_run_skia')]
if release:
cmd_to_run.extend(['--release'])
cmd_to_run.extend(['-d', device])
cmd_to_run.extend(['-s', serial])
cmd_to_run.extend(cmd)
shell_utils.run(cmd_to_run)
finally:
RunADB(serial, ['logcat', '-d', '-v', 'time'])