QEMU: more special characters understood
[ci.git] / htest / vm / qemu.py
blobaffa9b404df4db29f87d6e29f60ebc167f4455d4
1 #!/usr/bin/env python3
4 # Copyright (c) 2018 Vojtech Horky
5 # All rights reserved.
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
11 # - Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # - Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
16 # - The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 import subprocess
32 import socket
33 import logging
34 import os
35 import sys
37 from PIL import Image
39 from htest.utils import retries, format_command, format_command_pipe
40 from htest.vm.controller import VMController
42 class QemuVMController(VMController):
43 """
44 QEMU VM controller.
45 """
47 config = {
48 'amd64': [
49 'qemu-system-x86_64',
50 '-cdrom', '{BOOT}',
51 '-boot', 'd',
52 '-m', '{MEMORY}',
53 '-usb',
54 '-device', 'intel-hda', '-device', 'hda-duplex',
56 'arm32/integratorcp': [
57 'qemu-system-arm',
58 '-M', 'integratorcp',
59 '-usb',
60 '-kernel', '{BOOT}',
61 '-m', '{MEMORY}',
63 'ia32': [
64 'qemu-system-i386',
65 '-cdrom', '{BOOT}',
66 '-boot', 'd',
67 '-m', '{MEMORY}',
68 '-usb',
69 '-device', 'intel-hda', '-device', 'hda-duplex',
71 'ppc32': [
72 'qemu-system-ppc',
73 '-usb',
74 '-boot', 'd',
75 '-cdrom', '{BOOT}',
76 '-m', '{MEMORY}',
80 ocr_sed = os.path.join(
81 os.path.dirname(os.path.realpath(sys.argv[0])),
82 'ocr.sed'
85 def __init__(self, arch, name, config_ignored, boot_image, disk_image):
86 VMController.__init__(self, 'QEMU-' + arch)
87 self.arch = arch
88 self.booted = False
89 self.name = name
90 self.boot_image = boot_image
91 self.disk_image = disk_image
93 def is_supported(arch):
94 return arch in QemuVMController.config
96 def _get_image_dimensions(self, filename):
97 im = Image.open(filename)
98 width, height = im.size
99 im.close()
100 return ( width, height )
102 def _check_is_up(self):
103 if not self.booted:
104 raise Exception("Machine not launched")
106 def _send_command(self, command):
107 self._check_is_up()
108 self.logger.debug("Sending command '{}'".format(command))
109 command = command + '\n'
110 self.monitor.sendall(command.encode('utf-8'))
112 def _run_command(self, command):
113 proc = subprocess.Popen(command)
114 proc.wait()
115 if proc.returncode != 0:
116 raise Exception("Command {} failed.".format(command))
118 def _run_pipe(self, commands):
119 self.logger.debug("Running pipe {}".format(format_command_pipe(commands)))
120 procs = []
121 for command in commands:
122 inp = None
123 if len(procs) > 0:
124 inp = procs[-1].stdout
125 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp)
126 procs.append(proc)
127 procs[-1].communicate()
130 def boot(self, **kwargs):
131 self.monitor_file = self.get_temp('monitor')
132 cmd = []
133 for opt in QemuVMController.config[self.arch]:
134 if opt == '{BOOT}':
135 opt = self.boot_image
136 elif opt == '{MEMORY}':
137 opt = '{}'.format(self.memory)
138 cmd.append(opt)
139 if self.disk_image is not None:
140 cmd.append('-drive')
141 cmd.append('file={},index=0,media=disk,format=raw'.format(self.disk_image))
142 if self.is_headless:
143 cmd.append('-display')
144 cmd.append('none')
145 cmd.append('-monitor')
146 cmd.append('unix:{},server,nowait'.format(self.monitor_file))
147 for opt in self.extra_options:
148 cmd.append(opt)
149 self.logger.debug("Starting QEMU: {}".format(format_command(cmd)))
151 self.proc = subprocess.Popen(cmd)
152 self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
153 for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."):
154 try:
155 self.monitor.connect(self.monitor_file)
156 break
157 except FileNotFoundError:
158 pass
159 except ConnectionRefusedError:
160 pass
161 if self.proc.poll():
162 raise Exception("QEMU not started, aborting.")
164 self.booted = True
165 self.logger.info("Machine started.")
167 # Skip past GRUB
168 self.type('\n')
170 uspace_booted = False
171 for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"):
172 self.vterm = []
173 self.capture_vterm()
174 for l in self.vterm:
175 if l.find('to see a few survival tips') != -1:
176 uspace_booted = True
177 break
178 if uspace_booted:
179 break
181 assert uspace_booted
183 self.logger.info("Machine booted into userspace.")
185 return
187 def capture_vterm_impl(self):
188 screenshot_full = self.get_temp('screen-full.ppm')
189 screenshot_term = self.get_temp('screen-term.png')
190 screenshot_text = self.get_temp('screen-term.txt')
192 try:
193 os.remove(screenshot_full)
194 except IOError as e:
195 pass
197 self._send_command('screendump ' + screenshot_full)
199 for xxx in retries(timeout=10, interval=1, name="scrdump", message="Failed to capture screen"):
200 try:
201 self._run_command([
202 'convert',
203 screenshot_full,
204 '-crop', '640x480+4+26',
205 '+repage',
206 '-monochrome',
207 '-colors', '2',
208 screenshot_term
210 break
211 except:
212 pass
214 width, height = self._get_image_dimensions(screenshot_term)
215 cols = width // 8
216 rows = height // 16
217 self._run_pipe([
219 'convert',
220 screenshot_term,
221 '-crop', '{}x{}'.format(cols * 8, rows * 16),
222 '+repage',
223 '-crop', '8x16',
224 '+repage',
225 '+adjoin',
226 'txt:-',
229 'sed',
230 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
231 '-e', 's:^#.*:@:',
232 '-e', 's#000000#0#g',
233 '-e', 's#FFFFFF#F#',
235 [ 'tee', self.get_temp('1.txt') ],
237 'sed',
238 '-e', ':a',
239 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
240 '-e', 't a',
242 [ 'tee', self.get_temp('2.txt') ],
244 'sed',
245 '-f', QemuVMController.ocr_sed,
248 'sed',
249 '/../s#.*#?#',
251 [ 'tee', self.get_temp('3.txt') ],
253 'paste',
254 '-sd', '',
257 'fold',
258 '-w', '{}'.format(cols),
260 [ 'tee', self.get_temp('4.txt') ],
262 'head',
263 '-n', '{}'.format(rows),
266 'tee',
267 screenshot_text,
271 self.screenshot_filename = screenshot_full
273 with open(screenshot_text, 'r') as f:
274 lines = [ l.strip('\n') for l in f.readlines() ]
275 self.logger.debug("Captured text:")
276 for l in lines:
277 self.logger.debug("| " + l)
278 return lines
280 def terminate(self):
281 if not self.booted:
282 return
283 self._send_command('quit')
284 VMController.terminate(self)
286 def type(self, what):
287 translations = {
288 ' ': 'spc',
289 '.': 'dot',
290 '-': 'minus',
291 '/': 'slash',
292 '\\': 'backslash',
293 '\n': 'ret',
294 '_': 'shift-minus',
295 '|': 'shift-backslash',
296 '=': 'equal',
297 ':': 'shift-semicolon',
298 ';': 'semicolon',
300 for letter in what:
301 if letter.isupper():
302 letter = 'shift-' + letter.lower()
303 if letter in translations:
304 letter = translations[letter]
305 self._send_command('sendkey ' + letter)
306 pass