Resurrect alternative UIs

The various alternative UIs have been updated to once again be functional
with the latest bitbake internals.  Each of the UIs still have much room for
functional improvement.

In particular, they have been updated to:
 - interact with the new process based server
 - handle the current set of events and notifications fired from the server
   and its associated subsystems

(Bitbake rev: b947e7aa405966262c0614cae02e7978ec637095)

Signed-off-by: Bob Foerster <robert@erafx.com>
Signed-off-by: Richard Purdie <rpurdie@linux.intel.com>
This commit is contained in:
Bob Foerster 2010-12-17 23:20:39 +08:00 committed by Richard Purdie
parent 25ac24e02e
commit 2e0ef25a50
7 changed files with 300 additions and 91 deletions

View File

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

View File

@ -14,4 +14,4 @@ class ProgressBar(gtk.Dialog):
def update(self, x, y):
self.progress.set_fraction(float(x)/float(y))
self.progress.set_text("%d/%d (%2d %%)" % (x, y, x*100/y))
self.progress.set_text("%2d %%" % (x*100/y))

View File

@ -1,3 +1,4 @@
#
# BitBake Graphical GTK User Interface
#
@ -20,9 +21,20 @@
import gtk
import gobject
import logging
import time
import urllib
import urllib2
class Colors(object):
OK = "#ffffff"
RUNNING = "#aaffaa"
WARNING ="#f88017"
ERROR = "#ffaaaa"
class RunningBuildModel (gtk.TreeStore):
(COL_TYPE, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_ACTIVE) = (0, 1, 2, 3, 4, 5)
(COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7)
def __init__ (self):
gtk.TreeStore.__init__ (self,
gobject.TYPE_STRING,
@ -30,7 +42,8 @@ class RunningBuildModel (gtk.TreeStore):
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_BOOLEAN)
gobject.TYPE_STRING,
gobject.TYPE_INT)
class RunningBuild (gobject.GObject):
__gsignals__ = {
@ -63,32 +76,42 @@ class RunningBuild (gobject.GObject):
# for the message.
if hasattr(event, 'pid'):
pid = event.pid
if pid in self.pids_to_task:
(package, task) = self.pids_to_task[pid]
parent = self.tasks_to_iter[(package, task)]
if hasattr(event, 'process'):
pid = event.process
if isinstance(event, bb.msg.MsgBase):
# Ignore the "Running task i of n .."
if (event._message.startswith ("Running task")):
if pid and pid in self.pids_to_task:
(package, task) = self.pids_to_task[pid]
parent = self.tasks_to_iter[(package, task)]
if(isinstance(event, logging.LogRecord)):
if (event.msg.startswith ("Running task")):
return # don't add these to the list
# Set a pretty icon for the message based on it's type.
if isinstance(event, bb.msg.MsgWarn):
icon = "dialog-warning"
elif isinstance(event, bb.msg.MsgError):
if event.levelno >= logging.ERROR:
icon = "dialog-error"
color = Colors.ERROR
elif event.levelno >= logging.WARNING:
icon = "dialog-warning"
color = Colors.WARNING
else:
icon = None
color = Colors.OK
# if we know which package we belong to, we'll append onto its list.
# otherwise, we'll jump to the top of the master list
if parent:
tree_add = self.model.append
else:
tree_add = self.model.prepend
tree_add(parent,
(None,
package,
task,
event.getMessage(),
icon,
color,
0))
# Add the message to the tree either at the top level if parent is
# None otherwise as a descendent of a task.
self.model.append (parent,
(event.__class__.__name__.split()[-1], # e.g. MsgWarn, MsgError
package,
task,
event._message,
icon,
False))
elif isinstance(event, bb.build.TaskStarted):
(package, task) = (event._package, event._task)
@ -101,76 +124,142 @@ class RunningBuild (gobject.GObject):
if ((package, None) in self.tasks_to_iter):
parent = self.tasks_to_iter[(package, None)]
else:
parent = self.model.append (None, (None,
parent = self.model.prepend(None, (None,
package,
None,
"Package: %s" % (package),
None,
False))
Colors.OK,
0))
self.tasks_to_iter[(package, None)] = parent
# Because this parent package now has an active child mark it as
# such.
self.model.set(parent, self.model.COL_ICON, "gtk-execute")
# @todo if parent is already in error, don't mark it green
self.model.set(parent, self.model.COL_ICON, "gtk-execute",
self.model.COL_COLOR, Colors.RUNNING)
# Add an entry in the model for this task
i = self.model.append (parent, (None,
package,
task,
"Task: %s" % (task),
None,
False))
"gtk-execute",
Colors.RUNNING,
0))
# update the parent's active task count
num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1
self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
# Save out the iter so that we can find it when we have a message
# that we need to attach to a task.
self.tasks_to_iter[(package, task)] = i
# Mark this task as active.
self.model.set(i, self.model.COL_ICON, "gtk-execute")
elif isinstance(event, bb.build.TaskBase):
current = self.tasks_to_iter[(package, task)]
parent = self.tasks_to_iter[(package, None)]
# remove this task from the parent's active count
num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1
self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
if isinstance(event, bb.build.TaskFailed):
# Mark the task as failed
i = self.tasks_to_iter[(package, task)]
self.model.set(i, self.model.COL_ICON, "dialog-error")
# Mark the task and parent as failed
icon = "dialog-error"
color = Colors.ERROR
# Mark the parent package as failed
i = self.tasks_to_iter[(package, None)]
self.model.set(i, self.model.COL_ICON, "dialog-error")
logfile = event.logfile
if logfile and os.path.exists(logfile):
with open(logfile) as f:
logdata = f.read()
self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', Colors.OK, 0))
for i in (current, parent):
self.model.set(i, self.model.COL_ICON, icon,
self.model.COL_COLOR, color)
else:
icon = None
color = Colors.OK
# Mark the task as inactive
i = self.tasks_to_iter[(package, task)]
self.model.set(i, self.model.COL_ICON, None)
self.model.set(current, self.model.COL_ICON, icon,
self.model.COL_COLOR, color)
# Mark the parent package as inactive
# Mark the parent package as inactive, but make sure to
# preserve error and active states
i = self.tasks_to_iter[(package, None)]
self.model.set(i, self.model.COL_ICON, None)
if self.model.get(parent, self.model.COL_ICON) != 'dialog-error':
self.model.set(parent, self.model.COL_ICON, icon)
if num_active == 0:
self.model.set(parent, self.model.COL_COLOR, Colors.OK)
# Clear the iters and the pids since when the task goes away the
# pid will no longer be used for messages
del self.tasks_to_iter[(package, task)]
del self.pids_to_task[pid]
elif isinstance(event, bb.event.BuildStarted):
self.model.prepend(None, (None,
None,
None,
"Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
None,
Colors.OK,
0))
elif isinstance(event, bb.event.BuildCompleted):
failures = int (event._failures)
self.model.prepend(None, (None,
None,
None,
"Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
None,
Colors.OK,
0))
# Emit the appropriate signal depending on the number of failures
if (failures > 1):
if (failures >= 1):
self.emit ("build-failed")
else:
self.emit ("build-succeeded")
elif isinstance(event, bb.event.CacheLoadStarted) and pbar:
pbar.set_title("Loading cache")
self.progress_total = event.total
pbar.update(0, self.progress_total)
elif isinstance(event, bb.event.CacheLoadProgress) and pbar:
pbar.update(event.current, self.progress_total)
elif isinstance(event, bb.event.CacheLoadCompleted) and pbar:
pbar.update(self.progress_total, self.progress_total)
elif isinstance(event, bb.event.ParseStarted) and pbar:
pbar.set_title("Processing recipes")
self.progress_total = event.total
pbar.update(0, self.progress_total)
elif isinstance(event, bb.event.ParseProgress) and pbar:
x = event.sofar
y = event.total
if x == y:
pbar.hide()
return
pbar.update(x, y)
pbar.update(event.current, self.progress_total)
elif isinstance(event, bb.event.ParseCompleted) and pbar:
pbar.hide()
return
def do_pastebin(text):
url = 'http://pastebin.com/api_public.php'
params = {'paste_code': text, 'paste_format': 'text'}
req = urllib2.Request(url, urllib.urlencode(params))
response = urllib2.urlopen(req)
paste_url = response.read()
return paste_url
class RunningBuildTreeView (gtk.TreeView):
__gsignals__ = {
"button_press_event" : "override"
}
def __init__ (self):
gtk.TreeView.__init__ (self)
@ -181,6 +270,42 @@ class RunningBuildTreeView (gtk.TreeView):
self.append_column (col)
# The message of the build.
renderer = gtk.CellRendererText ()
col = gtk.TreeViewColumn ("Message", renderer, text=3)
self.append_column (col)
self.message_renderer = gtk.CellRendererText ()
self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3)
self.message_column.add_attribute(self.message_renderer, 'background', 5)
self.message_renderer.set_property('editable', 5)
self.append_column (self.message_column)
def do_button_press_event(self, event):
gtk.TreeView.do_button_press_event(self, event)
if event.button == 3:
selection = super(RunningBuildTreeView, self).get_selection()
(model, iter) = selection.get_selected()
if iter is not None:
can_paste = model.get(iter, model.COL_LOG)[0]
if can_paste == 'pastebin':
# build a simple menu with a pastebin option
menu = gtk.Menu()
menuitem = gtk.MenuItem("Send log to pastebin")
menu.append(menuitem)
menuitem.connect("activate", self.pastebin_handler, (model, iter))
menuitem.show()
menu.show()
menu.popup(None, None, None, event.button, event.time)
def pastebin_handler(self, widget, data):
"""
Send the log data to pastebin, then add the new paste url to the
clipboard.
"""
(model, iter) = data
paste_url = do_pastebin(model.get(iter, model.COL_MESSAGE)[0])
# @todo Provide visual feedback to the user that it is done and that
# it worked.
print paste_url
clipboard = gtk.clipboard_get()
clipboard.set_text(paste_url)
clipboard.store()

