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