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