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
2482c905dcSAndrew Geisslerfrom collections import defaultdict
25c926e17cSAndrew Geisslerimport importlib
26eb8dc403SDave Cobbley
27eb8dc403SDave Cobbley# Get Unicode non printable control chars
28eb8dc403SDave Cobbleycontrol_range = list(range(0,32))+list(range(127,160))
29eb8dc403SDave Cobbleycontrol_chars = [chr(x) for x in control_range
30eb8dc403SDave Cobbley                if chr(x) not in string.printable]
31eb8dc403SDave Cobbleyre_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
32eb8dc403SDave Cobbley
33eb8dc403SDave Cobbleyclass QemuRunner:
34eb8dc403SDave Cobbley
35*8f840685SAndrew Geissler    def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, use_kvm, logger, use_slirp=False,
36*8f840685SAndrew Geissler     serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None, tmpfsdir=None):
37eb8dc403SDave Cobbley
38eb8dc403SDave Cobbley        # Popen object for runqemu
39eb8dc403SDave Cobbley        self.runqemu = None
4082c905dcSAndrew Geissler        self.runqemu_exited = False
41eb8dc403SDave Cobbley        # pid of the qemu process that runqemu will start
42eb8dc403SDave Cobbley        self.qemupid = None
43eb8dc403SDave Cobbley        # target ip - from the command line or runqemu output
44eb8dc403SDave Cobbley        self.ip = None
45eb8dc403SDave Cobbley        # host ip - where qemu is running
46eb8dc403SDave Cobbley        self.server_ip = None
47eb8dc403SDave Cobbley        # target ip netmask
48eb8dc403SDave Cobbley        self.netmask = None
49eb8dc403SDave Cobbley
50eb8dc403SDave Cobbley        self.machine = machine
51eb8dc403SDave Cobbley        self.rootfs = rootfs
52eb8dc403SDave Cobbley        self.display = display
53eb8dc403SDave Cobbley        self.tmpdir = tmpdir
54eb8dc403SDave Cobbley        self.deploy_dir_image = deploy_dir_image
55eb8dc403SDave Cobbley        self.logfile = logfile
56eb8dc403SDave Cobbley        self.boottime = boottime
57eb8dc403SDave Cobbley        self.logged = False
58eb8dc403SDave Cobbley        self.thread = None
59eb8dc403SDave Cobbley        self.use_kvm = use_kvm
6082c905dcSAndrew Geissler        self.use_ovmf = use_ovmf
6119323693SBrad Bishop        self.use_slirp = use_slirp
6282c905dcSAndrew Geissler        self.serial_ports = serial_ports
63eb8dc403SDave Cobbley        self.msg = ''
6482c905dcSAndrew Geissler        self.boot_patterns = boot_patterns
653b8a17c1SAndrew Geissler        self.tmpfsdir = tmpfsdir
66eb8dc403SDave Cobbley
670903674eSAndrew Geissler        self.runqemutime = 300
68b7d28619SAndrew Geissler        if not workdir:
69b7d28619SAndrew Geissler            workdir = os.getcwd()
70b7d28619SAndrew Geissler        self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid())
7115ae2509SBrad Bishop        self.monitorpipe = None
72eb8dc403SDave Cobbley
73eb8dc403SDave Cobbley        self.logger = logger
74ac69b488SWilliam A. Kennington III        # Whether we're expecting an exit and should show related errors
75ac69b488SWilliam A. Kennington III        self.canexit = False
76eb8dc403SDave Cobbley
7782c905dcSAndrew Geissler        # Enable testing other OS's
7882c905dcSAndrew Geissler        # Set commands for target communication, and default to Linux ALWAYS
7982c905dcSAndrew Geissler        # Other OS's or baremetal applications need to provide their
8082c905dcSAndrew Geissler        # own implementation passing it through QemuRunner's constructor
8182c905dcSAndrew Geissler        # or by passing them through TESTIMAGE_BOOT_PATTERNS[flag]
8282c905dcSAndrew Geissler        # provided variables, where <flag> is one of the mentioned below.
8382c905dcSAndrew Geissler        accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished']
8482c905dcSAndrew Geissler        default_boot_patterns = defaultdict(str)
8582c905dcSAndrew Geissler        # Default to the usual paterns used to communicate with the target
8687f5cff0SAndrew Geissler        default_boot_patterns['search_reached_prompt'] = ' login:'
8782c905dcSAndrew Geissler        default_boot_patterns['send_login_user'] = 'root\n'
8882c905dcSAndrew Geissler        default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#"
8982c905dcSAndrew Geissler        default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
9082c905dcSAndrew Geissler
9182c905dcSAndrew Geissler        # Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n"
9282c905dcSAndrew Geissler        for pattern in accepted_patterns:
9382c905dcSAndrew Geissler            if not self.boot_patterns[pattern]:
9482c905dcSAndrew Geissler                self.boot_patterns[pattern] = default_boot_patterns[pattern]
9582c905dcSAndrew Geissler
96eb8dc403SDave Cobbley    def create_socket(self):
97eb8dc403SDave Cobbley        try:
98eb8dc403SDave Cobbley            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
99eb8dc403SDave Cobbley            sock.setblocking(0)
100eb8dc403SDave Cobbley            sock.bind(("127.0.0.1",0))
101eb8dc403SDave Cobbley            sock.listen(2)
102eb8dc403SDave Cobbley            port = sock.getsockname()[1]
103eb8dc403SDave Cobbley            self.logger.debug("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
104eb8dc403SDave Cobbley            return (sock, port)
105eb8dc403SDave Cobbley
106eb8dc403SDave Cobbley        except socket.error:
107eb8dc403SDave Cobbley            sock.close()
108eb8dc403SDave Cobbley            raise
109eb8dc403SDave Cobbley
11087f5cff0SAndrew Geissler    def decode_qemulog(self, todecode):
11187f5cff0SAndrew Geissler        # Sanitize the data received from qemu as it may contain control characters
11287f5cff0SAndrew Geissler        msg = todecode.decode("utf-8", errors='ignore')
11387f5cff0SAndrew Geissler        msg = re_control_char.sub('', msg)
11487f5cff0SAndrew Geissler        return msg
11587f5cff0SAndrew Geissler
116eb8dc403SDave Cobbley    def log(self, msg):
117eb8dc403SDave Cobbley        if self.logfile:
11887f5cff0SAndrew Geissler            msg = self.decode_qemulog(msg)
119eb8dc403SDave Cobbley            self.msg += msg
120eb8dc403SDave Cobbley            with codecs.open(self.logfile, "a", encoding="utf-8") as f:
121eb8dc403SDave Cobbley                f.write("%s" % msg)
122eb8dc403SDave Cobbley
123eb8dc403SDave Cobbley    def getOutput(self, o):
124eb8dc403SDave Cobbley        import fcntl
125eb8dc403SDave Cobbley        fl = fcntl.fcntl(o, fcntl.F_GETFL)
126eb8dc403SDave Cobbley        fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
127d159c7fbSAndrew Geissler        try:
128eb8dc403SDave Cobbley            return os.read(o.fileno(), 1000000).decode("utf-8")
129d159c7fbSAndrew Geissler        except BlockingIOError:
130d159c7fbSAndrew Geissler            return ""
131eb8dc403SDave Cobbley
132eb8dc403SDave Cobbley
133eb8dc403SDave Cobbley    def handleSIGCHLD(self, signum, frame):
134eb8dc403SDave Cobbley        if self.runqemu and self.runqemu.poll():
135eb8dc403SDave Cobbley            if self.runqemu.returncode:
13682c905dcSAndrew Geissler                self.logger.error('runqemu exited with code %d' % self.runqemu.returncode)
13782c905dcSAndrew Geissler                self.logger.error('Output from runqemu:\n%s' % self.getOutput(self.runqemu.stdout))
138eb8dc403SDave Cobbley                self.stop()
139eb8dc403SDave Cobbley
140eb8dc403SDave Cobbley    def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
141eb8dc403SDave Cobbley        env = os.environ.copy()
142eb8dc403SDave Cobbley        if self.display:
143eb8dc403SDave Cobbley            env["DISPLAY"] = self.display
144eb8dc403SDave Cobbley            # Set this flag so that Qemu doesn't do any grabs as SDL grabs
145eb8dc403SDave Cobbley            # interact badly with screensavers.
146eb8dc403SDave Cobbley            env["QEMU_DONT_GRAB"] = "1"
147eb8dc403SDave Cobbley        if not os.path.exists(self.rootfs):
148eb8dc403SDave Cobbley            self.logger.error("Invalid rootfs %s" % self.rootfs)
149eb8dc403SDave Cobbley            return False
150eb8dc403SDave Cobbley        if not os.path.exists(self.tmpdir):
151eb8dc403SDave Cobbley            self.logger.error("Invalid TMPDIR path %s" % self.tmpdir)
152eb8dc403SDave Cobbley            return False
153eb8dc403SDave Cobbley        else:
154eb8dc403SDave Cobbley            env["OE_TMPDIR"] = self.tmpdir
155eb8dc403SDave Cobbley        if not os.path.exists(self.deploy_dir_image):
156eb8dc403SDave Cobbley            self.logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
157eb8dc403SDave Cobbley            return False
158eb8dc403SDave Cobbley        else:
159eb8dc403SDave Cobbley            env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
160eb8dc403SDave Cobbley
1613b8a17c1SAndrew Geissler        if self.tmpfsdir:
1623b8a17c1SAndrew Geissler            env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
1633b8a17c1SAndrew Geissler
164eb8dc403SDave Cobbley        if not launch_cmd:
16508902b01SBrad Bishop            launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '')
166eb8dc403SDave Cobbley            if self.use_kvm:
167eb8dc403SDave Cobbley                self.logger.debug('Using kvm for runqemu')
168eb8dc403SDave Cobbley                launch_cmd += ' kvm'
169eb8dc403SDave Cobbley            else:
170eb8dc403SDave Cobbley                self.logger.debug('Not using kvm for runqemu')
171eb8dc403SDave Cobbley            if not self.display:
172eb8dc403SDave Cobbley                launch_cmd += ' nographic'
17319323693SBrad Bishop            if self.use_slirp:
17419323693SBrad Bishop                launch_cmd += ' slirp'
17582c905dcSAndrew Geissler            if self.use_ovmf:
17682c905dcSAndrew Geissler                launch_cmd += ' ovmf'
177517393d9SAndrew Geissler            launch_cmd += ' %s %s' % (runqemuparams, self.machine)
178517393d9SAndrew Geissler            if self.rootfs.endswith('.vmdk'):
179517393d9SAndrew Geissler                self.logger.debug('Bypassing VMDK rootfs for runqemu')
180517393d9SAndrew Geissler            else:
181517393d9SAndrew Geissler                launch_cmd += ' %s' % (self.rootfs)
182eb8dc403SDave Cobbley
183eb8dc403SDave Cobbley        return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
184eb8dc403SDave Cobbley
185eb8dc403SDave Cobbley    def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
186c926e17cSAndrew Geissler        # use logfile to determine the recipe-sysroot-native path and
187c926e17cSAndrew Geissler        # then add in the site-packages path components and add that
188b542dec1SPatrick Williams        # to the python sys.path so the qmp module can be found.
189c926e17cSAndrew Geissler        python_path = os.path.dirname(os.path.dirname(self.logfile))
190eff27476SAndrew Geissler        python_path += "/recipe-sysroot-native/usr/lib/qemu-python"
191c926e17cSAndrew Geissler        sys.path.append(python_path)
192c926e17cSAndrew Geissler        importlib.invalidate_caches()
193c926e17cSAndrew Geissler        try:
194c926e17cSAndrew Geissler            qmp = importlib.import_module("qmp")
19587f5cff0SAndrew Geissler        except Exception as e:
196b542dec1SPatrick Williams            self.logger.error("qemurunner: qmp module missing, please ensure it's installed in %s (%s)" % (python_path, str(e)))
197c926e17cSAndrew Geissler            return False
198c926e17cSAndrew Geissler        # Path relative to tmpdir used as cwd for qemu below to avoid unix socket path length issues
199c926e17cSAndrew Geissler        qmp_file = "." + next(tempfile._get_candidate_names())
200c926e17cSAndrew Geissler        qmp_param = ' -S -qmp unix:./%s,server,wait' % (qmp_file)
201c926e17cSAndrew Geissler        qmp_port = self.tmpdir + "/" + qmp_file
2020903674eSAndrew Geissler        # Create a second socket connection for debugging use,
2030903674eSAndrew Geissler        # note this will NOT cause qemu to block waiting for the connection
2040903674eSAndrew Geissler        qmp_file2 = "." + next(tempfile._get_candidate_names())
2050903674eSAndrew Geissler        qmp_param += ' -qmp unix:./%s,server,nowait' % (qmp_file2)
2060903674eSAndrew Geissler        qmp_port2 = self.tmpdir + "/" + qmp_file2
2070903674eSAndrew Geissler        self.logger.info("QMP Available for connection at %s" % (qmp_port2))
208c926e17cSAndrew Geissler
209eb8dc403SDave Cobbley        try:
21082c905dcSAndrew Geissler            if self.serial_ports >= 2:
211f86d0556SBrad Bishop                self.threadsock, threadport = self.create_socket()
212eb8dc403SDave Cobbley            self.server_socket, self.serverport = self.create_socket()
213eb8dc403SDave Cobbley        except socket.error as msg:
214eb8dc403SDave Cobbley            self.logger.error("Failed to create listening socket: %s" % msg[1])
215eb8dc403SDave Cobbley            return False
216eb8dc403SDave Cobbley
21795ac1b8dSAndrew Geissler        bootparams = ' printk.time=1'
218eb8dc403SDave Cobbley        if extra_bootparams:
219eb8dc403SDave Cobbley            bootparams = bootparams + ' ' + extra_bootparams
220eb8dc403SDave Cobbley
221eb8dc403SDave Cobbley        # Ask QEMU to store the QEMU process PID in file, this way we don't have to parse running processes
222eb8dc403SDave Cobbley        # and analyze descendents in order to determine it.
223eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
224eb8dc403SDave Cobbley            os.remove(self.qemu_pidfile)
225c926e17cSAndrew Geissler        self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
226c926e17cSAndrew Geissler
227eb8dc403SDave Cobbley        if qemuparams:
228eb8dc403SDave Cobbley            self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
229eb8dc403SDave Cobbley
23082c905dcSAndrew Geissler        if self.serial_ports >= 2:
23115ae2509SBrad Bishop            launch_cmd += ' tcpserial=%s:%s %s' % (threadport, self.serverport, self.qemuparams)
23282c905dcSAndrew Geissler        else:
23382c905dcSAndrew Geissler            launch_cmd += ' tcpserial=%s %s' % (self.serverport, self.qemuparams)
234eb8dc403SDave Cobbley
235eb8dc403SDave Cobbley        self.origchldhandler = signal.getsignal(signal.SIGCHLD)
236eb8dc403SDave Cobbley        signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
237eb8dc403SDave Cobbley
238eb8dc403SDave Cobbley        self.logger.debug('launchcmd=%s' % (launch_cmd))
239eb8dc403SDave Cobbley
240eb8dc403SDave Cobbley        # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
241eb8dc403SDave Cobbley        # blocking at the end of the runqemu script when using this within
242eb8dc403SDave Cobbley        # oe-selftest (this makes stty error out immediately). There ought
243eb8dc403SDave Cobbley        # to be a proper fix but this will suffice for now.
244c926e17cSAndrew 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)
245eb8dc403SDave Cobbley        output = self.runqemu.stdout
2465f35090dSAndrew Geissler        launch_time = time.time()
247eb8dc403SDave Cobbley
248eb8dc403SDave Cobbley        #
249eb8dc403SDave Cobbley        # We need the preexec_fn above so that all runqemu processes can easily be killed
250eb8dc403SDave Cobbley        # (by killing their process group). This presents a problem if this controlling
251eb8dc403SDave Cobbley        # process itself is killed however since those processes don't notice the death
252eb8dc403SDave Cobbley        # of the parent and merrily continue on.
253eb8dc403SDave Cobbley        #
254eb8dc403SDave Cobbley        # Rather than hack runqemu to deal with this, we add something here instead.
255eb8dc403SDave Cobbley        # Basically we fork off another process which holds an open pipe to the parent
256eb8dc403SDave Cobbley        # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
257eb8dc403SDave Cobbley        # the process group. This is like pctrl's PDEATHSIG but for a process group
258eb8dc403SDave Cobbley        # rather than a single process.
259eb8dc403SDave Cobbley        #
260eb8dc403SDave Cobbley        r, w = os.pipe()
261eb8dc403SDave Cobbley        self.monitorpid = os.fork()
262eb8dc403SDave Cobbley        if self.monitorpid:
263eb8dc403SDave Cobbley            os.close(r)
264eb8dc403SDave Cobbley            self.monitorpipe = os.fdopen(w, "w")
265eb8dc403SDave Cobbley        else:
266eb8dc403SDave Cobbley            # child process
267eb8dc403SDave Cobbley            os.setpgrp()
268eb8dc403SDave Cobbley            os.close(w)
269eb8dc403SDave Cobbley            r = os.fdopen(r)
270eb8dc403SDave Cobbley            x = r.read()
271eb8dc403SDave Cobbley            os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
27293c203f3SPatrick Williams            os._exit(0)
273eb8dc403SDave Cobbley
274eb8dc403SDave Cobbley        self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
2758e7b46e2SPatrick Williams        self.logger.debug("waiting at most %d seconds for qemu pid (%s)" %
276eb8dc403SDave Cobbley                          (self.runqemutime, time.strftime("%D %H:%M:%S")))
277eb8dc403SDave Cobbley        endtime = time.time() + self.runqemutime
278eb8dc403SDave Cobbley        while not self.is_alive() and time.time() < endtime:
279eb8dc403SDave Cobbley            if self.runqemu.poll():
28082c905dcSAndrew Geissler                if self.runqemu_exited:
281c926e17cSAndrew Geissler                    self.logger.warning("runqemu during is_alive() test")
28282c905dcSAndrew Geissler                    return False
283eb8dc403SDave Cobbley                if self.runqemu.returncode:
284eb8dc403SDave Cobbley                    # No point waiting any longer
28596ff1984SBrad Bishop                    self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
28696ff1984SBrad Bishop                    self.logger.warning("Output from runqemu:\n%s" % self.getOutput(output))
287f86d0556SBrad Bishop                    self.stop()
288eb8dc403SDave Cobbley                    return False
289eb8dc403SDave Cobbley            time.sleep(0.5)
290eb8dc403SDave Cobbley
29182c905dcSAndrew Geissler        if self.runqemu_exited:
292c926e17cSAndrew Geissler            self.logger.warning("runqemu after timeout")
29382c905dcSAndrew Geissler
294c926e17cSAndrew Geissler        if self.runqemu.returncode:
295c926e17cSAndrew Geissler            self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
296c926e17cSAndrew Geissler
297c926e17cSAndrew Geissler        if not self.is_alive():
2988e7b46e2SPatrick Williams            self.logger.error("Qemu pid didn't appear in %d seconds (%s)" %
299ac69b488SWilliam A. Kennington III                              (self.runqemutime, time.strftime("%D %H:%M:%S")))
300ac69b488SWilliam A. Kennington III
301ac69b488SWilliam A. Kennington III            qemu_pid = None
302ac69b488SWilliam A. Kennington III            if os.path.isfile(self.qemu_pidfile):
303ac69b488SWilliam A. Kennington III                with open(self.qemu_pidfile, 'r') as f:
304ac69b488SWilliam A. Kennington III                    qemu_pid = f.read().strip()
305ac69b488SWilliam A. Kennington III
306ac69b488SWilliam A. Kennington III            self.logger.error("Status information, poll status: %s, pidfile exists: %s, pidfile contents %s, proc pid exists %s"
307ac69b488SWilliam A. Kennington III                % (self.runqemu.poll(), os.path.isfile(self.qemu_pidfile), str(qemu_pid), os.path.exists("/proc/" + str(qemu_pid))))
308ac69b488SWilliam A. Kennington III
309ac69b488SWilliam A. Kennington III            # Dump all processes to help us to figure out what is going on...
310ac69b488SWilliam A. Kennington III            ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0]
311ac69b488SWilliam A. Kennington III            processes = ps.decode("utf-8")
312ac69b488SWilliam A. Kennington III            self.logger.debug("Running processes:\n%s" % processes)
313ac69b488SWilliam A. Kennington III            op = self.getOutput(output)
314ac69b488SWilliam A. Kennington III            self.stop()
315ac69b488SWilliam A. Kennington III            if op:
316ac69b488SWilliam A. Kennington III                self.logger.error("Output from runqemu:\n%s" % op)
317ac69b488SWilliam A. Kennington III            else:
318ac69b488SWilliam A. Kennington III                self.logger.error("No output from runqemu.\n")
319c926e17cSAndrew Geissler            return False
320c926e17cSAndrew Geissler
321c926e17cSAndrew Geissler        # Create the client socket for the QEMU Monitor Control Socket
322c926e17cSAndrew Geissler        # This will allow us to read status from Qemu if the the process
323c926e17cSAndrew Geissler        # is still alive
324c926e17cSAndrew Geissler        self.logger.debug("QMP Initializing to %s" % (qmp_port))
325c926e17cSAndrew Geissler        # chdir dance for path length issues with unix sockets
326c926e17cSAndrew Geissler        origpath = os.getcwd()
327c926e17cSAndrew Geissler        try:
328c926e17cSAndrew Geissler            os.chdir(os.path.dirname(qmp_port))
329c926e17cSAndrew Geissler            try:
33087f5cff0SAndrew Geissler                from qmp.legacy import QEMUMonitorProtocol
33187f5cff0SAndrew Geissler                self.qmp = QEMUMonitorProtocol(os.path.basename(qmp_port))
332c926e17cSAndrew Geissler            except OSError as msg:
333c926e17cSAndrew Geissler                self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
334c926e17cSAndrew Geissler                return False
335c926e17cSAndrew Geissler
336c926e17cSAndrew Geissler            self.logger.debug("QMP Connecting to %s" % (qmp_port))
337c926e17cSAndrew Geissler            if not os.path.exists(qmp_port) and self.is_alive():
338c926e17cSAndrew Geissler                self.logger.debug("QMP Port does not exist waiting for it to be created")
339c926e17cSAndrew Geissler                endtime = time.time() + self.runqemutime
340c926e17cSAndrew Geissler                while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
341c926e17cSAndrew Geissler                    self.logger.info("QMP port does not exist yet!")
342c926e17cSAndrew Geissler                    time.sleep(0.5)
343c926e17cSAndrew Geissler                if not os.path.exists(qmp_port) and self.is_alive():
344c926e17cSAndrew Geissler                    self.logger.warning("QMP Port still does not exist but QEMU is alive")
345c926e17cSAndrew Geissler                    return False
346c926e17cSAndrew Geissler
347c926e17cSAndrew Geissler            try:
3486aa7eec5SAndrew Geissler                # set timeout value for all QMP calls
3496aa7eec5SAndrew Geissler                self.qmp.settimeout(self.runqemutime)
350c926e17cSAndrew Geissler                self.qmp.connect()
3515f35090dSAndrew Geissler                connect_time = time.time()
3528e7b46e2SPatrick Williams                self.logger.info("QMP connected to QEMU at %s and took %.2f seconds" %
3535f35090dSAndrew Geissler                                  (time.strftime("%D %H:%M:%S"),
3545f35090dSAndrew Geissler                                   time.time() - launch_time))
355c926e17cSAndrew Geissler            except OSError as msg:
356c926e17cSAndrew Geissler                self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
357c926e17cSAndrew Geissler                return False
3587784c429SPatrick Williams            except qmp.legacy.QMPError as msg:
359c926e17cSAndrew Geissler                self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
360c926e17cSAndrew Geissler                return False
361c926e17cSAndrew Geissler        finally:
362c926e17cSAndrew Geissler            os.chdir(origpath)
363c926e17cSAndrew Geissler
3640903674eSAndrew Geissler        # We worry that mmap'd libraries may cause page faults which hang the qemu VM for periods
3650903674eSAndrew Geissler        # causing failures. Before we "start" qemu, read through it's mapped files to try and
3660903674eSAndrew Geissler        # ensure we don't hit page faults later
3670903674eSAndrew Geissler        mapdir = "/proc/" + str(self.qemupid) + "/map_files/"
3680903674eSAndrew Geissler        try:
3690903674eSAndrew Geissler            for f in os.listdir(mapdir):
3705f35090dSAndrew Geissler                try:
3710903674eSAndrew Geissler                    linktarget = os.readlink(os.path.join(mapdir, f))
3720903674eSAndrew Geissler                    if not linktarget.startswith("/") or linktarget.startswith("/dev") or "deleted" in linktarget:
3730903674eSAndrew Geissler                        continue
3740903674eSAndrew Geissler                    with open(linktarget, "rb") as readf:
3750903674eSAndrew Geissler                        data = True
3760903674eSAndrew Geissler                        while data:
3770903674eSAndrew Geissler                            data = readf.read(4096)
3785f35090dSAndrew Geissler                except FileNotFoundError:
3795f35090dSAndrew Geissler                    continue
3800903674eSAndrew Geissler        # Centos7 doesn't allow us to read /map_files/
3810903674eSAndrew Geissler        except PermissionError:
3820903674eSAndrew Geissler            pass
3830903674eSAndrew Geissler
3840903674eSAndrew Geissler        # Release the qemu process to continue running
385c926e17cSAndrew Geissler        self.run_monitor('cont')
3868e7b46e2SPatrick Williams        self.logger.info("QMP released QEMU at %s and took %.2f seconds from connect" %
3875f35090dSAndrew Geissler                          (time.strftime("%D %H:%M:%S"),
3885f35090dSAndrew Geissler                           time.time() - connect_time))
389c926e17cSAndrew Geissler
390eb8dc403SDave Cobbley        # We are alive: qemu is running
391eb8dc403SDave Cobbley        out = self.getOutput(output)
392eb8dc403SDave Cobbley        netconf = False # network configuration is not required by default
3938e7b46e2SPatrick Williams        self.logger.debug("qemu started in %.2f seconds - qemu procces pid is %s (%s)" %
394eb8dc403SDave Cobbley                          (time.time() - (endtime - self.runqemutime),
395eb8dc403SDave Cobbley                           self.qemupid, time.strftime("%D %H:%M:%S")))
396eb8dc403SDave Cobbley        cmdline = ''
39782c905dcSAndrew Geissler        if get_ip:
398eb8dc403SDave Cobbley            with open('/proc/%s/cmdline' % self.qemupid) as p:
399eb8dc403SDave Cobbley                cmdline = p.read()
400eb8dc403SDave Cobbley                # It is needed to sanitize the data received
401eb8dc403SDave Cobbley                # because is possible to have control characters
402eb8dc403SDave Cobbley                cmdline = re_control_char.sub(' ', cmdline)
403eb8dc403SDave Cobbley            try:
40419323693SBrad Bishop                if self.use_slirp:
405517393d9SAndrew Geissler                    tcp_ports = cmdline.split("hostfwd=tcp:")[1]
406517393d9SAndrew Geissler                    ip, tcp_ports = tcp_ports.split(":")[:2]
40719323693SBrad Bishop                    host_port = tcp_ports[:tcp_ports.find('-')]
408517393d9SAndrew Geissler                    self.ip = "%s:%s" % (ip, host_port)
40919323693SBrad Bishop                else:
410f86d0556SBrad Bishop                    ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
411eb8dc403SDave Cobbley                    self.ip = ips[0]
412eb8dc403SDave Cobbley                    self.server_ip = ips[1]
413eb8dc403SDave Cobbley                self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
414eb8dc403SDave Cobbley            except (IndexError, ValueError):
415eb8dc403SDave Cobbley                # Try to get network configuration from runqemu output
416595f6308SAndrew Geissler                match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+).*',
417eb8dc403SDave Cobbley                                 out, re.MULTILINE | re.DOTALL)
418eb8dc403SDave Cobbley                if match:
419eb8dc403SDave Cobbley                    self.ip, self.server_ip, self.netmask = match.groups()
420eb8dc403SDave Cobbley                    # network configuration is required as we couldn't get it
421eb8dc403SDave Cobbley                    # from the runqemu command line, so qemu doesn't run kernel
422eb8dc403SDave Cobbley                    # and guest networking is not configured
423eb8dc403SDave Cobbley                    netconf = True
424eb8dc403SDave Cobbley                else:
425eb8dc403SDave Cobbley                    self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
426eb8dc403SDave Cobbley                                 "Here is the qemu command line used:\n%s\n"
427eb8dc403SDave Cobbley                                 "and output from runqemu:\n%s" % (cmdline, out))
428eb8dc403SDave Cobbley                    self.stop()
429eb8dc403SDave Cobbley                    return False
430eb8dc403SDave Cobbley
431eb8dc403SDave Cobbley        self.logger.debug("Target IP: %s" % self.ip)
432eb8dc403SDave Cobbley        self.logger.debug("Server IP: %s" % self.server_ip)
433eb8dc403SDave Cobbley
43482c905dcSAndrew Geissler        if self.serial_ports >= 2:
435f86d0556SBrad Bishop            self.thread = LoggingThread(self.log, self.threadsock, self.logger)
436eb8dc403SDave Cobbley            self.thread.start()
437eb8dc403SDave Cobbley            if not self.thread.connection_established.wait(self.boottime):
438eb8dc403SDave Cobbley                self.logger.error("Didn't receive a console connection from qemu. "
439eb8dc403SDave Cobbley                             "Here is the qemu command line used:\n%s\nand "
440eb8dc403SDave Cobbley                             "output from runqemu:\n%s" % (cmdline, out))
441eb8dc403SDave Cobbley                self.stop_thread()
442eb8dc403SDave Cobbley                return False
443eb8dc403SDave Cobbley
444eb8dc403SDave Cobbley        self.logger.debug("Output from runqemu:\n%s", out)
445eb8dc403SDave Cobbley        self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
446eb8dc403SDave Cobbley                          (self.boottime, time.strftime("%D %H:%M:%S")))
447eb8dc403SDave Cobbley        endtime = time.time() + self.boottime
448e760df85SPatrick Williams        filelist = [self.server_socket, self.runqemu.stdout]
449eb8dc403SDave Cobbley        reachedlogin = False
450eb8dc403SDave Cobbley        stopread = False
451eb8dc403SDave Cobbley        qemusock = None
452eb8dc403SDave Cobbley        bootlog = b''
453eb8dc403SDave Cobbley        data = b''
454eb8dc403SDave Cobbley        while time.time() < endtime and not stopread:
455eb8dc403SDave Cobbley            try:
456e760df85SPatrick Williams                sread, swrite, serror = select.select(filelist, [], [], 5)
457eb8dc403SDave Cobbley            except InterruptedError:
458eb8dc403SDave Cobbley                continue
459e760df85SPatrick Williams            for file in sread:
460e760df85SPatrick Williams                if file is self.server_socket:
461eb8dc403SDave Cobbley                    qemusock, addr = self.server_socket.accept()
462e760df85SPatrick Williams                    qemusock.setblocking(False)
463e760df85SPatrick Williams                    filelist.append(qemusock)
464e760df85SPatrick Williams                    filelist.remove(self.server_socket)
465eb8dc403SDave Cobbley                    self.logger.debug("Connection from %s:%s" % addr)
466eb8dc403SDave Cobbley                else:
4676aa7eec5SAndrew Geissler                    # try to avoid reading only a single character at a time
4686aa7eec5SAndrew Geissler                    time.sleep(0.1)
469e760df85SPatrick Williams                    if hasattr(file, 'read'):
470e760df85SPatrick Williams                        read = file.read(1024)
471e760df85SPatrick Williams                    elif hasattr(file, 'recv'):
472e760df85SPatrick Williams                        read = file.recv(1024)
473e760df85SPatrick Williams                    else:
474e760df85SPatrick Williams                        self.logger.error('Invalid file type: %s\n%s' % (file))
475e760df85SPatrick Williams                        read = b''
476e760df85SPatrick Williams
477e760df85SPatrick Williams                    self.logger.debug2('Partial boot log:\n%s' % (read.decode('utf-8', errors='ignore')))
478e760df85SPatrick Williams                    data = data + read
479eb8dc403SDave Cobbley                    if data:
480eb8dc403SDave Cobbley                        bootlog += data
48182c905dcSAndrew Geissler                        if self.serial_ports < 2:
482e760df85SPatrick Williams                            # this file has mixed console/kernel data, log it to logfile
48382c905dcSAndrew Geissler                            self.log(data)
48482c905dcSAndrew Geissler
485eb8dc403SDave Cobbley                        data = b''
48687f5cff0SAndrew Geissler
48787f5cff0SAndrew Geissler                        decodedlog = self.decode_qemulog(bootlog)
48887f5cff0SAndrew Geissler                        if self.boot_patterns['search_reached_prompt'] in decodedlog:
489e760df85SPatrick Williams                            self.server_socket.close()
490eb8dc403SDave Cobbley                            self.server_socket = qemusock
491eb8dc403SDave Cobbley                            stopread = True
492eb8dc403SDave Cobbley                            reachedlogin = True
4938e7b46e2SPatrick Williams                            self.logger.debug("Reached login banner in %.2f seconds (%s)" %
494eb8dc403SDave Cobbley                                              (time.time() - (endtime - self.boottime),
4958e7b46e2SPatrick Williams                                              time.strftime("%D %H:%M:%S")))
496eb8dc403SDave Cobbley                    else:
497eb8dc403SDave Cobbley                        # no need to check if reachedlogin unless we support multiple connections
498eb8dc403SDave Cobbley                        self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
499eb8dc403SDave Cobbley                                          time.strftime("%D %H:%M:%S"))
500e760df85SPatrick Williams                        filelist.remove(file)
501e760df85SPatrick Williams                        file.close()
502eb8dc403SDave Cobbley                        stopread = True
503eb8dc403SDave Cobbley
504eb8dc403SDave Cobbley        if not reachedlogin:
505eb8dc403SDave Cobbley            if time.time() >= endtime:
50696ff1984SBrad Bishop                self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
507eb8dc403SDave Cobbley                                  (self.boottime, time.strftime("%D %H:%M:%S")))
508eb8dc403SDave Cobbley            tail = lambda l: "\n".join(l.splitlines()[-25:])
50987f5cff0SAndrew Geissler            bootlog = self.decode_qemulog(bootlog)
510eb8dc403SDave Cobbley            # in case bootlog is empty, use tail qemu log store at self.msg
511eb8dc403SDave Cobbley            lines = tail(bootlog if bootlog else self.msg)
51287f5cff0SAndrew Geissler            self.logger.warning("Last 25 lines of text (%d):\n%s" % (len(bootlog), lines))
51396ff1984SBrad Bishop            self.logger.warning("Check full boot log: %s" % self.logfile)
514eb8dc403SDave Cobbley            self.stop()
515eb8dc403SDave Cobbley            return False
516eb8dc403SDave Cobbley
517eb8dc403SDave Cobbley        # If we are not able to login the tests can continue
518eb8dc403SDave Cobbley        try:
519c3d88e4dSAndrew Geissler            (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120)
52082c905dcSAndrew Geissler            if re.search(self.boot_patterns['search_login_succeeded'], output):
521eb8dc403SDave Cobbley                self.logged = True
5228e7b46e2SPatrick Williams                self.logger.debug("Logged in as %s in serial console" % self.boot_patterns['send_login_user'].replace("\n", ""))
523eb8dc403SDave Cobbley                if netconf:
524eb8dc403SDave Cobbley                    # configure guest networking
525eb8dc403SDave Cobbley                    cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
526eb8dc403SDave Cobbley                    output = self.run_serial(cmd, raw=True)[1]
527f86d0556SBrad Bishop                    if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
528eb8dc403SDave Cobbley                        self.logger.debug("configured ip address %s", self.ip)
529eb8dc403SDave Cobbley                    else:
530eb8dc403SDave Cobbley                        self.logger.debug("Couldn't configure guest networking")
531eb8dc403SDave Cobbley            else:
53296ff1984SBrad Bishop                self.logger.warning("Couldn't login into serial console"
5338e7b46e2SPatrick Williams                            " as %s using blank password" % self.boot_patterns['send_login_user'].replace("\n", ""))
53496ff1984SBrad Bishop                self.logger.warning("The output:\n%s" % output)
535eb8dc403SDave Cobbley        except:
53696ff1984SBrad Bishop            self.logger.warning("Serial console failed while trying to login")
537eb8dc403SDave Cobbley        return True
538eb8dc403SDave Cobbley
539eb8dc403SDave Cobbley    def stop(self):
540eb8dc403SDave Cobbley        if hasattr(self, "origchldhandler"):
541eb8dc403SDave Cobbley            signal.signal(signal.SIGCHLD, self.origchldhandler)
5421a4b7ee2SBrad Bishop        self.stop_thread()
5431a4b7ee2SBrad Bishop        self.stop_qemu_system()
544eb8dc403SDave Cobbley        if self.runqemu:
545eb8dc403SDave Cobbley            if hasattr(self, "monitorpid"):
546eb8dc403SDave Cobbley                os.kill(self.monitorpid, signal.SIGKILL)
547eb8dc403SDave Cobbley                self.logger.debug("Sending SIGTERM to runqemu")
548eb8dc403SDave Cobbley                try:
549eb8dc403SDave Cobbley                    os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
550eb8dc403SDave Cobbley                except OSError as e:
551eb8dc403SDave Cobbley                    if e.errno != errno.ESRCH:
552eb8dc403SDave Cobbley                        raise
553864cc43bSPatrick Williams            try:
554864cc43bSPatrick Williams                outs, errs = self.runqemu.communicate(timeout=self.runqemutime)
555864cc43bSPatrick Williams                if outs:
556864cc43bSPatrick Williams                    self.logger.info("Output from runqemu:\n%s", outs.decode("utf-8"))
557864cc43bSPatrick Williams                if errs:
558864cc43bSPatrick Williams                    self.logger.info("Stderr from runqemu:\n%s", errs.decode("utf-8"))
5598e7b46e2SPatrick Williams            except subprocess.TimeoutExpired:
560eb8dc403SDave Cobbley                self.logger.debug("Sending SIGKILL to runqemu")
561eb8dc403SDave Cobbley                os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
562d159c7fbSAndrew Geissler            if not self.runqemu.stdout.closed:
563d159c7fbSAndrew Geissler                self.logger.info("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
564f86d0556SBrad Bishop            self.runqemu.stdin.close()
565f86d0556SBrad Bishop            self.runqemu.stdout.close()
56682c905dcSAndrew Geissler            self.runqemu_exited = True
567f86d0556SBrad Bishop
568c926e17cSAndrew Geissler        if hasattr(self, 'qmp') and self.qmp:
569c926e17cSAndrew Geissler            self.qmp.close()
570c926e17cSAndrew Geissler            self.qmp = None
571eb8dc403SDave Cobbley        if hasattr(self, 'server_socket') and self.server_socket:
572eb8dc403SDave Cobbley            self.server_socket.close()
573eb8dc403SDave Cobbley            self.server_socket = None
574f86d0556SBrad Bishop        if hasattr(self, 'threadsock') and self.threadsock:
575f86d0556SBrad Bishop            self.threadsock.close()
576f86d0556SBrad Bishop            self.threadsock = None
577eb8dc403SDave Cobbley        self.qemupid = None
578eb8dc403SDave Cobbley        self.ip = None
579eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
58082c905dcSAndrew Geissler            try:
581eb8dc403SDave Cobbley                os.remove(self.qemu_pidfile)
58282c905dcSAndrew Geissler            except FileNotFoundError as e:
58382c905dcSAndrew Geissler                # We raced, ignore
58482c905dcSAndrew Geissler                pass
585f86d0556SBrad Bishop        if self.monitorpipe:
586f86d0556SBrad Bishop            self.monitorpipe.close()
587eb8dc403SDave Cobbley
588eb8dc403SDave Cobbley    def stop_qemu_system(self):
589eb8dc403SDave Cobbley        if self.qemupid:
590eb8dc403SDave Cobbley            try:
591eb8dc403SDave Cobbley                # qemu-system behaves well and a SIGTERM is enough
592eb8dc403SDave Cobbley                os.kill(self.qemupid, signal.SIGTERM)
593eb8dc403SDave Cobbley            except ProcessLookupError as e:
5941a4b7ee2SBrad Bishop                self.logger.warning('qemu-system ended unexpectedly')
595eb8dc403SDave Cobbley
596eb8dc403SDave Cobbley    def stop_thread(self):
597eb8dc403SDave Cobbley        if self.thread and self.thread.is_alive():
598eb8dc403SDave Cobbley            self.thread.stop()
599eb8dc403SDave Cobbley            self.thread.join()
600eb8dc403SDave Cobbley
601c926e17cSAndrew Geissler    def allowexit(self):
602ac69b488SWilliam A. Kennington III        self.canexit = True
603c926e17cSAndrew Geissler        if self.thread:
604c926e17cSAndrew Geissler            self.thread.allowexit()
605c926e17cSAndrew Geissler
606eb8dc403SDave Cobbley    def restart(self, qemuparams = None):
60796ff1984SBrad Bishop        self.logger.warning("Restarting qemu process")
608eb8dc403SDave Cobbley        if self.runqemu.poll() is None:
609eb8dc403SDave Cobbley            self.stop()
610eb8dc403SDave Cobbley        if self.start(qemuparams):
611eb8dc403SDave Cobbley            return True
612eb8dc403SDave Cobbley        return False
613eb8dc403SDave Cobbley
614eb8dc403SDave Cobbley    def is_alive(self):
61582c905dcSAndrew Geissler        if not self.runqemu or self.runqemu.poll() is not None or self.runqemu_exited:
616eb8dc403SDave Cobbley            return False
617eb8dc403SDave Cobbley        if os.path.isfile(self.qemu_pidfile):
61896ff1984SBrad Bishop            # when handling pidfile, qemu creates the file, stat it, lock it and then write to it
61996ff1984SBrad Bishop            # so it's possible that the file has been created but the content is empty
62096ff1984SBrad Bishop            pidfile_timeout = time.time() + 3
62196ff1984SBrad Bishop            while time.time() < pidfile_timeout:
62296ff1984SBrad Bishop                with open(self.qemu_pidfile, 'r') as f:
62396ff1984SBrad Bishop                    qemu_pid = f.read().strip()
62496ff1984SBrad Bishop                # file created but not yet written contents
62596ff1984SBrad Bishop                if not qemu_pid:
62696ff1984SBrad Bishop                    time.sleep(0.5)
62796ff1984SBrad Bishop                    continue
62896ff1984SBrad Bishop                else:
62996ff1984SBrad Bishop                    if os.path.exists("/proc/" + qemu_pid):
63096ff1984SBrad Bishop                        self.qemupid = int(qemu_pid)
631eb8dc403SDave Cobbley                        return True
632eb8dc403SDave Cobbley        return False
633eb8dc403SDave Cobbley
6345f35090dSAndrew Geissler    def run_monitor(self, command, args=None, timeout=60):
6355f35090dSAndrew Geissler        if hasattr(self, 'qmp') and self.qmp:
6366aa7eec5SAndrew Geissler            self.qmp.settimeout(timeout)
6375f35090dSAndrew Geissler            if args is not None:
6385f35090dSAndrew Geissler                return self.qmp.cmd(command, args)
6395f35090dSAndrew Geissler            else:
640c926e17cSAndrew Geissler                return self.qmp.cmd(command)
641c926e17cSAndrew Geissler
642977dc1acSBrad Bishop    def run_serial(self, command, raw=False, timeout=60):
64392b42cb3SPatrick Williams        # Returns (status, output) where status is 1 on success and 0 on error
64492b42cb3SPatrick Williams
645eb8dc403SDave Cobbley        # We assume target system have echo to get command status
646eb8dc403SDave Cobbley        if not raw:
647eb8dc403SDave Cobbley            command = "%s; echo $?\n" % command
648eb8dc403SDave Cobbley
649eb8dc403SDave Cobbley        data = ''
650eb8dc403SDave Cobbley        status = 0
651eb8dc403SDave Cobbley        self.server_socket.sendall(command.encode('utf-8'))
652eb8dc403SDave Cobbley        start = time.time()
653eb8dc403SDave Cobbley        end = start + timeout
654eb8dc403SDave Cobbley        while True:
655eb8dc403SDave Cobbley            now = time.time()
656eb8dc403SDave Cobbley            if now >= end:
657eb8dc403SDave Cobbley                data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
658eb8dc403SDave Cobbley                break
659eb8dc403SDave Cobbley            try:
660eb8dc403SDave Cobbley                sread, _, _ = select.select([self.server_socket],[],[], end - now)
661eb8dc403SDave Cobbley            except InterruptedError:
662eb8dc403SDave Cobbley                continue
663eb8dc403SDave Cobbley            if sread:
6646aa7eec5SAndrew Geissler                # try to avoid reading single character at a time
6656aa7eec5SAndrew Geissler                time.sleep(0.1)
666eb8dc403SDave Cobbley                answer = self.server_socket.recv(1024)
667eb8dc403SDave Cobbley                if answer:
668eb8dc403SDave Cobbley                    data += answer.decode('utf-8')
669eb8dc403SDave Cobbley                    # Search the prompt to stop
67082c905dcSAndrew Geissler                    if re.search(self.boot_patterns['search_cmd_finished'], data):
671eb8dc403SDave Cobbley                        break
672eb8dc403SDave Cobbley                else:
673ac69b488SWilliam A. Kennington III                    if self.canexit:
674ac69b488SWilliam A. Kennington III                        return (1, "")
675ac69b488SWilliam A. Kennington III                    raise Exception("No data on serial console socket, connection closed?")
676eb8dc403SDave Cobbley
677eb8dc403SDave Cobbley        if data:
678eb8dc403SDave Cobbley            if raw:
679eb8dc403SDave Cobbley                status = 1
680eb8dc403SDave Cobbley            else:
681eb8dc403SDave Cobbley                # Remove first line (command line) and last line (prompt)
682eb8dc403SDave Cobbley                data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
683eb8dc403SDave Cobbley                index = data.rfind('\r\n')
684eb8dc403SDave Cobbley                if index == -1:
685eb8dc403SDave Cobbley                    status_cmd = data
686eb8dc403SDave Cobbley                    data = ""
687eb8dc403SDave Cobbley                else:
688eb8dc403SDave Cobbley                    status_cmd = data[index+2:]
689eb8dc403SDave Cobbley                    data = data[:index]
690eb8dc403SDave Cobbley                if (status_cmd == "0"):
691eb8dc403SDave Cobbley                    status = 1
692eb8dc403SDave Cobbley        return (status, str(data))
693eb8dc403SDave Cobbley
694eb8dc403SDave Cobbley# This class is for reading data from a socket and passing it to logfunc
695eb8dc403SDave Cobbley# to be processed. It's completely event driven and has a straightforward
696eb8dc403SDave Cobbley# event loop. The mechanism for stopping the thread is a simple pipe which
697eb8dc403SDave Cobbley# will wake up the poll and allow for tearing everything down.
698eb8dc403SDave Cobbleyclass LoggingThread(threading.Thread):
699eb8dc403SDave Cobbley    def __init__(self, logfunc, sock, logger):
700eb8dc403SDave Cobbley        self.connection_established = threading.Event()
701eb8dc403SDave Cobbley        self.serversock = sock
702eb8dc403SDave Cobbley        self.logfunc = logfunc
703eb8dc403SDave Cobbley        self.logger = logger
704eb8dc403SDave Cobbley        self.readsock = None
705eb8dc403SDave Cobbley        self.running = False
706c926e17cSAndrew Geissler        self.canexit = False
707eb8dc403SDave Cobbley
708eb8dc403SDave Cobbley        self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
709eb8dc403SDave Cobbley        self.readevents = select.POLLIN | select.POLLPRI
710eb8dc403SDave Cobbley
711eb8dc403SDave Cobbley        threading.Thread.__init__(self, target=self.threadtarget)
712eb8dc403SDave Cobbley
713eb8dc403SDave Cobbley    def threadtarget(self):
714eb8dc403SDave Cobbley        try:
715eb8dc403SDave Cobbley            self.eventloop()
716eb8dc403SDave Cobbley        finally:
717eb8dc403SDave Cobbley            self.teardown()
718eb8dc403SDave Cobbley
719eb8dc403SDave Cobbley    def run(self):
720eb8dc403SDave Cobbley        self.logger.debug("Starting logging thread")
721eb8dc403SDave Cobbley        self.readpipe, self.writepipe = os.pipe()
722eb8dc403SDave Cobbley        threading.Thread.run(self)
723eb8dc403SDave Cobbley
724eb8dc403SDave Cobbley    def stop(self):
725eb8dc403SDave Cobbley        self.logger.debug("Stopping logging thread")
726eb8dc403SDave Cobbley        if self.running:
727eb8dc403SDave Cobbley            os.write(self.writepipe, bytes("stop", "utf-8"))
728eb8dc403SDave Cobbley
729eb8dc403SDave Cobbley    def teardown(self):
730eb8dc403SDave Cobbley        self.logger.debug("Tearing down logging thread")
731eb8dc403SDave Cobbley        self.close_socket(self.serversock)
732eb8dc403SDave Cobbley
733eb8dc403SDave Cobbley        if self.readsock is not None:
734eb8dc403SDave Cobbley            self.close_socket(self.readsock)
735eb8dc403SDave Cobbley
736eb8dc403SDave Cobbley        self.close_ignore_error(self.readpipe)
737eb8dc403SDave Cobbley        self.close_ignore_error(self.writepipe)
738eb8dc403SDave Cobbley        self.running = False
739eb8dc403SDave Cobbley
740c926e17cSAndrew Geissler    def allowexit(self):
741c926e17cSAndrew Geissler        self.canexit = True
742c926e17cSAndrew Geissler
743eb8dc403SDave Cobbley    def eventloop(self):
744eb8dc403SDave Cobbley        poll = select.poll()
745eb8dc403SDave Cobbley        event_read_mask = self.errorevents | self.readevents
746eb8dc403SDave Cobbley        poll.register(self.serversock.fileno())
747eb8dc403SDave Cobbley        poll.register(self.readpipe, event_read_mask)
748eb8dc403SDave Cobbley
749eb8dc403SDave Cobbley        breakout = False
750eb8dc403SDave Cobbley        self.running = True
751eb8dc403SDave Cobbley        self.logger.debug("Starting thread event loop")
752eb8dc403SDave Cobbley        while not breakout:
753eb8dc403SDave Cobbley            events = poll.poll()
754eb8dc403SDave Cobbley            for event in events:
755eb8dc403SDave Cobbley                # An error occurred, bail out
756eb8dc403SDave Cobbley                if event[1] & self.errorevents:
757eb8dc403SDave Cobbley                    raise Exception(self.stringify_event(event[1]))
758eb8dc403SDave Cobbley
759eb8dc403SDave Cobbley                # Event to stop the thread
760eb8dc403SDave Cobbley                if self.readpipe == event[0]:
761eb8dc403SDave Cobbley                    self.logger.debug("Stop event received")
762eb8dc403SDave Cobbley                    breakout = True
763eb8dc403SDave Cobbley                    break
764eb8dc403SDave Cobbley
765eb8dc403SDave Cobbley                # A connection request was received
766eb8dc403SDave Cobbley                elif self.serversock.fileno() == event[0]:
767eb8dc403SDave Cobbley                    self.logger.debug("Connection request received")
768eb8dc403SDave Cobbley                    self.readsock, _ = self.serversock.accept()
769eb8dc403SDave Cobbley                    self.readsock.setblocking(0)
770eb8dc403SDave Cobbley                    poll.unregister(self.serversock.fileno())
771eb8dc403SDave Cobbley                    poll.register(self.readsock.fileno(), event_read_mask)
772eb8dc403SDave Cobbley
773eb8dc403SDave Cobbley                    self.logger.debug("Setting connection established event")
774eb8dc403SDave Cobbley                    self.connection_established.set()
775eb8dc403SDave Cobbley
776eb8dc403SDave Cobbley                # Actual data to be logged
777eb8dc403SDave Cobbley                elif self.readsock.fileno() == event[0]:
778eb8dc403SDave Cobbley                    data = self.recv(1024)
779eb8dc403SDave Cobbley                    self.logfunc(data)
780eb8dc403SDave Cobbley
781eb8dc403SDave Cobbley    # Since the socket is non-blocking make sure to honor EAGAIN
782eb8dc403SDave Cobbley    # and EWOULDBLOCK.
783eb8dc403SDave Cobbley    def recv(self, count):
784eb8dc403SDave Cobbley        try:
785eb8dc403SDave Cobbley            data = self.readsock.recv(count)
786eb8dc403SDave Cobbley        except socket.error as e:
787eb8dc403SDave Cobbley            if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
788ac69b488SWilliam A. Kennington III                return b''
789eb8dc403SDave Cobbley            else:
790eb8dc403SDave Cobbley                raise
791eb8dc403SDave Cobbley
792eb8dc403SDave Cobbley        if data is None:
793eb8dc403SDave Cobbley            raise Exception("No data on read ready socket")
794eb8dc403SDave Cobbley        elif not data:
795eb8dc403SDave Cobbley            # This actually means an orderly shutdown
796eb8dc403SDave Cobbley            # happened. But for this code it counts as an
797eb8dc403SDave Cobbley            # error since the connection shouldn't go away
798eb8dc403SDave Cobbley            # until qemu exits.
799c926e17cSAndrew Geissler            if not self.canexit:
800eb8dc403SDave Cobbley                raise Exception("Console connection closed unexpectedly")
801ac69b488SWilliam A. Kennington III            return b''
802eb8dc403SDave Cobbley
803eb8dc403SDave Cobbley        return data
804eb8dc403SDave Cobbley
805eb8dc403SDave Cobbley    def stringify_event(self, event):
806eb8dc403SDave Cobbley        val = ''
807eb8dc403SDave Cobbley        if select.POLLERR == event:
808eb8dc403SDave Cobbley            val = 'POLLER'
809eb8dc403SDave Cobbley        elif select.POLLHUP == event:
810eb8dc403SDave Cobbley            val = 'POLLHUP'
811eb8dc403SDave Cobbley        elif select.POLLNVAL == event:
812eb8dc403SDave Cobbley            val = 'POLLNVAL'
813eb8dc403SDave Cobbley        return val
814eb8dc403SDave Cobbley
815eb8dc403SDave Cobbley    def close_socket(self, sock):
816eb8dc403SDave Cobbley        sock.shutdown(socket.SHUT_RDWR)
817eb8dc403SDave Cobbley        sock.close()
818eb8dc403SDave Cobbley
819eb8dc403SDave Cobbley    def close_ignore_error(self, fd):
820eb8dc403SDave Cobbley        try:
821eb8dc403SDave Cobbley            os.close(fd)
822eb8dc403SDave Cobbley        except OSError:
823eb8dc403SDave Cobbley            pass
824