diff options
Diffstat (limited to 'bitbake/lib/bb/ui/knotty.py')
-rw-r--r-- | bitbake/lib/bb/ui/knotty.py | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/bitbake/lib/bb/ui/knotty.py b/bitbake/lib/bb/ui/knotty.py new file mode 100644 index 0000000000..c1ee9f5269 --- /dev/null +++ b/bitbake/lib/bb/ui/knotty.py | |||
@@ -0,0 +1,541 @@ | |||
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 | # This program is free software; you can redistribute it and/or modify | ||
9 | # it under the terms of the GNU General Public License version 2 as | ||
10 | # published by the Free Software Foundation. | ||
11 | # | ||
12 | # This program is distributed in the hope that it will be useful, | ||
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
15 | # GNU General Public License for more details. | ||
16 | # | ||
17 | # You should have received a copy of the GNU General Public License along | ||
18 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
20 | |||
21 | from __future__ import division | ||
22 | |||
23 | import os | ||
24 | import sys | ||
25 | import xmlrpclib | ||
26 | import logging | ||
27 | import progressbar | ||
28 | import signal | ||
29 | import bb.msg | ||
30 | import time | ||
31 | import fcntl | ||
32 | import struct | ||
33 | import copy | ||
34 | from bb.ui import uihelper | ||
35 | |||
36 | logger = logging.getLogger("BitBake") | ||
37 | interactive = sys.stdout.isatty() | ||
38 | |||
39 | class BBProgress(progressbar.ProgressBar): | ||
40 | def __init__(self, msg, maxval): | ||
41 | self.msg = msg | ||
42 | widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', | ||
43 | progressbar.ETA()] | ||
44 | |||
45 | try: | ||
46 | self._resize_default = signal.getsignal(signal.SIGWINCH) | ||
47 | except: | ||
48 | self._resize_default = None | ||
49 | progressbar.ProgressBar.__init__(self, maxval, [self.msg + ": "] + widgets) | ||
50 | |||
51 | def _handle_resize(self, signum, frame): | ||
52 | progressbar.ProgressBar._handle_resize(self, signum, frame) | ||
53 | if self._resize_default: | ||
54 | self._resize_default(signum, frame) | ||
55 | def finish(self): | ||
56 | progressbar.ProgressBar.finish(self) | ||
57 | if self._resize_default: | ||
58 | signal.signal(signal.SIGWINCH, self._resize_default) | ||
59 | |||
60 | class NonInteractiveProgress(object): | ||
61 | fobj = sys.stdout | ||
62 | |||
63 | def __init__(self, msg, maxval): | ||
64 | self.msg = msg | ||
65 | self.maxval = maxval | ||
66 | |||
67 | def start(self): | ||
68 | self.fobj.write("%s..." % self.msg) | ||
69 | self.fobj.flush() | ||
70 | return self | ||
71 | |||
72 | def update(self, value): | ||
73 | pass | ||
74 | |||
75 | def finish(self): | ||
76 | self.fobj.write("done.\n") | ||
77 | self.fobj.flush() | ||
78 | |||
79 | def new_progress(msg, maxval): | ||
80 | if interactive: | ||
81 | return BBProgress(msg, maxval) | ||
82 | else: | ||
83 | return NonInteractiveProgress(msg, maxval) | ||
84 | |||
85 | def pluralise(singular, plural, qty): | ||
86 | if(qty == 1): | ||
87 | return singular % qty | ||
88 | else: | ||
89 | return plural % qty | ||
90 | |||
91 | |||
92 | class InteractConsoleLogFilter(logging.Filter): | ||
93 | def __init__(self, tf, format): | ||
94 | self.tf = tf | ||
95 | self.format = format | ||
96 | |||
97 | def filter(self, record): | ||
98 | if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("recipe ")): | ||
99 | return False | ||
100 | self.tf.clearFooter() | ||
101 | return True | ||
102 | |||
103 | class TerminalFilter(object): | ||
104 | columns = 80 | ||
105 | |||
106 | def sigwinch_handle(self, signum, frame): | ||
107 | self.columns = self.getTerminalColumns() | ||
108 | if self._sigwinch_default: | ||
109 | self._sigwinch_default(signum, frame) | ||
110 | |||
111 | def getTerminalColumns(self): | ||
112 | def ioctl_GWINSZ(fd): | ||
113 | try: | ||
114 | cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234')) | ||
115 | except: | ||
116 | return None | ||
117 | return cr | ||
118 | cr = ioctl_GWINSZ(sys.stdout.fileno()) | ||
119 | if not cr: | ||
120 | try: | ||
121 | fd = os.open(os.ctermid(), os.O_RDONLY) | ||
122 | cr = ioctl_GWINSZ(fd) | ||
123 | os.close(fd) | ||
124 | except: | ||
125 | pass | ||
126 | if not cr: | ||
127 | try: | ||
128 | cr = (env['LINES'], env['COLUMNS']) | ||
129 | except: | ||
130 | cr = (25, 80) | ||
131 | return cr[1] | ||
132 | |||
133 | def __init__(self, main, helper, console, format): | ||
134 | self.main = main | ||
135 | self.helper = helper | ||
136 | self.cuu = None | ||
137 | self.stdinbackup = None | ||
138 | self.interactive = sys.stdout.isatty() | ||
139 | self.footer_present = False | ||
140 | self.lastpids = [] | ||
141 | |||
142 | if not self.interactive: | ||
143 | return | ||
144 | |||
145 | try: | ||
146 | import curses | ||
147 | except ImportError: | ||
148 | sys.exit("FATAL: The knotty ui could not load the required curses python module.") | ||
149 | |||
150 | import termios | ||
151 | self.curses = curses | ||
152 | self.termios = termios | ||
153 | try: | ||
154 | fd = sys.stdin.fileno() | ||
155 | self.stdinbackup = termios.tcgetattr(fd) | ||
156 | new = copy.deepcopy(self.stdinbackup) | ||
157 | new[3] = new[3] & ~termios.ECHO | ||
158 | termios.tcsetattr(fd, termios.TCSADRAIN, new) | ||
159 | curses.setupterm() | ||
160 | if curses.tigetnum("colors") > 2: | ||
161 | format.enable_color() | ||
162 | self.ed = curses.tigetstr("ed") | ||
163 | if self.ed: | ||
164 | self.cuu = curses.tigetstr("cuu") | ||
165 | try: | ||
166 | self._sigwinch_default = signal.getsignal(signal.SIGWINCH) | ||
167 | signal.signal(signal.SIGWINCH, self.sigwinch_handle) | ||
168 | except: | ||
169 | pass | ||
170 | self.columns = self.getTerminalColumns() | ||
171 | except: | ||
172 | self.cuu = None | ||
173 | console.addFilter(InteractConsoleLogFilter(self, format)) | ||
174 | |||
175 | def clearFooter(self): | ||
176 | if self.footer_present: | ||
177 | lines = self.footer_present | ||
178 | sys.stdout.write(self.curses.tparm(self.cuu, lines)) | ||
179 | sys.stdout.write(self.curses.tparm(self.ed)) | ||
180 | self.footer_present = False | ||
181 | |||
182 | def updateFooter(self): | ||
183 | if not self.cuu: | ||
184 | return | ||
185 | activetasks = self.helper.running_tasks | ||
186 | failedtasks = self.helper.failed_tasks | ||
187 | runningpids = self.helper.running_pids | ||
188 | if self.footer_present and (self.lastcount == self.helper.tasknumber_current) and (self.lastpids == runningpids): | ||
189 | return | ||
190 | if self.footer_present: | ||
191 | self.clearFooter() | ||
192 | if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks): | ||
193 | return | ||
194 | tasks = [] | ||
195 | for t in runningpids: | ||
196 | tasks.append("%s (pid %s)" % (activetasks[t]["title"], t)) | ||
197 | |||
198 | if self.main.shutdown: | ||
199 | content = "Waiting for %s running tasks to finish:" % len(activetasks) | ||
200 | elif not len(activetasks): | ||
201 | content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total) | ||
202 | else: | ||
203 | content = "Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total) | ||
204 | print(content) | ||
205 | lines = 1 + int(len(content) / (self.columns + 1)) | ||
206 | for tasknum, task in enumerate(tasks): | ||
207 | content = "%s: %s" % (tasknum, task) | ||
208 | print(content) | ||
209 | lines = lines + 1 + int(len(content) / (self.columns + 1)) | ||
210 | self.footer_present = lines | ||
211 | self.lastpids = runningpids[:] | ||
212 | self.lastcount = self.helper.tasknumber_current | ||
213 | |||
214 | def finish(self): | ||
215 | if self.stdinbackup: | ||
216 | fd = sys.stdin.fileno() | ||
217 | self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup) | ||
218 | |||
219 | def _log_settings_from_server(server): | ||
220 | # Get values of variables which control our output | ||
221 | includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"]) | ||
222 | if error: | ||
223 | logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error) | ||
224 | raise BaseException(error) | ||
225 | loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"]) | ||
226 | if error: | ||
227 | logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) | ||
228 | raise BaseException(error) | ||
229 | consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"]) | ||
230 | if error: | ||
231 | logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error) | ||
232 | raise BaseException(error) | ||
233 | return includelogs, loglines, consolelogfile | ||
234 | |||
235 | _evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord", | ||
236 | "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted", | ||
237 | "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted", | ||
238 | "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed", | ||
239 | "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit", | ||
240 | "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted", | ||
241 | "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed", | ||
242 | "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent"] | ||
243 | |||
244 | def main(server, eventHandler, params, tf = TerminalFilter): | ||
245 | |||
246 | includelogs, loglines, consolelogfile = _log_settings_from_server(server) | ||
247 | |||
248 | if sys.stdin.isatty() and sys.stdout.isatty(): | ||
249 | log_exec_tty = True | ||
250 | else: | ||
251 | log_exec_tty = False | ||
252 | |||
253 | helper = uihelper.BBUIHelper() | ||
254 | |||
255 | console = logging.StreamHandler(sys.stdout) | ||
256 | format_str = "%(levelname)s: %(message)s" | ||
257 | format = bb.msg.BBLogFormatter(format_str) | ||
258 | bb.msg.addDefaultlogFilter(console) | ||
259 | console.setFormatter(format) | ||
260 | logger.addHandler(console) | ||
261 | |||
262 | if params.options.remote_server and params.options.kill_server: | ||
263 | server.terminateServer() | ||
264 | return | ||
265 | |||
266 | if consolelogfile and not params.options.show_environment: | ||
267 | bb.utils.mkdirhier(os.path.dirname(consolelogfile)) | ||
268 | conlogformat = bb.msg.BBLogFormatter(format_str) | ||
269 | consolelog = logging.FileHandler(consolelogfile) | ||
270 | bb.msg.addDefaultlogFilter(consolelog) | ||
271 | consolelog.setFormatter(conlogformat) | ||
272 | logger.addHandler(consolelog) | ||
273 | |||
274 | llevel, debug_domains = bb.msg.constructLogOptions() | ||
275 | server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list]) | ||
276 | |||
277 | if not params.observe_only: | ||
278 | params.updateFromServer(server) | ||
279 | cmdline = params.parseActions() | ||
280 | if not cmdline: | ||
281 | print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") | ||
282 | return 1 | ||
283 | if 'msg' in cmdline and cmdline['msg']: | ||
284 | logger.error(cmdline['msg']) | ||
285 | return 1 | ||
286 | |||
287 | ret, error = server.runCommand(cmdline['action']) | ||
288 | if error: | ||
289 | logger.error("Command '%s' failed: %s" % (cmdline, error)) | ||
290 | return 1 | ||
291 | elif ret != True: | ||
292 | logger.error("Command '%s' failed: returned %s" % (cmdline, ret)) | ||
293 | return 1 | ||
294 | |||
295 | |||
296 | parseprogress = None | ||
297 | cacheprogress = None | ||
298 | main.shutdown = 0 | ||
299 | interrupted = False | ||
300 | return_value = 0 | ||
301 | errors = 0 | ||
302 | warnings = 0 | ||
303 | taskfailures = [] | ||
304 | |||
305 | termfilter = tf(main, helper, console, format) | ||
306 | |||
307 | while True: | ||
308 | try: | ||
309 | event = eventHandler.waitEvent(0) | ||
310 | if event is None: | ||
311 | termfilter.updateFooter() | ||
312 | event = eventHandler.waitEvent(0.25) | ||
313 | if event is None: | ||
314 | if main.shutdown > 1: | ||
315 | break | ||
316 | continue | ||
317 | helper.eventHandler(event) | ||
318 | if isinstance(event, bb.runqueue.runQueueExitWait): | ||
319 | if not main.shutdown: | ||
320 | main.shutdown = 1 | ||
321 | |||
322 | if isinstance(event, bb.event.LogExecTTY): | ||
323 | if log_exec_tty: | ||
324 | tries = event.retries | ||
325 | while tries: | ||
326 | print("Trying to run: %s" % event.prog) | ||
327 | if os.system(event.prog) == 0: | ||
328 | break | ||
329 | time.sleep(event.sleep_delay) | ||
330 | tries -= 1 | ||
331 | if tries: | ||
332 | continue | ||
333 | logger.warn(event.msg) | ||
334 | continue | ||
335 | |||
336 | if isinstance(event, logging.LogRecord): | ||
337 | if event.levelno >= format.ERROR: | ||
338 | errors = errors + 1 | ||
339 | return_value = 1 | ||
340 | elif event.levelno == format.WARNING: | ||
341 | warnings = warnings + 1 | ||
342 | # For "normal" logging conditions, don't show note logs from tasks | ||
343 | # but do show them if the user has changed the default log level to | ||
344 | # include verbose/debug messages | ||
345 | if event.taskpid != 0 and event.levelno <= format.NOTE: | ||
346 | continue | ||
347 | logger.handle(event) | ||
348 | continue | ||
349 | |||
350 | if isinstance(event, bb.build.TaskFailed): | ||
351 | return_value = 1 | ||
352 | logfile = event.logfile | ||
353 | if logfile and os.path.exists(logfile): | ||
354 | termfilter.clearFooter() | ||
355 | bb.error("Logfile of failure stored in: %s" % logfile) | ||
356 | if includelogs and not event.errprinted: | ||
357 | print("Log data follows:") | ||
358 | f = open(logfile, "r") | ||
359 | lines = [] | ||
360 | while True: | ||
361 | l = f.readline() | ||
362 | if l == '': | ||
363 | break | ||
364 | l = l.rstrip() | ||
365 | if loglines: | ||
366 | lines.append(' | %s' % l) | ||
367 | if len(lines) > int(loglines): | ||
368 | lines.pop(0) | ||
369 | else: | ||
370 | print('| %s' % l) | ||
371 | f.close() | ||
372 | if lines: | ||
373 | for line in lines: | ||
374 | print(line) | ||
375 | if isinstance(event, bb.build.TaskBase): | ||
376 | logger.info(event._message) | ||
377 | continue | ||
378 | if isinstance(event, bb.event.ParseStarted): | ||
379 | if event.total == 0: | ||
380 | continue | ||
381 | parseprogress = new_progress("Parsing recipes", event.total).start() | ||
382 | continue | ||
383 | if isinstance(event, bb.event.ParseProgress): | ||
384 | parseprogress.update(event.current) | ||
385 | continue | ||
386 | if isinstance(event, bb.event.ParseCompleted): | ||
387 | if not parseprogress: | ||
388 | continue | ||
389 | |||
390 | parseprogress.finish() | ||
391 | print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors." | ||
392 | % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors))) | ||
393 | continue | ||
394 | |||
395 | if isinstance(event, bb.event.CacheLoadStarted): | ||
396 | cacheprogress = new_progress("Loading cache", event.total).start() | ||
397 | continue | ||
398 | if isinstance(event, bb.event.CacheLoadProgress): | ||
399 | cacheprogress.update(event.current) | ||
400 | continue | ||
401 | if isinstance(event, bb.event.CacheLoadCompleted): | ||
402 | cacheprogress.finish() | ||
403 | print("Loaded %d entries from dependency cache." % event.num_entries) | ||
404 | continue | ||
405 | |||
406 | if isinstance(event, bb.command.CommandFailed): | ||
407 | return_value = event.exitcode | ||
408 | if event.error: | ||
409 | errors = errors + 1 | ||
410 | logger.error("Command execution failed: %s", event.error) | ||
411 | main.shutdown = 2 | ||
412 | continue | ||
413 | if isinstance(event, bb.command.CommandExit): | ||
414 | if not return_value: | ||
415 | return_value = event.exitcode | ||
416 | continue | ||
417 | if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)): | ||
418 | main.shutdown = 2 | ||
419 | continue | ||
420 | if isinstance(event, bb.event.MultipleProviders): | ||
421 | logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "", | ||
422 | event._item, | ||
423 | ", ".join(event._candidates)) | ||
424 | logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item) | ||
425 | continue | ||
426 | if isinstance(event, bb.event.NoProvider): | ||
427 | return_value = 1 | ||
428 | errors = errors + 1 | ||
429 | if event._runtime: | ||
430 | r = "R" | ||
431 | else: | ||
432 | r = "" | ||
433 | |||
434 | extra = '' | ||
435 | if not event._reasons: | ||
436 | if event._close_matches: | ||
437 | extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) | ||
438 | |||
439 | if event._dependees: | ||
440 | logger.error("Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s", r, event._item, ", ".join(event._dependees), r, extra) | ||
441 | else: | ||
442 | logger.error("Nothing %sPROVIDES '%s'%s", r, event._item, extra) | ||
443 | if event._reasons: | ||
444 | for reason in event._reasons: | ||
445 | logger.error("%s", reason) | ||
446 | continue | ||
447 | |||
448 | if isinstance(event, bb.runqueue.sceneQueueTaskStarted): | ||
449 | logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring)) | ||
450 | continue | ||
451 | |||
452 | if isinstance(event, bb.runqueue.runQueueTaskStarted): | ||
453 | if event.noexec: | ||
454 | tasktype = 'noexec task' | ||
455 | else: | ||
456 | tasktype = 'task' | ||
457 | logger.info("Running %s %s of %s (ID: %s, %s)", | ||
458 | tasktype, | ||
459 | event.stats.completed + event.stats.active + | ||
460 | event.stats.failed + 1, | ||
461 | event.stats.total, event.taskid, event.taskstring) | ||
462 | continue | ||
463 | |||
464 | if isinstance(event, bb.runqueue.runQueueTaskFailed): | ||
465 | taskfailures.append(event.taskstring) | ||
466 | logger.error("Task %s (%s) failed with exit code '%s'", | ||
467 | event.taskid, event.taskstring, event.exitcode) | ||
468 | continue | ||
469 | |||
470 | if isinstance(event, bb.runqueue.sceneQueueTaskFailed): | ||
471 | logger.warn("Setscene task %s (%s) failed with exit code '%s' - real task will be run instead", | ||
472 | event.taskid, event.taskstring, event.exitcode) | ||
473 | continue | ||
474 | |||
475 | if isinstance(event, bb.event.DepTreeGenerated): | ||
476 | continue | ||
477 | |||
478 | # ignore | ||
479 | if isinstance(event, (bb.event.BuildBase, | ||
480 | bb.event.MetadataEvent, | ||
481 | bb.event.StampUpdate, | ||
482 | bb.event.ConfigParsed, | ||
483 | bb.event.RecipeParsed, | ||
484 | bb.event.RecipePreFinalise, | ||
485 | bb.runqueue.runQueueEvent, | ||
486 | bb.runqueue.runQueueExitWait, | ||
487 | bb.event.OperationStarted, | ||
488 | bb.event.OperationCompleted, | ||
489 | bb.event.OperationProgress, | ||
490 | bb.event.DiskFull)): | ||
491 | continue | ||
492 | |||
493 | logger.error("Unknown event: %s", event) | ||
494 | |||
495 | except EnvironmentError as ioerror: | ||
496 | termfilter.clearFooter() | ||
497 | # ignore interrupted io | ||
498 | if ioerror.args[0] == 4: | ||
499 | pass | ||
500 | except KeyboardInterrupt: | ||
501 | termfilter.clearFooter() | ||
502 | if params.observe_only: | ||
503 | print("\nKeyboard Interrupt, exiting observer...") | ||
504 | main.shutdown = 2 | ||
505 | if not params.observe_only and main.shutdown == 1: | ||
506 | print("\nSecond Keyboard Interrupt, stopping...\n") | ||
507 | _, error = server.runCommand(["stateForceShutdown"]) | ||
508 | if error: | ||
509 | logger.error("Unable to cleanly stop: %s" % error) | ||
510 | if not params.observe_only and main.shutdown == 0: | ||
511 | print("\nKeyboard Interrupt, closing down...\n") | ||
512 | interrupted = True | ||
513 | _, error = server.runCommand(["stateShutdown"]) | ||
514 | if error: | ||
515 | logger.error("Unable to cleanly shutdown: %s" % error) | ||
516 | main.shutdown = main.shutdown + 1 | ||
517 | pass | ||
518 | |||
519 | summary = "" | ||
520 | if taskfailures: | ||
521 | summary += pluralise("\nSummary: %s task failed:", | ||
522 | "\nSummary: %s tasks failed:", len(taskfailures)) | ||
523 | for failure in taskfailures: | ||
524 | summary += "\n %s" % failure | ||
525 | if warnings: | ||
526 | summary += pluralise("\nSummary: There was %s WARNING message shown.", | ||
527 | "\nSummary: There were %s WARNING messages shown.", warnings) | ||
528 | if return_value and errors: | ||
529 | summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.", | ||
530 | "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors) | ||
531 | if summary: | ||
532 | print(summary) | ||
533 | |||
534 | if interrupted: | ||
535 | print("Execution was interrupted, returning a non-zero exit code.") | ||
536 | if return_value == 0: | ||
537 | return_value = 1 | ||
538 | |||
539 | termfilter.finish() | ||
540 | |||
541 | return return_value | ||