blob: 2bee242eb0403866d2fdde430e9215ce1ddb58ab [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):
107 columns = 80
108
109 def sigwinch_handle(self, signum, frame):
110 self.columns = self.getTerminalColumns()
111 if self._sigwinch_default:
112 self._sigwinch_default(signum, frame)
113
114 def getTerminalColumns(self):
115 def ioctl_GWINSZ(fd):
116 try:
117 cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234'))
118 except:
119 return None
120 return cr
121 cr = ioctl_GWINSZ(sys.stdout.fileno())
122 if not cr:
123 try:
124 fd = os.open(os.ctermid(), os.O_RDONLY)
125 cr = ioctl_GWINSZ(fd)
126 os.close(fd)
127 except:
128 pass
129 if not cr:
130 try:
131 cr = (env['LINES'], env['COLUMNS'])
132 except:
133 cr = (25, 80)
134 return cr[1]
135
136 def __init__(self, main, helper, console, errconsole, format):
137 self.main = main
138 self.helper = helper
139 self.cuu = None
140 self.stdinbackup = None
141 self.interactive = sys.stdout.isatty()
142 self.footer_present = False
143 self.lastpids = []
144
145 if not self.interactive:
146 return
147
148 try:
149 import curses
150 except ImportError:
151 sys.exit("FATAL: The knotty ui could not load the required curses python module.")
152
153 import termios
154 self.curses = curses
155 self.termios = termios
156 try:
157 fd = sys.stdin.fileno()
158 self.stdinbackup = termios.tcgetattr(fd)
159 new = copy.deepcopy(self.stdinbackup)
160 new[3] = new[3] & ~termios.ECHO
161 termios.tcsetattr(fd, termios.TCSADRAIN, new)
162 curses.setupterm()
163 if curses.tigetnum("colors") > 2:
164 format.enable_color()
165 self.ed = curses.tigetstr("ed")
166 if self.ed:
167 self.cuu = curses.tigetstr("cuu")
168 try:
169 self._sigwinch_default = signal.getsignal(signal.SIGWINCH)
170 signal.signal(signal.SIGWINCH, self.sigwinch_handle)
171 except:
172 pass
173 self.columns = self.getTerminalColumns()
174 except:
175 self.cuu = None
176 console.addFilter(InteractConsoleLogFilter(self, format))
177 errconsole.addFilter(InteractConsoleLogFilter(self, format))
178
179 def clearFooter(self):
180 if self.footer_present:
181 lines = self.footer_present
182 sys.stdout.write(self.curses.tparm(self.cuu, lines))
183 sys.stdout.write(self.curses.tparm(self.ed))
184 self.footer_present = False
185
186 def updateFooter(self):
187 if not self.cuu:
188 return
189 activetasks = self.helper.running_tasks
190 failedtasks = self.helper.failed_tasks
191 runningpids = self.helper.running_pids
192 if self.footer_present and (self.lastcount == self.helper.tasknumber_current) and (self.lastpids == runningpids):
193 return
194 if self.footer_present:
195 self.clearFooter()
196 if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks):
197 return
198 tasks = []
199 for t in runningpids:
200 tasks.append("%s (pid %s)" % (activetasks[t]["title"], t))
201
202 if self.main.shutdown:
203 content = "Waiting for %s running tasks to finish:" % len(activetasks)
204 elif not len(activetasks):
205 content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total)
206 else:
207 content = "Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total)
208 print(content)
209 lines = 1 + int(len(content) / (self.columns + 1))
210 for tasknum, task in enumerate(tasks):
211 content = "%s: %s" % (tasknum, task)
212 print(content)
213 lines = lines + 1 + int(len(content) / (self.columns + 1))
214 self.footer_present = lines
215 self.lastpids = runningpids[:]
216 self.lastcount = self.helper.tasknumber_current
217
218 def finish(self):
219 if self.stdinbackup:
220 fd = sys.stdin.fileno()
221 self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup)
222
223def _log_settings_from_server(server):
224 # Get values of variables which control our output
225 includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
226 if error:
227 logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error)
228 raise BaseException(error)
229 loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"])
230 if error:
231 logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error)
232 raise BaseException(error)
233 consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"])
234 if error:
235 logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error)
236 raise BaseException(error)
237 return includelogs, loglines, consolelogfile
238
239_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord",
240 "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted",
241 "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted",
242 "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed",
243 "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit",
244 "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted",
245 "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed",
246 "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent"]
247
248def main(server, eventHandler, params, tf = TerminalFilter):
249
250 includelogs, loglines, consolelogfile = _log_settings_from_server(server)
251
252 if sys.stdin.isatty() and sys.stdout.isatty():
253 log_exec_tty = True
254 else:
255 log_exec_tty = False
256
257 helper = uihelper.BBUIHelper()
258
259 console = logging.StreamHandler(sys.stdout)
260 errconsole = logging.StreamHandler(sys.stderr)
261 format_str = "%(levelname)s: %(message)s"
262 format = bb.msg.BBLogFormatter(format_str)
263 bb.msg.addDefaultlogFilter(console, bb.msg.BBLogFilterStdOut)
264 bb.msg.addDefaultlogFilter(errconsole, bb.msg.BBLogFilterStdErr)
265 console.setFormatter(format)
266 errconsole.setFormatter(format)
267 logger.addHandler(console)
268 logger.addHandler(errconsole)
269
270 if params.options.remote_server and params.options.kill_server:
271 server.terminateServer()
272 return
273
274 if consolelogfile and not params.options.show_environment and not params.options.show_versions:
275 bb.utils.mkdirhier(os.path.dirname(consolelogfile))
276 conlogformat = bb.msg.BBLogFormatter(format_str)
277 consolelog = logging.FileHandler(consolelogfile)
278 bb.msg.addDefaultlogFilter(consolelog)
279 consolelog.setFormatter(conlogformat)
280 logger.addHandler(consolelog)
281
282 llevel, debug_domains = bb.msg.constructLogOptions()
283 server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list])
284
285 if not params.observe_only:
286 params.updateFromServer(server)
287 params.updateToServer(server, os.environ.copy())
288 cmdline = params.parseActions()
289 if not cmdline:
290 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
291 return 1
292 if 'msg' in cmdline and cmdline['msg']:
293 logger.error(cmdline['msg'])
294 return 1
295
296 ret, error = server.runCommand(cmdline['action'])
297 if error:
298 logger.error("Command '%s' failed: %s" % (cmdline, error))
299 return 1
300 elif ret != True:
301 logger.error("Command '%s' failed: returned %s" % (cmdline, ret))
302 return 1
303
304
305 parseprogress = None
306 cacheprogress = None
307 main.shutdown = 0
308 interrupted = False
309 return_value = 0
310 errors = 0
311 warnings = 0
312 taskfailures = []
313
314 termfilter = tf(main, helper, console, errconsole, format)
315 atexit.register(termfilter.finish)
316
317 while True:
318 try:
319 event = eventHandler.waitEvent(0)
320 if event is None:
321 if main.shutdown > 1:
322 break
323 termfilter.updateFooter()
324 event = eventHandler.waitEvent(0.25)
325 if event is None:
326 continue
327 helper.eventHandler(event)
328 if isinstance(event, bb.runqueue.runQueueExitWait):
329 if not main.shutdown:
330 main.shutdown = 1
331 continue
332 if isinstance(event, bb.event.LogExecTTY):
333 if log_exec_tty:
334 tries = event.retries
335 while tries:
336 print("Trying to run: %s" % event.prog)
337 if os.system(event.prog) == 0:
338 break
339 time.sleep(event.sleep_delay)
340 tries -= 1
341 if tries:
342 continue
343 logger.warn(event.msg)
344 continue
345
346 if isinstance(event, logging.LogRecord):
347 if event.levelno >= format.ERROR:
348 errors = errors + 1
349 return_value = 1
350 elif event.levelno == format.WARNING:
351 warnings = warnings + 1
352 # For "normal" logging conditions, don't show note logs from tasks
353 # but do show them if the user has changed the default log level to
354 # include verbose/debug messages
355 if event.taskpid != 0 and event.levelno <= format.NOTE and (event.levelno < llevel or (event.levelno == format.NOTE and llevel != format.VERBOSE)):
356 continue
357 logger.handle(event)
358 continue
359
360 if isinstance(event, bb.build.TaskFailedSilent):
361 logger.warn("Logfile for failed setscene task is %s" % event.logfile)
362 continue
363 if isinstance(event, bb.build.TaskFailed):
364 return_value = 1
365 logfile = event.logfile
366 if logfile and os.path.exists(logfile):
367 termfilter.clearFooter()
368 bb.error("Logfile of failure stored in: %s" % logfile)
369 if includelogs and not event.errprinted:
370 print("Log data follows:")
371 f = open(logfile, "r")
372 lines = []
373 while True:
374 l = f.readline()
375 if l == '':
376 break
377 l = l.rstrip()
378 if loglines:
379 lines.append(' | %s' % l)
380 if len(lines) > int(loglines):
381 lines.pop(0)
382 else:
383 print('| %s' % l)
384 f.close()
385 if lines:
386 for line in lines:
387 print(line)
388 if isinstance(event, bb.build.TaskBase):
389 logger.info(event._message)
390 continue
391 if isinstance(event, bb.event.ParseStarted):
392 if event.total == 0:
393 continue
394 parseprogress = new_progress("Parsing recipes", event.total).start()
395 continue
396 if isinstance(event, bb.event.ParseProgress):
397 parseprogress.update(event.current)
398 continue
399 if isinstance(event, bb.event.ParseCompleted):
400 if not parseprogress:
401 continue
402
403 parseprogress.finish()
404 print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors."
405 % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors)))
406 continue
407
408 if isinstance(event, bb.event.CacheLoadStarted):
409 cacheprogress = new_progress("Loading cache", event.total).start()
410 continue
411 if isinstance(event, bb.event.CacheLoadProgress):
412 cacheprogress.update(event.current)
413 continue
414 if isinstance(event, bb.event.CacheLoadCompleted):
415 cacheprogress.finish()
416 print("Loaded %d entries from dependency cache." % event.num_entries)
417 continue
418
419 if isinstance(event, bb.command.CommandFailed):
420 return_value = event.exitcode
421 if event.error:
422 errors = errors + 1
423 logger.error("Command execution failed: %s", event.error)
424 main.shutdown = 2
425 continue
426 if isinstance(event, bb.command.CommandExit):
427 if not return_value:
428 return_value = event.exitcode
429 continue
430 if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)):
431 main.shutdown = 2
432 continue
433 if isinstance(event, bb.event.MultipleProviders):
434 logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
435 event._item,
436 ", ".join(event._candidates))
437 logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item)
438 continue
439 if isinstance(event, bb.event.NoProvider):
440 return_value = 1
441 errors = errors + 1
442 if event._runtime:
443 r = "R"
444 else:
445 r = ""
446
447 extra = ''
448 if not event._reasons:
449 if event._close_matches:
450 extra = ". Close matches:\n %s" % '\n '.join(event._close_matches)
451
452 if event._dependees:
453 logger.error("Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s", r, event._item, ", ".join(event._dependees), r, extra)
454 else:
455 logger.error("Nothing %sPROVIDES '%s'%s", r, event._item, extra)
456 if event._reasons:
457 for reason in event._reasons:
458 logger.error("%s", reason)
459 continue
460
461 if isinstance(event, bb.runqueue.sceneQueueTaskStarted):
462 logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring))
463 continue
464
465 if isinstance(event, bb.runqueue.runQueueTaskStarted):
466 if event.noexec:
467 tasktype = 'noexec task'
468 else:
469 tasktype = 'task'
470 logger.info("Running %s %s of %s (ID: %s, %s)",
471 tasktype,
472 event.stats.completed + event.stats.active +
473 event.stats.failed + 1,
474 event.stats.total, event.taskid, event.taskstring)
475 continue
476
477 if isinstance(event, bb.runqueue.runQueueTaskFailed):
478 taskfailures.append(event.taskstring)
479 logger.error("Task %s (%s) failed with exit code '%s'",
480 event.taskid, event.taskstring, event.exitcode)
481 continue
482
483 if isinstance(event, bb.runqueue.sceneQueueTaskFailed):
484 logger.warn("Setscene task %s (%s) failed with exit code '%s' - real task will be run instead",
485 event.taskid, event.taskstring, event.exitcode)
486 continue
487
488 if isinstance(event, bb.event.DepTreeGenerated):
489 continue
490
491 # ignore
492 if isinstance(event, (bb.event.BuildBase,
493 bb.event.MetadataEvent,
494 bb.event.StampUpdate,
495 bb.event.ConfigParsed,
496 bb.event.RecipeParsed,
497 bb.event.RecipePreFinalise,
498 bb.runqueue.runQueueEvent,
499 bb.event.OperationStarted,
500 bb.event.OperationCompleted,
501 bb.event.OperationProgress,
502 bb.event.DiskFull)):
503 continue
504
505 logger.error("Unknown event: %s", event)
506
507 except EnvironmentError as ioerror:
508 termfilter.clearFooter()
509 # ignore interrupted io
510 if ioerror.args[0] == 4:
511 continue
512 sys.stderr.write(str(ioerror))
513 if not params.observe_only:
514 _, error = server.runCommand(["stateForceShutdown"])
515 main.shutdown = 2
516 except KeyboardInterrupt:
517 termfilter.clearFooter()
518 if params.observe_only:
519 print("\nKeyboard Interrupt, exiting observer...")
520 main.shutdown = 2
521 if not params.observe_only and main.shutdown == 1:
522 print("\nSecond Keyboard Interrupt, stopping...\n")
523 _, error = server.runCommand(["stateForceShutdown"])
524 if error:
525 logger.error("Unable to cleanly stop: %s" % error)
526 if not params.observe_only and main.shutdown == 0:
527 print("\nKeyboard Interrupt, closing down...\n")
528 interrupted = True
529 _, error = server.runCommand(["stateShutdown"])
530 if error:
531 logger.error("Unable to cleanly shutdown: %s" % error)
532 main.shutdown = main.shutdown + 1
533 pass
534 except Exception as e:
535 sys.stderr.write(str(e))
536 if not params.observe_only:
537 _, error = server.runCommand(["stateForceShutdown"])
538 main.shutdown = 2
539 try:
540 summary = ""
541 if taskfailures:
542 summary += pluralise("\nSummary: %s task failed:",
543 "\nSummary: %s tasks failed:", len(taskfailures))
544 for failure in taskfailures:
545 summary += "\n %s" % failure
546 if warnings:
547 summary += pluralise("\nSummary: There was %s WARNING message shown.",
548 "\nSummary: There were %s WARNING messages shown.", warnings)
549 if return_value and errors:
550 summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.",
551 "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors)
552 if summary:
553 print(summary)
554
555 if interrupted:
556 print("Execution was interrupted, returning a non-zero exit code.")
557 if return_value == 0:
558 return_value = 1
559 except IOError as e:
560 import errno
561 if e.errno == errno.EPIPE:
562 pass
563
564 return return_value