#!/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'])