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