| # Copyright (c) 2016 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Utility to run a Brillo emulator programmatically. |
| |
| Requires system.img, userdata.img and kernel to be in imagedir. If running an |
| arm emulator kernel.dtb (or another dtb file) must also be in imagedir. |
| |
| WARNING: Processes created by this utility may not die unless |
| EmulatorManager.stop is called. Call EmulatorManager.verify_stop to |
| confirm process has stopped and port is free. |
| """ |
| |
| import os |
| import time |
| |
| import common |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import utils |
| |
| |
| class EmulatorManagerException(Exception): |
| """Bad port, missing artifact or non-existant imagedir.""" |
| pass |
| |
| |
| class EmulatorManager(object): |
| """Manage an instance of a device emulator. |
| |
| @param imagedir: directory of emulator images. |
| @param port: Port number for emulator's adbd. Note this port is one higher |
| than the port in the emulator's serial number. |
| @param run: Function used to execute shell commands. |
| """ |
| def __init__(self, imagedir, port, run=utils.run): |
| if not port % 2 or port < 5555 or port > 5585: |
| raise EmulatorManagerException('Port must be an odd number ' |
| 'between 5555 and 5585.') |
| try: |
| run('test -f %s' % os.path.join(imagedir, 'system.img')) |
| except error.GenericHostRunError: |
| raise EmulatorManagerException('Image directory must exist and ' |
| 'contain emulator images.') |
| |
| self.port = port |
| self.imagedir = imagedir |
| self.run = run |
| |
| |
| def verify_stop(self, timeout_secs=3): |
| """Wait for emulator on our port to stop. |
| |
| @param timeout_secs: Max seconds to wait for the emulator to stop. |
| |
| @return: Bool - True if emulator stops. |
| """ |
| cycles = 0 |
| pid = self.find() |
| while pid: |
| cycles += 1 |
| time.sleep(0.1) |
| pid = self.find() |
| if cycles >= timeout_secs*10 and pid: |
| return False |
| return True |
| |
| |
| def _find_dtb(self): |
| """Detect a dtb file in the image directory |
| |
| @return: Path to dtb file or None. |
| """ |
| cmd_result = self.run('find "%s" -name "*.dtb"' % self.imagedir) |
| dtb = cmd_result.stdout.split('\n')[0] |
| return dtb.strip() or None |
| |
| |
| def start(self): |
| """Start an emulator with the images and port specified. |
| |
| If an emulator is already running on the port it will be killed. |
| """ |
| self.force_stop() |
| time.sleep(1) # Wait for port to be free |
| # TODO(jgiorgi): Add support for x86 / x64 emulators |
| args = [ |
| '-dmS', 'emulator-%s' % self.port, 'qemu-system-arm', |
| '-M', 'vexpress-a9', |
| '-m', '1024M', |
| '-kernel', os.path.join(self.imagedir, 'kernel'), |
| '-append', ('"console=ttyAMA0 ro root=/dev/sda ' |
| 'androidboot.hardware=qemu qemu=1 rootwait noinitrd ' |
| 'init=/init androidboot.selinux=enforcing"'), |
| '-nographic', |
| '-device', 'virtio-scsi-device,id=scsi', |
| '-device', 'scsi-hd,drive=system', |
| '-drive', ('file="%s,if=none,id=system,format=raw"' |
| % os.path.join(self.imagedir, 'system.img')), |
| '-device', 'scsi-hd,drive=userdata', |
| '-drive', ('file="%s,if=none,id=userdata,format=raw"' |
| % os.path.join(self.imagedir, 'userdata.img')), |
| '-redir', 'tcp:%s::5555' % self.port, |
| ] |
| |
| # DTB file produced and required for arm but not x86 emulators |
| dtb = self._find_dtb() |
| if dtb: |
| args += ['-dtb', dtb] |
| else: |
| raise EmulatorManagerException('DTB file missing. Required for arm ' |
| 'emulators.') |
| |
| self.run(' '.join(['screen'] + args)) |
| |
| |
| def find(self): |
| """Detect the PID of a qemu process running on our port. |
| |
| @return: PID or None |
| """ |
| running = self.run('netstat -nlpt').stdout |
| for proc in running.split('\n'): |
| if ':%s' % self.port in proc: |
| process = proc.split()[-1] |
| if '/' in process: # Program identified, we started and can kill |
| return process.split('/')[0] |
| |
| |
| def stop(self, kill=False): |
| """Send signal to stop emulator process. |
| |
| Signal is sent to any running qemu process on our port regardless of how |
| it was started. Silent no-op if no running qemu processes on the port. |
| |
| @param kill: Send SIGKILL signal instead of SIGTERM. |
| """ |
| pid = self.find() |
| if pid: |
| cmd = 'kill -9 %s' if kill else 'kill %s' |
| self.run(cmd % pid) |
| |
| |
| def force_stop(self): |
| """Attempt graceful shutdown, kill if not dead after 3 seconds. |
| """ |
| self.stop() |
| if not self.verify_stop(timeout_secs=3): |
| self.stop(kill=True) |
| if not self.verify_stop(): |
| raise RuntimeError('Emulator running on port %s failed to stop.' |
| % self.port) |
| |