1# 2# BitBake (No)TTY UI Implementation 3# 4# Handling output to TTYs or files (no TTY) 5# 6# Copyright (C) 2006-2012 Richard Purdie 7# 8# SPDX-License-Identifier: GPL-2.0-only 9# 10 11from __future__ import division 12 13import os 14import sys 15import xmlrpc.client as xmlrpclib 16import logging 17import progressbar 18import signal 19import bb.msg 20import time 21import fcntl 22import struct 23import copy 24import atexit 25 26from bb.ui import uihelper 27 28featureSet = [bb.cooker.CookerFeatures.SEND_SANITYEVENTS] 29 30logger = logging.getLogger("BitBake") 31interactive = sys.stdout.isatty() 32 33class BBProgress(progressbar.ProgressBar): 34 def __init__(self, msg, maxval, widgets=None, extrapos=-1, resize_handler=None): 35 self.msg = msg 36 self.extrapos = extrapos 37 if not widgets: 38 widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', 39 progressbar.ETA()] 40 self.extrapos = 4 41 42 if resize_handler: 43 self._resize_default = resize_handler 44 else: 45 self._resize_default = signal.getsignal(signal.SIGWINCH) 46 progressbar.ProgressBar.__init__(self, maxval, [self.msg + ": "] + widgets, fd=sys.stdout) 47 48 def _handle_resize(self, signum=None, frame=None): 49 progressbar.ProgressBar._handle_resize(self, signum, frame) 50 if self._resize_default: 51 self._resize_default(signum, frame) 52 53 def finish(self): 54 progressbar.ProgressBar.finish(self) 55 if self._resize_default: 56 signal.signal(signal.SIGWINCH, self._resize_default) 57 58 def setmessage(self, msg): 59 self.msg = msg 60 self.widgets[0] = msg 61 62 def setextra(self, extra): 63 if self.extrapos > -1: 64 if extra: 65 extrastr = str(extra) 66 if extrastr[0] != ' ': 67 extrastr = ' ' + extrastr 68 else: 69 extrastr = '' 70 self.widgets[self.extrapos] = extrastr 71 72 def _need_update(self): 73 # We always want the bar to print when update() is called 74 return True 75 76class NonInteractiveProgress(object): 77 fobj = sys.stdout 78 79 def __init__(self, msg, maxval): 80 self.msg = msg 81 self.maxval = maxval 82 self.finished = False 83 84 def start(self, update=True): 85 self.fobj.write("%s..." % self.msg) 86 self.fobj.flush() 87 return self 88 89 def update(self, value): 90 pass 91 92 def finish(self): 93 if self.finished: 94 return 95 self.fobj.write("done.\n") 96 self.fobj.flush() 97 self.finished = True 98 99def new_progress(msg, maxval): 100 if interactive: 101 return BBProgress(msg, maxval) 102 else: 103 return NonInteractiveProgress(msg, maxval) 104 105def pluralise(singular, plural, qty): 106 if(qty == 1): 107 return singular % qty 108 else: 109 return plural % qty 110 111 112class InteractConsoleLogFilter(logging.Filter): 113 def __init__(self, tf, format): 114 self.tf = tf 115 self.format = format 116 117 def filter(self, record): 118 if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("recipe ")): 119 return False 120 self.tf.clearFooter() 121 return True 122 123class TerminalFilter(object): 124 rows = 25 125 columns = 80 126 127 def sigwinch_handle(self, signum, frame): 128 self.rows, self.columns = self.getTerminalColumns() 129 if self._sigwinch_default: 130 self._sigwinch_default(signum, frame) 131 132 def getTerminalColumns(self): 133 def ioctl_GWINSZ(fd): 134 try: 135 cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234')) 136 except: 137 return None 138 return cr 139 cr = ioctl_GWINSZ(sys.stdout.fileno()) 140 if not cr: 141 try: 142 fd = os.open(os.ctermid(), os.O_RDONLY) 143 cr = ioctl_GWINSZ(fd) 144 os.close(fd) 145 except: 146 pass 147 if not cr: 148 try: 149 cr = (env['LINES'], env['COLUMNS']) 150 except: 151 cr = (25, 80) 152 return cr 153 154 def __init__(self, main, helper, console, errconsole, format, quiet): 155 self.main = main 156 self.helper = helper 157 self.cuu = None 158 self.stdinbackup = None 159 self.interactive = sys.stdout.isatty() 160 self.footer_present = False 161 self.lastpids = [] 162 self.lasttime = None 163 self.quiet = quiet 164 165 if not self.interactive: 166 return 167 168 try: 169 import curses 170 except ImportError: 171 sys.exit("FATAL: The knotty ui could not load the required curses python module.") 172 173 import termios 174 self.curses = curses 175 self.termios = termios 176 try: 177 fd = sys.stdin.fileno() 178 self.stdinbackup = termios.tcgetattr(fd) 179 new = copy.deepcopy(self.stdinbackup) 180 new[3] = new[3] & ~termios.ECHO 181 termios.tcsetattr(fd, termios.TCSADRAIN, new) 182 curses.setupterm() 183 if curses.tigetnum("colors") > 2: 184 format.enable_color() 185 self.ed = curses.tigetstr("ed") 186 if self.ed: 187 self.cuu = curses.tigetstr("cuu") 188 try: 189 self._sigwinch_default = signal.getsignal(signal.SIGWINCH) 190 signal.signal(signal.SIGWINCH, self.sigwinch_handle) 191 except: 192 pass 193 self.rows, self.columns = self.getTerminalColumns() 194 except: 195 self.cuu = None 196 if not self.cuu: 197 self.interactive = False 198 bb.note("Unable to use interactive mode for this terminal, using fallback") 199 return 200 if console: 201 console.addFilter(InteractConsoleLogFilter(self, format)) 202 if errconsole: 203 errconsole.addFilter(InteractConsoleLogFilter(self, format)) 204 205 self.main_progress = None 206 207 def clearFooter(self): 208 if self.footer_present: 209 lines = self.footer_present 210 sys.stdout.buffer.write(self.curses.tparm(self.cuu, lines)) 211 sys.stdout.buffer.write(self.curses.tparm(self.ed)) 212 sys.stdout.flush() 213 self.footer_present = False 214 215 def elapsed(self, sec): 216 hrs = int(sec / 3600.0) 217 sec -= hrs * 3600 218 min = int(sec / 60.0) 219 sec -= min * 60 220 if hrs > 0: 221 return "%dh%dm%ds" % (hrs, min, sec) 222 elif min > 0: 223 return "%dm%ds" % (min, sec) 224 else: 225 return "%ds" % (sec) 226 227 def keepAlive(self, t): 228 if not self.cuu: 229 print("Bitbake still alive (%ds)" % t) 230 sys.stdout.flush() 231 232 def updateFooter(self): 233 if not self.cuu: 234 return 235 activetasks = self.helper.running_tasks 236 failedtasks = self.helper.failed_tasks 237 runningpids = self.helper.running_pids 238 currenttime = time.time() 239 if not self.lasttime or (currenttime - self.lasttime > 5): 240 self.helper.needUpdate = True 241 self.lasttime = currenttime 242 if self.footer_present and not self.helper.needUpdate: 243 return 244 self.helper.needUpdate = False 245 if self.footer_present: 246 self.clearFooter() 247 if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks): 248 return 249 tasks = [] 250 for t in runningpids: 251 progress = activetasks[t].get("progress", None) 252 if progress is not None: 253 pbar = activetasks[t].get("progressbar", None) 254 rate = activetasks[t].get("rate", None) 255 start_time = activetasks[t].get("starttime", None) 256 if not pbar or pbar.bouncing != (progress < 0): 257 if progress < 0: 258 pbar = BBProgress("0: %s (pid %s) " % (activetasks[t]["title"], t), 100, widgets=[progressbar.BouncingSlider(), ''], extrapos=2, resize_handler=self.sigwinch_handle) 259 pbar.bouncing = True 260 else: 261 pbar = BBProgress("0: %s (pid %s) " % (activetasks[t]["title"], t), 100, widgets=[progressbar.Percentage(), ' ', progressbar.Bar(), ''], extrapos=4, resize_handler=self.sigwinch_handle) 262 pbar.bouncing = False 263 activetasks[t]["progressbar"] = pbar 264 tasks.append((pbar, progress, rate, start_time)) 265 else: 266 start_time = activetasks[t].get("starttime", None) 267 if start_time: 268 tasks.append("%s - %s (pid %s)" % (activetasks[t]["title"], self.elapsed(currenttime - start_time), t)) 269 else: 270 tasks.append("%s (pid %s)" % (activetasks[t]["title"], t)) 271 272 if self.main.shutdown: 273 content = "Waiting for %s running tasks to finish:" % len(activetasks) 274 print(content) 275 else: 276 if self.quiet: 277 content = "Running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total) 278 elif not len(activetasks): 279 content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total) 280 else: 281 content = "Currently %2s running tasks (%s of %s)" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total) 282 maxtask = self.helper.tasknumber_total 283 if not self.main_progress or self.main_progress.maxval != maxtask: 284 widgets = [' ', progressbar.Percentage(), ' ', progressbar.Bar()] 285 self.main_progress = BBProgress("Running tasks", maxtask, widgets=widgets, resize_handler=self.sigwinch_handle) 286 self.main_progress.start(False) 287 self.main_progress.setmessage(content) 288 progress = self.helper.tasknumber_current - 1 289 if progress < 0: 290 progress = 0 291 content = self.main_progress.update(progress) 292 print('') 293 lines = 1 + int(len(content) / (self.columns + 1)) 294 if self.quiet == 0: 295 for tasknum, task in enumerate(tasks[:(self.rows - 2)]): 296 if isinstance(task, tuple): 297 pbar, progress, rate, start_time = task 298 if not pbar.start_time: 299 pbar.start(False) 300 if start_time: 301 pbar.start_time = start_time 302 pbar.setmessage('%s:%s' % (tasknum, pbar.msg.split(':', 1)[1])) 303 pbar.setextra(rate) 304 if progress > -1: 305 content = pbar.update(progress) 306 else: 307 content = pbar.update(1) 308 print('') 309 else: 310 content = "%s: %s" % (tasknum, task) 311 print(content) 312 lines = lines + 1 + int(len(content) / (self.columns + 1)) 313 self.footer_present = lines 314 self.lastpids = runningpids[:] 315 self.lastcount = self.helper.tasknumber_current 316 317 def finish(self): 318 if self.stdinbackup: 319 fd = sys.stdin.fileno() 320 self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup) 321 322def print_event_log(event, includelogs, loglines, termfilter): 323 # FIXME refactor this out further 324 logfile = event.logfile 325 if logfile and os.path.exists(logfile): 326 termfilter.clearFooter() 327 bb.error("Logfile of failure stored in: %s" % logfile) 328 if includelogs and not event.errprinted: 329 print("Log data follows:") 330 f = open(logfile, "r") 331 lines = [] 332 while True: 333 l = f.readline() 334 if l == '': 335 break 336 l = l.rstrip() 337 if loglines: 338 lines.append(' | %s' % l) 339 if len(lines) > int(loglines): 340 lines.pop(0) 341 else: 342 print('| %s' % l) 343 f.close() 344 if lines: 345 for line in lines: 346 print(line) 347 348def _log_settings_from_server(server, observe_only): 349 # Get values of variables which control our output 350 includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"]) 351 if error: 352 logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error) 353 raise BaseException(error) 354 loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"]) 355 if error: 356 logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) 357 raise BaseException(error) 358 if observe_only: 359 cmd = 'getVariable' 360 else: 361 cmd = 'getSetVariable' 362 consolelogfile, error = server.runCommand([cmd, "BB_CONSOLELOG"]) 363 if error: 364 logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error) 365 raise BaseException(error) 366 return includelogs, loglines, consolelogfile 367 368_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord", 369 "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted", 370 "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted", 371 "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed", 372 "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit", 373 "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted", 374 "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed", 375 "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent", 376 "bb.build.TaskProgress", "bb.event.ProcessStarted", "bb.event.ProcessProgress", "bb.event.ProcessFinished"] 377 378def main(server, eventHandler, params, tf = TerminalFilter): 379 380 if not params.observe_only: 381 params.updateToServer(server, os.environ.copy()) 382 383 includelogs, loglines, consolelogfile = _log_settings_from_server(server, params.observe_only) 384 385 if sys.stdin.isatty() and sys.stdout.isatty(): 386 log_exec_tty = True 387 else: 388 log_exec_tty = False 389 390 helper = uihelper.BBUIHelper() 391 392 console = logging.StreamHandler(sys.stdout) 393 errconsole = logging.StreamHandler(sys.stderr) 394 format_str = "%(levelname)s: %(message)s" 395 format = bb.msg.BBLogFormatter(format_str) 396 if params.options.quiet == 0: 397 forcelevel = None 398 elif params.options.quiet > 2: 399 forcelevel = bb.msg.BBLogFormatter.ERROR 400 else: 401 forcelevel = bb.msg.BBLogFormatter.WARNING 402 bb.msg.addDefaultlogFilter(console, bb.msg.BBLogFilterStdOut, forcelevel) 403 bb.msg.addDefaultlogFilter(errconsole, bb.msg.BBLogFilterStdErr) 404 console.setFormatter(format) 405 errconsole.setFormatter(format) 406 if not bb.msg.has_console_handler(logger): 407 logger.addHandler(console) 408 logger.addHandler(errconsole) 409 410 bb.utils.set_process_name("KnottyUI") 411 412 if params.options.remote_server and params.options.kill_server: 413 server.terminateServer() 414 return 415 416 consolelog = None 417 if consolelogfile and not params.options.show_environment and not params.options.show_versions: 418 bb.utils.mkdirhier(os.path.dirname(consolelogfile)) 419 conlogformat = bb.msg.BBLogFormatter(format_str) 420 consolelog = logging.FileHandler(consolelogfile) 421 bb.msg.addDefaultlogFilter(consolelog) 422 consolelog.setFormatter(conlogformat) 423 logger.addHandler(consolelog) 424 loglink = os.path.join(os.path.dirname(consolelogfile), 'console-latest.log') 425 bb.utils.remove(loglink) 426 try: 427 os.symlink(os.path.basename(consolelogfile), loglink) 428 except OSError: 429 pass 430 431 llevel, debug_domains = bb.msg.constructLogOptions() 432 server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list]) 433 434 universe = False 435 if not params.observe_only: 436 params.updateFromServer(server) 437 cmdline = params.parseActions() 438 if not cmdline: 439 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") 440 return 1 441 if 'msg' in cmdline and cmdline['msg']: 442 logger.error(cmdline['msg']) 443 return 1 444 if cmdline['action'][0] == "buildTargets" and "universe" in cmdline['action'][1]: 445 universe = True 446 447 ret, error = server.runCommand(cmdline['action']) 448 if error: 449 logger.error("Command '%s' failed: %s" % (cmdline, error)) 450 return 1 451 elif ret != True: 452 logger.error("Command '%s' failed: returned %s" % (cmdline, ret)) 453 return 1 454 455 456 parseprogress = None 457 cacheprogress = None 458 main.shutdown = 0 459 interrupted = False 460 return_value = 0 461 errors = 0 462 warnings = 0 463 taskfailures = [] 464 465 printinterval = 5000 466 lastprint = time.time() 467 468 termfilter = tf(main, helper, console, errconsole, format, params.options.quiet) 469 atexit.register(termfilter.finish) 470 471 while True: 472 try: 473 if (lastprint + printinterval) <= time.time(): 474 termfilter.keepAlive(printinterval) 475 printinterval += 5000 476 event = eventHandler.waitEvent(0) 477 if event is None: 478 if main.shutdown > 1: 479 break 480 termfilter.updateFooter() 481 event = eventHandler.waitEvent(0.25) 482 if event is None: 483 continue 484 helper.eventHandler(event) 485 if isinstance(event, bb.runqueue.runQueueExitWait): 486 if not main.shutdown: 487 main.shutdown = 1 488 continue 489 if isinstance(event, bb.event.LogExecTTY): 490 if log_exec_tty: 491 tries = event.retries 492 while tries: 493 print("Trying to run: %s" % event.prog) 494 if os.system(event.prog) == 0: 495 break 496 time.sleep(event.sleep_delay) 497 tries -= 1 498 if tries: 499 continue 500 logger.warning(event.msg) 501 continue 502 503 if isinstance(event, logging.LogRecord): 504 lastprint = time.time() 505 printinterval = 5000 506 if event.levelno >= format.ERROR: 507 errors = errors + 1 508 return_value = 1 509 elif event.levelno == format.WARNING: 510 warnings = warnings + 1 511 512 if event.taskpid != 0: 513 # For "normal" logging conditions, don't show note logs from tasks 514 # but do show them if the user has changed the default log level to 515 # include verbose/debug messages 516 if event.levelno <= format.NOTE and (event.levelno < llevel or (event.levelno == format.NOTE and llevel != format.VERBOSE)): 517 continue 518 519 # Prefix task messages with recipe/task 520 if event.taskpid in helper.running_tasks and event.levelno != format.PLAIN: 521 taskinfo = helper.running_tasks[event.taskpid] 522 event.msg = taskinfo['title'] + ': ' + event.msg 523 if hasattr(event, 'fn'): 524 event.msg = event.fn + ': ' + event.msg 525 logger.handle(event) 526 continue 527 528 if isinstance(event, bb.build.TaskFailedSilent): 529 logger.warning("Logfile for failed setscene task is %s" % event.logfile) 530 continue 531 if isinstance(event, bb.build.TaskFailed): 532 return_value = 1 533 print_event_log(event, includelogs, loglines, termfilter) 534 if isinstance(event, bb.build.TaskBase): 535 logger.info(event._message) 536 continue 537 if isinstance(event, bb.event.ParseStarted): 538 if params.options.quiet > 1: 539 continue 540 if event.total == 0: 541 continue 542 parseprogress = new_progress("Parsing recipes", event.total).start() 543 continue 544 if isinstance(event, bb.event.ParseProgress): 545 if params.options.quiet > 1: 546 continue 547 if parseprogress: 548 parseprogress.update(event.current) 549 else: 550 bb.warn("Got ParseProgress event for parsing that never started?") 551 continue 552 if isinstance(event, bb.event.ParseCompleted): 553 if params.options.quiet > 1: 554 continue 555 if not parseprogress: 556 continue 557 parseprogress.finish() 558 pasreprogress = None 559 if params.options.quiet == 0: 560 print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors." 561 % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors))) 562 continue 563 564 if isinstance(event, bb.event.CacheLoadStarted): 565 if params.options.quiet > 1: 566 continue 567 cacheprogress = new_progress("Loading cache", event.total).start() 568 continue 569 if isinstance(event, bb.event.CacheLoadProgress): 570 if params.options.quiet > 1: 571 continue 572 cacheprogress.update(event.current) 573 continue 574 if isinstance(event, bb.event.CacheLoadCompleted): 575 if params.options.quiet > 1: 576 continue 577 cacheprogress.finish() 578 if params.options.quiet == 0: 579 print("Loaded %d entries from dependency cache." % event.num_entries) 580 continue 581 582 if isinstance(event, bb.command.CommandFailed): 583 return_value = event.exitcode 584 if event.error: 585 errors = errors + 1 586 logger.error(str(event)) 587 main.shutdown = 2 588 continue 589 if isinstance(event, bb.command.CommandExit): 590 if not return_value: 591 return_value = event.exitcode 592 continue 593 if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)): 594 main.shutdown = 2 595 continue 596 if isinstance(event, bb.event.MultipleProviders): 597 logger.info(str(event)) 598 continue 599 if isinstance(event, bb.event.NoProvider): 600 # For universe builds, only show these as warnings, not errors 601 if not universe: 602 return_value = 1 603 errors = errors + 1 604 logger.error(str(event)) 605 else: 606 logger.warning(str(event)) 607 continue 608 609 if isinstance(event, bb.runqueue.sceneQueueTaskStarted): 610 logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring)) 611 continue 612 613 if isinstance(event, bb.runqueue.runQueueTaskStarted): 614 if event.noexec: 615 tasktype = 'noexec task' 616 else: 617 tasktype = 'task' 618 logger.info("Running %s %d of %d (%s)", 619 tasktype, 620 event.stats.completed + event.stats.active + 621 event.stats.failed + 1, 622 event.stats.total, event.taskstring) 623 continue 624 625 if isinstance(event, bb.runqueue.runQueueTaskFailed): 626 return_value = 1 627 taskfailures.append(event.taskstring) 628 logger.error(str(event)) 629 continue 630 631 if isinstance(event, bb.runqueue.sceneQueueTaskFailed): 632 logger.warning(str(event)) 633 continue 634 635 if isinstance(event, bb.event.DepTreeGenerated): 636 continue 637 638 if isinstance(event, bb.event.ProcessStarted): 639 if params.options.quiet > 1: 640 continue 641 parseprogress = new_progress(event.processname, event.total) 642 parseprogress.start(False) 643 continue 644 if isinstance(event, bb.event.ProcessProgress): 645 if params.options.quiet > 1: 646 continue 647 if parseprogress: 648 parseprogress.update(event.progress) 649 else: 650 bb.warn("Got ProcessProgress event for someting that never started?") 651 continue 652 if isinstance(event, bb.event.ProcessFinished): 653 if params.options.quiet > 1: 654 continue 655 if parseprogress: 656 parseprogress.finish() 657 parseprogress = None 658 continue 659 660 # ignore 661 if isinstance(event, (bb.event.BuildBase, 662 bb.event.MetadataEvent, 663 bb.event.ConfigParsed, 664 bb.event.MultiConfigParsed, 665 bb.event.RecipeParsed, 666 bb.event.RecipePreFinalise, 667 bb.runqueue.runQueueEvent, 668 bb.event.OperationStarted, 669 bb.event.OperationCompleted, 670 bb.event.OperationProgress, 671 bb.event.DiskFull, 672 bb.event.HeartbeatEvent, 673 bb.build.TaskProgress)): 674 continue 675 676 logger.error("Unknown event: %s", event) 677 678 except EnvironmentError as ioerror: 679 termfilter.clearFooter() 680 # ignore interrupted io 681 if ioerror.args[0] == 4: 682 continue 683 sys.stderr.write(str(ioerror)) 684 if not params.observe_only: 685 _, error = server.runCommand(["stateForceShutdown"]) 686 main.shutdown = 2 687 except KeyboardInterrupt: 688 termfilter.clearFooter() 689 if params.observe_only: 690 print("\nKeyboard Interrupt, exiting observer...") 691 main.shutdown = 2 692 693 def state_force_shutdown(): 694 print("\nSecond Keyboard Interrupt, stopping...\n") 695 _, error = server.runCommand(["stateForceShutdown"]) 696 if error: 697 logger.error("Unable to cleanly stop: %s" % error) 698 699 if not params.observe_only and main.shutdown == 1: 700 state_force_shutdown() 701 702 if not params.observe_only and main.shutdown == 0: 703 print("\nKeyboard Interrupt, closing down...\n") 704 interrupted = True 705 # Capture the second KeyboardInterrupt during stateShutdown is running 706 try: 707 _, error = server.runCommand(["stateShutdown"]) 708 if error: 709 logger.error("Unable to cleanly shutdown: %s" % error) 710 except KeyboardInterrupt: 711 state_force_shutdown() 712 713 main.shutdown = main.shutdown + 1 714 pass 715 except Exception as e: 716 import traceback 717 sys.stderr.write(traceback.format_exc()) 718 if not params.observe_only: 719 _, error = server.runCommand(["stateForceShutdown"]) 720 main.shutdown = 2 721 return_value = 1 722 try: 723 termfilter.clearFooter() 724 summary = "" 725 if taskfailures: 726 summary += pluralise("\nSummary: %s task failed:", 727 "\nSummary: %s tasks failed:", len(taskfailures)) 728 for failure in taskfailures: 729 summary += "\n %s" % failure 730 if warnings: 731 summary += pluralise("\nSummary: There was %s WARNING message shown.", 732 "\nSummary: There were %s WARNING messages shown.", warnings) 733 if return_value and errors: 734 summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.", 735 "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors) 736 if summary and params.options.quiet == 0: 737 print(summary) 738 739 if interrupted: 740 print("Execution was interrupted, returning a non-zero exit code.") 741 if return_value == 0: 742 return_value = 1 743 except IOError as e: 744 import errno 745 if e.errno == errno.EPIPE: 746 pass 747 748 if consolelog: 749 logger.removeHandler(consolelog) 750 consolelog.close() 751 752 return return_value 753