View File

@ -19,6 +19,7 @@
import gobject
import gtk
import Queue
import threading
import xmlrpclib
import bb
@ -32,6 +33,7 @@ from bb.ui.crumbs.progress import ProgressBar
(TYPE_DEP, TYPE_RDEP) = (0, 1)
(COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2)
class PackageDepView(gtk.TreeView):
def __init__(self, model, dep_type, label):
gtk.TreeView.__init__(self)
@ -52,6 +54,7 @@ class PackageDepView(gtk.TreeView):
self.current = package
self.filter_model.refilter()
class PackageReverseDepView(gtk.TreeView):
def __init__(self, model, label):
gtk.TreeView.__init__(self)
@ -69,6 +72,7 @@ class PackageReverseDepView(gtk.TreeView):
self.current = package
self.filter_model.refilter()
class DepExplorer(gtk.Window):
def __init__(self):
gtk.Window.__init__(self)
@ -90,9 +94,12 @@ class DepExplorer(gtk.Window):
scrolled = gtk.ScrolledWindow()
scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scrolled.set_shadow_type(gtk.SHADOW_IN)
self.pkg_treeview = gtk.TreeView(self.pkg_model)
self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed)
self.pkg_treeview.append_column(gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME))
column = gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME)
self.pkg_treeview.append_column(column)
column.set_sort_column_id(COL_PKG_NAME)
pane.add1(scrolled)
scrolled.add(self.pkg_treeview)
@ -158,7 +165,6 @@ class DepExplorer(gtk.Window):
def parse(depgraph, pkg_model, depends_model):
for package in depgraph["pn"]:
pkg_model.set(pkg_model.append(), COL_PKG_NAME, package)
@ -176,6 +182,7 @@ def parse(depgraph, pkg_model, depends_model):
COL_DEP_PARENT, package,
COL_DEP_PACKAGE, rdepend)
class gtkthread(threading.Thread):
quit = threading.Event()
def __init__(self, shutdown):
@ -189,8 +196,8 @@ class gtkthread(threading.Thread):
gtk.main()
gtkthread.quit.set()
def main(server, eventHandler):
def main(server, eventHandler):
try:
cmdline = server.runCommand(["getCmdLineAction"])
if not cmdline or cmdline[0] != "generateDotGraph":
@ -214,25 +221,54 @@ def main(server, eventHandler):
pbar = ProgressBar(dep)
gtk.gdk.threads_leave()
progress_total = 0
while True:
try:
event = eventHandler.waitEvent(0.25)
if gtkthread.quit.isSet():
server.runCommand(["stateStop"])
break
if event is None:
continue
if isinstance(event, bb.event.ParseProgress):
x = event.sofar
y = event.total
if x == y:
print(("\nParsing finished. %d cached, %d parsed, %d skipped, %d masked, %d errors."
% ( event.cached, event.parsed, event.skipped, event.masked, event.errors)))
pbar.hide()
continue
if isinstance(event, bb.event.CacheLoadStarted):
progress_total = event.total
gtk.gdk.threads_enter()
pbar.update(x, y)
pbar.set_title("Loading Cache")
pbar.update(0, progress_total)
gtk.gdk.threads_leave()
if isinstance(event, bb.event.CacheLoadProgress):
x = event.current
gtk.gdk.threads_enter()
pbar.update(x, progress_total)
gtk.gdk.threads_leave()
continue
if isinstance(event, bb.event.CacheLoadCompleted):
gtk.gdk.threads_enter()
pbar.update(progress_total, progress_total)
gtk.gdk.threads_leave()
continue
if isinstance(event, bb.event.ParseStarted):
progress_total = event.total
gtk.gdk.threads_enter()
pbar.set_title("Processing recipes")
pbar.update(0, progress_total)
gtk.gdk.threads_leave()
if isinstance(event, bb.event.ParseProgress):
x = event.current
gtk.gdk.threads_enter()
pbar.update(x, progress_total)
gtk.gdk.threads_leave()
continue
if isinstance(event, bb.event.ParseCompleted):
pbar.hide()
continue
if isinstance(event, bb.event.DepTreeGenerated):
@ -242,16 +278,22 @@ def main(server, eventHandler):
if isinstance(event, bb.command.CommandCompleted):
continue
if isinstance(event, bb.command.CommandFailed):
print("Command execution failed: %s" % event.error)
return event.exitcode
if isinstance(event, bb.command.CommandExit):
return event.exitcode
if isinstance(event, bb.cooker.CookerExit):
break
continue
except EnvironmentError as ioerror:
# ignore interrupted io
if ioerror.args[0] == 4:
pass
except KeyboardInterrupt:
if shutdown == 2:
print("\nThird Keyboard Interrupt, exit.\n")

