ui/knotty: Add a footer to the build output for interactive terminals as knotty2 UI

On terminals which support it, add summary information to the end of the
build output about the number of tasks currently running and how many tasks
we've run so far.

This provides a summary at a glace of what the current state of the build is
and what the build is currently doing which is lacking in the current UI.

Also disable echo of characters on stdin since this corrupts the disable,
particularly Crtl+C.

The "waiting for X tasks" code can be merged into this code too since
that is only useful on interactive terminals and this improves the
readability of that output too.

Improvements since v0:

* The tasks are ordered in execution order.
* The display is only updated when the list of tasks changes or there
  is output above the footer.
* Running task x oy y and package messages are supressed from the console

This UI can be accessed with "bitbake -u knotty2".

(From Poky rev: e38b4569648f2916c4370871c79e6a6090eb8bc1)

(Bitbake rev: 156189c799d2bb1f69bdaa04b5cd718fe7881425)

Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Richard Purdie 2011-11-26 13:37:52 +00:00
parent 5b84c902eb
commit eeb0529e13
3 changed files with 162 additions and 19 deletions

View File

@ -69,7 +69,7 @@ def get_ui(config):
return getattr(module, interface).main return getattr(module, interface).main
except AttributeError: except AttributeError:
sys.exit("FATAL: Invalid user interface '%s' specified.\n" sys.exit("FATAL: Invalid user interface '%s' specified.\n"
"Valid interfaces: depexp, goggle, ncurses, knotty [default]." % interface) "Valid interfaces: depexp, goggle, ncurses, hob, knotty [default], knotty2." % interface)
# Display bitbake/OE warnings via the BitBake.Warnings logger, ignoring others""" # Display bitbake/OE warnings via the BitBake.Warnings logger, ignoring others"""

View File

@ -3,7 +3,7 @@
# #
# Handling output to TTYs or files (no TTY) # Handling output to TTYs or files (no TTY)
# #
# Copyright (C) 2006-2007 Richard Purdie # Copyright (C) 2006-2012 Richard Purdie
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as # it under the terms of the GNU General Public License version 2 as
@ -70,7 +70,39 @@ def pluralise(singular, plural, qty):
else: else:
return plural % qty return plural % qty
def main(server, eventHandler): class TerminalFilter(object):
def __init__(self, main, helper, console, format):
self.main = main
self.helper = helper
def clearFooter(self):
return
def updateFooter(self):
if not main.shutdown or not self.helper.needUpdate:
return
activetasks = self.helper.running_tasks
runningpids = self.helper.running_pids
if len(runningpids) == 0:
return
tasks = []
for t in runningpids:
tasks.append("%s (pid %s)" % (activetasks[t]["title"], t))
if main.shutdown:
print("Waiting for %s running tasks to finish:" % len(activetasks))
else:
print("Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total))
for tasknum, task in enumerate(tasks):
print("%s: %s" % (tasknum, task))
def finish(self):
return
def main(server, eventHandler, tf = TerminalFilter):
# Get values of variables which control our output # Get values of variables which control our output
includelogs = server.runCommand(["getVariable", "BBINCLUDELOGS"]) includelogs = server.runCommand(["getVariable", "BBINCLUDELOGS"])
@ -106,32 +138,29 @@ def main(server, eventHandler):
print("XMLRPC Fault getting commandline:\n %s" % x) print("XMLRPC Fault getting commandline:\n %s" % x)
return 1 return 1
parseprogress = None parseprogress = None
cacheprogress = None cacheprogress = None
shutdown = 0 main.shutdown = 0
interrupted = False interrupted = False
return_value = 0 return_value = 0
errors = 0 errors = 0
warnings = 0 warnings = 0
taskfailures = [] taskfailures = []
termfilter = tf(main, helper, console, format)
while True: while True:
try: try:
termfilter.updateFooter()
event = eventHandler.waitEvent(0.25) event = eventHandler.waitEvent(0.25)
if event is None: if event is None:
if shutdown > 1: if main.shutdown > 1:
break break
continue continue
helper.eventHandler(event) helper.eventHandler(event)
if isinstance(event, bb.runqueue.runQueueExitWait): if isinstance(event, bb.runqueue.runQueueExitWait):
if not shutdown: if not main.shutdown:
shutdown = 1 main.shutdown = 1
if shutdown and helper.needUpdate:
activetasks, failedtasks = helper.getTasks()
if activetasks:
print("Waiting for %s active tasks to finish:" % len(activetasks))
for tasknum, task in enumerate(activetasks):
print("%s: %s (pid %s)" % (tasknum, activetasks[task]["title"], task))
if isinstance(event, logging.LogRecord): if isinstance(event, logging.LogRecord):
if event.levelno >= format.ERROR: if event.levelno >= format.ERROR:
@ -151,6 +180,7 @@ def main(server, eventHandler):
return_value = 1 return_value = 1
logfile = event.logfile logfile = event.logfile
if logfile and os.path.exists(logfile): if logfile and os.path.exists(logfile):
termfilter.clearFooter()
print("ERROR: Logfile of failure stored in: %s" % logfile) print("ERROR: Logfile of failure stored in: %s" % logfile)
if includelogs and not event.errprinted: if includelogs and not event.errprinted:
print("Log data follows:") print("Log data follows:")
@ -206,14 +236,14 @@ def main(server, eventHandler):
return_value = event.exitcode return_value = event.exitcode
errors = errors + 1 errors = errors + 1
logger.error("Command execution failed: %s", event.error) logger.error("Command execution failed: %s", event.error)
shutdown = 2 main.shutdown = 2
continue continue
if isinstance(event, bb.command.CommandExit): if isinstance(event, bb.command.CommandExit):
if not return_value: if not return_value:
return_value = event.exitcode return_value = event.exitcode
continue continue
if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)): if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)):
shutdown = 2 main.shutdown = 2
continue continue
if isinstance(event, bb.event.MultipleProviders): if isinstance(event, bb.event.MultipleProviders):
logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "", logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
@ -281,18 +311,20 @@ def main(server, eventHandler):
logger.error("Unknown event: %s", event) logger.error("Unknown event: %s", event)
except EnvironmentError as ioerror: except EnvironmentError as ioerror:
termfilter.clearFooter()
# ignore interrupted io # ignore interrupted io
if ioerror.args[0] == 4: if ioerror.args[0] == 4:
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
if shutdown == 1: termfilter.clearFooter()
if main.shutdown == 1:
print("\nSecond Keyboard Interrupt, stopping...\n") print("\nSecond Keyboard Interrupt, stopping...\n")
server.runCommand(["stateStop"]) server.runCommand(["stateStop"])
if shutdown == 0: if main.shutdown == 0:
interrupted = True interrupted = True
print("\nKeyboard Interrupt, closing down...\n") print("\nKeyboard Interrupt, closing down...\n")
server.runCommand(["stateShutdown"]) server.runCommand(["stateShutdown"])
shutdown = shutdown + 1 main.shutdown = main.shutdown + 1
pass pass
summary = "" summary = ""
@ -315,4 +347,6 @@ def main(server, eventHandler):
if return_value == 0: if return_value == 0:
return_value = 1 return_value = 1
termfilter.finish()
return return_value return return_value

