1c342db35SBrad Bishop#
2eb8dc403SDave Cobbley# Copyright (C) 2013 Intel Corporation
3eb8dc403SDave Cobbley#
4c342db35SBrad Bishop# SPDX-License-Identifier: MIT
5c342db35SBrad Bishop#
6eb8dc403SDave Cobbley
7eb8dc403SDave Cobbley# This module provides a class for starting qemu images using runqemu.
8eb8dc403SDave Cobbley# It's used by testimage.bbclass.
9eb8dc403SDave Cobbley
10eb8dc403SDave Cobbleyimport subprocess
11eb8dc403SDave Cobbleyimport os
12eb8dc403SDave Cobbleyimport sys
13eb8dc403SDave Cobbleyimport time
14eb8dc403SDave Cobbleyimport signal
15eb8dc403SDave Cobbleyimport re
16eb8dc403SDave Cobbleyimport socket
17eb8dc403SDave Cobbleyimport select
18eb8dc403SDave Cobbleyimport errno
19eb8dc403SDave Cobbleyimport string
20eb8dc403SDave Cobbleyimport threading
21eb8dc403SDave Cobbleyimport codecs
22eb8dc403SDave Cobbleyimport logging
23c926e17cSAndrew Geisslerimport tempfile
24eb8dc403SDave Cobbleyfrom oeqa.utils.dump import HostDumper
2582c905dcSAndrew Geisslerfrom collections import defaultdict
26c926e17cSAndrew Geisslerimport importlib
27eb8dc403SDave Cobbley
28eb8dc403SDave Cobbley# Get Unicode non printable control chars
29eb8dc403SDave Cobbleycontrol_range = list(range(0,32))+list(range(127,160))
30eb8dc403SDave Cobbleycontrol_chars = [chr(x) for x in control_range
31eb8dc403SDave Cobbley                if chr(x) not in string.printable]
32eb8dc403SDave Cobbleyre_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
33eb8dc403SDave Cobbley
34eb8dc403SDave Cobbleyclass QemuRunner:
35eb8dc403SDave Cobbley
3619323693SBrad Bishop    def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds,
373b8a17c1SAndrew Geissler                 use_kvm, logger, use_slirp=False, serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None, tmpfsdir=None):
38eb8dc403SDave Cobbley
39eb8dc403SDave Cobbley        # Popen object for runqemu
40eb8dc403SDave Cobbley        self.runqemu = None
4182c905dcSAndrew Geissler        self.runqemu_exited = False
42eb8dc403SDave Cobbley        # pid of the qemu process that runqemu will start
43eb8dc403SDave Cobbley        self.qemupid = None
44eb8dc403SDave Cobbley        # target ip - from the command line or runqemu output
45eb8dc403SDave Cobbley        self.ip = None
46eb8dc403SDave Cobbley        # host ip - where qemu is running
47eb8dc403SDave Cobbley        self.server_ip = None
48eb8dc403SDave Cobbley        # target ip netmask
49eb8dc403SDave Cobbley        self.netmask = None
50eb8dc403SDave Cobbley
51eb8dc403SDave Cobbley        self.machine = machine
52eb8dc403SDave Cobbley        self.rootfs = rootfs
53eb8dc403SDave Cobbley        self.display = display
54eb8dc403SDave Cobbley        self.tmpdir = tmpdir
55eb8dc403SDave Cobbley        self.deploy_dir_image = deploy_dir_image
56eb8dc403SDave Cobbley        self.logfile = logfile
57eb8dc403SDave Cobbley        self.boottime = boottime
58eb8dc403SDave Cobbley        self.logged = False
59eb8dc403SDave Cobbley        self.thread = None
60eb8dc403SDave Cobbley        self.use_kvm = use_kvm
6182c905dcSAndrew Geissler        self.use_ovmf = use_ovmf
6219323693SBrad Bishop        self.use_slirp = use_slirp
6382c905dcSAndrew Geissler        self.serial_ports = serial_ports
64eb8dc403SDave Cobbley        self.msg = ''
6582c905dcSAndrew Geissler        self.boot_patterns = boot_patterns
663b8a17c1SAndrew Geissler        self.tmpfsdir = tmpfsdir
67eb8dc403SDave Cobbley
680903674eSAndrew Geissler        self.runqemutime = 300
69b7d28619SAndrew Geissler        if not workdir:
70b7d28619SAndrew Geissler            workdir = os.getcwd()
71b7d28619SAndrew Geissler        self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid())
72eb8dc403SDave Cobbley        self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
7315ae2509SBrad Bishop        self.monitorpipe = None
74eb8dc403SDave Cobbley
75eb8dc403SDave Cobbley        self.logger = logger
76ac69b488SWilliam A. Kennington III        # Whether we're expecting an exit and should show related errors
77ac69b488SWilliam A. Kennington III        self.canexit = False
78eb8dc403SDave Cobbley
7982c905dcSAndrew Geissler        # Enable testing other OS's
8082c905dcSAndrew Geissler        # Set commands for target communication, and default to Linux ALWAYS
8182c905dcSAndrew Geissler        # Other OS's or baremetal applications need to provide their
8282c905dcSAndrew Geissler        # own implementation passing it through QemuRunner's constructor
8382c905dcSAndrew Geissler        # or by passing them through TESTIMAGE_BOOT_PATTERNS[flag]
8482c905dcSAndrew Geissler        # provided variables, where <flag> is one of the mentioned below.
8582c905dcSAndrew Geissler        accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished']
8682c905dcSAndrew Geissler        default_boot_patterns = defaultdict(str)
8782c905dcSAndrew Geissler        # Default to the usual paterns used to communicate with the target
8887f5cff0SAndrew Geissler        default_boot_patterns['search_reached_prompt'] = ' login:'
8982c905dcSAndrew Geissler        default_boot_patterns['send_login_user'] = 'root\n'
9082c905dcSAndrew Geissler        default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#"
9182c905dcSAndrew Geissler        default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
9282c905dcSAndrew Geissler
9382c905dcSAndrew Geissler        # Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n"
9482c905dcSAndrew Geissler        for pattern in accepted_patterns:
9582c905dcSAndrew Geissler            if not self.boot_patterns[pattern]:
9682c905dcSAndrew Geissler                self.boot_patterns[pattern] = default_boot_patterns[pattern]
9782c905dcSAndrew Geissler
98eb8dc403SDave Cobbley    def create_socket(self):
99eb8dc403SDave Cobbley        try:
100eb8dc403SDave Cobbley            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
101eb8dc403SDave Cobbley            sock.setblocking(0)
102eb8dc403SDave Cobbley            sock.bind(("127.0.0.1",0))
103eb8dc403SDave Cobbley            sock.listen(2)
104eb8dc403SDave Cobbley            port = sock.getsockname()[1]
105eb8dc403SDave Cobbley            self.logger.debug("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
106eb8dc403SDave Cobbley            return (sock, port)
107eb8dc403SDave Cobbley
108eb8dc403SDave Cobbley        except socket.error:
109eb8dc403SDave Cobbley            sock.close()
110eb8dc403SDave Cobbley            raise
111eb8dc403SDave Cobbley
11287f5cff0SAndrew Geissler    def decode_qemulog(self, todecode):
11387f5cff0SAndrew Geissler        # Sanitize the data received from qemu as it may contain control characters
11487f5cff0SAndrew Geissler        msg = todecode.decode("utf-8", errors='ignore')
11587f5cff0SAndrew Geissler        msg = re_control_char.sub('', msg)
11687f5cff0SAndrew Geissler        return msg
11787f5cff0SAndrew Geissler
118eb8dc403SDave Cobbley    def log(self, msg):
119eb8dc403SDave Cobbley        if self.logfile:
12087f5cff0SAndrew Geissler            msg = self.decode_qemulog(msg)
121eb8dc403SDave Cobbley            self.msg += msg
122eb8dc403SDave Cobbley            with codecs.open(self.logfile, "a", encoding="utf-8") as f:
123eb8dc403SDave Cobbley                f.write("%s" % msg)
124eb8dc403SDave Cobbley
125eb8dc403SDave Cobbley    def getOutput(self, o):
126eb8dc403SDave Cobbley        import fcntl
127eb8dc403SDave Cobbley        fl = fcntl.fcntl(o, fcntl.F_GETFL)
128eb8dc403SDave Cobbley        fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
129d159c7fbSAndrew Geissler        try:
130eb8dc403SDave Cobbley            return os.read(o.fileno(), 1000000).decode("utf-8")
131d159c7fbSAndrew Geissler        except BlockingIOError:
132d159c7fbSAndrew Geissler            return ""
133eb8dc403SDave Cobbley
134eb8dc403SDave Cobbley
135eb8dc403SDave Cobbley    def handleSIGCHLD(self, signum, frame):
136eb8dc403SDave Cobbley        if self.runqemu and self.runqemu.poll():
137eb8dc403SDave Cobbley            if self.runqemu.returncode:
13882c905dcSAndrew Geissler                self.logger.error('runqemu exited with code %d' % self.runqemu.returncode)
13982c905dcSAndrew Geissler                self.logger.error('Output from runqemu:\n%s' % self.getOutput(self.runqemu.stdout))
140eb8dc403SDave Cobbley                self.stop()
141eb8dc403SDave Cobbley                self._dump_host()
142eb8dc403SDave Cobbley
143eb8dc403SDave Cobbley    def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
144eb8dc403SDave Cobbley        env = os.environ.copy()
145eb8dc403SDave Cobbley        if self.display:
146eb8dc403SDave Cobbley            env["DISPLAY"] = self.display
147eb8dc403SDave Cobbley            # Set this flag so that Qemu doesn't do any grabs as SDL grabs
148eb8dc403SDave Cobbley            # interact badly with screensavers.
149eb8dc403SDave Cobbley            env["QEMU_DONT_GRAB"] = "1"
150eb8dc403SDave Cobbley        if not os.path.exists(self.rootfs):
151eb8dc403SDave Cobbley            self.logger.error("Invalid rootfs %s" % self.rootfs)
152eb8dc403SDave Cobbley            return False
153eb8dc403SDave Cobbley        if not os.path.exists(self.tmpdir):
154eb8dc403SDave Cobbley            self.logger.error("Invalid TMPDIR path %s" % self.tmpdir)
155eb8dc403SDave Cobbley            return False
156eb8dc403SDave Cobbley        else:
157eb8dc403SDave Cobbley            env["OE_TMPDIR"] = self.tmpdir
158eb8dc403SDave Cobbley        if not os.path.exists(self.deploy_dir_image):
159eb8dc403SDave Cobbley            self.logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
160eb8dc403SDave Cobbley            return False
161eb8dc403SDave Cobbley        else:
162eb8dc403SDave Cobbley            env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
163eb8dc403SDave Cobbley
1643b8a17c1SAndrew Geissler        if self.tmpfsdir:
1653b8a17c1SAndrew Geissler            env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
1663b8a17c1SAndrew Geissler
167eb8dc403SDave Cobbley        if not launch_cmd:
16808902b01SBrad Bishop            launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '')
169eb8dc403SDave Cobbley            if self.use_kvm:
170eb8dc403SDave Cobbley                self.logger.debug('Using kvm for runqemu')
171eb8dc403SDave Cobbley                launch_cmd += ' kvm'
172eb8dc403SDave Cobbley            else:
173eb8dc403SDave Cobbley                self.logger.debug('Not using kvm for runqemu')
174eb8dc403SDave Cobbley            if not self.display:
175eb8dc403SDave Cobbley                launch_cmd += ' nographic'
17619323693SBrad Bishop            if self.use_slirp:
17719323693SBrad Bishop                launch_cmd += ' slirp'
17882c905dcSAndrew Geissler            if self.use_ovmf:
17982c905dcSAndrew Geissler                launch_cmd += ' ovmf'
180517393d9SAndrew Geissler            launch_cmd += ' %s %s' % (runqemuparams, self.machine)
181517393d9SAndrew Geissler            if self.rootfs.endswith('.vmdk'):
182517393d9SAndrew Geissler                self.logger.debug('Bypassing VMDK rootfs for runqemu')
183517393d9SAndrew Geissler            else:
184517393d9SAndrew Geissler                launch_cmd += ' %s' % (self.rootfs)
185eb8dc403SDave Cobbley
186eb8dc403SDave Cobbley        return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
187eb8dc403SDave Cobbley
188eb8dc403SDave Cobbley    def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
189c926e17cSAndrew Geissler        # use logfile to determine the recipe-sysroot-native path and
190c926e17cSAndrew Geissler        # then add in the site-packages path components and add that
191c926e17cSAndrew Geissler        # to the python sys.path so qmp.py can be found.
192c926e17cSAndrew Geissler        python_path = os.path.dirname(os.path.dirname(self.logfile))
193eff27476SAndrew Geissler        python_path += "/recipe-sysroot-native/usr/lib/qemu-python"
194c926e17cSAndrew Geissler        sys.path.append(python_path)
195c926e17cSAndrew Geissler        importlib.invalidate_caches()
196c926e17cSAndrew Geissler        try:
197c926e17cSAndrew Geissler            qmp = importlib.import_module("qmp")
19887f5cff0SAndrew Geissler        except Exception as e:
19987f5cff0SAndrew Geissler            self.logger.error("qemurunner: qmp.py missing, please ensure it's installed (%s)" % str(e))
200c926e17cSAndrew Geissler            return False
201c926e17cSAndrew Geissler        # Path relative to tmpdir used as cwd for qemu below to avoid unix socket path length issues
202c926e17cSAndrew Geissler        qmp_file = "." + next(tempfile._get_candidate_names())
203c926e17cSAndrew Geissler        qmp_param = ' -S -qmp unix:./%s,server,wait' % (qmp_file)
204c926e17cSAndrew Geissler        qmp_port = self.tmpdir + "/" + qmp_file
2050903674eSAndrew Geissler        # Create a second socket connection for debugging use,
2060903674eSAndrew Geissler        # note this will NOT cause qemu to block waiting for the connection
2070903674eSAndrew Geissler        qmp_file2 = "." + next(tempfile._get_candidate_names())
2080903674eSAndrew Geissler        qmp_param += ' -qmp unix:./%s,server,nowait' % (qmp_file2)
2090903674eSAndrew Geissler        qmp_port2 = self.tmpdir + "/" + qmp_file2
2100903674eSAndrew Geissler        self.logger.info("QMP Available for connection at %s" % (qmp_port2))
211c926e17cSAndrew Geissler
212eb8dc403SDave Cobbley        try:
21382c905dcSAndrew Geissler            if self.serial_ports >= 2:
214f86d0556SBrad Bishop                self.threadsock, threadport = self.create_socket()
215eb8dc403SDave Cobbley            self.server_socket, self.serverport = self.create_socket()
216eb8dc403SDave Cobbley        except socket.error as msg:
217eb8dc403SDave Cobbley            self.logger.error("Failed to create listening socket: %s" % msg[1])
218eb8dc403SDave Cobbley            return False
219eb8dc403SDave Cobbley
22095ac1b8dSAndrew Geissler        bootparams = ' printk.time=1'
221eb8dc403SDave Cobbley        if extra_bootparams:
222eb8dc403SDave Cobbley            bootparams = bootparams + ' ' + extra_bootparams
223eb8dc403SDave Cobbley
224eb8dc403SDave Cobbley        # Ask QEMU to store the QEMU process PID in file, this way we don't have to parse running processes
225eb8dc403SDave Cobbley        # and analyze descendents in order to determine it.
226eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
227eb8dc403SDave Cobbley            os.remove(self.qemu_pidfile)
228c926e17cSAndrew Geissler        self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
229c926e17cSAndrew Geissler
230eb8dc403SDave Cobbley        if qemuparams:
231eb8dc403SDave Cobbley            self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
232eb8dc403SDave Cobbley
23382c905dcSAndrew Geissler        if self.serial_ports >= 2:
23415ae2509SBrad Bishop            launch_cmd += ' tcpserial=%s:%s %s' % (threadport, self.serverport, self.qemuparams)
23582c905dcSAndrew Geissler        else:
23682c905dcSAndrew Geissler            launch_cmd += ' tcpserial=%s %s' % (self.serverport, self.qemuparams)
237eb8dc403SDave Cobbley
238eb8dc403SDave Cobbley        self.origchldhandler = signal.getsignal(signal.SIGCHLD)
239eb8dc403SDave Cobbley        signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
240eb8dc403SDave Cobbley
241eb8dc403SDave Cobbley        self.logger.debug('launchcmd=%s' % (launch_cmd))
242eb8dc403SDave Cobbley
243eb8dc403SDave Cobbley        # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
244eb8dc403SDave Cobbley        # blocking at the end of the runqemu script when using this within
245eb8dc403SDave Cobbley        # oe-selftest (this makes stty error out immediately). There ought
246eb8dc403SDave Cobbley        # to be a proper fix but this will suffice for now.
247c926e17cSAndrew Geissler        self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp, env=env, cwd=self.tmpdir)
248eb8dc403SDave Cobbley        output = self.runqemu.stdout
2495f35090dSAndrew Geissler        launch_time = time.time()
250eb8dc403SDave Cobbley
251eb8dc403SDave Cobbley        #
252eb8dc403SDave Cobbley        # We need the preexec_fn above so that all runqemu processes can easily be killed
253eb8dc403SDave Cobbley        # (by killing their process group). This presents a problem if this controlling
254eb8dc403SDave Cobbley        # process itself is killed however since those processes don't notice the death
255eb8dc403SDave Cobbley        # of the parent and merrily continue on.
256eb8dc403SDave Cobbley        #
257eb8dc403SDave Cobbley        # Rather than hack runqemu to deal with this, we add something here instead.
258eb8dc403SDave Cobbley        # Basically we fork off another process which holds an open pipe to the parent
259eb8dc403SDave Cobbley        # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
260eb8dc403SDave Cobbley        # the process group. This is like pctrl's PDEATHSIG but for a process group
261eb8dc403SDave Cobbley        # rather than a single process.
262eb8dc403SDave Cobbley        #
263eb8dc403SDave Cobbley        r, w = os.pipe()
264eb8dc403SDave Cobbley        self.monitorpid = os.fork()
265eb8dc403SDave Cobbley        if self.monitorpid:
266eb8dc403SDave Cobbley            os.close(r)
267eb8dc403SDave Cobbley            self.monitorpipe = os.fdopen(w, "w")
268eb8dc403SDave Cobbley        else:
269eb8dc403SDave Cobbley            # child process
270eb8dc403SDave Cobbley            os.setpgrp()
271eb8dc403SDave Cobbley            os.close(w)
272eb8dc403SDave Cobbley            r = os.fdopen(r)
273eb8dc403SDave Cobbley            x = r.read()
274eb8dc403SDave Cobbley            os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
27593c203f3SPatrick Williams            os._exit(0)
276eb8dc403SDave Cobbley
277eb8dc403SDave Cobbley        self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
2788e7b46e2SPatrick Williams        self.logger.debug("waiting at most %d seconds for qemu pid (%s)" %
279eb8dc403SDave Cobbley                          (self.runqemutime, time.strftime("%D %H:%M:%S")))
280eb8dc403SDave Cobbley        endtime = time.time() + self.runqemutime
281eb8dc403SDave Cobbley        while not self.is_alive() and time.time() < endtime:
282eb8dc403SDave Cobbley            if self.runqemu.poll():
28382c905dcSAndrew Geissler                if self.runqemu_exited:
284c926e17cSAndrew Geissler                    self.logger.warning("runqemu during is_alive() test")
28582c905dcSAndrew Geissler                    return False
286eb8dc403SDave Cobbley                if self.runqemu.returncode:
287eb8dc403SDave Cobbley                    # No point waiting any longer
28896ff1984SBrad Bishop                    self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
289eb8dc403SDave Cobbley                    self._dump_host()
29096ff1984SBrad Bishop                    self.logger.warning("Output from runqemu:\n%s" % self.getOutput(output))
291f86d0556SBrad Bishop                    self.stop()
292eb8dc403SDave Cobbley                    return False
293eb8dc403SDave Cobbley            time.sleep(0.5)
294eb8dc403SDave Cobbley
29582c905dcSAndrew Geissler        if self.runqemu_exited:
296c926e17cSAndrew Geissler            self.logger.warning("runqemu after timeout")
29782c905dcSAndrew Geissler
298c926e17cSAndrew Geissler        if self.runqemu.returncode:
299c926e17cSAndrew Geissler            self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
300c926e17cSAndrew Geissler
301c926e17cSAndrew Geissler        if not self.is_alive():
3028e7b46e2SPatrick Williams            self.logger.error("Qemu pid didn't appear in %d seconds (%s)" %
303ac69b488SWilliam A. Kennington III                              (self.runqemutime, time.strftime("%D %H:%M:%S")))
304ac69b488SWilliam A. Kennington III
305ac69b488SWilliam A. Kennington III            qemu_pid = None
306ac69b488SWilliam A. Kennington III            if os.path.isfile(self.qemu_pidfile):
307ac69b488SWilliam A. Kennington III                with open(self.qemu_pidfile, 'r') as f:
308ac69b488SWilliam A. Kennington III                    qemu_pid = f.read().strip()
309ac69b488SWilliam A. Kennington III
310ac69b488SWilliam A. Kennington III            self.logger.error("Status information, poll status: %s, pidfile exists: %s, pidfile contents %s, proc pid exists %s"
311ac69b488SWilliam A. Kennington III                % (self.runqemu.poll(), os.path.isfile(self.qemu_pidfile), str(qemu_pid), os.path.exists("/proc/" + str(qemu_pid))))
312ac69b488SWilliam A. Kennington III
313ac69b488SWilliam A. Kennington III            # Dump all processes to help us to figure out what is going on...
314ac69b488SWilliam A. Kennington III            ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0]
315ac69b488SWilliam A. Kennington III            processes = ps.decode("utf-8")
316ac69b488SWilliam A. Kennington III            self.logger.debug("Running processes:\n%s" % processes)
317ac69b488SWilliam A. Kennington III            self._dump_host()
318ac69b488SWilliam A. Kennington III            op = self.getOutput(output)
319ac69b488SWilliam A. Kennington III            self.stop()
320ac69b488SWilliam A. Kennington III            if op:
321ac69b488SWilliam A. Kennington III                self.logger.error("Output from runqemu:\n%s" % op)
322ac69b488SWilliam A. Kennington III            else:
323ac69b488SWilliam A. Kennington III                self.logger.error("No output from runqemu.\n")
324c926e17cSAndrew Geissler            return False
325c926e17cSAndrew Geissler
326c926e17cSAndrew Geissler        # Create the client socket for the QEMU Monitor Control Socket
327c926e17cSAndrew Geissler        # This will allow us to read status from Qemu if the the process
328c926e17cSAndrew Geissler        # is still alive
329c926e17cSAndrew Geissler        self.logger.debug("QMP Initializing to %s" % (qmp_port))
330c926e17cSAndrew Geissler        # chdir dance for path length issues with unix sockets
331c926e17cSAndrew Geissler        origpath = os.getcwd()
332c926e17cSAndrew Geissler        try:
333c926e17cSAndrew Geissler            os.chdir(os.path.dirname(qmp_port))
334c926e17cSAndrew Geissler            try:
33587f5cff0SAndrew Geissler                from qmp.legacy import QEMUMonitorProtocol
33687f5cff0SAndrew Geissler                self.qmp = QEMUMonitorProtocol(os.path.basename(qmp_port))
337c926e17cSAndrew Geissler            except OSError as msg:
338c926e17cSAndrew Geissler                self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
339c926e17cSAndrew Geissler                return False
340c926e17cSAndrew Geissler
341c926e17cSAndrew Geissler            self.logger.debug("QMP Connecting to %s" % (qmp_port))
342c926e17cSAndrew Geissler            if not os.path.exists(qmp_port) and self.is_alive():
343c926e17cSAndrew Geissler                self.logger.debug("QMP Port does not exist waiting for it to be created")
344c926e17cSAndrew Geissler                endtime = time.time() + self.runqemutime
345c926e17cSAndrew Geissler                while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
346c926e17cSAndrew Geissler                    self.logger.info("QMP port does not exist yet!")
347c926e17cSAndrew Geissler                    time.sleep(0.5)
348c926e17cSAndrew Geissler                if not os.path.exists(qmp_port) and self.is_alive():
349c926e17cSAndrew Geissler                    self.logger.warning("QMP Port still does not exist but QEMU is alive")
350c926e17cSAndrew Geissler                    return False
351c926e17cSAndrew Geissler
352c926e17cSAndrew Geissler            try:
3536aa7eec5SAndrew Geissler                # set timeout value for all QMP calls
3546aa7eec5SAndrew Geissler                self.qmp.settimeout(self.runqemutime)
355c926e17cSAndrew Geissler                self.qmp.connect()
3565f35090dSAndrew Geissler                connect_time = time.time()
3578e7b46e2SPatrick Williams                self.logger.info("QMP connected to QEMU at %s and took %.2f seconds" %
3585f35090dSAndrew Geissler                                  (time.strftime("%D %H:%M:%S"),
3595f35090dSAndrew Geissler                                   time.time() - launch_time))
360c926e17cSAndrew Geissler            except OSError as msg:
361c926e17cSAndrew Geissler                self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
362c926e17cSAndrew Geissler                return False
3637784c429SPatrick Williams            except qmp.legacy.QMPError as msg:
364c926e17cSAndrew Geissler                self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
365c926e17cSAndrew Geissler                return False
366c926e17cSAndrew Geissler        finally:
367c926e17cSAndrew Geissler            os.chdir(origpath)
368c926e17cSAndrew Geissler
3690903674eSAndrew Geissler        # We worry that mmap'd libraries may cause page faults which hang the qemu VM for periods
3700903674eSAndrew Geissler        # causing failures. Before we "start" qemu, read through it's mapped files to try and
3710903674eSAndrew Geissler        # ensure we don't hit page faults later
3720903674eSAndrew Geissler        mapdir = "/proc/" + str(self.qemupid) + "/map_files/"
3730903674eSAndrew Geissler        try:
3740903674eSAndrew Geissler            for f in os.listdir(mapdir):
3755f35090dSAndrew Geissler                try:
3760903674eSAndrew Geissler                    linktarget = os.readlink(os.path.join(mapdir, f))
3770903674eSAndrew Geissler                    if not linktarget.startswith("/") or linktarget.startswith("/dev") or "deleted" in linktarget:
3780903674eSAndrew Geissler                        continue
3790903674eSAndrew Geissler                    with open(linktarget, "rb") as readf:
3800903674eSAndrew Geissler                        data = True
3810903674eSAndrew Geissler                        while data:
3820903674eSAndrew Geissler                            data = readf.read(4096)
3835f35090dSAndrew Geissler                except FileNotFoundError:
3845f35090dSAndrew Geissler                    continue
3850903674eSAndrew Geissler        # Centos7 doesn't allow us to read /map_files/
3860903674eSAndrew Geissler        except PermissionError:
3870903674eSAndrew Geissler            pass
3880903674eSAndrew Geissler
3890903674eSAndrew Geissler        # Release the qemu process to continue running
390c926e17cSAndrew Geissler        self.run_monitor('cont')
3918e7b46e2SPatrick Williams        self.logger.info("QMP released QEMU at %s and took %.2f seconds from connect" %
3925f35090dSAndrew Geissler                          (time.strftime("%D %H:%M:%S"),
3935f35090dSAndrew Geissler                           time.time() - connect_time))
394c926e17cSAndrew Geissler
395eb8dc403SDave Cobbley        # We are alive: qemu is running
396eb8dc403SDave Cobbley        out = self.getOutput(output)
397eb8dc403SDave Cobbley        netconf = False # network configuration is not required by default
3988e7b46e2SPatrick Williams        self.logger.debug("qemu started in %.2f seconds - qemu procces pid is %s (%s)" %
399eb8dc403SDave Cobbley                          (time.time() - (endtime - self.runqemutime),
400eb8dc403SDave Cobbley                           self.qemupid, time.strftime("%D %H:%M:%S")))
401eb8dc403SDave Cobbley        cmdline = ''
40282c905dcSAndrew Geissler        if get_ip:
403eb8dc403SDave Cobbley            with open('/proc/%s/cmdline' % self.qemupid) as p:
404eb8dc403SDave Cobbley                cmdline = p.read()
405eb8dc403SDave Cobbley                # It is needed to sanitize the data received
406eb8dc403SDave Cobbley                # because is possible to have control characters
407eb8dc403SDave Cobbley                cmdline = re_control_char.sub(' ', cmdline)
408eb8dc403SDave Cobbley            try:
40919323693SBrad Bishop                if self.use_slirp:
410517393d9SAndrew Geissler                    tcp_ports = cmdline.split("hostfwd=tcp:")[1]
411517393d9SAndrew Geissler                    ip, tcp_ports = tcp_ports.split(":")[:2]
41219323693SBrad Bishop                    host_port = tcp_ports[:tcp_ports.find('-')]
413517393d9SAndrew Geissler                    self.ip = "%s:%s" % (ip, host_port)
41419323693SBrad Bishop                else:
415f86d0556SBrad Bishop                    ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
416eb8dc403SDave Cobbley                    self.ip = ips[0]
417eb8dc403SDave Cobbley                    self.server_ip = ips[1]
418eb8dc403SDave Cobbley                self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
419eb8dc403SDave Cobbley            except (IndexError, ValueError):
420eb8dc403SDave Cobbley                # Try to get network configuration from runqemu output
421595f6308SAndrew Geissler                match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+).*',
422eb8dc403SDave Cobbley                                 out, re.MULTILINE | re.DOTALL)
423eb8dc403SDave Cobbley                if match:
424eb8dc403SDave Cobbley                    self.ip, self.server_ip, self.netmask = match.groups()
425eb8dc403SDave Cobbley                    # network configuration is required as we couldn't get it
426eb8dc403SDave Cobbley                    # from the runqemu command line, so qemu doesn't run kernel
427eb8dc403SDave Cobbley                    # and guest networking is not configured
428eb8dc403SDave Cobbley                    netconf = True
429eb8dc403SDave Cobbley                else:
430eb8dc403SDave Cobbley                    self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
431eb8dc403SDave Cobbley                                 "Here is the qemu command line used:\n%s\n"
432eb8dc403SDave Cobbley                                 "and output from runqemu:\n%s" % (cmdline, out))
433eb8dc403SDave Cobbley                    self._dump_host()
434eb8dc403SDave Cobbley                    self.stop()
435eb8dc403SDave Cobbley                    return False
436eb8dc403SDave Cobbley
437eb8dc403SDave Cobbley        self.logger.debug("Target IP: %s" % self.ip)
438eb8dc403SDave Cobbley        self.logger.debug("Server IP: %s" % self.server_ip)
439eb8dc403SDave Cobbley
44082c905dcSAndrew Geissler        if self.serial_ports >= 2:
441f86d0556SBrad Bishop            self.thread = LoggingThread(self.log, self.threadsock, self.logger)
442eb8dc403SDave Cobbley            self.thread.start()
443eb8dc403SDave Cobbley            if not self.thread.connection_established.wait(self.boottime):
444eb8dc403SDave Cobbley                self.logger.error("Didn't receive a console connection from qemu. "
445eb8dc403SDave Cobbley                             "Here is the qemu command line used:\n%s\nand "
446eb8dc403SDave Cobbley                             "output from runqemu:\n%s" % (cmdline, out))
447eb8dc403SDave Cobbley                self.stop_thread()
448eb8dc403SDave Cobbley                return False
449eb8dc403SDave Cobbley
450eb8dc403SDave Cobbley        self.logger.debug("Output from runqemu:\n%s", out)
451eb8dc403SDave Cobbley        self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
452eb8dc403SDave Cobbley                          (self.boottime, time.strftime("%D %H:%M:%S")))
453eb8dc403SDave Cobbley        endtime = time.time() + self.boottime
454*e760df85SPatrick Williams        filelist = [self.server_socket, self.runqemu.stdout]
455eb8dc403SDave Cobbley        reachedlogin = False
456eb8dc403SDave Cobbley        stopread = False
457eb8dc403SDave Cobbley        qemusock = None
458eb8dc403SDave Cobbley        bootlog = b''
459eb8dc403SDave Cobbley        data = b''
460eb8dc403SDave Cobbley        while time.time() < endtime and not stopread:
461eb8dc403SDave Cobbley            try:
462*e760df85SPatrick Williams                sread, swrite, serror = select.select(filelist, [], [], 5)
463eb8dc403SDave Cobbley            except InterruptedError:
464eb8dc403SDave Cobbley                continue
465*e760df85SPatrick Williams            for file in sread:
466*e760df85SPatrick Williams                if file is self.server_socket:
467eb8dc403SDave Cobbley                    qemusock, addr = self.server_socket.accept()
468*e760df85SPatrick Williams                    qemusock.setblocking(False)
469*e760df85SPatrick Williams                    filelist.append(qemusock)
470*e760df85SPatrick Williams                    filelist.remove(self.server_socket)
471eb8dc403SDave Cobbley                    self.logger.debug("Connection from %s:%s" % addr)
472eb8dc403SDave Cobbley                else:
4736aa7eec5SAndrew Geissler                    # try to avoid reading only a single character at a time
4746aa7eec5SAndrew Geissler                    time.sleep(0.1)
475*e760df85SPatrick Williams                    if hasattr(file, 'read'):
476*e760df85SPatrick Williams                        read = file.read(1024)
477*e760df85SPatrick Williams                    elif hasattr(file, 'recv'):
478*e760df85SPatrick Williams                        read = file.recv(1024)
479*e760df85SPatrick Williams                    else:
480*e760df85SPatrick Williams                        self.logger.error('Invalid file type: %s\n%s' % (file))
481*e760df85SPatrick Williams                        read = b''
482*e760df85SPatrick Williams
483*e760df85SPatrick Williams                    self.logger.debug2('Partial boot log:\n%s' % (read.decode('utf-8', errors='ignore')))
484*e760df85SPatrick Williams                    data = data + read
485eb8dc403SDave Cobbley                    if data:
486eb8dc403SDave Cobbley                        bootlog += data
48782c905dcSAndrew Geissler                        if self.serial_ports < 2:
488*e760df85SPatrick Williams                            # this file has mixed console/kernel data, log it to logfile
48982c905dcSAndrew Geissler                            self.log(data)
49082c905dcSAndrew Geissler
491eb8dc403SDave Cobbley                        data = b''
49287f5cff0SAndrew Geissler
49387f5cff0SAndrew Geissler                        decodedlog = self.decode_qemulog(bootlog)
49487f5cff0SAndrew Geissler                        if self.boot_patterns['search_reached_prompt'] in decodedlog:
495*e760df85SPatrick Williams                            self.server_socket.close()
496eb8dc403SDave Cobbley                            self.server_socket = qemusock
497eb8dc403SDave Cobbley                            stopread = True
498eb8dc403SDave Cobbley                            reachedlogin = True
4998e7b46e2SPatrick Williams                            self.logger.debug("Reached login banner in %.2f seconds (%s)" %
500eb8dc403SDave Cobbley                                              (time.time() - (endtime - self.boottime),
5018e7b46e2SPatrick Williams                                              time.strftime("%D %H:%M:%S")))
502eb8dc403SDave Cobbley                    else:
503eb8dc403SDave Cobbley                        # no need to check if reachedlogin unless we support multiple connections
504eb8dc403SDave Cobbley                        self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
505eb8dc403SDave Cobbley                                          time.strftime("%D %H:%M:%S"))
506*e760df85SPatrick Williams                        filelist.remove(file)
507*e760df85SPatrick Williams                        file.close()
508eb8dc403SDave Cobbley                        stopread = True
509eb8dc403SDave Cobbley
510eb8dc403SDave Cobbley        if not reachedlogin:
511eb8dc403SDave Cobbley            if time.time() >= endtime:
51296ff1984SBrad Bishop                self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
513eb8dc403SDave Cobbley                                  (self.boottime, time.strftime("%D %H:%M:%S")))
514eb8dc403SDave Cobbley            tail = lambda l: "\n".join(l.splitlines()[-25:])
51587f5cff0SAndrew Geissler            bootlog = self.decode_qemulog(bootlog)
516eb8dc403SDave Cobbley            # in case bootlog is empty, use tail qemu log store at self.msg
517eb8dc403SDave Cobbley            lines = tail(bootlog if bootlog else self.msg)
51887f5cff0SAndrew Geissler            self.logger.warning("Last 25 lines of text (%d):\n%s" % (len(bootlog), lines))
51996ff1984SBrad Bishop            self.logger.warning("Check full boot log: %s" % self.logfile)
520eb8dc403SDave Cobbley            self._dump_host()
521eb8dc403SDave Cobbley            self.stop()
522eb8dc403SDave Cobbley            return False
523eb8dc403SDave Cobbley
524eb8dc403SDave Cobbley        # If we are not able to login the tests can continue
525eb8dc403SDave Cobbley        try:
526c3d88e4dSAndrew Geissler            (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120)
52782c905dcSAndrew Geissler            if re.search(self.boot_patterns['search_login_succeeded'], output):
528eb8dc403SDave Cobbley                self.logged = True
5298e7b46e2SPatrick Williams                self.logger.debug("Logged in as %s in serial console" % self.boot_patterns['send_login_user'].replace("\n", ""))
530eb8dc403SDave Cobbley                if netconf:
531eb8dc403SDave Cobbley                    # configure guest networking
532eb8dc403SDave Cobbley                    cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
533eb8dc403SDave Cobbley                    output = self.run_serial(cmd, raw=True)[1]
534f86d0556SBrad Bishop                    if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
535eb8dc403SDave Cobbley                        self.logger.debug("configured ip address %s", self.ip)
536eb8dc403SDave Cobbley                    else:
537eb8dc403SDave Cobbley                        self.logger.debug("Couldn't configure guest networking")
538eb8dc403SDave Cobbley            else:
53996ff1984SBrad Bishop                self.logger.warning("Couldn't login into serial console"
5408e7b46e2SPatrick Williams                            " as %s using blank password" % self.boot_patterns['send_login_user'].replace("\n", ""))
54196ff1984SBrad Bishop                self.logger.warning("The output:\n%s" % output)
542eb8dc403SDave Cobbley        except:
54396ff1984SBrad Bishop            self.logger.warning("Serial console failed while trying to login")
544eb8dc403SDave Cobbley        return True
545eb8dc403SDave Cobbley
546eb8dc403SDave Cobbley    def stop(self):
547eb8dc403SDave Cobbley        if hasattr(self, "origchldhandler"):
548eb8dc403SDave Cobbley            signal.signal(signal.SIGCHLD, self.origchldhandler)
5491a4b7ee2SBrad Bishop        self.stop_thread()
5501a4b7ee2SBrad Bishop        self.stop_qemu_system()
551eb8dc403SDave Cobbley        if self.runqemu:
552eb8dc403SDave Cobbley            if hasattr(self, "monitorpid"):
553eb8dc403SDave Cobbley                os.kill(self.monitorpid, signal.SIGKILL)
554eb8dc403SDave Cobbley                self.logger.debug("Sending SIGTERM to runqemu")
555eb8dc403SDave Cobbley                try:
556eb8dc403SDave Cobbley                    os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
557eb8dc403SDave Cobbley                except OSError as e:
558eb8dc403SDave Cobbley                    if e.errno != errno.ESRCH:
559eb8dc403SDave Cobbley                        raise
560864cc43bSPatrick Williams            try:
561864cc43bSPatrick Williams                outs, errs = self.runqemu.communicate(timeout=self.runqemutime)
562864cc43bSPatrick Williams                if outs:
563864cc43bSPatrick Williams                    self.logger.info("Output from runqemu:\n%s", outs.decode("utf-8"))
564864cc43bSPatrick Williams                if errs:
565864cc43bSPatrick Williams                    self.logger.info("Stderr from runqemu:\n%s", errs.decode("utf-8"))
5668e7b46e2SPatrick Williams            except subprocess.TimeoutExpired:
567eb8dc403SDave Cobbley                self.logger.debug("Sending SIGKILL to runqemu")
568eb8dc403SDave Cobbley                os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
569d159c7fbSAndrew Geissler            if not self.runqemu.stdout.closed:
570d159c7fbSAndrew Geissler                self.logger.info("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
571f86d0556SBrad Bishop            self.runqemu.stdin.close()
572f86d0556SBrad Bishop            self.runqemu.stdout.close()
57382c905dcSAndrew Geissler            self.runqemu_exited = True
574f86d0556SBrad Bishop
575c926e17cSAndrew Geissler        if hasattr(self, 'qmp') and self.qmp:
576c926e17cSAndrew Geissler            self.qmp.close()
577c926e17cSAndrew Geissler            self.qmp = None
578eb8dc403SDave Cobbley        if hasattr(self, 'server_socket') and self.server_socket:
579eb8dc403SDave Cobbley            self.server_socket.close()
580eb8dc403SDave Cobbley            self.server_socket = None
581f86d0556SBrad Bishop        if hasattr(self, 'threadsock') and self.threadsock:
582f86d0556SBrad Bishop            self.threadsock.close()
583f86d0556SBrad Bishop            self.threadsock = None
584eb8dc403SDave Cobbley        self.qemupid = None
585eb8dc403SDave Cobbley        self.ip = None
586eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
58782c905dcSAndrew Geissler            try:
588eb8dc403SDave Cobbley                os.remove(self.qemu_pidfile)
58982c905dcSAndrew Geissler            except FileNotFoundError as e:
59082c905dcSAndrew Geissler                # We raced, ignore
59182c905dcSAndrew Geissler                pass
592f86d0556SBrad Bishop        if self.monitorpipe:
593f86d0556SBrad Bishop            self.monitorpipe.close()
594eb8dc403SDave Cobbley
595eb8dc403SDave Cobbley    def stop_qemu_system(self):
596eb8dc403SDave Cobbley        if self.qemupid:
597eb8dc403SDave Cobbley            try:
598eb8dc403SDave Cobbley                # qemu-system behaves well and a SIGTERM is enough
599eb8dc403SDave Cobbley                os.kill(self.qemupid, signal.SIGTERM)
600eb8dc403SDave Cobbley            except ProcessLookupError as e:
6011a4b7ee2SBrad Bishop                self.logger.warning('qemu-system ended unexpectedly')
602eb8dc403SDave Cobbley
603eb8dc403SDave Cobbley    def stop_thread(self):
604eb8dc403SDave Cobbley        if self.thread and self.thread.is_alive():
605eb8dc403SDave Cobbley            self.thread.stop()
606eb8dc403SDave Cobbley            self.thread.join()
607eb8dc403SDave Cobbley
608c926e17cSAndrew Geissler    def allowexit(self):
609ac69b488SWilliam A. Kennington III        self.canexit = True
610c926e17cSAndrew Geissler        if self.thread:
611c926e17cSAndrew Geissler            self.thread.allowexit()
612c926e17cSAndrew Geissler
613eb8dc403SDave Cobbley    def restart(self, qemuparams = None):
61496ff1984SBrad Bishop        self.logger.warning("Restarting qemu process")
615eb8dc403SDave Cobbley        if self.runqemu.poll() is None:
616eb8dc403SDave Cobbley            self.stop()
617eb8dc403SDave Cobbley        if self.start(qemuparams):
618eb8dc403SDave Cobbley            return True
619eb8dc403SDave Cobbley        return False
620eb8dc403SDave Cobbley
621eb8dc403SDave Cobbley    def is_alive(self):
62282c905dcSAndrew Geissler        if not self.runqemu or self.runqemu.poll() is not None or self.runqemu_exited:
623eb8dc403SDave Cobbley            return False
624eb8dc403SDave Cobbley        if os.path.isfile(self.qemu_pidfile):
62596ff1984SBrad Bishop            # when handling pidfile, qemu creates the file, stat it, lock it and then write to it
62696ff1984SBrad Bishop            # so it's possible that the file has been created but the content is empty
62796ff1984SBrad Bishop            pidfile_timeout = time.time() + 3
62896ff1984SBrad Bishop            while time.time() < pidfile_timeout:
62996ff1984SBrad Bishop                with open(self.qemu_pidfile, 'r') as f:
63096ff1984SBrad Bishop                    qemu_pid = f.read().strip()
63196ff1984SBrad Bishop                # file created but not yet written contents
63296ff1984SBrad Bishop                if not qemu_pid:
63396ff1984SBrad Bishop                    time.sleep(0.5)
63496ff1984SBrad Bishop                    continue
63596ff1984SBrad Bishop                else:
63696ff1984SBrad Bishop                    if os.path.exists("/proc/" + qemu_pid):
63796ff1984SBrad Bishop                        self.qemupid = int(qemu_pid)
638eb8dc403SDave Cobbley                        return True
639eb8dc403SDave Cobbley        return False
640eb8dc403SDave Cobbley
6415f35090dSAndrew Geissler    def run_monitor(self, command, args=None, timeout=60):
6425f35090dSAndrew Geissler        if hasattr(self, 'qmp') and self.qmp:
6436aa7eec5SAndrew Geissler            self.qmp.settimeout(timeout)
6445f35090dSAndrew Geissler            if args is not None:
6455f35090dSAndrew Geissler                return self.qmp.cmd(command, args)
6465f35090dSAndrew Geissler            else:
647c926e17cSAndrew Geissler                return self.qmp.cmd(command)
648c926e17cSAndrew Geissler
649977dc1acSBrad Bishop    def run_serial(self, command, raw=False, timeout=60):
65092b42cb3SPatrick Williams        # Returns (status, output) where status is 1 on success and 0 on error
65192b42cb3SPatrick Williams
652eb8dc403SDave Cobbley        # We assume target system have echo to get command status
653eb8dc403SDave Cobbley        if not raw:
654eb8dc403SDave Cobbley            command = "%s; echo $?\n" % command
655eb8dc403SDave Cobbley
656eb8dc403SDave Cobbley        data = ''
657eb8dc403SDave Cobbley        status = 0
658eb8dc403SDave Cobbley        self.server_socket.sendall(command.encode('utf-8'))
659eb8dc403SDave Cobbley        start = time.time()
660eb8dc403SDave Cobbley        end = start + timeout
661eb8dc403SDave Cobbley        while True:
662eb8dc403SDave Cobbley            now = time.time()
663eb8dc403SDave Cobbley            if now >= end:
664eb8dc403SDave Cobbley                data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
665eb8dc403SDave Cobbley                break
666eb8dc403SDave Cobbley            try:
667eb8dc403SDave Cobbley                sread, _, _ = select.select([self.server_socket],[],[], end - now)
668eb8dc403SDave Cobbley            except InterruptedError:
669eb8dc403SDave Cobbley                continue
670eb8dc403SDave Cobbley            if sread:
6716aa7eec5SAndrew Geissler                # try to avoid reading single character at a time
6726aa7eec5SAndrew Geissler                time.sleep(0.1)
673eb8dc403SDave Cobbley                answer = self.server_socket.recv(1024)
674eb8dc403SDave Cobbley                if answer:
675eb8dc403SDave Cobbley                    data += answer.decode('utf-8')
676eb8dc403SDave Cobbley                    # Search the prompt to stop
67782c905dcSAndrew Geissler                    if re.search(self.boot_patterns['search_cmd_finished'], data):
678eb8dc403SDave Cobbley                        break
679eb8dc403SDave Cobbley                else:
680ac69b488SWilliam A. Kennington III                    if self.canexit:
681ac69b488SWilliam A. Kennington III                        return (1, "")
682ac69b488SWilliam A. Kennington III                    raise Exception("No data on serial console socket, connection closed?")
683eb8dc403SDave Cobbley
684eb8dc403SDave Cobbley        if data:
685eb8dc403SDave Cobbley            if raw:
686eb8dc403SDave Cobbley                status = 1
687eb8dc403SDave Cobbley            else:
688eb8dc403SDave Cobbley                # Remove first line (command line) and last line (prompt)
689eb8dc403SDave Cobbley                data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
690eb8dc403SDave Cobbley                index = data.rfind('\r\n')
691eb8dc403SDave Cobbley                if index == -1:
692eb8dc403SDave Cobbley                    status_cmd = data
693eb8dc403SDave Cobbley                    data = ""
694eb8dc403SDave Cobbley                else:
695eb8dc403SDave Cobbley                    status_cmd = data[index+2:]
696eb8dc403SDave Cobbley                    data = data[:index]
697eb8dc403SDave Cobbley                if (status_cmd == "0"):
698eb8dc403SDave Cobbley                    status = 1
699eb8dc403SDave Cobbley        return (status, str(data))
700eb8dc403SDave Cobbley
701eb8dc403SDave Cobbley
702eb8dc403SDave Cobbley    def _dump_host(self):
703eb8dc403SDave Cobbley        self.host_dumper.create_dir("qemu")
7041a4b7ee2SBrad Bishop        self.logger.warning("Qemu ended unexpectedly, dump data from host"
705eb8dc403SDave Cobbley                " is in %s" % self.host_dumper.dump_dir)
706eb8dc403SDave Cobbley        self.host_dumper.dump_host()
707eb8dc403SDave Cobbley
708eb8dc403SDave Cobbley# This class is for reading data from a socket and passing it to logfunc
709eb8dc403SDave Cobbley# to be processed. It's completely event driven and has a straightforward
710eb8dc403SDave Cobbley# event loop. The mechanism for stopping the thread is a simple pipe which
711eb8dc403SDave Cobbley# will wake up the poll and allow for tearing everything down.
712eb8dc403SDave Cobbleyclass LoggingThread(threading.Thread):
713eb8dc403SDave Cobbley    def __init__(self, logfunc, sock, logger):
714eb8dc403SDave Cobbley        self.connection_established = threading.Event()
715eb8dc403SDave Cobbley        self.serversock = sock
716eb8dc403SDave Cobbley        self.logfunc = logfunc
717eb8dc403SDave Cobbley        self.logger = logger
718eb8dc403SDave Cobbley        self.readsock = None
719eb8dc403SDave Cobbley        self.running = False
720c926e17cSAndrew Geissler        self.canexit = False
721eb8dc403SDave Cobbley
722eb8dc403SDave Cobbley        self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
723eb8dc403SDave Cobbley        self.readevents = select.POLLIN | select.POLLPRI
724eb8dc403SDave Cobbley
725eb8dc403SDave Cobbley        threading.Thread.__init__(self, target=self.threadtarget)
726eb8dc403SDave Cobbley
727eb8dc403SDave Cobbley    def threadtarget(self):
728eb8dc403SDave Cobbley        try:
729eb8dc403SDave Cobbley            self.eventloop()
730eb8dc403SDave Cobbley        finally:
731eb8dc403SDave Cobbley            self.teardown()
732eb8dc403SDave Cobbley
733eb8dc403SDave Cobbley    def run(self):
734eb8dc403SDave Cobbley        self.logger.debug("Starting logging thread")
735eb8dc403SDave Cobbley        self.readpipe, self.writepipe = os.pipe()
736eb8dc403SDave Cobbley        threading.Thread.run(self)
737eb8dc403SDave Cobbley
738eb8dc403SDave Cobbley    def stop(self):
739eb8dc403SDave Cobbley        self.logger.debug("Stopping logging thread")
740eb8dc403SDave Cobbley        if self.running:
741eb8dc403SDave Cobbley            os.write(self.writepipe, bytes("stop", "utf-8"))
742eb8dc403SDave Cobbley
743eb8dc403SDave Cobbley    def teardown(self):
744eb8dc403SDave Cobbley        self.logger.debug("Tearing down logging thread")
745eb8dc403SDave Cobbley        self.close_socket(self.serversock)
746eb8dc403SDave Cobbley
747eb8dc403SDave Cobbley        if self.readsock is not None:
748eb8dc403SDave Cobbley            self.close_socket(self.readsock)
749eb8dc403SDave Cobbley
750eb8dc403SDave Cobbley        self.close_ignore_error(self.readpipe)
751eb8dc403SDave Cobbley        self.close_ignore_error(self.writepipe)
752eb8dc403SDave Cobbley        self.running = False
753eb8dc403SDave Cobbley
754c926e17cSAndrew Geissler    def allowexit(self):
755c926e17cSAndrew Geissler        self.canexit = True
756c926e17cSAndrew Geissler
757eb8dc403SDave Cobbley    def eventloop(self):
758eb8dc403SDave Cobbley        poll = select.poll()
759eb8dc403SDave Cobbley        event_read_mask = self.errorevents | self.readevents
760eb8dc403SDave Cobbley        poll.register(self.serversock.fileno())
761eb8dc403SDave Cobbley        poll.register(self.readpipe, event_read_mask)
762eb8dc403SDave Cobbley
763eb8dc403SDave Cobbley        breakout = False
764eb8dc403SDave Cobbley        self.running = True
765eb8dc403SDave Cobbley        self.logger.debug("Starting thread event loop")
766eb8dc403SDave Cobbley        while not breakout:
767eb8dc403SDave Cobbley            events = poll.poll()
768eb8dc403SDave Cobbley            for event in events:
769eb8dc403SDave Cobbley                # An error occurred, bail out
770eb8dc403SDave Cobbley                if event[1] & self.errorevents:
771eb8dc403SDave Cobbley                    raise Exception(self.stringify_event(event[1]))
772eb8dc403SDave Cobbley
773eb8dc403SDave Cobbley                # Event to stop the thread
774eb8dc403SDave Cobbley                if self.readpipe == event[0]:
775eb8dc403SDave Cobbley                    self.logger.debug("Stop event received")
776eb8dc403SDave Cobbley                    breakout = True
777eb8dc403SDave Cobbley                    break
778eb8dc403SDave Cobbley
779eb8dc403SDave Cobbley                # A connection request was received
780eb8dc403SDave Cobbley                elif self.serversock.fileno() == event[0]:
781eb8dc403SDave Cobbley                    self.logger.debug("Connection request received")
782eb8dc403SDave Cobbley                    self.readsock, _ = self.serversock.accept()
783eb8dc403SDave Cobbley                    self.readsock.setblocking(0)
784eb8dc403SDave Cobbley                    poll.unregister(self.serversock.fileno())
785eb8dc403SDave Cobbley                    poll.register(self.readsock.fileno(), event_read_mask)
786eb8dc403SDave Cobbley
787eb8dc403SDave Cobbley                    self.logger.debug("Setting connection established event")
788eb8dc403SDave Cobbley                    self.connection_established.set()
789eb8dc403SDave Cobbley
790eb8dc403SDave Cobbley                # Actual data to be logged
791eb8dc403SDave Cobbley                elif self.readsock.fileno() == event[0]:
792eb8dc403SDave Cobbley                    data = self.recv(1024)
793eb8dc403SDave Cobbley                    self.logfunc(data)
794eb8dc403SDave Cobbley
795eb8dc403SDave Cobbley    # Since the socket is non-blocking make sure to honor EAGAIN
796eb8dc403SDave Cobbley    # and EWOULDBLOCK.
797eb8dc403SDave Cobbley    def recv(self, count):
798eb8dc403SDave Cobbley        try:
799eb8dc403SDave Cobbley            data = self.readsock.recv(count)
800eb8dc403SDave Cobbley        except socket.error as e:
801eb8dc403SDave Cobbley            if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
802ac69b488SWilliam A. Kennington III                return b''
803eb8dc403SDave Cobbley            else:
804eb8dc403SDave Cobbley                raise
805eb8dc403SDave Cobbley
806eb8dc403SDave Cobbley        if data is None:
807eb8dc403SDave Cobbley            raise Exception("No data on read ready socket")
808eb8dc403SDave Cobbley        elif not data:
809eb8dc403SDave Cobbley            # This actually means an orderly shutdown
810eb8dc403SDave Cobbley            # happened. But for this code it counts as an
811eb8dc403SDave Cobbley            # error since the connection shouldn't go away
812eb8dc403SDave Cobbley            # until qemu exits.
813c926e17cSAndrew Geissler            if not self.canexit:
814eb8dc403SDave Cobbley                raise Exception("Console connection closed unexpectedly")
815ac69b488SWilliam A. Kennington III            return b''
816eb8dc403SDave Cobbley
817eb8dc403SDave Cobbley        return data
818eb8dc403SDave Cobbley
819eb8dc403SDave Cobbley    def stringify_event(self, event):
820eb8dc403SDave Cobbley        val = ''
821eb8dc403SDave Cobbley        if select.POLLERR == event:
822eb8dc403SDave Cobbley            val = 'POLLER'
823eb8dc403SDave Cobbley        elif select.POLLHUP == event:
824eb8dc403SDave Cobbley            val = 'POLLHUP'
825eb8dc403SDave Cobbley        elif select.POLLNVAL == event:
826eb8dc403SDave Cobbley            val = 'POLLNVAL'
827eb8dc403SDave Cobbley        return val
828eb8dc403SDave Cobbley
829eb8dc403SDave Cobbley    def close_socket(self, sock):
830eb8dc403SDave Cobbley        sock.shutdown(socket.SHUT_RDWR)
831eb8dc403SDave Cobbley        sock.close()
832eb8dc403SDave Cobbley
833eb8dc403SDave Cobbley    def close_ignore_error(self, fd):
834eb8dc403SDave Cobbley        try:
835eb8dc403SDave Cobbley            os.close(fd)
836eb8dc403SDave Cobbley        except OSError:
837eb8dc403SDave Cobbley            pass
838