# # BitBake Graphical GTK User Interface # # Copyright (C) 2008 Intel Corporation # # Authored by Rob Bradford # # 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. import gtk import gobject import logging import time import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse import pango from bb.ui.crumbs.hobcolor import HobColors from bb.ui.crumbs.hobwidget import HobWarpCellRendererText, HobCellRendererPixbuf class RunningBuildModel (gtk.TreeStore): (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = list(range(7)) def __init__ (self): gtk.TreeStore.__init__ (self, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT) def failure_model_filter(self, model, it): color = model.get(it, self.COL_COLOR)[0] if not color: return False if color == HobColors.ERROR or color == HobColors.WARNING: return True return False def failure_model(self): model = self.filter_new() model.set_visible_func(self.failure_model_filter) return model def foreach_cell_func(self, model, path, iter, usr_data=None): if model.get_value(iter, self.COL_ICON) == "gtk-execute": model.set(iter, self.COL_ICON, "") def close_task_refresh(self): self.foreach(self.foreach_cell_func, None) class RunningBuild (gobject.GObject): __gsignals__ = { 'build-started' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'build-succeeded' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'build-failed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'build-complete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'build-aborted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'task-started' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'log-error' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'log-warning' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'disk-full' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'no-provider' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'log' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)), } pids_to_task = {} tasks_to_iter = {} def __init__ (self, sequential=False): gobject.GObject.__init__ (self) self.model = RunningBuildModel() self.sequential = sequential self.buildaborted = False def reset (self): self.pids_to_task.clear() self.tasks_to_iter.clear() self.model.clear() def handle_event (self, event, pbar=None): # Handle an event from the event queue, this may result in updating # the model and thus the UI. Or it may be to tell us that the build # has finished successfully (or not, as the case may be.) parent = None pid = 0 package = None task = None # If we have a pid attached to this message/event try and get the # (package, task) pair for it. If we get that then get the parent iter # for the message. if hasattr(event, 'pid'): pid = event.pid if hasattr(event, 'process'): pid = event.process 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.taskpid == 0 or event.levelno > logging.INFO: self.emit("log", "handle", event) # FIXME: this is a hack! More info in Yocto #1433 # http://bugzilla.pokylinux.org/show_bug.cgi?id=1433, temporarily # mask the error message as it's not informative for the user. if event.msg.startswith("Execution of event handler 'run_buildstats' failed"): return if (event.levelno < logging.INFO or event.msg.startswith("Running task")): return # don't add these to the list if event.levelno >= logging.ERROR: icon = "dialog-error" color = HobColors.ERROR self.emit("log-error") elif event.levelno >= logging.WARNING: icon = "dialog-warning" color = HobColors.WARNING self.emit("log-warning") else: icon = None color = HobColors.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 self.sequential or not parent: tree_add = self.model.append else: tree_add = self.model.prepend tree_add(parent, (None, package, task, event.getMessage(), icon, color, 0)) # if there are warnings while processing a package # (parent), mark the task with warning color; # in case there are errors, the updates will be # handled on TaskFailed. if color == HobColors.WARNING and parent: self.model.set(parent, self.model.COL_COLOR, color) if task: #then we have a parent (package), and update it's color self.model.set(self.tasks_to_iter[(package, None)], self.model.COL_COLOR, color) elif isinstance(event, bb.build.TaskStarted): (package, task) = (event._package, event._task) # Save out this PID. self.pids_to_task[pid] = (package, task) # Check if we already have this package in our model. If so then # that can be the parent for the task. Otherwise we create a new # top level for the package. if ((package, None) in self.tasks_to_iter): parent = self.tasks_to_iter[(package, None)] else: if self.sequential: add = self.model.append else: add = self.model.prepend parent = add(None, (None, package, None, "Package: %s" % (package), None, HobColors.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") parent_color = self.model.get(parent, self.model.COL_COLOR)[0] if parent_color != HobColors.ERROR and parent_color != HobColors.WARNING: self.model.set(parent, self.model.COL_COLOR, HobColors.RUNNING) # Add an entry in the model for this task i = self.model.append (parent, (None, package, task, "Task: %s" % (task), "gtk-execute", HobColors.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 elif isinstance(event, bb.build.TaskBase): self.emit("log", "info", event._message) 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 and parent as failed icon = "dialog-error" color = HobColors.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', HobColors.OK, 0)) for i in (current, parent): self.model.set(i, self.model.COL_ICON, icon, self.model.COL_COLOR, color) else: # Mark the parent package and the task as inactive, # but make sure to preserve error, warnings and active # states parent_color = self.model.get(parent, self.model.COL_COLOR)[0] task_color = self.model.get(current, self.model.COL_COLOR)[0] # Mark the task as inactive self.model.set(current, self.model.COL_ICON, None) if task_color != HobColors.ERROR: if task_color == HobColors.WARNING: self.model.set(current, self.model.COL_ICON, 'dialog-warning') else: self.model.set(current, self.model.COL_COLOR, HobColors.OK) # Mark the parent as inactive if parent_color != HobColors.ERROR: if parent_color == HobColors.WARNING: self.model.set(parent, self.model.COL_ICON, "dialog-warning") else: self.model.set(parent, self.model.COL_ICON, None) if num_active == 0: self.model.set(parent, self.model.COL_COLOR, HobColors.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.emit("build-started") self.model.prepend(None, (None, None, None, "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), None, HobColors.OK, 0)) if pbar: pbar.update(0, self.progress_total) pbar.set_title(bb.event.getName(event)) 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, HobColors.OK, 0)) # Emit the appropriate signal depending on the number of failures if self.buildaborted: self.emit ("build-aborted") self.buildaborted = False elif (failures >= 1): self.emit ("build-failed") else: self.emit ("build-succeeded") # Emit a generic "build-complete" signal for things wishing to # handle when the build is finished self.emit("build-complete") # reset the all cell's icon indicator self.model.close_task_refresh() if pbar: pbar.set_text(event.msg) elif isinstance(event, bb.event.DiskFull): self.buildaborted = True self.emit("disk-full") elif isinstance(event, bb.command.CommandFailed): self.emit("log", "error", "Command execution failed: %s" % (event.error)) if event.error.startswith("Exited with"): # If the command fails with an exit code we're done, emit the # generic signal for the UI to notify the user self.emit("build-complete") # reset the all cell's icon indicator self.model.close_task_refresh() 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) pbar.hide() elif isinstance(event, bb.event.ParseStarted) and pbar: if event.total == 0: return pbar.set_title("Processing recipes") self.progress_total = event.total pbar.update(0, self.progress_total) elif isinstance(event, bb.event.ParseProgress) and pbar: pbar.update(event.current, self.progress_total) elif isinstance(event, bb.event.ParseCompleted) and pbar: pbar.hide() #using runqueue events as many as possible to update the progress bar elif isinstance(event, bb.runqueue.runQueueTaskFailed): self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode)) elif isinstance(event, bb.runqueue.sceneQueueTaskFailed): self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \ % (event.taskid, event.taskstring, event.exitcode)) elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)): if isinstance(event, bb.runqueue.sceneQueueTaskStarted): self.emit("log", "info", "Running setscene task %d of %d (%s)" % \ (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring)) else: if event.noexec: tasktype = 'noexec task' else: tasktype = 'task' self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \ (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskid, event.taskstring)) message = {} message["eventname"] = bb.event.getName(event) num_of_completed = event.stats.completed + event.stats.failed message["current"] = num_of_completed message["total"] = event.stats.total message["title"] = "" message["task"] = event.taskstring self.emit("task-started", message) elif isinstance(event, bb.event.MultipleProviders): self.emit("log", "info", "multiple providers are available for %s%s (%s)" \ % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates))) self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item)) elif isinstance(event, bb.event.NoProvider): msg = "" if event._runtime: r = "R" else: r = "" extra = '' if not event._reasons: if event._close_matches: extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) if event._dependees: msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s\n" % (r, event._item, ", ".join(event._dependees), r, extra) else: msg = "Nothing %sPROVIDES '%s'%s\n" % (r, event._item, extra) if event._reasons: for reason in event._reasons: msg += ("%s\n" % reason) self.emit("no-provider", msg) self.emit("log", "error", msg) elif isinstance(event, bb.event.LogExecTTY): icon = "dialog-warning" color = HobColors.WARNING if self.sequential or not parent: tree_add = self.model.append else: tree_add = self.model.prepend tree_add(parent, (None, package, task, event.msg, icon, color, 0)) else: if not isinstance(event, (bb.event.BuildBase, bb.event.StampUpdate, bb.event.ConfigParsed, bb.event.RecipeParsed, bb.event.RecipePreFinalise, bb.runqueue.runQueueEvent, bb.runqueue.runQueueExitWait, bb.event.OperationStarted, bb.event.OperationCompleted, bb.event.OperationProgress)): self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error')) return def do_pastebin(text): url = 'http://pastebin.com/api_public.php' params = {'paste_code': text, 'paste_format': 'text'} req = urllib.request.Request(url, urllib.parse.urlencode(params)) response = urllib.request.urlopen(req) paste_url = response.read() return paste_url class RunningBuildTreeView (gtk.TreeView): __gsignals__ = { "button_press_event" : "override" } def __init__ (self, readonly=False, hob=False): gtk.TreeView.__init__ (self) self.readonly = readonly # The icon that indicates whether we're building or failed. # add 'hob' flag because there has not only hob to share this code if hob: renderer = HobCellRendererPixbuf () else: renderer = gtk.CellRendererPixbuf() col = gtk.TreeViewColumn ("Status", renderer) col.add_attribute (renderer, "icon-name", 4) self.append_column (col) # The message of the build. # add 'hob' flag because there has not only hob to share this code if hob: self.message_renderer = HobWarpCellRendererText (col_number=1) else: 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', (not self.readonly)) 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, it) = selection.get_selected() if it is not None: can_paste = model.get(it, model.COL_LOG)[0] if can_paste == 'pastebin': # build a simple menu with a pastebin option menu = gtk.Menu() menuitem = gtk.MenuItem("Copy") menu.append(menuitem) menuitem.connect("activate", self.clipboard_handler, (model, it)) menuitem.show() menuitem = gtk.MenuItem("Send log to pastebin") menu.append(menuitem) menuitem.connect("activate", self.pastebin_handler, (model, it)) menuitem.show() menu.show() menu.popup(None, None, None, event.button, event.time) def _add_to_clipboard(self, clipping): """ Add the contents of clipping to the system clipboard. """ clipboard = gtk.clipboard_get() clipboard.set_text(clipping) clipboard.store() def pastebin_handler(self, widget, data): """ Send the log data to pastebin, then add the new paste url to the clipboard. """ (model, it) = data paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0]) # @todo Provide visual feedback to the user that it is done and that # it worked. print(paste_url) self._add_to_clipboard(paste_url) def clipboard_handler(self, widget, data): """ """ (model, it) = data message = model.get(it, model.COL_MESSAGE)[0] self._add_to_clipboard(message) class BuildFailureTreeView(gtk.TreeView): def __init__ (self): gtk.TreeView.__init__(self) self.set_rules_hint(False) self.set_headers_visible(False) self.get_selection().set_mode(gtk.SELECTION_SINGLE) # The icon that indicates whether we're building or failed. renderer = HobCellRendererPixbuf () col = gtk.TreeViewColumn ("Status", renderer) col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON) self.append_column (col) # The message of the build. self.message_renderer = HobWarpCellRendererText (col_number=1) self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR) self.append_column (self.message_column)