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
23import tempfile
24from oeqa.utils.dump import HostDumper
25from collections import defaultdict
26import importlib
27
28# Get Unicode non printable control chars
29control_range = list(range(0,32))+list(range(127,160))
30control_chars = [chr(x) for x in control_range
31                if chr(x) not in string.printable]
32re_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
33
34class QemuRunner:
35
36    def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds,
37                 use_kvm, logger, use_slirp=False, serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None, tmpfsdir=None):
38
39        # Popen object for runqemu
40        self.runqemu = None
41        self.runqemu_exited = False
42        # pid of the qemu process that runqemu will start
43        self.qemupid = None
44        # target ip - from the command line or runqemu output
45        self.ip = None
46        # host ip - where qemu is running
47        self.server_ip = None
48        # target ip netmask
49        self.netmask = None
50
51        self.machine = machine
52        self.rootfs = rootfs
53        self.display = display
54        self.tmpdir = tmpdir
55        self.deploy_dir_image = deploy_dir_image
56        self.logfile = logfile
57        self.boottime = boottime
58        self.logged = False
59        self.thread = None
60        self.use_kvm = use_kvm
61        self.use_ovmf = use_ovmf
62        self.use_slirp = use_slirp
63        self.serial_ports = serial_ports
64        self.msg = ''
65        self.boot_patterns = boot_patterns
66        self.tmpfsdir = tmpfsdir
67
68        self.runqemutime = 300
69        if not workdir:
70            workdir = os.getcwd()
71        self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid())
72        self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
73        self.monitorpipe = None
74
75        self.logger = logger
76        # Whether we're expecting an exit and should show related errors
77        self.canexit = False
78
79        # Enable testing other OS's
80        # Set commands for target communication, and default to Linux ALWAYS
81        # Other OS's or baremetal applications need to provide their
82        # own implementation passing it through QemuRunner's constructor
83        # or by passing them through TESTIMAGE_BOOT_PATTERNS[flag]
84        # provided variables, where <flag> is one of the mentioned below.
85        accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished']
86        default_boot_patterns = defaultdict(str)
87        # Default to the usual paterns used to communicate with the target
88        default_boot_patterns['search_reached_prompt'] = b' login:'
89        default_boot_patterns['send_login_user'] = 'root\n'
90        default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#"
91        default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
92
93        # Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n"
94        for pattern in accepted_patterns:
95            if not self.boot_patterns[pattern]:
96                self.boot_patterns[pattern] = default_boot_patterns[pattern]
97
98    def create_socket(self):
99        try:
100            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
101            sock.setblocking(0)
102            sock.bind(("127.0.0.1",0))
103            sock.listen(2)
104            port = sock.getsockname()[1]
105            self.logger.debug("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
106            return (sock, port)
107
108        except socket.error:
109            sock.close()
110            raise
111
112    def log(self, msg):
113        if self.logfile:
114            # It is needed to sanitize the data received from qemu
115            # because is possible to have control characters
116            msg = msg.decode("utf-8", errors='ignore')
117            msg = re_control_char.sub('', msg)
118            self.msg += msg
119            with codecs.open(self.logfile, "a", encoding="utf-8") as f:
120                f.write("%s" % msg)
121
122    def getOutput(self, o):
123        import fcntl
124        fl = fcntl.fcntl(o, fcntl.F_GETFL)
125        fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
126        try:
127            return os.read(o.fileno(), 1000000).decode("utf-8")
128        except BlockingIOError:
129            return ""
130
131
132    def handleSIGCHLD(self, signum, frame):
133        if self.runqemu and self.runqemu.poll():
134            if self.runqemu.returncode:
135                self.logger.error('runqemu exited with code %d' % self.runqemu.returncode)
136                self.logger.error('Output from runqemu:\n%s' % self.getOutput(self.runqemu.stdout))
137                self.stop()
138                self._dump_host()
139
140    def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
141        env = os.environ.copy()
142        if self.display:
143            env["DISPLAY"] = self.display
144            # Set this flag so that Qemu doesn't do any grabs as SDL grabs
145            # interact badly with screensavers.
146            env["QEMU_DONT_GRAB"] = "1"
147        if not os.path.exists(self.rootfs):
148            self.logger.error("Invalid rootfs %s" % self.rootfs)
149            return False
150        if not os.path.exists(self.tmpdir):
151            self.logger.error("Invalid TMPDIR path %s" % self.tmpdir)
152            return False
153        else:
154            env["OE_TMPDIR"] = self.tmpdir
155        if not os.path.exists(self.deploy_dir_image):
156            self.logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
157            return False
158        else:
159            env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
160
161        if self.tmpfsdir:
162            env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
163
164        if not launch_cmd:
165            launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '')
166            if self.use_kvm:
167                self.logger.debug('Using kvm for runqemu')
168                launch_cmd += ' kvm'
169            else:
170                self.logger.debug('Not using kvm for runqemu')
171            if not self.display:
172                launch_cmd += ' nographic'
173            if self.use_slirp:
174                launch_cmd += ' slirp'
175            if self.use_ovmf:
176                launch_cmd += ' ovmf'
177            launch_cmd += ' %s %s %s' % (runqemuparams, self.machine, self.rootfs)
178
179        return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
180
181    def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
182        # use logfile to determine the recipe-sysroot-native path and
183        # then add in the site-packages path components and add that
184        # to the python sys.path so qmp.py can be found.
185        python_path = os.path.dirname(os.path.dirname(self.logfile))
186        python_path += "/recipe-sysroot-native/usr/lib/python3.9/site-packages"
187        sys.path.append(python_path)
188        importlib.invalidate_caches()
189        try:
190            qmp = importlib.import_module("qmp")
191        except:
192            self.logger.error("qemurunner: qmp.py missing, please ensure it's installed")
193            return False
194        # Path relative to tmpdir used as cwd for qemu below to avoid unix socket path length issues
195        qmp_file = "." + next(tempfile._get_candidate_names())
196        qmp_param = ' -S -qmp unix:./%s,server,wait' % (qmp_file)
197        qmp_port = self.tmpdir + "/" + qmp_file
198        # Create a second socket connection for debugging use,
199        # note this will NOT cause qemu to block waiting for the connection
200        qmp_file2 = "." + next(tempfile._get_candidate_names())
201        qmp_param += ' -qmp unix:./%s,server,nowait' % (qmp_file2)
202        qmp_port2 = self.tmpdir + "/" + qmp_file2
203        self.logger.info("QMP Available for connection at %s" % (qmp_port2))
204
205        try:
206            if self.serial_ports >= 2:
207                self.threadsock, threadport = self.create_socket()
208            self.server_socket, self.serverport = self.create_socket()
209        except socket.error as msg:
210            self.logger.error("Failed to create listening socket: %s" % msg[1])
211            return False
212
213        bootparams = ' printk.time=1'
214        if extra_bootparams:
215            bootparams = bootparams + ' ' + extra_bootparams
216
217        # Ask QEMU to store the QEMU process PID in file, this way we don't have to parse running processes
218        # and analyze descendents in order to determine it.
219        if os.path.exists(self.qemu_pidfile):
220            os.remove(self.qemu_pidfile)
221        self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
222
223        if qemuparams:
224            self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
225
226        if self.serial_ports >= 2:
227            launch_cmd += ' tcpserial=%s:%s %s' % (threadport, self.serverport, self.qemuparams)
228        else:
229            launch_cmd += ' tcpserial=%s %s' % (self.serverport, self.qemuparams)
230
231        self.origchldhandler = signal.getsignal(signal.SIGCHLD)
232        signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
233
234        self.logger.debug('launchcmd=%s'%(launch_cmd))
235
236        # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
237        # blocking at the end of the runqemu script when using this within
238        # oe-selftest (this makes stty error out immediately). There ought
239        # to be a proper fix but this will suffice for now.
240        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)
241        output = self.runqemu.stdout
242        launch_time = time.time()
243
244        #
245        # We need the preexec_fn above so that all runqemu processes can easily be killed
246        # (by killing their process group). This presents a problem if this controlling
247        # process itself is killed however since those processes don't notice the death
248        # of the parent and merrily continue on.
249        #
250        # Rather than hack runqemu to deal with this, we add something here instead.
251        # Basically we fork off another process which holds an open pipe to the parent
252        # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
253        # the process group. This is like pctrl's PDEATHSIG but for a process group
254        # rather than a single process.
255        #
256        r, w = os.pipe()
257        self.monitorpid = os.fork()
258        if self.monitorpid:
259            os.close(r)
260            self.monitorpipe = os.fdopen(w, "w")
261        else:
262            # child process
263            os.setpgrp()
264            os.close(w)
265            r = os.fdopen(r)
266            x = r.read()
267            os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
268            os._exit(0)
269
270        self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
271        self.logger.debug("waiting at most %s seconds for qemu pid (%s)" %
272                          (self.runqemutime, time.strftime("%D %H:%M:%S")))
273        endtime = time.time() + self.runqemutime
274        while not self.is_alive() and time.time() < endtime:
275            if self.runqemu.poll():
276                if self.runqemu_exited:
277                    self.logger.warning("runqemu during is_alive() test")
278                    return False
279                if self.runqemu.returncode:
280                    # No point waiting any longer
281                    self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
282                    self._dump_host()
283                    self.logger.warning("Output from runqemu:\n%s" % self.getOutput(output))
284                    self.stop()
285                    return False
286            time.sleep(0.5)
287
288        if self.runqemu_exited:
289            self.logger.warning("runqemu after timeout")
290
291        if self.runqemu.returncode:
292            self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
293
294        if not self.is_alive():
295            self.logger.error("Qemu pid didn't appear in %s seconds (%s)" %
296                              (self.runqemutime, time.strftime("%D %H:%M:%S")))
297
298            qemu_pid = None
299            if os.path.isfile(self.qemu_pidfile):
300                with open(self.qemu_pidfile, 'r') as f:
301                    qemu_pid = f.read().strip()
302
303            self.logger.error("Status information, poll status: %s, pidfile exists: %s, pidfile contents %s, proc pid exists %s"
304                % (self.runqemu.poll(), os.path.isfile(self.qemu_pidfile), str(qemu_pid), os.path.exists("/proc/" + str(qemu_pid))))
305
306            # Dump all processes to help us to figure out what is going on...
307            ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0]
308            processes = ps.decode("utf-8")
309            self.logger.debug("Running processes:\n%s" % processes)
310            self._dump_host()
311            op = self.getOutput(output)
312            self.stop()
313            if op:
314                self.logger.error("Output from runqemu:\n%s" % op)
315            else:
316                self.logger.error("No output from runqemu.\n")
317            return False
318
319        # Create the client socket for the QEMU Monitor Control Socket
320        # This will allow us to read status from Qemu if the the process
321        # is still alive
322        self.logger.debug("QMP Initializing to %s" % (qmp_port))
323        # chdir dance for path length issues with unix sockets
324        origpath = os.getcwd()
325        try:
326            os.chdir(os.path.dirname(qmp_port))
327            try:
328               self.qmp = qmp.QEMUMonitorProtocol(os.path.basename(qmp_port))
329            except OSError as msg:
330                self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
331                return False
332
333            self.logger.debug("QMP Connecting to %s" % (qmp_port))
334            if not os.path.exists(qmp_port) and self.is_alive():
335                self.logger.debug("QMP Port does not exist waiting for it to be created")
336                endtime = time.time() + self.runqemutime
337                while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
338                   self.logger.info("QMP port does not exist yet!")
339                   time.sleep(0.5)
340                if not os.path.exists(qmp_port) and self.is_alive():
341                    self.logger.warning("QMP Port still does not exist but QEMU is alive")
342                    return False
343
344            try:
345                self.qmp.connect()
346                connect_time = time.time()
347                self.logger.info("QMP connected to QEMU at %s and took %s seconds" %
348                                  (time.strftime("%D %H:%M:%S"),
349                                   time.time() - launch_time))
350            except OSError as msg:
351                self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
352                return False
353            except qmp.QMPConnectError as msg:
354                self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
355                return False
356        finally:
357            os.chdir(origpath)
358
359        # We worry that mmap'd libraries may cause page faults which hang the qemu VM for periods
360        # causing failures. Before we "start" qemu, read through it's mapped files to try and
361        # ensure we don't hit page faults later
362        mapdir = "/proc/" + str(self.qemupid) + "/map_files/"
363        try:
364            for f in os.listdir(mapdir):
365                try:
366                    linktarget = os.readlink(os.path.join(mapdir, f))
367                    if not linktarget.startswith("/") or linktarget.startswith("/dev") or "deleted" in linktarget:
368                        continue
369                    with open(linktarget, "rb") as readf:
370                        data = True
371                        while data:
372                            data = readf.read(4096)
373                except FileNotFoundError:
374                    continue
375        # Centos7 doesn't allow us to read /map_files/
376        except PermissionError:
377            pass
378
379        # Release the qemu process to continue running
380        self.run_monitor('cont')
381        self.logger.info("QMP released QEMU at %s and took %s seconds from connect" %
382                          (time.strftime("%D %H:%M:%S"),
383                           time.time() - connect_time))
384
385        # We are alive: qemu is running
386        out = self.getOutput(output)
387        netconf = False # network configuration is not required by default
388        self.logger.debug("qemu started in %s seconds - qemu procces pid is %s (%s)" %
389                          (time.time() - (endtime - self.runqemutime),
390                           self.qemupid, time.strftime("%D %H:%M:%S")))
391        cmdline = ''
392        if get_ip:
393            with open('/proc/%s/cmdline' % self.qemupid) as p:
394                cmdline = p.read()
395                # It is needed to sanitize the data received
396                # because is possible to have control characters
397                cmdline = re_control_char.sub(' ', cmdline)
398            try:
399                if self.use_slirp:
400                    tcp_ports = cmdline.split("hostfwd=tcp::")[1]
401                    host_port = tcp_ports[:tcp_ports.find('-')]
402                    self.ip = "localhost:%s" % host_port
403                else:
404                    ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
405                    self.ip = ips[0]
406                    self.server_ip = ips[1]
407                self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
408            except (IndexError, ValueError):
409                # Try to get network configuration from runqemu output
410                match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+)$.*',
411                                 out, re.MULTILINE|re.DOTALL)
412                if match:
413                    self.ip, self.server_ip, self.netmask = match.groups()
414                    # network configuration is required as we couldn't get it
415                    # from the runqemu command line, so qemu doesn't run kernel
416                    # and guest networking is not configured
417                    netconf = True
418                else:
419                    self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
420                                 "Here is the qemu command line used:\n%s\n"
421                                 "and output from runqemu:\n%s" % (cmdline, out))
422                    self._dump_host()
423                    self.stop()
424                    return False
425
426        self.logger.debug("Target IP: %s" % self.ip)
427        self.logger.debug("Server IP: %s" % self.server_ip)
428
429        if self.serial_ports >= 2:
430            self.thread = LoggingThread(self.log, self.threadsock, self.logger)
431            self.thread.start()
432            if not self.thread.connection_established.wait(self.boottime):
433                self.logger.error("Didn't receive a console connection from qemu. "
434                             "Here is the qemu command line used:\n%s\nand "
435                             "output from runqemu:\n%s" % (cmdline, out))
436                self.stop_thread()
437                return False
438
439        self.logger.debug("Output from runqemu:\n%s", out)
440        self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
441                          (self.boottime, time.strftime("%D %H:%M:%S")))
442        endtime = time.time() + self.boottime
443        socklist = [self.server_socket]
444        reachedlogin = False
445        stopread = False
446        qemusock = None
447        bootlog = b''
448        data = b''
449        while time.time() < endtime and not stopread:
450            try:
451                sread, swrite, serror = select.select(socklist, [], [], 5)
452            except InterruptedError:
453                continue
454            for sock in sread:
455                if sock is self.server_socket:
456                    qemusock, addr = self.server_socket.accept()
457                    qemusock.setblocking(0)
458                    socklist.append(qemusock)
459                    socklist.remove(self.server_socket)
460                    self.logger.debug("Connection from %s:%s" % addr)
461                else:
462                    data = data + sock.recv(1024)
463                    if data:
464                        bootlog += data
465                        if self.serial_ports < 2:
466                            # this socket has mixed console/kernel data, log it to logfile
467                            self.log(data)
468
469                        data = b''
470                        if self.boot_patterns['search_reached_prompt'] in bootlog:
471                            self.server_socket = qemusock
472                            stopread = True
473                            reachedlogin = True
474                            self.logger.debug("Reached login banner in %s seconds (%s)" %
475                                              (time.time() - (endtime - self.boottime),
476                                              time.strftime("%D %H:%M:%S")))
477                    else:
478                        # no need to check if reachedlogin unless we support multiple connections
479                        self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
480                                          time.strftime("%D %H:%M:%S"))
481                        socklist.remove(sock)
482                        sock.close()
483                        stopread = True
484
485        if not reachedlogin:
486            if time.time() >= endtime:
487                self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
488                                  (self.boottime, time.strftime("%D %H:%M:%S")))
489            tail = lambda l: "\n".join(l.splitlines()[-25:])
490            bootlog = bootlog.decode("utf-8")
491            # in case bootlog is empty, use tail qemu log store at self.msg
492            lines = tail(bootlog if bootlog else self.msg)
493            self.logger.warning("Last 25 lines of text:\n%s" % lines)
494            self.logger.warning("Check full boot log: %s" % self.logfile)
495            self._dump_host()
496            self.stop()
497            return False
498
499        # If we are not able to login the tests can continue
500        try:
501            (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120)
502            if re.search(self.boot_patterns['search_login_succeeded'], output):
503                self.logged = True
504                self.logger.debug("Logged as root in serial console")
505                if netconf:
506                    # configure guest networking
507                    cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
508                    output = self.run_serial(cmd, raw=True)[1]
509                    if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
510                        self.logger.debug("configured ip address %s", self.ip)
511                    else:
512                        self.logger.debug("Couldn't configure guest networking")
513            else:
514                self.logger.warning("Couldn't login into serial console"
515                            " as root using blank password")
516                self.logger.warning("The output:\n%s" % output)
517        except:
518            self.logger.warning("Serial console failed while trying to login")
519        return True
520
521    def stop(self):
522        if hasattr(self, "origchldhandler"):
523            signal.signal(signal.SIGCHLD, self.origchldhandler)
524        self.stop_thread()
525        self.stop_qemu_system()
526        if self.runqemu:
527            if hasattr(self, "monitorpid"):
528                os.kill(self.monitorpid, signal.SIGKILL)
529                self.logger.debug("Sending SIGTERM to runqemu")
530                try:
531                    os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
532                except OSError as e:
533                    if e.errno != errno.ESRCH:
534                        raise
535            endtime = time.time() + self.runqemutime
536            while self.runqemu.poll() is None and time.time() < endtime:
537                time.sleep(1)
538            if self.runqemu.poll() is None:
539                self.logger.debug("Sending SIGKILL to runqemu")
540                os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
541            if not self.runqemu.stdout.closed:
542                self.logger.info("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
543            self.runqemu.stdin.close()
544            self.runqemu.stdout.close()
545            self.runqemu_exited = True
546
547        if hasattr(self, 'qmp') and self.qmp:
548            self.qmp.close()
549            self.qmp = None
550        if hasattr(self, 'server_socket') and self.server_socket:
551            self.server_socket.close()
552            self.server_socket = None
553        if hasattr(self, 'threadsock') and self.threadsock:
554            self.threadsock.close()
555            self.threadsock = None
556        self.qemupid = None
557        self.ip = None
558        if os.path.exists(self.qemu_pidfile):
559            try:
560                os.remove(self.qemu_pidfile)
561            except FileNotFoundError as e:
562                # We raced, ignore
563                pass
564        if self.monitorpipe:
565            self.monitorpipe.close()
566
567    def stop_qemu_system(self):
568        if self.qemupid:
569            try:
570                # qemu-system behaves well and a SIGTERM is enough
571                os.kill(self.qemupid, signal.SIGTERM)
572            except ProcessLookupError as e:
573                self.logger.warning('qemu-system ended unexpectedly')
574
575    def stop_thread(self):
576        if self.thread and self.thread.is_alive():
577            self.thread.stop()
578            self.thread.join()
579
580    def allowexit(self):
581        self.canexit = True
582        if self.thread:
583            self.thread.allowexit()
584
585    def restart(self, qemuparams = None):
586        self.logger.warning("Restarting qemu process")
587        if self.runqemu.poll() is None:
588            self.stop()
589        if self.start(qemuparams):
590            return True
591        return False
592
593    def is_alive(self):
594        if not self.runqemu or self.runqemu.poll() is not None or self.runqemu_exited:
595            return False
596        if os.path.isfile(self.qemu_pidfile):
597            # when handling pidfile, qemu creates the file, stat it, lock it and then write to it
598            # so it's possible that the file has been created but the content is empty
599            pidfile_timeout = time.time() + 3
600            while time.time() < pidfile_timeout:
601                with open(self.qemu_pidfile, 'r') as f:
602                    qemu_pid = f.read().strip()
603                # file created but not yet written contents
604                if not qemu_pid:
605                    time.sleep(0.5)
606                    continue
607                else:
608                    if os.path.exists("/proc/" + qemu_pid):
609                        self.qemupid = int(qemu_pid)
610                        return True
611        return False
612
613    def run_monitor(self, command, args=None, timeout=60):
614        if hasattr(self, 'qmp') and self.qmp:
615            if args is not None:
616                return self.qmp.cmd(command, args)
617            else:
618                return self.qmp.cmd(command)
619
620    def run_serial(self, command, raw=False, timeout=60):
621        # We assume target system have echo to get command status
622        if not raw:
623            command = "%s; echo $?\n" % command
624
625        data = ''
626        status = 0
627        self.server_socket.sendall(command.encode('utf-8'))
628        start = time.time()
629        end = start + timeout
630        while True:
631            now = time.time()
632            if now >= end:
633                data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
634                break
635            try:
636                sread, _, _ = select.select([self.server_socket],[],[], end - now)
637            except InterruptedError:
638                continue
639            if sread:
640                answer = self.server_socket.recv(1024)
641                if answer:
642                    data += answer.decode('utf-8')
643                    # Search the prompt to stop
644                    if re.search(self.boot_patterns['search_cmd_finished'], data):
645                        break
646                else:
647                    if self.canexit:
648                        return (1, "")
649                    raise Exception("No data on serial console socket, connection closed?")
650
651        if data:
652            if raw:
653                status = 1
654            else:
655                # Remove first line (command line) and last line (prompt)
656                data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
657                index = data.rfind('\r\n')
658                if index == -1:
659                    status_cmd = data
660                    data = ""
661                else:
662                    status_cmd = data[index+2:]
663                    data = data[:index]
664                if (status_cmd == "0"):
665                    status = 1
666        return (status, str(data))
667
668
669    def _dump_host(self):
670        self.host_dumper.create_dir("qemu")
671        self.logger.warning("Qemu ended unexpectedly, dump data from host"
672                " is in %s" % self.host_dumper.dump_dir)
673        self.host_dumper.dump_host()
674
675# This class is for reading data from a socket and passing it to logfunc
676# to be processed. It's completely event driven and has a straightforward
677# event loop. The mechanism for stopping the thread is a simple pipe which
678# will wake up the poll and allow for tearing everything down.
679class LoggingThread(threading.Thread):
680    def __init__(self, logfunc, sock, logger):
681        self.connection_established = threading.Event()
682        self.serversock = sock
683        self.logfunc = logfunc
684        self.logger = logger
685        self.readsock = None
686        self.running = False
687        self.canexit = False
688
689        self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
690        self.readevents = select.POLLIN | select.POLLPRI
691
692        threading.Thread.__init__(self, target=self.threadtarget)
693
694    def threadtarget(self):
695        try:
696            self.eventloop()
697        finally:
698            self.teardown()
699
700    def run(self):
701        self.logger.debug("Starting logging thread")
702        self.readpipe, self.writepipe = os.pipe()
703        threading.Thread.run(self)
704
705    def stop(self):
706        self.logger.debug("Stopping logging thread")
707        if self.running:
708            os.write(self.writepipe, bytes("stop", "utf-8"))
709
710    def teardown(self):
711        self.logger.debug("Tearing down logging thread")
712        self.close_socket(self.serversock)
713
714        if self.readsock is not None:
715            self.close_socket(self.readsock)
716
717        self.close_ignore_error(self.readpipe)
718        self.close_ignore_error(self.writepipe)
719        self.running = False
720
721    def allowexit(self):
722        self.canexit = True
723
724    def eventloop(self):
725        poll = select.poll()
726        event_read_mask = self.errorevents | self.readevents
727        poll.register(self.serversock.fileno())
728        poll.register(self.readpipe, event_read_mask)
729
730        breakout = False
731        self.running = True
732        self.logger.debug("Starting thread event loop")
733        while not breakout:
734            events = poll.poll()
735            for event in events:
736                # An error occurred, bail out
737                if event[1] & self.errorevents:
738                    raise Exception(self.stringify_event(event[1]))
739
740                # Event to stop the thread
741                if self.readpipe == event[0]:
742                    self.logger.debug("Stop event received")
743                    breakout = True
744                    break
745
746                # A connection request was received
747                elif self.serversock.fileno() == event[0]:
748                    self.logger.debug("Connection request received")
749                    self.readsock, _ = self.serversock.accept()
750                    self.readsock.setblocking(0)
751                    poll.unregister(self.serversock.fileno())
752                    poll.register(self.readsock.fileno(), event_read_mask)
753
754                    self.logger.debug("Setting connection established event")
755                    self.connection_established.set()
756
757                # Actual data to be logged
758                elif self.readsock.fileno() == event[0]:
759                    data = self.recv(1024)
760                    self.logfunc(data)
761
762    # Since the socket is non-blocking make sure to honor EAGAIN
763    # and EWOULDBLOCK.
764    def recv(self, count):
765        try:
766            data = self.readsock.recv(count)
767        except socket.error as e:
768            if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
769                return b''
770            else:
771                raise
772
773        if data is None:
774            raise Exception("No data on read ready socket")
775        elif not data:
776            # This actually means an orderly shutdown
777            # happened. But for this code it counts as an
778            # error since the connection shouldn't go away
779            # until qemu exits.
780            if not self.canexit:
781                raise Exception("Console connection closed unexpectedly")
782            return b''
783
784        return data
785
786    def stringify_event(self, event):
787        val = ''
788        if select.POLLERR == event:
789            val = 'POLLER'
790        elif select.POLLHUP == event:
791            val = 'POLLHUP'
792        elif select.POLLNVAL == event:
793            val = 'POLLNVAL'
794        return val
795
796    def close_socket(self, sock):
797        sock.shutdown(socket.SHUT_RDWR)
798        sock.close()
799
800    def close_ignore_error(self, fd):
801        try:
802            os.close(fd)
803        except OSError:
804            pass
805