View File

@ -0,0 +1,109 @@
#
# BitBake (No)TTY UI Implementation (v2)
#
# Handling output to TTYs or files (no TTY)
#
# Copyright (C) 2012 Richard Purdie
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from bb.ui import knotty
import logging
import sys
logger = logging.getLogger("BitBake")
class InteractConsoleLogFilter(logging.Filter):
def __init__(self, tf, format):
self.tf = tf
self.format = format
def filter(self, record):
if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("package ")):
return False
self.tf.clearFooter()
return True
class TerminalFilter2(object):
def __init__(self, main, helper, console, format):
self.main = main
self.helper = helper
self.cuu = None
self.stdinbackup = None
self.interactive = sys.stdout.isatty()
self.footer_present = False
self.lastpids = []
if not self.interactive:
return
import curses
import termios
import copy
self.curses = curses
self.termios = termios
try:
fd = sys.stdin.fileno()
self.stdinbackup = termios.tcgetattr(fd)
new = copy.deepcopy(self.stdinbackup)
new[3] = new[3] & ~termios.ECHO
termios.tcsetattr(fd, termios.TCSADRAIN, new)
curses.setupterm()
self.ed = curses.tigetstr("ed")
if self.ed:
self.cuu = curses.tigetstr("cuu")
except:
self.cuu = None
console.addFilter(InteractConsoleLogFilter(self, format))
def clearFooter(self):
if self.footer_present:
lines = self.footer_present
sys.stdout.write(self.curses.tparm(self.cuu, lines))
sys.stdout.write(self.curses.tparm(self.ed))
self.footer_present = False
def updateFooter(self):
if not self.cuu:
return
activetasks = self.helper.running_tasks
failedtasks = self.helper.failed_tasks
runningpids = self.helper.running_pids
if self.footer_present and (self.lastpids == runningpids):
return
if self.footer_present:
self.clearFooter()
if not activetasks:
return
lines = 1
tasks = []
for t in runningpids:
tasks.append("%s (pid %s)" % (activetasks[t]["title"], t))
if self.main.shutdown:
print("Waiting for %s running tasks to finish:" % len(activetasks))
else:
print("Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total))
for tasknum, task in enumerate(tasks):
print("%s: %s" % (tasknum, task))
lines = lines + 1
self.footer_present = lines
self.lastpids = runningpids[:]
def finish(self):
if self.stdinbackup:
fd = sys.stdin.fileno()
self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup)
def main(server, eventHandler):
bb.ui.knotty.main(server, eventHandler, TerminalFilter2)