Home | History | Annotate | Download | only in utils
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides an interface to start and stop Android emulator.
      6 
      7   Emulator: The class provides the methods to launch/shutdown the emulator with
      8             the android virtual device named 'avd_armeabi' .
      9 """
     10 
     11 import logging
     12 import os
     13 import signal
     14 import subprocess
     15 import time
     16 
     17 # TODO(craigdh): Move these pylib dependencies to pylib/utils/.
     18 from pylib import android_commands
     19 from pylib import cmd_helper
     20 from pylib import constants
     21 from pylib import pexpect
     22 from pylib.device import device_utils
     23 from pylib.utils import time_profile
     24 
     25 import errors
     26 import run_command
     27 
     28 # SD card size
     29 SDCARD_SIZE = '512M'
     30 
     31 # Template used to generate config.ini files for the emulator
     32 CONFIG_TEMPLATE = """avd.ini.encoding=ISO-8859-1
     33 hw.dPad=no
     34 hw.lcd.density=320
     35 sdcard.size=512M
     36 hw.cpu.arch={hw.cpu.arch}
     37 hw.device.hash=-708107041
     38 hw.camera.back=none
     39 disk.dataPartition.size=800M
     40 hw.gpu.enabled=yes
     41 skin.path=720x1280
     42 skin.dynamic=yes
     43 hw.keyboard=yes
     44 hw.ramSize=1024
     45 hw.device.manufacturer=Google
     46 hw.sdCard=yes
     47 hw.mainKeys=no
     48 hw.accelerometer=yes
     49 skin.name=720x1280
     50 abi.type={abi.type}
     51 hw.trackBall=no
     52 hw.device.name=Galaxy Nexus
     53 hw.battery=yes
     54 hw.sensors.proximity=yes
     55 image.sysdir.1=system-images/android-{api.level}/{abi.type}/
     56 hw.sensors.orientation=yes
     57 hw.audioInput=yes
     58 hw.camera.front=none
     59 hw.gps=yes
     60 vm.heapSize=128
     61 {extras}"""
     62 
     63 CONFIG_REPLACEMENTS = {
     64   'x86': {
     65     '{hw.cpu.arch}': 'x86',
     66     '{abi.type}': 'x86',
     67     '{extras}': ''
     68   },
     69   'arm': {
     70     '{hw.cpu.arch}': 'arm',
     71     '{abi.type}': 'armeabi-v7a',
     72     '{extras}': 'hw.cpu.model=cortex-a8\n'
     73   },
     74   'mips': {
     75     '{hw.cpu.arch}': 'mips',
     76     '{abi.type}': 'mips',
     77     '{extras}': ''
     78   }
     79 }
     80 
     81 class EmulatorLaunchException(Exception):
     82   """Emulator failed to launch."""
     83   pass
     84 
     85 def _KillAllEmulators():
     86   """Kill all running emulators that look like ones we started.
     87 
     88   There are odd 'sticky' cases where there can be no emulator process
     89   running but a device slot is taken.  A little bot trouble and and
     90   we're out of room forever.
     91   """
     92   emulators = android_commands.GetAttachedDevices(hardware=False)
     93   if not emulators:
     94     return
     95   for emu_name in emulators:
     96     cmd_helper.RunCmd(['adb', '-s', emu_name, 'emu', 'kill'])
     97   logging.info('Emulator killing is async; give a few seconds for all to die.')
     98   for _ in range(5):
     99     if not android_commands.GetAttachedDevices(hardware=False):
    100       return
    101     time.sleep(1)
    102 
    103 
    104 def DeleteAllTempAVDs():
    105   """Delete all temporary AVDs which are created for tests.
    106 
    107   If the test exits abnormally and some temporary AVDs created when testing may
    108   be left in the system. Clean these AVDs.
    109   """
    110   avds = device_utils.GetAVDs()
    111   if not avds:
    112     return
    113   for avd_name in avds:
    114     if 'run_tests_avd' in avd_name:
    115       cmd = ['android', '-s', 'delete', 'avd', '--name', avd_name]
    116       cmd_helper.RunCmd(cmd)
    117       logging.info('Delete AVD %s' % avd_name)
    118 
    119 
    120 class PortPool(object):
    121   """Pool for emulator port starting position that changes over time."""
    122   _port_min = 5554
    123   _port_max = 5585
    124   _port_current_index = 0
    125 
    126   @classmethod
    127   def port_range(cls):
    128     """Return a range of valid ports for emulator use.
    129 
    130     The port must be an even number between 5554 and 5584.  Sometimes
    131     a killed emulator "hangs on" to a port long enough to prevent
    132     relaunch.  This is especially true on slow machines (like a bot).
    133     Cycling through a port start position helps make us resilient."""
    134     ports = range(cls._port_min, cls._port_max, 2)
    135     n = cls._port_current_index
    136     cls._port_current_index = (n + 1) % len(ports)
    137     return ports[n:] + ports[:n]
    138 
    139 
    140 def _GetAvailablePort():
    141   """Returns an available TCP port for the console."""
    142   used_ports = []
    143   emulators = android_commands.GetAttachedDevices(hardware=False)
    144   for emulator in emulators:
    145     used_ports.append(emulator.split('-')[1])
    146   for port in PortPool.port_range():
    147     if str(port) not in used_ports:
    148       return port
    149 
    150 
    151 def LaunchTempEmulators(emulator_count, abi, api_level, wait_for_boot=True):
    152   """Create and launch temporary emulators and wait for them to boot.
    153 
    154   Args:
    155     emulator_count: number of emulators to launch.
    156     abi: the emulator target platform
    157     api_level: the api level (e.g., 19 for Android v4.4 - KitKat release)
    158     wait_for_boot: whether or not to wait for emulators to boot up
    159 
    160   Returns:
    161     List of emulators.
    162   """
    163   emulators = []
    164   for n in xrange(emulator_count):
    165     t = time_profile.TimeProfile('Emulator launch %d' % n)
    166     # Creates a temporary AVD.
    167     avd_name = 'run_tests_avd_%d' % n
    168     logging.info('Emulator launch %d with avd_name=%s and api=%d',
    169         n, avd_name, api_level)
    170     emulator = Emulator(avd_name, abi)
    171     emulator.CreateAVD(api_level)
    172     emulator.Launch(kill_all_emulators=n == 0)
    173     t.Stop()
    174     emulators.append(emulator)
    175   # Wait for all emulators to boot completed.
    176   if wait_for_boot:
    177     for emulator in emulators:
    178       emulator.ConfirmLaunch(True)
    179   return emulators
    180 
    181 
    182 def LaunchEmulator(avd_name, abi):
    183   """Launch an existing emulator with name avd_name.
    184 
    185   Args:
    186     avd_name: name of existing emulator
    187     abi: the emulator target platform
    188 
    189   Returns:
    190     emulator object.
    191   """
    192   logging.info('Specified emulator named avd_name=%s launched', avd_name)
    193   emulator = Emulator(avd_name, abi)
    194   emulator.Launch(kill_all_emulators=True)
    195   emulator.ConfirmLaunch(True)
    196   return emulator
    197 
    198 
    199 class Emulator(object):
    200   """Provides the methods to launch/shutdown the emulator.
    201 
    202   The emulator has the android virtual device named 'avd_armeabi'.
    203 
    204   The emulator could use any even TCP port between 5554 and 5584 for the
    205   console communication, and this port will be part of the device name like
    206   'emulator-5554'. Assume it is always True, as the device name is the id of
    207   emulator managed in this class.
    208 
    209   Attributes:
    210     emulator: Path of Android's emulator tool.
    211     popen: Popen object of the running emulator process.
    212     device: Device name of this emulator.
    213   """
    214 
    215   # Signals we listen for to kill the emulator on
    216   _SIGNALS = (signal.SIGINT, signal.SIGHUP)
    217 
    218   # Time to wait for an emulator launch, in seconds.  This includes
    219   # the time to launch the emulator and a wait-for-device command.
    220   _LAUNCH_TIMEOUT = 120
    221 
    222   # Timeout interval of wait-for-device command before bouncing to a a
    223   # process life check.
    224   _WAITFORDEVICE_TIMEOUT = 5
    225 
    226   # Time to wait for a "wait for boot complete" (property set on device).
    227   _WAITFORBOOT_TIMEOUT = 300
    228 
    229   def __init__(self, avd_name, abi):
    230     """Init an Emulator.
    231 
    232     Args:
    233       avd_name: name of the AVD to create
    234       abi: target platform for emulator being created, defaults to x86
    235     """
    236     android_sdk_root = os.path.join(constants.EMULATOR_SDK_ROOT, 'sdk')
    237     self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator')
    238     self.android = os.path.join(android_sdk_root, 'tools', 'android')
    239     self.popen = None
    240     self.device_serial = None
    241     self.abi = abi
    242     self.avd_name = avd_name
    243 
    244   @staticmethod
    245   def _DeviceName():
    246     """Return our device name."""
    247     port = _GetAvailablePort()
    248     return ('emulator-%d' % port, port)
    249 
    250   def CreateAVD(self, api_level):
    251     """Creates an AVD with the given name.
    252 
    253     Args:
    254       api_level: the api level of the image
    255 
    256     Return avd_name.
    257     """
    258 
    259     if self.abi == 'arm':
    260       abi_option = 'armeabi-v7a'
    261     elif self.abi == 'mips':
    262       abi_option = 'mips'
    263     else:
    264       abi_option = 'x86'
    265 
    266     api_target = 'android-%s' % api_level
    267 
    268     avd_command = [
    269         self.android,
    270         '--silent',
    271         'create', 'avd',
    272         '--name', self.avd_name,
    273         '--abi', abi_option,
    274         '--target', api_target,
    275         '--sdcard', SDCARD_SIZE,
    276         '--force',
    277     ]
    278     avd_cmd_str = ' '.join(avd_command)
    279     logging.info('Create AVD command: %s', avd_cmd_str)
    280     avd_process = pexpect.spawn(avd_cmd_str)
    281 
    282     # Instead of creating a custom profile, we overwrite config files.
    283     avd_process.expect('Do you wish to create a custom hardware profile')
    284     avd_process.sendline('no\n')
    285     avd_process.expect('Created AVD \'%s\'' % self.avd_name)
    286 
    287     # Replace current configuration with default Galaxy Nexus config.
    288     avds_dir = os.path.join(os.path.expanduser('~'), '.android', 'avd')
    289     ini_file = os.path.join(avds_dir, '%s.ini' % self.avd_name)
    290     new_config_ini = os.path.join(avds_dir, '%s.avd' % self.avd_name,
    291                                   'config.ini')
    292 
    293     # Remove config files with defaults to replace with Google's GN settings.
    294     os.unlink(ini_file)
    295     os.unlink(new_config_ini)
    296 
    297     # Create new configuration files with Galaxy Nexus by Google settings.
    298     with open(ini_file, 'w') as new_ini:
    299       new_ini.write('avd.ini.encoding=ISO-8859-1\n')
    300       new_ini.write('target=%s\n' % api_target)
    301       new_ini.write('path=%s/%s.avd\n' % (avds_dir, self.avd_name))
    302       new_ini.write('path.rel=avd/%s.avd\n' % self.avd_name)
    303 
    304     custom_config = CONFIG_TEMPLATE
    305     replacements = CONFIG_REPLACEMENTS[self.abi]
    306     for key in replacements:
    307       custom_config = custom_config.replace(key, replacements[key])
    308     custom_config = custom_config.replace('{api.level}', str(api_level))
    309 
    310     with open(new_config_ini, 'w') as new_config_ini:
    311       new_config_ini.write(custom_config)
    312 
    313     return self.avd_name
    314 
    315 
    316   def _DeleteAVD(self):
    317     """Delete the AVD of this emulator."""
    318     avd_command = [
    319         self.android,
    320         '--silent',
    321         'delete',
    322         'avd',
    323         '--name', self.avd_name,
    324     ]
    325     logging.info('Delete AVD command: %s', ' '.join(avd_command))
    326     cmd_helper.RunCmd(avd_command)
    327 
    328 
    329   def Launch(self, kill_all_emulators):
    330     """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the
    331     emulator is ready for use.
    332 
    333     If fails, an exception will be raised.
    334     """
    335     if kill_all_emulators:
    336       _KillAllEmulators()  # just to be sure
    337     self._AggressiveImageCleanup()
    338     (self.device_serial, port) = self._DeviceName()
    339     emulator_command = [
    340         self.emulator,
    341         # Speed up emulator launch by 40%.  Really.
    342         '-no-boot-anim',
    343         # The default /data size is 64M.
    344         # That's not enough for 8 unit test bundles and their data.
    345         '-partition-size', '512',
    346         # Use a familiar name and port.
    347         '-avd', self.avd_name,
    348         '-port', str(port),
    349         # Wipe the data.  We've seen cases where an emulator gets 'stuck' if we
    350         # don't do this (every thousand runs or so).
    351         '-wipe-data',
    352         # Enable GPU by default.
    353         '-gpu', 'on',
    354         '-qemu', '-m', '1024',
    355         ]
    356     if self.abi == 'x86':
    357       emulator_command.extend([
    358           # For x86 emulator --enable-kvm will fail early, avoiding accidental
    359           # runs in a slow mode (i.e. without hardware virtualization support).
    360           '--enable-kvm',
    361           ])
    362 
    363     logging.info('Emulator launch command: %s', ' '.join(emulator_command))
    364     self.popen = subprocess.Popen(args=emulator_command,
    365                                   stderr=subprocess.STDOUT)
    366     self._InstallKillHandler()
    367 
    368   @staticmethod
    369   def _AggressiveImageCleanup():
    370     """Aggressive cleanup of emulator images.
    371 
    372     Experimentally it looks like our current emulator use on the bot
    373     leaves image files around in /tmp/android-$USER.  If a "random"
    374     name gets reused, we choke with a 'File exists' error.
    375     TODO(jrg): is there a less hacky way to accomplish the same goal?
    376     """
    377     logging.info('Aggressive Image Cleanup')
    378     emulator_imagedir = '/tmp/android-%s' % os.environ['USER']
    379     if not os.path.exists(emulator_imagedir):
    380       return
    381     for image in os.listdir(emulator_imagedir):
    382       full_name = os.path.join(emulator_imagedir, image)
    383       if 'emulator' in full_name:
    384         logging.info('Deleting emulator image %s', full_name)
    385         os.unlink(full_name)
    386 
    387   def ConfirmLaunch(self, wait_for_boot=False):
    388     """Confirm the emulator launched properly.
    389 
    390     Loop on a wait-for-device with a very small timeout.  On each
    391     timeout, check the emulator process is still alive.
    392     After confirming a wait-for-device can be successful, make sure
    393     it returns the right answer.
    394     """
    395     seconds_waited = 0
    396     number_of_waits = 2  # Make sure we can wfd twice
    397     # TODO(jbudorick) Un-handroll this in the implementation switch.
    398     adb_cmd = "adb -s %s %s" % (self.device_serial, 'wait-for-device')
    399     while seconds_waited < self._LAUNCH_TIMEOUT:
    400       try:
    401         run_command.RunCommand(adb_cmd,
    402                                timeout_time=self._WAITFORDEVICE_TIMEOUT,
    403                                retry_count=1)
    404         number_of_waits -= 1
    405         if not number_of_waits:
    406           break
    407       except errors.WaitForResponseTimedOutError:
    408         seconds_waited += self._WAITFORDEVICE_TIMEOUT
    409         adb_cmd = "adb -s %s %s" % (self.device_serial, 'kill-server')
    410         run_command.RunCommand(adb_cmd)
    411       self.popen.poll()
    412       if self.popen.returncode != None:
    413         raise EmulatorLaunchException('EMULATOR DIED')
    414     if seconds_waited >= self._LAUNCH_TIMEOUT:
    415       raise EmulatorLaunchException('TIMEOUT with wait-for-device')
    416     logging.info('Seconds waited on wait-for-device: %d', seconds_waited)
    417     if wait_for_boot:
    418       # Now that we checked for obvious problems, wait for a boot complete.
    419       # Waiting for the package manager is sometimes problematic.
    420       # TODO(jbudorick) Convert this once waiting for the package manager and
    421       #                 the external storage is no longer problematic.
    422       d = device_utils.DeviceUtils(self.device_serial)
    423       d.old_interface.WaitForSystemBootCompleted(self._WAITFORBOOT_TIMEOUT)
    424 
    425   def Shutdown(self):
    426     """Shuts down the process started by launch."""
    427     self._DeleteAVD()
    428     if self.popen:
    429       self.popen.poll()
    430       if self.popen.returncode == None:
    431         self.popen.kill()
    432       self.popen = None
    433 
    434   def _ShutdownOnSignal(self, _signum, _frame):
    435     logging.critical('emulator _ShutdownOnSignal')
    436     for sig in self._SIGNALS:
    437       signal.signal(sig, signal.SIG_DFL)
    438     self.Shutdown()
    439     raise KeyboardInterrupt  # print a stack
    440 
    441   def _InstallKillHandler(self):
    442     """Install a handler to kill the emulator when we exit unexpectedly."""
    443     for sig in self._SIGNALS:
    444       signal.signal(sig, self._ShutdownOnSignal)
    445