153 lines
5.2 KiB
Python
153 lines
5.2 KiB
Python
# 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)
|
|
|