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