View File

@ -24,19 +24,32 @@ import xmlrpclib
from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild
from bb.ui.crumbs.progress import ProgressBar
import Queue
def event_handle_idle_func (eventHandler, build, pbar):
# Consume as many messages as we can in the time available to us
event = eventHandler.getEvent()
while event:
build.handle_event (event, pbar)
event = eventHandler.getEvent()
build.handle_event (event, pbar)
return True
def scroll_tv_cb (model, path, iter, view):
view.scroll_to_cell (path)
# @todo hook these into the GUI so the user has feedback...
def running_build_failed_cb (running_build):
pass
def running_build_succeeded_cb (running_build):
pass
class MainWindow (gtk.Window):
def __init__ (self):
gtk.Window.__init__ (self, gtk.WINDOW_TOPLEVEL)
@ -49,6 +62,7 @@ class MainWindow (gtk.Window):
self.set_default_size(640, 480)
scrolled_window.add (self.cur_build_tv)
def main (server, eventHandler):
gobject.threads_init()
gtk.gdk.threads_init()
@ -61,9 +75,11 @@ def main (server, eventHandler):
running_build = RunningBuild ()
window.cur_build_tv.set_model (running_build.model)
running_build.model.connect("row-inserted", scroll_tv_cb, window.cur_build_tv)
running_build.connect ("build-succeeded", running_build_succeeded_cb)
running_build.connect ("build-failed", running_build_failed_cb)
try:
cmdline = server.runCommand(["getCmdLineAction"])
print(cmdline)
if not cmdline:
return 1
ret = server.runCommand(cmdline)
@ -76,10 +92,18 @@ def main (server, eventHandler):
# Use a timeout function for probing the event queue to find out if we
# have a message waiting for us.
gobject.timeout_add (200,
gobject.timeout_add (100,
event_handle_idle_func,
eventHandler,
running_build,
pbar)
gtk.main()
try:
gtk.main()
except EnvironmentError as ioerror:
# ignore interrupted io
if ioerror.args[0] == 4:
pass
finally:
server.runCommand(["stateStop"])

View File

@ -228,6 +228,10 @@ def main(server, eventHandler):
logger.error("Unknown event: %s", event)
except EnvironmentError as ioerror:
# ignore interrupted io
if ioerror.args[0] == 4:
pass
except KeyboardInterrupt:
if shutdown == 2:
print("\nThird Keyboard Interrupt, exit.\n")

View File

@ -44,8 +44,9 @@
"""
from __future__ import division
from __future__ import division
import logging
import os, sys, curses, itertools, time
import bb
import xmlrpclib
@ -246,29 +247,35 @@ class NCursesUI:
event = eventHandler.waitEvent(0.25)
if not event:
continue
helper.eventHandler(event)
#mw.appendText("%s\n" % event[0])
if isinstance(event, bb.build.TaskBase):
mw.appendText("NOTE: %s\n" % event._message)
if isinstance(event, bb.msg.MsgDebug):
mw.appendText('DEBUG: ' + event._message + '\n')
if isinstance(event, bb.msg.MsgNote):
mw.appendText('NOTE: ' + event._message + '\n')
if isinstance(event, bb.msg.MsgWarn):
mw.appendText('WARNING: ' + event._message + '\n')
if isinstance(event, bb.msg.MsgError):
mw.appendText('ERROR: ' + event._message + '\n')
if isinstance(event, bb.msg.MsgFatal):
mw.appendText('FATAL: ' + event._message + '\n')
if isinstance(event, logging.LogRecord):
mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n')
if isinstance(event, bb.event.CacheLoadStarted):
self.parse_total = event.total
if isinstance(event, bb.event.CacheLoadProgress):
x = event.current
y = self.parse_total
mw.setStatus("Loading Cache: %s [%2d %%]" % ( next(parsespin), x*100/y ) )
if isinstance(event, bb.event.CacheLoadCompleted):
mw.setStatus("Idle")
mw.appendText("Loaded %d entries from dependency cache.\n"
% ( event.num_entries))
if isinstance(event, bb.event.ParseStarted):
self.parse_total = event.total
if isinstance(event, bb.event.ParseProgress):
x = event.sofar
y = event.total
if x == y:
mw.setStatus("Idle")
mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked."
x = event.current
y = self.parse_total
mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) )
if isinstance(event, bb.event.ParseCompleted):
mw.setStatus("Idle")
mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n"
% ( event.cached, event.parsed, event.skipped, event.masked ))
else:
mw.setStatus("Parsing: %s (%04d/%04d) [%2d %%]" % ( next(parsespin), x, y, x*100//y ) )
# if isinstance(event, bb.build.TaskFailed):
# if event.logfile:
# if data.getVar("BBINCLUDELOGS", d):
@ -289,7 +296,9 @@ class NCursesUI:
# bb.msg.error(bb.msg.domain.Build, "see log in %s" % logfile)
if isinstance(event, bb.command.CommandCompleted):
exitflag = True
# stop so the user can see the result of the build, but
# also allow them to now exit with a single ^C
shutdown = 2
if isinstance(event, bb.command.CommandFailed):
mw.appendText("Command execution failed: %s" % event.error)
time.sleep(2)
@ -306,13 +315,18 @@ class NCursesUI:
if activetasks:
taw.appendText("Active Tasks:\n")
for task in activetasks.itervalues():
taw.appendText(task["title"])
taw.appendText(task["title"] + '\n')
if failedtasks:
taw.appendText("Failed Tasks:\n")
for task in failedtasks:
taw.appendText(task["title"])
taw.appendText(task["title"] + '\n')
curses.doupdate()
except EnvironmentError as ioerror:
# ignore interrupted io
if ioerror.args[0] == 4:
pass
except KeyboardInterrupt:
if shutdown == 2:
mw.appendText("Third Keyboard Interrupt, exit.\n")