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
):
53 '-device', 'intel-hda', '-device', 'hda-duplex',
55 'arm32/integratorcp': [
67 '-device', 'intel-hda', '-device', 'hda-duplex',
78 ocr_sed
= os
.path
.join(
79 os
.path
.dirname(os
.path
.realpath(sys
.argv
[0])),
83 def __init__(self
, arch
, name
, boot_image
):
84 VMController
.__init
__(self
, 'QEMU-' + arch
)
88 self
.boot_image
= boot_image
90 def is_supported(arch
):
91 return arch
in QemuVMController
.config
93 def _get_image_dimensions(self
, filename
):
94 im
= Image
.open(filename
)
95 width
, height
= im
.size
97 return ( width
, height
)
99 def _check_is_up(self
):
101 raise Exception("Machine not launched")
103 def _send_command(self
, command
):
105 self
.logger
.debug("Sending command '{}'".format(command
))
106 command
= command
+ '\n'
107 self
.monitor
.sendall(command
.encode('utf-8'))
109 def _run_command(self
, command
):
110 proc
= subprocess
.Popen(command
)
112 if proc
.returncode
!= 0:
113 raise Exception("Command {} failed.".format(command
))
115 def _run_pipe(self
, commands
):
116 self
.logger
.debug("Running pipe {}".format(format_command_pipe(commands
)))
118 for command
in commands
:
121 inp
= procs
[-1].stdout
122 proc
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
, stdin
=inp
)
124 procs
[-1].communicate()
127 def boot(self
, **kwargs
):
128 self
.monitor_file
= self
.get_temp('monitor')
130 for opt
in QemuVMController
.config
[self
.arch
]:
132 opt
= self
.boot_image
133 elif opt
== '{MEMORY}':
134 opt
= '{}'.format(self
.memory
)
137 cmd
.append('-display')
139 cmd
.append('-monitor')
140 cmd
.append('unix:{},server,nowait'.format(self
.monitor_file
))
141 for opt
in self
.extra_options
:
143 self
.logger
.debug("Starting QEMU: {}".format(format_command(cmd
)))
145 self
.proc
= subprocess
.Popen(cmd
)
146 self
.monitor
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
147 for xxx
in retries(timeout
=30, interval
=2, name
="ctl-socket", message
="Failed to connect to QEMU control socket."):
149 self
.monitor
.connect(self
.monitor_file
)
151 except FileNotFoundError
:
153 except ConnectionRefusedError
:
156 raise Exception("QEMU not started, aborting.")
159 self
.logger
.info("Machine started.")
164 uspace_booted
= False
165 for xxx
in retries(timeout
=3*60, interval
=5, name
="vterm", message
="Failed to boot into userspace"):
169 if l
.find('to see a few survival tips') != -1:
176 self
.full_vterm
= self
.vterm
178 self
.logger
.info("Machine booted into userspace.")
182 def capture_vterm_impl(self
):
183 screenshot_full
= self
.get_temp('screen-full.ppm')
184 screenshot_term
= self
.get_temp('screen-term.png')
185 screenshot_text
= self
.get_temp('screen-term.txt')
187 self
._send
_command
('screendump ' + screenshot_full
)
189 for xxx
in retries(timeout
=5, interval
=1, name
="scrdump", message
="Failed to capture screen"):
194 '-crop', '640x480+4+24',
204 width
, height
= self
._get
_image
_dimensions
(screenshot_term
)
211 '-crop', '{}x{}'.format(cols
* 8, rows
* 16),
220 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
222 '-e', 's#000000#0#g',
225 [ 'tee', self
.get_temp('1.txt') ],
229 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
232 [ 'tee', self
.get_temp('2.txt') ],
235 '-f', QemuVMController
.ocr_sed
,
241 [ 'tee', self
.get_temp('3.txt') ],
248 '-w', '{}'.format(cols
),
250 [ 'tee', self
.get_temp('4.txt') ],
253 '-n', '{}'.format(rows
),
261 self
.screenshot_filename
= screenshot_full
263 with
open(screenshot_text
, 'r') as f
:
264 lines
= [ l
.strip('\n') for l
in f
.readlines() ]
265 self
.logger
.debug("Captured text:")
267 self
.logger
.debug("| " + l
)
273 self
._send
_command
('quit')
274 VMController
.terminate(self
)
276 def type(self
, what
):
284 '|': 'shift-backslash',
289 letter
= 'shift-' + letter
.lower()
290 if letter
in translations
:
291 letter
= translations
[letter
]
292 self
._send
_command
('sendkey ' + letter
)