Justin Giorgi | dd05a94 | 2016-07-05 20:53:12 -0700 | [diff] [blame] | 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 |
Justin Giorgi | be534fa | 2016-07-25 13:03:58 -0700 | [diff] [blame] | 18 | from autotest_lib.client.common_lib import error |
Justin Giorgi | dd05a94 | 2016-07-05 20:53:12 -0700 | [diff] [blame] | 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.') |
Justin Giorgi | be534fa | 2016-07-25 13:03:58 -0700 | [diff] [blame] | 39 | try: |
| 40 | run('test -f %s' % os.path.join(imagedir, 'system.img')) |
Justin Giorgi | 6ee6453 | 2016-08-10 11:32:04 -0700 | [diff] [blame] | 41 | except error.GenericHostRunError: |
Justin Giorgi | be534fa | 2016-07-25 13:03:58 -0700 | [diff] [blame] | 42 | raise EmulatorManagerException('Image directory must exist and ' |
| 43 | 'contain emulator images.') |
Justin Giorgi | dd05a94 | 2016-07-05 20:53:12 -0700 | [diff] [blame] | 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', |
Justin Giorgi | 6a52e9c | 2016-08-08 15:32:26 -0700 | [diff] [blame] | 97 | '-drive', ('file="%s,if=none,id=system,format=raw"' |
Justin Giorgi | dd05a94 | 2016-07-05 20:53:12 -0700 | [diff] [blame] | 98 | % os.path.join(self.imagedir, 'system.img')), |
| 99 | '-device', 'scsi-hd,drive=userdata', |
Justin Giorgi | 6a52e9c | 2016-08-08 15:32:26 -0700 | [diff] [blame] | 100 | '-drive', ('file="%s,if=none,id=userdata,format=raw"' |
Justin Giorgi | dd05a94 | 2016-07-05 20:53:12 -0700 | [diff] [blame] | 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 | |