4 # Copyright (c) 2018 Vojtech Horky
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
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.
39 from htest
.utils
import retries
, format_command
, format_command_pipe
40 from htest
.vm
.controller
import VMController
42 class QemuVMController(VMController
):
54 '-device', 'intel-hda', '-device', 'hda-duplex',
56 'arm32/integratorcp': [
69 '-device', 'intel-hda', '-device', 'hda-duplex',
80 ocr_sed
= os
.path
.join(
81 os
.path
.dirname(os
.path
.realpath(sys
.argv
[0])),
85 def __init__(self
, arch
, name
, boot_image
, disk_image
):
86 VMController
.__init
__(self
, 'QEMU-' + arch
)
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
100 return ( width
, height
)
102 def _check_is_up(self
):
104 raise Exception("Machine not launched")
106 def _send_command(self
, command
):
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
)
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
)))
121 for command
in commands
:
124 inp
= procs
[-1].stdout
125 proc
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
, stdin
=inp
)
127 procs
[-1].communicate()
130 def boot(self
, **kwargs
):
131 self
.monitor_file
= self
.get_temp('monitor')
133 for opt
in QemuVMController
.config
[self
.arch
]:
135 opt
= self
.boot_image
136 elif opt
== '{MEMORY}':
137 opt
= '{}'.format(self
.memory
)
139 if self
.disk_image
is not None:
141 cmd
.append('file={},index=0,media=disk,format=raw'.format(self
.disk_image
))
143 cmd
.append('-display')
145 cmd
.append('-monitor')
146 cmd
.append('unix:{},server,nowait'.format(self
.monitor_file
))
147 for opt
in self
.extra_options
:
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."):
155 self
.monitor
.connect(self
.monitor_file
)
157 except FileNotFoundError
:
159 except ConnectionRefusedError
:
162 raise Exception("QEMU not started, aborting.")
165 self
.logger
.info("Machine started.")
170 uspace_booted
= False
171 for xxx
in retries(timeout
=3*60, interval
=5, name
="vterm", message
="Failed to boot into userspace"):
175 if l
.find('to see a few survival tips') != -1:
182 self
.full_vterm
= self
.vterm
184 self
.logger
.info("Machine booted into userspace.")
188 def capture_vterm_impl(self
):
189 screenshot_full
= self
.get_temp('screen-full.ppm')
190 screenshot_term
= self
.get_temp('screen-term.png')
191 screenshot_text
= self
.get_temp('screen-term.txt')
194 os
.remove(screenshot_full
)
198 self
._send
_command
('screendump ' + screenshot_full
)
200 for xxx
in retries(timeout
=10, interval
=1, name
="scrdump", message
="Failed to capture screen"):
205 '-crop', '640x480+4+24',
215 width
, height
= self
._get
_image
_dimensions
(screenshot_term
)
222 '-crop', '{}x{}'.format(cols
* 8, rows
* 16),
231 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
233 '-e', 's#000000#0#g',
236 [ 'tee', self
.get_temp('1.txt') ],
240 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
243 [ 'tee', self
.get_temp('2.txt') ],
246 '-f', QemuVMController
.ocr_sed
,
252 [ 'tee', self
.get_temp('3.txt') ],
259 '-w', '{}'.format(cols
),
261 [ 'tee', self
.get_temp('4.txt') ],
264 '-n', '{}'.format(rows
),
272 self
.screenshot_filename
= screenshot_full
274 with
open(screenshot_text
, 'r') as f
:
275 lines
= [ l
.strip('\n') for l
in f
.readlines() ]
276 self
.logger
.debug("Captured text:")
278 self
.logger
.debug("| " + l
)
284 self
._send
_command
('quit')
285 VMController
.terminate(self
)
287 def type(self
, what
):
295 '|': 'shift-backslash',
300 letter
= 'shift-' + letter
.lower()
301 if letter
in translations
:
302 letter
= translations
[letter
]
303 self
._send
_command
('sendkey ' + letter
)