blob: 268562770a21fee8a3d94159f9ef1e10697f56c3 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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
21from __future__ import division
22
23import os
24import sys
25import xmlrpclib
26import logging
27import progressbar
28import signal
29import bb.msg
30import time
31import fcntl
32import struct
33import copy
34import atexit
35from bb.ui import uihelper
36
37featureSet = [bb.cooker.CookerFeatures.SEND_SANITYEVENTS]
38
39logger = logging.getLogger("BitBake")
40interactive = sys.stdout.isatty()
41
42class BBProgress(progressbar.ProgressBar):
43 def __init__(self, msg, maxval):
44 self.msg = msg
45 widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ',
46 progressbar.ETA()]
47
48 try:
49 self._resize_default = signal.getsignal(signal.SIGWINCH)
50 except:
51 self._resize_default = None
52 progressbar.ProgressBar.__init__(self, maxval, [self.msg + ": "] + widgets, fd=sys.stdout)
53
54 def _handle_resize(self, signum, frame):
55 progressbar.ProgressBar._handle_resize(self, signum, frame)
56 if self._resize_default:
57 self._resize_default(signum, frame)
58 def finish(self):
59 progressbar.ProgressBar.finish(self)
60 if self._resize_default:
61 signal.signal(signal.SIGWINCH, self._resize_default)
62
63class NonInteractiveProgress(object):
64 fobj = sys.stdout
65
66 def __init__(self, msg, maxval):
67 self.msg = msg
68 self.maxval = maxval
69
70 def start(self):
71 self.fobj.write("%s..." % self.msg)
72 self.fobj.flush()
73 return self
74
75 def update(self, value):
76 pass
77
78 def finish(self):
79 self.fobj.write("done.\n")
80 self.fobj.flush()
81
82def new_progress(msg, maxval):
83 if interactive:
84 return BBProgress(msg, maxval)
85 else:
86 return NonInteractiveProgress(msg, maxval)
87
88def pluralise(singular, plural, qty):
89 if(qty == 1):
90 return singular % qty
91 else:
92 return plural % qty
93
94
95class InteractConsoleLogFilter(logging.Filter):
96 def __init__(self, tf, format):
97 self.tf = tf
98 self.format = format
99
100 def filter(self, record):
101 if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("recipe ")):
102 return False
103 self.tf.clearFooter()
104 return True
105
106class TerminalFilter(object):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500107 rows = 25
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500108 columns = 80
109
110 def sigwinch_handle(self, signum, frame):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500111 self.rows, self.columns = self.getTerminalColumns()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500112 if self._sigwinch_default:
113 self._sigwinch_default(signum, frame)
114
115 def getTerminalColumns(self):
116 def ioctl_GWINSZ(fd):
117 try:
118 cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234'))
119 except:
120 return None
121 return cr
122 cr = ioctl_GWINSZ(sys.stdout.fileno())
123 if not cr:
124 try:
125 fd = os.open(os.ctermid(), os.O_RDONLY)
126 cr = ioctl_GWINSZ(fd)
127 os.close(fd)
128 except:
129 pass
130 if not cr:
131 try:
132 cr = (env['LINES'], env['COLUMNS'])
133 except:
134 cr = (25, 80)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500135 return cr
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500136
137 def __init__(self, main, helper, console, errconsole, format):
138 self.main = main
139 self.helper = helper
140 self.cuu = None
141 self.stdinbackup = None
142 self.interactive = sys.stdout.isatty()
143 self.footer_present = False
144 self.lastpids = []
145
146 if not self.interactive:
147 return
148
149 try:
150 import curses
151 except ImportError:
152 sys.exit("FATAL: The knotty ui could not load the required curses python module.")
153
154 import termios
155 self.curses = curses
156 self.termios = termios
157 try:
158 fd = sys.stdin.fileno()
159 self.stdinbackup = termios.tcgetattr(fd)
160 new = copy.deepcopy(self.stdinbackup)
161 new[3] = new[3] & ~termios.ECHO
162 termios.tcsetattr(fd, termios.TCSADRAIN, new)
163 curses.setupterm()
164 if curses.tigetnum("colors") > 2:
165 format.enable_color()
166 self.ed = curses.tigetstr("ed")
167 if self.ed:
168 self.cuu = curses.tigetstr("cuu")
169 try:
170 self._sigwinch_default = signal.getsignal(signal.SIGWINCH)
171 signal.signal(signal.SIGWINCH, self.sigwinch_handle)
172 except:
173 pass
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500174 self.rows, self.columns = self.getTerminalColumns()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500175 except:
176 self.cuu = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500177 if not self.cuu:
178 self.interactive = False
179 bb.note("Unable to use interactive mode for this terminal, using fallback")
180 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181 console.addFilter(InteractConsoleLogFilter(self, format))
182 errconsole.addFilter(InteractConsoleLogFilter(self, format))
183
184 def clearFooter(self):
185 if self.footer_present:
186 lines = self.footer_present
187 sys.stdout.write(self.curses.tparm(self.cuu, lines))
188 sys.stdout.write(self.curses.tparm(self.ed))
189 self.footer_present = False
190
191 def updateFooter(self):
192 if not self.cuu:
193 return
194 activetasks = self.helper.running_tasks
195 failedtasks = self.helper.failed_tasks
196 runningpids = self.helper.running_pids
197 if self.footer_present and (self.lastcount == self.helper.tasknumber_current) and (self.lastpids == runningpids):
198 return
199 if self.footer_present:
200 self.clearFooter()
201 if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks):
202 return
203 tasks = []
204 for t in runningpids:
205 tasks.append("%s (pid %s)" % (activetasks[t]["title"], t))
206
207 if self.main.shutdown:
208 content = "Waiting for %s running tasks to finish:" % len(activetasks)
209 elif not len(activetasks):
210 content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total)
211 else:
212 content = "Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total)
213 print(content)
214 lines = 1 + int(len(content) / (self.columns + 1))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500215 for tasknum, task in enumerate(tasks[:(self.rows - 2)]):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500216 content = "%s: %s" % (tasknum, task)
217 print(content)
218 lines = lines + 1 + int(len(content) / (self.columns + 1))
219 self.footer_present = lines
220 self.lastpids = runningpids[:]
221 self.lastcount = self.helper.tasknumber_current
222
223 def finish(self):
224 if self.stdinbackup:
225 fd = sys.stdin.fileno()
226 self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup)
227
228def _log_settings_from_server(server):
229 # Get values of variables which control our output
230 includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
231 if error:
232 logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error)
233 raise BaseException(error)
234 loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"])
235 if error:
236 logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error)
237 raise BaseException(error)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500238 consolelogfile, error = server.runCommand(["getSetVariable", "BB_CONSOLELOG"])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500239 if error:
240 logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error)
241 raise BaseException(error)
242 return includelogs, loglines, consolelogfile
243
244_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord",
245 "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted",
246 "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted",
247 "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed",
248 "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit",
249 "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted",
250 "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed",
251 "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent"]
252
253def main(server, eventHandler, params, tf = TerminalFilter):
254
255 includelogs, loglines, consolelogfile = _log_settings_from_server(server)
256
257 if sys.stdin.isatty() and sys.stdout.isatty():
258 log_exec_tty = True
259 else:
260 log_exec_tty = False
261
262 helper = uihelper.BBUIHelper()
263
264 console = logging.StreamHandler(sys.stdout)
265 errconsole = logging.StreamHandler(sys.stderr)
266 format_str = "%(levelname)s: %(message)s"
267 format = bb.msg.BBLogFormatter(format_str)
268 bb.msg.addDefaultlogFilter(console, bb.msg.BBLogFilterStdOut)
269 bb.msg.addDefaultlogFilter(errconsole, bb.msg.BBLogFilterStdErr)
270 console.setFormatter(format)
271 errconsole.setFormatter(format)
272 logger.addHandler(console)
273 logger.addHandler(errconsole)
274
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 bb.utils.set_process_name("KnottyUI")
276
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500277 if params.options.remote_server and params.options.kill_server:
278 server.terminateServer()
279 return
280
281 if consolelogfile and not params.options.show_environment and not params.options.show_versions:
282 bb.utils.mkdirhier(os.path.dirname(consolelogfile))
283 conlogformat = bb.msg.BBLogFormatter(format_str)
284 consolelog = logging.FileHandler(consolelogfile)
285 bb.msg.addDefaultlogFilter(consolelog)
286 consolelog.setFormatter(conlogformat)
287 logger.addHandler(consolelog)
288
289 llevel, debug_domains = bb.msg.constructLogOptions()
290 server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list])
291
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500292 universe = False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500293 if not params.observe_only:
294 params.updateFromServer(server)
295 params.updateToServer(server, os.environ.copy())
296 cmdline = params.parseActions()
297 if not cmdline:
298 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
299 return 1
300 if 'msg' in cmdline and cmdline['msg']:
301 logger.error(cmdline['msg'])
302 return 1
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500303 if cmdline['action'][0] == "buildTargets" and "universe" in cmdline['action'][1]:
304 universe = True
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500305
306 ret, error = server.runCommand(cmdline['action'])
307 if error:
308 logger.error("Command '%s' failed: %s" % (cmdline, error))
309 return 1
310 elif ret != True:
311 logger.error("Command '%s' failed: returned %s" % (cmdline, ret))
312 return 1
313
314
315 parseprogress = None
316 cacheprogress = None
317 main.shutdown = 0
318 interrupted = False
319 return_value = 0
320 errors = 0
321 warnings = 0
322 taskfailures = []
323
324 termfilter = tf(main, helper, console, errconsole, format)
325 atexit.register(termfilter.finish)
326
327 while True:
328 try:
329 event = eventHandler.waitEvent(0)
330 if event is None:
331 if main.shutdown > 1:
332 break
333 termfilter.updateFooter()
334 event = eventHandler.waitEvent(0.25)
335 if event is None:
336 continue
337 helper.eventHandler(event)
338 if isinstance(event, bb.runqueue.runQueueExitWait):
339 if not main.shutdown:
340 main.shutdown = 1
341 continue
342 if isinstance(event, bb.event.LogExecTTY):
343 if log_exec_tty:
344 tries = event.retries
345 while tries:
346 print("Trying to run: %s" % event.prog)
347 if os.system(event.prog) == 0:
348 break
349 time.sleep(event.sleep_delay)
350 tries -= 1
351 if tries:
352 continue
353 logger.warn(event.msg)
354 continue
355
356 if isinstance(event, logging.LogRecord):
357 if event.levelno >= format.ERROR:
358 errors = errors + 1
359 return_value = 1
360 elif event.levelno == format.WARNING:
361 warnings = warnings + 1
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500362
363 if event.taskpid != 0:
364 # For "normal" logging conditions, don't show note logs from tasks
365 # but do show them if the user has changed the default log level to
366 # include verbose/debug messages
367 if event.levelno <= format.NOTE and (event.levelno < llevel or (event.levelno == format.NOTE and llevel != format.VERBOSE)):
368 continue
369
370 # Prefix task messages with recipe/task
371 if event.taskpid in helper.running_tasks:
372 taskinfo = helper.running_tasks[event.taskpid]
373 event.msg = taskinfo['title'] + ': ' + event.msg
374 if hasattr(event, 'fn'):
375 event.msg = event.fn + ': ' + event.msg
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500376 logger.handle(event)
377 continue
378
379 if isinstance(event, bb.build.TaskFailedSilent):
380 logger.warn("Logfile for failed setscene task is %s" % event.logfile)
381 continue
382 if isinstance(event, bb.build.TaskFailed):
383 return_value = 1
384 logfile = event.logfile
385 if logfile and os.path.exists(logfile):
386 termfilter.clearFooter()
387 bb.error("Logfile of failure stored in: %s" % logfile)
388 if includelogs and not event.errprinted:
389 print("Log data follows:")
390 f = open(logfile, "r")
391 lines = []
392 while True:
393 l = f.readline()
394 if l == '':
395 break
396 l = l.rstrip()
397 if loglines:
398 lines.append(' | %s' % l)
399 if len(lines) > int(loglines):
400 lines.pop(0)
401 else:
402 print('| %s' % l)
403 f.close()
404 if lines:
405 for line in lines:
406 print(line)
407 if isinstance(event, bb.build.TaskBase):
408 logger.info(event._message)
409 continue
410 if isinstance(event, bb.event.ParseStarted):
411 if event.total == 0:
412 continue
413 parseprogress = new_progress("Parsing recipes", event.total).start()
414 continue
415 if isinstance(event, bb.event.ParseProgress):
416 parseprogress.update(event.current)
417 continue
418 if isinstance(event, bb.event.ParseCompleted):
419 if not parseprogress:
420 continue
421
422 parseprogress.finish()
423 print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors."
424 % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors)))
425 continue
426
427 if isinstance(event, bb.event.CacheLoadStarted):
428 cacheprogress = new_progress("Loading cache", event.total).start()
429 continue
430 if isinstance(event, bb.event.CacheLoadProgress):
431 cacheprogress.update(event.current)
432 continue
433 if isinstance(event, bb.event.CacheLoadCompleted):
434 cacheprogress.finish()
435 print("Loaded %d entries from dependency cache." % event.num_entries)
436 continue
437
438 if isinstance(event, bb.command.CommandFailed):
439 return_value = event.exitcode
440 if event.error:
441 errors = errors + 1
442 logger.error("Command execution failed: %s", event.error)
443 main.shutdown = 2
444 continue
445 if isinstance(event, bb.command.CommandExit):
446 if not return_value:
447 return_value = event.exitcode
448 continue
449 if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)):
450 main.shutdown = 2
451 continue
452 if isinstance(event, bb.event.MultipleProviders):
453 logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
454 event._item,
455 ", ".join(event._candidates))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500456 rtime = ""
457 if event._is_runtime:
458 rtime = "R"
459 logger.info("consider defining a PREFERRED_%sPROVIDER entry to match %s" % (rtime, event._item))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500460 continue
461 if isinstance(event, bb.event.NoProvider):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500462 if event._runtime:
463 r = "R"
464 else:
465 r = ""
466
467 extra = ''
468 if not event._reasons:
469 if event._close_matches:
470 extra = ". Close matches:\n %s" % '\n '.join(event._close_matches)
471
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500472 # For universe builds, only show these as warnings, not errors
473 h = logger.warning
474 if not universe:
475 return_value = 1
476 errors = errors + 1
477 h = logger.error
478
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500479 if event._dependees:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500480 h("Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s", r, event._item, ", ".join(event._dependees), r, extra)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500481 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500482 h("Nothing %sPROVIDES '%s'%s", r, event._item, extra)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500483 if event._reasons:
484 for reason in event._reasons:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500485 h("%s", reason)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500486 continue
487
488 if isinstance(event, bb.runqueue.sceneQueueTaskStarted):
489 logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring))
490 continue
491
492 if isinstance(event, bb.runqueue.runQueueTaskStarted):
493 if event.noexec:
494 tasktype = 'noexec task'
495 else:
496 tasktype = 'task'
497 logger.info("Running %s %s of %s (ID: %s, %s)",
498 tasktype,
499 event.stats.completed + event.stats.active +
500 event.stats.failed + 1,
501 event.stats.total, event.taskid, event.taskstring)
502 continue
503
504 if isinstance(event, bb.runqueue.runQueueTaskFailed):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500505 return_value = 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500506 taskfailures.append(event.taskstring)
507 logger.error("Task %s (%s) failed with exit code '%s'",
508 event.taskid, event.taskstring, event.exitcode)
509 continue
510
511 if isinstance(event, bb.runqueue.sceneQueueTaskFailed):
512 logger.warn("Setscene task %s (%s) failed with exit code '%s' - real task will be run instead",
513 event.taskid, event.taskstring, event.exitcode)
514 continue
515
516 if isinstance(event, bb.event.DepTreeGenerated):
517 continue
518
519 # ignore
520 if isinstance(event, (bb.event.BuildBase,
521 bb.event.MetadataEvent,
522 bb.event.StampUpdate,
523 bb.event.ConfigParsed,
524 bb.event.RecipeParsed,
525 bb.event.RecipePreFinalise,
526 bb.runqueue.runQueueEvent,
527 bb.event.OperationStarted,
528 bb.event.OperationCompleted,
529 bb.event.OperationProgress,
530 bb.event.DiskFull)):
531 continue
532
533 logger.error("Unknown event: %s", event)
534
535 except EnvironmentError as ioerror:
536 termfilter.clearFooter()
537 # ignore interrupted io
538 if ioerror.args[0] == 4:
539 continue
540 sys.stderr.write(str(ioerror))
541 if not params.observe_only:
542 _, error = server.runCommand(["stateForceShutdown"])
543 main.shutdown = 2
544 except KeyboardInterrupt:
545 termfilter.clearFooter()
546 if params.observe_only:
547 print("\nKeyboard Interrupt, exiting observer...")
548 main.shutdown = 2
549 if not params.observe_only and main.shutdown == 1:
550 print("\nSecond Keyboard Interrupt, stopping...\n")
551 _, error = server.runCommand(["stateForceShutdown"])
552 if error:
553 logger.error("Unable to cleanly stop: %s" % error)
554 if not params.observe_only and main.shutdown == 0:
555 print("\nKeyboard Interrupt, closing down...\n")
556 interrupted = True
557 _, error = server.runCommand(["stateShutdown"])
558 if error:
559 logger.error("Unable to cleanly shutdown: %s" % error)
560 main.shutdown = main.shutdown + 1
561 pass
562 except Exception as e:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500563 import traceback
564 sys.stderr.write(traceback.format_exc())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500565 if not params.observe_only:
566 _, error = server.runCommand(["stateForceShutdown"])
567 main.shutdown = 2
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500568 return_value = 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500569 try:
570 summary = ""
571 if taskfailures:
572 summary += pluralise("\nSummary: %s task failed:",
573 "\nSummary: %s tasks failed:", len(taskfailures))
574 for failure in taskfailures:
575 summary += "\n %s" % failure
576 if warnings:
577 summary += pluralise("\nSummary: There was %s WARNING message shown.",
578 "\nSummary: There were %s WARNING messages shown.", warnings)
579 if return_value and errors:
580 summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.",
581 "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors)
582 if summary:
583 print(summary)
584
585 if interrupted:
586 print("Execution was interrupted, returning a non-zero exit code.")
587 if return_value == 0:
588 return_value = 1
589 except IOError as e:
590 import errno
591 if e.errno == errno.EPIPE:
592 pass
593
594 return return_value