michael@0: #!/usr/bin/env python michael@0: # michael@0: # Copyright (c) 2012 The Chromium Authors. All rights reserved. michael@0: # Use of this source code is governed by a BSD-style license that can be michael@0: # found in the LICENSE file. michael@0: michael@0: """Provides an interface to start and stop Android emulator. michael@0: michael@0: Assumes system environment ANDROID_NDK_ROOT has been set. michael@0: michael@0: Emulator: The class provides the methods to launch/shutdown the emulator with michael@0: the android virtual device named 'avd_armeabi' . michael@0: """ michael@0: michael@0: import logging michael@0: import os michael@0: import signal michael@0: import subprocess michael@0: import sys michael@0: import time michael@0: michael@0: from pylib import android_commands michael@0: from pylib import cmd_helper michael@0: michael@0: # adb_interface.py is under ../../third_party/android_testrunner/ michael@0: sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', michael@0: '..', 'third_party', 'android_testrunner')) michael@0: import adb_interface michael@0: import errors michael@0: import run_command michael@0: michael@0: class EmulatorLaunchException(Exception): michael@0: """Emulator failed to launch.""" michael@0: pass michael@0: michael@0: def _KillAllEmulators(): michael@0: """Kill all running emulators that look like ones we started. michael@0: michael@0: There are odd 'sticky' cases where there can be no emulator process michael@0: running but a device slot is taken. A little bot trouble and and michael@0: we're out of room forever. michael@0: """ michael@0: emulators = android_commands.GetEmulators() michael@0: if not emulators: michael@0: return michael@0: for emu_name in emulators: michael@0: cmd_helper.GetCmdOutput(['adb', '-s', emu_name, 'emu', 'kill']) michael@0: logging.info('Emulator killing is async; give a few seconds for all to die.') michael@0: for i in range(5): michael@0: if not android_commands.GetEmulators(): michael@0: return michael@0: time.sleep(1) michael@0: michael@0: michael@0: def DeleteAllTempAVDs(): michael@0: """Delete all temporary AVDs which are created for tests. michael@0: michael@0: If the test exits abnormally and some temporary AVDs created when testing may michael@0: be left in the system. Clean these AVDs. michael@0: """ michael@0: avds = android_commands.GetAVDs() michael@0: if not avds: michael@0: return michael@0: for avd_name in avds: michael@0: if 'run_tests_avd' in avd_name: michael@0: cmd = ['android', '-s', 'delete', 'avd', '--name', avd_name] michael@0: cmd_helper.GetCmdOutput(cmd) michael@0: logging.info('Delete AVD %s' % avd_name) michael@0: michael@0: michael@0: class PortPool(object): michael@0: """Pool for emulator port starting position that changes over time.""" michael@0: _port_min = 5554 michael@0: _port_max = 5585 michael@0: _port_current_index = 0 michael@0: michael@0: @classmethod michael@0: def port_range(cls): michael@0: """Return a range of valid ports for emulator use. michael@0: michael@0: The port must be an even number between 5554 and 5584. Sometimes michael@0: a killed emulator "hangs on" to a port long enough to prevent michael@0: relaunch. This is especially true on slow machines (like a bot). michael@0: Cycling through a port start position helps make us resilient.""" michael@0: ports = range(cls._port_min, cls._port_max, 2) michael@0: n = cls._port_current_index michael@0: cls._port_current_index = (n + 1) % len(ports) michael@0: return ports[n:] + ports[:n] michael@0: michael@0: michael@0: def _GetAvailablePort(): michael@0: """Returns an available TCP port for the console.""" michael@0: used_ports = [] michael@0: emulators = android_commands.GetEmulators() michael@0: for emulator in emulators: michael@0: used_ports.append(emulator.split('-')[1]) michael@0: for port in PortPool.port_range(): michael@0: if str(port) not in used_ports: michael@0: return port michael@0: michael@0: michael@0: class Emulator(object): michael@0: """Provides the methods to lanuch/shutdown the emulator. michael@0: michael@0: The emulator has the android virtual device named 'avd_armeabi'. michael@0: michael@0: The emulator could use any even TCP port between 5554 and 5584 for the michael@0: console communication, and this port will be part of the device name like michael@0: 'emulator-5554'. Assume it is always True, as the device name is the id of michael@0: emulator managed in this class. michael@0: michael@0: Attributes: michael@0: emulator: Path of Android's emulator tool. michael@0: popen: Popen object of the running emulator process. michael@0: device: Device name of this emulator. michael@0: """ michael@0: michael@0: # Signals we listen for to kill the emulator on michael@0: _SIGNALS = (signal.SIGINT, signal.SIGHUP) michael@0: michael@0: # Time to wait for an emulator launch, in seconds. This includes michael@0: # the time to launch the emulator and a wait-for-device command. michael@0: _LAUNCH_TIMEOUT = 120 michael@0: michael@0: # Timeout interval of wait-for-device command before bouncing to a a michael@0: # process life check. michael@0: _WAITFORDEVICE_TIMEOUT = 5 michael@0: michael@0: # Time to wait for a "wait for boot complete" (property set on device). michael@0: _WAITFORBOOT_TIMEOUT = 300 michael@0: michael@0: def __init__(self, new_avd_name, fast_and_loose): michael@0: """Init an Emulator. michael@0: michael@0: Args: michael@0: nwe_avd_name: If set, will create a new temporary AVD. michael@0: fast_and_loose: Loosen up the rules for reliable running for speed. michael@0: Intended for quick testing or re-testing. michael@0: michael@0: """ michael@0: try: michael@0: android_sdk_root = os.environ['ANDROID_SDK_ROOT'] michael@0: except KeyError: michael@0: logging.critical('The ANDROID_SDK_ROOT must be set to run the test on ' michael@0: 'emulator.') michael@0: raise michael@0: self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator') michael@0: self.android = os.path.join(android_sdk_root, 'tools', 'android') michael@0: self.popen = None michael@0: self.device = None michael@0: self.default_avd = True michael@0: self.fast_and_loose = fast_and_loose michael@0: self.abi = 'armeabi-v7a' michael@0: self.avd = 'avd_armeabi' michael@0: if 'x86' in os.environ.get('TARGET_PRODUCT', ''): michael@0: self.abi = 'x86' michael@0: self.avd = 'avd_x86' michael@0: if new_avd_name: michael@0: self.default_avd = False michael@0: self.avd = self._CreateAVD(new_avd_name) michael@0: michael@0: def _DeviceName(self): michael@0: """Return our device name.""" michael@0: port = _GetAvailablePort() michael@0: return ('emulator-%d' % port, port) michael@0: michael@0: def _CreateAVD(self, avd_name): michael@0: """Creates an AVD with the given name. michael@0: michael@0: Return avd_name. michael@0: """ michael@0: avd_command = [ michael@0: self.android, michael@0: '--silent', michael@0: 'create', 'avd', michael@0: '--name', avd_name, michael@0: '--abi', self.abi, michael@0: '--target', 'android-16', michael@0: '-c', '128M', michael@0: '--force', michael@0: ] michael@0: avd_process = subprocess.Popen(args=avd_command, michael@0: stdin=subprocess.PIPE, michael@0: stdout=subprocess.PIPE, michael@0: stderr=subprocess.STDOUT) michael@0: avd_process.stdin.write('no\n') michael@0: avd_process.wait() michael@0: logging.info('Create AVD command: %s', ' '.join(avd_command)) michael@0: return avd_name michael@0: michael@0: def _DeleteAVD(self): michael@0: """Delete the AVD of this emulator.""" michael@0: avd_command = [ michael@0: self.android, michael@0: '--silent', michael@0: 'delete', michael@0: 'avd', michael@0: '--name', self.avd, michael@0: ] michael@0: avd_process = subprocess.Popen(args=avd_command, michael@0: stdout=subprocess.PIPE, michael@0: stderr=subprocess.STDOUT) michael@0: logging.info('Delete AVD command: %s', ' '.join(avd_command)) michael@0: avd_process.wait() michael@0: michael@0: def Launch(self, kill_all_emulators): michael@0: """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the michael@0: emulator is ready for use. michael@0: michael@0: If fails, an exception will be raised. michael@0: """ michael@0: if kill_all_emulators: michael@0: _KillAllEmulators() # just to be sure michael@0: if not self.fast_and_loose: michael@0: self._AggressiveImageCleanup() michael@0: (self.device, port) = self._DeviceName() michael@0: emulator_command = [ michael@0: self.emulator, michael@0: # Speed up emulator launch by 40%. Really. michael@0: '-no-boot-anim', michael@0: # The default /data size is 64M. michael@0: # That's not enough for 8 unit test bundles and their data. michael@0: '-partition-size', '512', michael@0: # Enable GPU by default. michael@0: '-gpu', 'on', michael@0: # Use a familiar name and port. michael@0: '-avd', self.avd, michael@0: '-port', str(port)] michael@0: if not self.fast_and_loose: michael@0: emulator_command.extend([ michael@0: # Wipe the data. We've seen cases where an emulator michael@0: # gets 'stuck' if we don't do this (every thousand runs or michael@0: # so). michael@0: '-wipe-data', michael@0: ]) michael@0: logging.info('Emulator launch command: %s', ' '.join(emulator_command)) michael@0: self.popen = subprocess.Popen(args=emulator_command, michael@0: stderr=subprocess.STDOUT) michael@0: self._InstallKillHandler() michael@0: michael@0: def _AggressiveImageCleanup(self): michael@0: """Aggressive cleanup of emulator images. michael@0: michael@0: Experimentally it looks like our current emulator use on the bot michael@0: leaves image files around in /tmp/android-$USER. If a "random" michael@0: name gets reused, we choke with a 'File exists' error. michael@0: TODO(jrg): is there a less hacky way to accomplish the same goal? michael@0: """ michael@0: logging.info('Aggressive Image Cleanup') michael@0: emulator_imagedir = '/tmp/android-%s' % os.environ['USER'] michael@0: if not os.path.exists(emulator_imagedir): michael@0: return michael@0: for image in os.listdir(emulator_imagedir): michael@0: full_name = os.path.join(emulator_imagedir, image) michael@0: if 'emulator' in full_name: michael@0: logging.info('Deleting emulator image %s', full_name) michael@0: os.unlink(full_name) michael@0: michael@0: def ConfirmLaunch(self, wait_for_boot=False): michael@0: """Confirm the emulator launched properly. michael@0: michael@0: Loop on a wait-for-device with a very small timeout. On each michael@0: timeout, check the emulator process is still alive. michael@0: After confirming a wait-for-device can be successful, make sure michael@0: it returns the right answer. michael@0: """ michael@0: seconds_waited = 0 michael@0: number_of_waits = 2 # Make sure we can wfd twice michael@0: adb_cmd = "adb -s %s %s" % (self.device, 'wait-for-device') michael@0: while seconds_waited < self._LAUNCH_TIMEOUT: michael@0: try: michael@0: run_command.RunCommand(adb_cmd, michael@0: timeout_time=self._WAITFORDEVICE_TIMEOUT, michael@0: retry_count=1) michael@0: number_of_waits -= 1 michael@0: if not number_of_waits: michael@0: break michael@0: except errors.WaitForResponseTimedOutError as e: michael@0: seconds_waited += self._WAITFORDEVICE_TIMEOUT michael@0: adb_cmd = "adb -s %s %s" % (self.device, 'kill-server') michael@0: run_command.RunCommand(adb_cmd) michael@0: self.popen.poll() michael@0: if self.popen.returncode != None: michael@0: raise EmulatorLaunchException('EMULATOR DIED') michael@0: if seconds_waited >= self._LAUNCH_TIMEOUT: michael@0: raise EmulatorLaunchException('TIMEOUT with wait-for-device') michael@0: logging.info('Seconds waited on wait-for-device: %d', seconds_waited) michael@0: if wait_for_boot: michael@0: # Now that we checked for obvious problems, wait for a boot complete. michael@0: # Waiting for the package manager is sometimes problematic. michael@0: a = android_commands.AndroidCommands(self.device) michael@0: a.WaitForSystemBootCompleted(self._WAITFORBOOT_TIMEOUT) michael@0: michael@0: def Shutdown(self): michael@0: """Shuts down the process started by launch.""" michael@0: if not self.default_avd: michael@0: self._DeleteAVD() michael@0: if self.popen: michael@0: self.popen.poll() michael@0: if self.popen.returncode == None: michael@0: self.popen.kill() michael@0: self.popen = None michael@0: michael@0: def _ShutdownOnSignal(self, signum, frame): michael@0: logging.critical('emulator _ShutdownOnSignal') michael@0: for sig in self._SIGNALS: michael@0: signal.signal(sig, signal.SIG_DFL) michael@0: self.Shutdown() michael@0: raise KeyboardInterrupt # print a stack michael@0: michael@0: def _InstallKillHandler(self): michael@0: """Install a handler to kill the emulator when we exit unexpectedly.""" michael@0: for sig in self._SIGNALS: michael@0: signal.signal(sig, self._ShutdownOnSignal) michael@0: michael@0: def main(argv): michael@0: Emulator(None, True).Launch(True) michael@0: michael@0: michael@0: if __name__ == '__main__': michael@0: main(sys.argv)