1#
2# Copyright (C) 2013 Intel Corporation
3#
4# SPDX-License-Identifier: MIT
5#
6
7# This module provides a class for starting qemu images using runqemu.
8# It's used by testimage.bbclass.
9
10import subprocess
11import os
12import sys
13import time
14import signal
15import re
16import socket
17import select
18import errno
19import string
20import threading
21import codecs
22import logging
23from oeqa.utils.dump import HostDumper
24
25# Get Unicode non printable control chars
26control_range = list(range(0,32))+list(range(127,160))
27control_chars = [chr(x) for x in control_range
28                if chr(x) not in string.printable]
29re_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
30
31class QemuRunner:
32
33    def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds,
34                 use_kvm, logger, use_slirp=False):
35
36        # Popen object for runqemu
37        self.runqemu = None
38        # pid of the qemu process that runqemu will start
39        self.qemupid = None
40        # target ip - from the command line or runqemu output
41        self.ip = None
42        # host ip - where qemu is running
43        self.server_ip = None
44        # target ip netmask
45        self.netmask = None
46
47        self.machine = machine
48        self.rootfs = rootfs
49        self.display = display
50        self.tmpdir = tmpdir
51        self.deploy_dir_image = deploy_dir_image
52        self.logfile = logfile
53        self.boottime = boottime
54        self.logged = False
55        self.thread = None
56        self.use_kvm = use_kvm
57        self.use_slirp = use_slirp
58        self.msg = ''
59
60        self.runqemutime = 120
61        self.qemu_pidfile = 'pidfile_'+str(os.getpid())
62        self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
63        self.monitorpipe = None
64
65        self.logger = logger
66
67    def create_socket(self):
68        try:
69            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
70            sock.setblocking(0)
71            sock.bind(("127.0.0.1",0))
72            sock.listen(2)
73            port = sock.getsockname()[1]
74            self.logger.debug("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
75            return (sock, port)
76
77        except socket.error:
78            sock.close()
79            raise
80
81    def log(self, msg):
82        if self.logfile:
83            # It is needed to sanitize the data received from qemu
84            # because is possible to have control characters
85            msg = msg.decode("utf-8", errors='ignore')
86            msg = re_control_char.sub('', msg)
87            self.msg += msg
88            with codecs.open(self.logfile, "a", encoding="utf-8") as f:
89                f.write("%s" % msg)
90
91    def getOutput(self, o):
92        import fcntl
93        fl = fcntl.fcntl(o, fcntl.F_GETFL)
94        fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
95        return os.read(o.fileno(), 1000000).decode("utf-8")
96
97
98    def handleSIGCHLD(self, signum, frame):
99        if self.runqemu and self.runqemu.poll():
100            if self.runqemu.returncode:
101                self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
102                self.logger.debug("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
103                self.stop()
104                self._dump_host()
105                raise SystemExit
106
107    def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
108        env = os.environ.copy()
109        if self.display:
110            env["DISPLAY"] = self.display
111            # Set this flag so that Qemu doesn't do any grabs as SDL grabs
112            # interact badly with screensavers.
113            env["QEMU_DONT_GRAB"] = "1"
114        if not os.path.exists(self.rootfs):
115            self.logger.error("Invalid rootfs %s" % self.rootfs)
116            return False
117        if not os.path.exists(self.tmpdir):
118            self.logger.error("Invalid TMPDIR path %s" % self.tmpdir)
119            return False
120        else:
121            env["OE_TMPDIR"] = self.tmpdir
122        if not os.path.exists(self.deploy_dir_image):
123            self.logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
124            return False
125        else:
126            env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
127
128        if not launch_cmd:
129            launch_cmd = 'runqemu %s %s ' % ('snapshot' if discard_writes else '', runqemuparams)
130            if self.use_kvm:
131                self.logger.debug('Using kvm for runqemu')
132                launch_cmd += ' kvm'
133            else:
134                self.logger.debug('Not using kvm for runqemu')
135            if not self.display:
136                launch_cmd += ' nographic'
137            if self.use_slirp:
138                launch_cmd += ' slirp'
139            launch_cmd += ' %s %s' % (self.machine, self.rootfs)
140
141        return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
142
143    def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
144        try:
145            self.threadsock, threadport = self.create_socket()
146            self.server_socket, self.serverport = self.create_socket()
147        except socket.error as msg:
148            self.logger.error("Failed to create listening socket: %s" % msg[1])
149            return False
150
151        bootparams = 'console=tty1 console=ttyS0,115200n8 printk.time=1'
152        if extra_bootparams:
153            bootparams = bootparams + ' ' + extra_bootparams
154
155        # Ask QEMU to store the QEMU process PID in file, this way we don't have to parse running processes
156        # and analyze descendents in order to determine it.
157        if os.path.exists(self.qemu_pidfile):
158            os.remove(self.qemu_pidfile)
159        self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile)
160        if qemuparams:
161            self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
162
163        launch_cmd += ' tcpserial=%s:%s %s' % (threadport, self.serverport, self.qemuparams)
164
165        self.origchldhandler = signal.getsignal(signal.SIGCHLD)
166        signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
167
168        self.logger.debug('launchcmd=%s'%(launch_cmd))
169
170        # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
171        # blocking at the end of the runqemu script when using this within
172        # oe-selftest (this makes stty error out immediately). There ought
173        # to be a proper fix but this will suffice for now.
174        self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp, env=env)
175        output = self.runqemu.stdout
176
177        #
178        # We need the preexec_fn above so that all runqemu processes can easily be killed
179        # (by killing their process group). This presents a problem if this controlling
180        # process itself is killed however since those processes don't notice the death
181        # of the parent and merrily continue on.
182        #
183        # Rather than hack runqemu to deal with this, we add something here instead.
184        # Basically we fork off another process which holds an open pipe to the parent
185        # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
186        # the process group. This is like pctrl's PDEATHSIG but for a process group
187        # rather than a single process.
188        #
189        r, w = os.pipe()
190        self.monitorpid = os.fork()
191        if self.monitorpid:
192            os.close(r)
193            self.monitorpipe = os.fdopen(w, "w")
194        else:
195            # child process
196            os.setpgrp()
197            os.close(w)
198            r = os.fdopen(r)
199            x = r.read()
200            os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
201            sys.exit(0)
202
203        self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
204        self.logger.debug("waiting at most %s seconds for qemu pid (%s)" %
205                          (self.runqemutime, time.strftime("%D %H:%M:%S")))
206        endtime = time.time() + self.runqemutime
207        while not self.is_alive() and time.time() < endtime:
208            if self.runqemu.poll():
209                if self.runqemu.returncode:
210                    # No point waiting any longer
211                    self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
212                    self._dump_host()
213                    self.logger.warning("Output from runqemu:\n%s" % self.getOutput(output))
214                    self.stop()
215                    return False
216            time.sleep(0.5)
217
218        if not self.is_alive():
219            self.logger.error("Qemu pid didn't appear in %s seconds (%s)" %
220                              (self.runqemutime, time.strftime("%D %H:%M:%S")))
221            # Dump all processes to help us to figure out what is going on...
222            ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,command '], stdout=subprocess.PIPE).communicate()[0]
223            processes = ps.decode("utf-8")
224            self.logger.debug("Running processes:\n%s" % processes)
225            self._dump_host()
226            op = self.getOutput(output)
227            self.stop()
228            if op:
229                self.logger.error("Output from runqemu:\n%s" % op)
230            else:
231                self.logger.error("No output from runqemu.\n")
232            return False
233
234        # We are alive: qemu is running
235        out = self.getOutput(output)
236        netconf = False # network configuration is not required by default
237        self.logger.debug("qemu started in %s seconds - qemu procces pid is %s (%s)" %
238                          (time.time() - (endtime - self.runqemutime),
239                           self.qemupid, time.strftime("%D %H:%M:%S")))
240        if get_ip:
241            cmdline = ''
242            with open('/proc/%s/cmdline' % self.qemupid) as p:
243                cmdline = p.read()
244                # It is needed to sanitize the data received
245                # because is possible to have control characters
246                cmdline = re_control_char.sub(' ', cmdline)
247            try:
248                if self.use_slirp:
249                    tcp_ports = cmdline.split("hostfwd=tcp::")[1]
250                    host_port = tcp_ports[:tcp_ports.find('-')]
251                    self.ip = "localhost:%s" % host_port
252                else:
253                    ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
254                    self.ip = ips[0]
255                    self.server_ip = ips[1]
256                self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
257            except (IndexError, ValueError):
258                # Try to get network configuration from runqemu output
259                match = re.match(r'.*Network configuration: ([0-9.]+)::([0-9.]+):([0-9.]+)$.*',
260                                 out, re.MULTILINE|re.DOTALL)
261                if match:
262                    self.ip, self.server_ip, self.netmask = match.groups()
263                    # network configuration is required as we couldn't get it
264                    # from the runqemu command line, so qemu doesn't run kernel
265                    # and guest networking is not configured
266                    netconf = True
267                else:
268                    self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
269                                 "Here is the qemu command line used:\n%s\n"
270                                 "and output from runqemu:\n%s" % (cmdline, out))
271                    self._dump_host()
272                    self.stop()
273                    return False
274
275        self.logger.debug("Target IP: %s" % self.ip)
276        self.logger.debug("Server IP: %s" % self.server_ip)
277
278        self.thread = LoggingThread(self.log, self.threadsock, self.logger)
279        self.thread.start()
280        if not self.thread.connection_established.wait(self.boottime):
281            self.logger.error("Didn't receive a console connection from qemu. "
282                         "Here is the qemu command line used:\n%s\nand "
283                         "output from runqemu:\n%s" % (cmdline, out))
284            self.stop_thread()
285            return False
286
287        self.logger.debug("Output from runqemu:\n%s", out)
288        self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
289                          (self.boottime, time.strftime("%D %H:%M:%S")))
290        endtime = time.time() + self.boottime
291        socklist = [self.server_socket]
292        reachedlogin = False
293        stopread = False
294        qemusock = None
295        bootlog = b''
296        data = b''
297        while time.time() < endtime and not stopread:
298            try:
299                sread, swrite, serror = select.select(socklist, [], [], 5)
300            except InterruptedError:
301                continue
302            for sock in sread:
303                if sock is self.server_socket:
304                    qemusock, addr = self.server_socket.accept()
305                    qemusock.setblocking(0)
306                    socklist.append(qemusock)
307                    socklist.remove(self.server_socket)
308                    self.logger.debug("Connection from %s:%s" % addr)
309                else:
310                    data = data + sock.recv(1024)
311                    if data:
312                        bootlog += data
313                        data = b''
314                        if b' login:' in bootlog:
315                            self.server_socket = qemusock
316                            stopread = True
317                            reachedlogin = True
318                            self.logger.debug("Reached login banner in %s seconds (%s)" %
319                                              (time.time() - (endtime - self.boottime),
320                                              time.strftime("%D %H:%M:%S")))
321                    else:
322                        # no need to check if reachedlogin unless we support multiple connections
323                        self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
324                                          time.strftime("%D %H:%M:%S"))
325                        socklist.remove(sock)
326                        sock.close()
327                        stopread = True
328
329
330        if not reachedlogin:
331            if time.time() >= endtime:
332                self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
333                                  (self.boottime, time.strftime("%D %H:%M:%S")))
334            tail = lambda l: "\n".join(l.splitlines()[-25:])
335            bootlog = bootlog.decode("utf-8")
336            # in case bootlog is empty, use tail qemu log store at self.msg
337            lines = tail(bootlog if bootlog else self.msg)
338            self.logger.warning("Last 25 lines of text:\n%s" % lines)
339            self.logger.warning("Check full boot log: %s" % self.logfile)
340            self._dump_host()
341            self.stop()
342            return False
343
344        # If we are not able to login the tests can continue
345        try:
346            (status, output) = self.run_serial("root\n", raw=True)
347            if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
348                self.logged = True
349                self.logger.debug("Logged as root in serial console")
350                if netconf:
351                    # configure guest networking
352                    cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
353                    output = self.run_serial(cmd, raw=True)[1]
354                    if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
355                        self.logger.debug("configured ip address %s", self.ip)
356                    else:
357                        self.logger.debug("Couldn't configure guest networking")
358            else:
359                self.logger.warning("Couldn't login into serial console"
360                            " as root using blank password")
361                self.logger.warning("The output:\n%s" % output)
362        except:
363            self.logger.warning("Serial console failed while trying to login")
364        return True
365
366    def stop(self):
367        if hasattr(self, "origchldhandler"):
368            signal.signal(signal.SIGCHLD, self.origchldhandler)
369        self.stop_thread()
370        self.stop_qemu_system()
371        if self.runqemu:
372            if hasattr(self, "monitorpid"):
373                os.kill(self.monitorpid, signal.SIGKILL)
374                self.logger.debug("Sending SIGTERM to runqemu")
375                try:
376                    os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
377                except OSError as e:
378                    if e.errno != errno.ESRCH:
379                        raise
380            endtime = time.time() + self.runqemutime
381            while self.runqemu.poll() is None and time.time() < endtime:
382                time.sleep(1)
383            if self.runqemu.poll() is None:
384                self.logger.debug("Sending SIGKILL to runqemu")
385                os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
386            self.runqemu.stdin.close()
387            self.runqemu.stdout.close()
388            self.runqemu = None
389
390        if hasattr(self, 'server_socket') and self.server_socket:
391            self.server_socket.close()
392            self.server_socket = None
393        if hasattr(self, 'threadsock') and self.threadsock:
394            self.threadsock.close()
395            self.threadsock = None
396        self.qemupid = None
397        self.ip = None
398        if os.path.exists(self.qemu_pidfile):
399            os.remove(self.qemu_pidfile)
400        if self.monitorpipe:
401            self.monitorpipe.close()
402
403    def stop_qemu_system(self):
404        if self.qemupid:
405            try:
406                # qemu-system behaves well and a SIGTERM is enough
407                os.kill(self.qemupid, signal.SIGTERM)
408            except ProcessLookupError as e:
409                self.logger.warning('qemu-system ended unexpectedly')
410
411    def stop_thread(self):
412        if self.thread and self.thread.is_alive():
413            self.thread.stop()
414            self.thread.join()
415
416    def restart(self, qemuparams = None):
417        self.logger.warning("Restarting qemu process")
418        if self.runqemu.poll() is None:
419            self.stop()
420        if self.start(qemuparams):
421            return True
422        return False
423
424    def is_alive(self):
425        if not self.runqemu or self.runqemu.poll() is not None:
426            return False
427        if os.path.isfile(self.qemu_pidfile):
428            # when handling pidfile, qemu creates the file, stat it, lock it and then write to it
429            # so it's possible that the file has been created but the content is empty
430            pidfile_timeout = time.time() + 3
431            while time.time() < pidfile_timeout:
432                with open(self.qemu_pidfile, 'r') as f:
433                    qemu_pid = f.read().strip()
434                # file created but not yet written contents
435                if not qemu_pid:
436                    time.sleep(0.5)
437                    continue
438                else:
439                    if os.path.exists("/proc/" + qemu_pid):
440                        self.qemupid = int(qemu_pid)
441                        return True
442        return False
443
444    def run_serial(self, command, raw=False, timeout=60):
445        # We assume target system have echo to get command status
446        if not raw:
447            command = "%s; echo $?\n" % command
448
449        data = ''
450        status = 0
451        self.server_socket.sendall(command.encode('utf-8'))
452        start = time.time()
453        end = start + timeout
454        while True:
455            now = time.time()
456            if now >= end:
457                data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
458                break
459            try:
460                sread, _, _ = select.select([self.server_socket],[],[], end - now)
461            except InterruptedError:
462                continue
463            if sread:
464                answer = self.server_socket.recv(1024)
465                if answer:
466                    data += answer.decode('utf-8')
467                    # Search the prompt to stop
468                    if re.search(r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#", data):
469                        break
470                else:
471                    raise Exception("No data on serial console socket")
472
473        if data:
474            if raw:
475                status = 1
476            else:
477                # Remove first line (command line) and last line (prompt)
478                data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
479                index = data.rfind('\r\n')
480                if index == -1:
481                    status_cmd = data
482                    data = ""
483                else:
484                    status_cmd = data[index+2:]
485                    data = data[:index]
486                if (status_cmd == "0"):
487                    status = 1
488        return (status, str(data))
489
490
491    def _dump_host(self):
492        self.host_dumper.create_dir("qemu")
493        self.logger.warning("Qemu ended unexpectedly, dump data from host"
494                " is in %s" % self.host_dumper.dump_dir)
495        self.host_dumper.dump_host()
496
497# This class is for reading data from a socket and passing it to logfunc
498# to be processed. It's completely event driven and has a straightforward
499# event loop. The mechanism for stopping the thread is a simple pipe which
500# will wake up the poll and allow for tearing everything down.
501class LoggingThread(threading.Thread):
502    def __init__(self, logfunc, sock, logger):
503        self.connection_established = threading.Event()
504        self.serversock = sock
505        self.logfunc = logfunc
506        self.logger = logger
507        self.readsock = None
508        self.running = False
509
510        self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
511        self.readevents = select.POLLIN | select.POLLPRI
512
513        threading.Thread.__init__(self, target=self.threadtarget)
514
515    def threadtarget(self):
516        try:
517            self.eventloop()
518        finally:
519            self.teardown()
520
521    def run(self):
522        self.logger.debug("Starting logging thread")
523        self.readpipe, self.writepipe = os.pipe()
524        threading.Thread.run(self)
525
526    def stop(self):
527        self.logger.debug("Stopping logging thread")
528        if self.running:
529            os.write(self.writepipe, bytes("stop", "utf-8"))
530
531    def teardown(self):
532        self.logger.debug("Tearing down logging thread")
533        self.close_socket(self.serversock)
534
535        if self.readsock is not None:
536            self.close_socket(self.readsock)
537
538        self.close_ignore_error(self.readpipe)
539        self.close_ignore_error(self.writepipe)
540        self.running = False
541
542    def eventloop(self):
543        poll = select.poll()
544        event_read_mask = self.errorevents | self.readevents
545        poll.register(self.serversock.fileno())
546        poll.register(self.readpipe, event_read_mask)
547
548        breakout = False
549        self.running = True
550        self.logger.debug("Starting thread event loop")
551        while not breakout:
552            events = poll.poll()
553            for event in events:
554                # An error occurred, bail out
555                if event[1] & self.errorevents:
556                    raise Exception(self.stringify_event(event[1]))
557
558                # Event to stop the thread
559                if self.readpipe == event[0]:
560                    self.logger.debug("Stop event received")
561                    breakout = True
562                    break
563
564                # A connection request was received
565                elif self.serversock.fileno() == event[0]:
566                    self.logger.debug("Connection request received")
567                    self.readsock, _ = self.serversock.accept()
568                    self.readsock.setblocking(0)
569                    poll.unregister(self.serversock.fileno())
570                    poll.register(self.readsock.fileno(), event_read_mask)
571
572                    self.logger.debug("Setting connection established event")
573                    self.connection_established.set()
574
575                # Actual data to be logged
576                elif self.readsock.fileno() == event[0]:
577                    data = self.recv(1024)
578                    self.logfunc(data)
579
580    # Since the socket is non-blocking make sure to honor EAGAIN
581    # and EWOULDBLOCK.
582    def recv(self, count):
583        try:
584            data = self.readsock.recv(count)
585        except socket.error as e:
586            if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
587                return ''
588            else:
589                raise
590
591        if data is None:
592            raise Exception("No data on read ready socket")
593        elif not data:
594            # This actually means an orderly shutdown
595            # happened. But for this code it counts as an
596            # error since the connection shouldn't go away
597            # until qemu exits.
598            raise Exception("Console connection closed unexpectedly")
599
600        return data
601
602    def stringify_event(self, event):
603        val = ''
604        if select.POLLERR == event:
605            val = 'POLLER'
606        elif select.POLLHUP == event:
607            val = 'POLLHUP'
608        elif select.POLLNVAL == event:
609            val = 'POLLNVAL'
610        return val
611
612    def close_socket(self, sock):
613        sock.shutdown(socket.SHUT_RDWR)
614        sock.close()
615
616    def close_ignore_error(self, fd):
617        try:
618            os.close(fd)
619        except OSError:
620            pass
621