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