xref: /openbmc/openbmc/poky/bitbake/lib/bb/ui/knotty.py (revision e1615142)
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