Home | History | Annotate | Download | only in utils
      1 # Copyright (c) 2016 The Chromium OS 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 """Utility to run a Brillo emulator programmatically.
      5 
      6 Requires system.img, userdata.img and kernel to be in imagedir. If running an
      7 arm emulator kernel.dtb (or another dtb file) must also be in imagedir.
      8 
      9 WARNING: Processes created by this utility may not die unless
     10 EmulatorManager.stop is called. Call EmulatorManager.verify_stop to
     11 confirm process has stopped and port is free.
     12 """
     13 
     14 import os
     15 import time
     16 
     17 import common
     18 from autotest_lib.client.common_lib import error
     19 from autotest_lib.client.common_lib import utils
     20 
     21 
     22 class EmulatorManagerException(Exception):
     23     """Bad port, missing artifact or non-existant imagedir."""
     24     pass
     25 
     26 
     27 class EmulatorManager(object):
     28     """Manage an instance of a device emulator.
     29 
     30     @param imagedir: directory of emulator images.
     31     @param port: Port number for emulator's adbd. Note this port is one higher
     32                  than the port in the emulator's serial number.
     33     @param run: Function used to execute shell commands.
     34     """
     35     def __init__(self, imagedir, port, run=utils.run):
     36         if not port % 2 or port < 5555 or port > 5585:
     37              raise EmulatorManagerException('Port must be an odd number '
     38                                             'between 5555 and 5585.')
     39         try:
     40             run('test -f %s' % os.path.join(imagedir, 'system.img'))
     41         except error.GenericHostRunError:
     42             raise EmulatorManagerException('Image directory must exist and '
     43                                            'contain emulator images.')
     44 
     45         self.port = port
     46         self.imagedir = imagedir
     47         self.run = run
     48 
     49 
     50     def verify_stop(self, timeout_secs=3):
     51         """Wait for emulator on our port to stop.
     52 
     53         @param timeout_secs: Max seconds to wait for the emulator to stop.
     54 
     55         @return: Bool - True if emulator stops.
     56         """
     57         cycles = 0
     58         pid = self.find()
     59         while pid:
     60             cycles += 1
     61             time.sleep(0.1)
     62             pid = self.find()
     63             if cycles >= timeout_secs*10 and pid:
     64                 return False
     65         return True
     66 
     67 
     68     def _find_dtb(self):
     69         """Detect a dtb file in the image directory
     70 
     71         @return: Path to dtb file or None.
     72         """
     73         cmd_result = self.run('find "%s" -name "*.dtb"' % self.imagedir)
     74         dtb = cmd_result.stdout.split('\n')[0]
     75         return dtb.strip() or None
     76 
     77 
     78     def start(self):
     79         """Start an emulator with the images and port specified.
     80 
     81         If an emulator is already running on the port it will be killed.
     82         """
     83         self.force_stop()
     84         time.sleep(1) # Wait for port to be free
     85         # TODO(jgiorgi): Add support for x86 / x64 emulators
     86         args = [
     87             '-dmS', 'emulator-%s' % self.port, 'qemu-system-arm',
     88             '-M', 'vexpress-a9',
     89             '-m', '1024M',
     90             '-kernel', os.path.join(self.imagedir, 'kernel'),
     91             '-append', ('"console=ttyAMA0 ro root=/dev/sda '
     92                         'androidboot.hardware=qemu qemu=1 rootwait noinitrd '
     93                         'init=/init androidboot.selinux=enforcing"'),
     94             '-nographic',
     95             '-device', 'virtio-scsi-device,id=scsi',
     96             '-device', 'scsi-hd,drive=system',
     97             '-drive', ('file="%s,if=none,id=system,format=raw"'
     98                        % os.path.join(self.imagedir, 'system.img')),
     99             '-device', 'scsi-hd,drive=userdata',
    100             '-drive', ('file="%s,if=none,id=userdata,format=raw"'
    101                        % os.path.join(self.imagedir, 'userdata.img')),
    102             '-redir', 'tcp:%s::5555' % self.port,
    103         ]
    104 
    105         # DTB file produced and required for arm but not x86 emulators
    106         dtb = self._find_dtb()
    107         if dtb:
    108             args += ['-dtb', dtb]
    109         else:
    110             raise EmulatorManagerException('DTB file missing. Required for arm '
    111                                            'emulators.')
    112 
    113         self.run(' '.join(['screen'] + args))
    114 
    115 
    116     def find(self):
    117         """Detect the PID of a qemu process running on our port.
    118 
    119         @return: PID or None
    120         """
    121         running = self.run('netstat -nlpt').stdout
    122         for proc in running.split('\n'):
    123             if ':%s' % self.port in proc:
    124                 process = proc.split()[-1]
    125                 if '/' in process: # Program identified, we started and can kill
    126                     return process.split('/')[0]
    127 
    128 
    129     def stop(self, kill=False):
    130         """Send signal to stop emulator process.
    131 
    132         Signal is sent to any running qemu process on our port regardless of how
    133         it was started. Silent no-op if no running qemu processes on the port.
    134 
    135         @param kill: Send SIGKILL signal instead of SIGTERM.
    136         """
    137         pid = self.find()
    138         if pid:
    139             cmd = 'kill -9 %s' if kill else 'kill %s'
    140             self.run(cmd % pid)
    141 
    142 
    143     def force_stop(self):
    144         """Attempt graceful shutdown, kill if not dead after 3 seconds.
    145         """
    146         self.stop()
    147         if not self.verify_stop(timeout_secs=3):
    148             self.stop(kill=True)
    149         if not self.verify_stop():
    150             raise RuntimeError('Emulator running on port %s failed to stop.'
    151                                % self.port)
    152 
    153