event.py, knotty.py, ncurses.py, runningbuild.py: Add support for LogExecTTY event
[bitbake.git] / lib / bb / ui / crumbs / runningbuild.py
1
2 #
3 # BitBake Graphical GTK User Interface
4 #
5 # Copyright (C) 2008        Intel Corporation
6 #
7 # Authored by Rob Bradford <rob@linux.intel.com>
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License version 2 as
11 # published by the Free Software Foundation.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License along
19 # with this program; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22 import gtk
23 import gobject
24 import logging
25 import time
26 import urllib
27 import urllib2
28 import pango
29 from bb.ui.crumbs.hobcolor import HobColors
30 from bb.ui.crumbs.hobwidget import HobWarpCellRendererText, HobCellRendererPixbuf
31
32 class RunningBuildModel (gtk.TreeStore):
33     (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7)
34
35     def __init__ (self):
36         gtk.TreeStore.__init__ (self,
37                                 gobject.TYPE_STRING,
38                                 gobject.TYPE_STRING,
39                                 gobject.TYPE_STRING,
40                                 gobject.TYPE_STRING,
41                                 gobject.TYPE_STRING,
42                                 gobject.TYPE_STRING,
43                                 gobject.TYPE_INT)
44
45     def failure_model_filter(self, model, it):
46         color = model.get(it, self.COL_COLOR)[0]
47         if not color:
48             return False
49         if color == HobColors.ERROR:
50             return True
51         return False
52
53     def failure_model(self):
54         model = self.filter_new()
55         model.set_visible_func(self.failure_model_filter)
56         return model
57
58     def foreach_cell_func(self, model, path, iter, usr_data=None):
59         if model.get_value(iter, self.COL_ICON) == "gtk-execute":
60             model.set(iter, self.COL_ICON, "")
61
62     def close_task_refresh(self):
63         self.foreach(self.foreach_cell_func, None)
64
65 class RunningBuild (gobject.GObject):
66     __gsignals__ = {
67           'build-started'   :  (gobject.SIGNAL_RUN_LAST,
68                                 gobject.TYPE_NONE,
69                                ()),
70           'build-succeeded' :  (gobject.SIGNAL_RUN_LAST,
71                                 gobject.TYPE_NONE,
72                                ()),
73           'build-failed'    :  (gobject.SIGNAL_RUN_LAST,
74                                 gobject.TYPE_NONE,
75                                ()),
76           'build-complete'  :  (gobject.SIGNAL_RUN_LAST,
77                                 gobject.TYPE_NONE,
78                                ()),
79           'build-aborted'     :  (gobject.SIGNAL_RUN_LAST,
80                                 gobject.TYPE_NONE,
81                                ()),
82           'task-started'    :  (gobject.SIGNAL_RUN_LAST,
83                                 gobject.TYPE_NONE,
84                                (gobject.TYPE_PYOBJECT,)),
85           'log-error'       :  (gobject.SIGNAL_RUN_LAST,
86                                 gobject.TYPE_NONE,
87                                ()),
88           'no-provider'     :  (gobject.SIGNAL_RUN_LAST,
89                                 gobject.TYPE_NONE,
90                                (gobject.TYPE_PYOBJECT,)),
91           'log'             :  (gobject.SIGNAL_RUN_LAST,
92                                 gobject.TYPE_NONE,
93                                (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)),
94           }
95     pids_to_task = {}
96     tasks_to_iter = {}
97
98     def __init__ (self, sequential=False):
99         gobject.GObject.__init__ (self)
100         self.model = RunningBuildModel()
101         self.sequential = sequential
102         self.buildaborted = False
103
104     def reset (self):
105         self.pids_to_task.clear()
106         self.tasks_to_iter.clear()
107         self.model.clear()
108
109     def handle_event (self, event, pbar=None):
110         # Handle an event from the event queue, this may result in updating
111         # the model and thus the UI. Or it may be to tell us that the build
112         # has finished successfully (or not, as the case may be.)
113
114         parent = None
115         pid = 0
116         package = None
117         task = None
118
119         # If we have a pid attached to this message/event try and get the
120         # (package, task) pair for it. If we get that then get the parent iter
121         # for the message.
122         if hasattr(event, 'pid'):
123             pid = event.pid
124         if hasattr(event, 'process'):
125             pid = event.process
126
127         if pid and pid in self.pids_to_task:
128             (package, task) = self.pids_to_task[pid]
129             parent = self.tasks_to_iter[(package, task)]
130
131         if(isinstance(event, logging.LogRecord)):
132             if event.taskpid == 0 or event.levelno > logging.INFO:
133                 self.emit("log", "handle", event)
134             # FIXME: this is a hack! More info in Yocto #1433
135             # http://bugzilla.pokylinux.org/show_bug.cgi?id=1433, temporarily
136             # mask the error message as it's not informative for the user.
137             if event.msg.startswith("Execution of event handler 'run_buildstats' failed"):
138                 return
139
140             if (event.levelno < logging.INFO or
141                 event.msg.startswith("Running task")):
142                 return # don't add these to the list
143
144             if event.levelno >= logging.ERROR:
145                 icon = "dialog-error"
146                 color = HobColors.ERROR
147                 self.emit("log-error")
148             elif event.levelno >= logging.WARNING:
149                 icon = "dialog-warning"
150                 color = HobColors.WARNING
151             else:
152                 icon = None
153                 color = HobColors.OK
154
155             # if we know which package we belong to, we'll append onto its list.
156             # otherwise, we'll jump to the top of the master list
157             if self.sequential or not parent:
158                 tree_add = self.model.append
159             else:
160                 tree_add = self.model.prepend
161             tree_add(parent,
162                      (None,
163                       package,
164                       task,
165                       event.getMessage(),
166                       icon,
167                       color,
168                       0))
169
170         elif isinstance(event, bb.build.TaskStarted):
171             (package, task) = (event._package, event._task)
172
173             # Save out this PID.
174             self.pids_to_task[pid] = (package, task)
175
176             # Check if we already have this package in our model. If so then
177             # that can be the parent for the task. Otherwise we create a new
178             # top level for the package.
179             if ((package, None) in self.tasks_to_iter):
180                 parent = self.tasks_to_iter[(package, None)]
181             else:
182                 if self.sequential:
183                     add = self.model.append
184                 else:
185                     add = self.model.prepend
186                 parent = add(None, (None,
187                                     package,
188                                     None,
189                                     "Package: %s" % (package),
190                                     None,
191                                     HobColors.OK,
192                                     0))
193                 self.tasks_to_iter[(package, None)] = parent
194
195             # Because this parent package now has an active child mark it as
196             # such.
197             # @todo if parent is already in error, don't mark it green
198             self.model.set(parent, self.model.COL_ICON, "gtk-execute",
199                            self.model.COL_COLOR, HobColors.RUNNING)
200
201             # Add an entry in the model for this task
202             i = self.model.append (parent, (None,
203                                             package,
204                                             task,
205                                             "Task: %s" % (task),
206                                             "gtk-execute",
207                                             HobColors.RUNNING,
208                                             0))
209
210             # update the parent's active task count
211             num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1
212             self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
213
214             # Save out the iter so that we can find it when we have a message
215             # that we need to attach to a task.
216             self.tasks_to_iter[(package, task)] = i
217
218         elif isinstance(event, bb.build.TaskBase):
219             self.emit("log", "info", event._message)
220             current = self.tasks_to_iter[(package, task)]
221             parent = self.tasks_to_iter[(package, None)]
222
223             # remove this task from the parent's active count
224             num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1
225             self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
226
227             if isinstance(event, bb.build.TaskFailed):
228                 # Mark the task and parent as failed
229                 icon = "dialog-error"
230                 color = HobColors.ERROR
231
232                 logfile = event.logfile
233                 if logfile and os.path.exists(logfile):
234                     with open(logfile) as f:
235                         logdata = f.read()
236                         self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', HobColors.OK, 0))
237
238                 for i in (current, parent):
239                     self.model.set(i, self.model.COL_ICON, icon,
240                                    self.model.COL_COLOR, color)
241             else:
242                 icon = None
243                 color = HobColors.OK
244
245                 # Mark the task as inactive
246                 self.model.set(current, self.model.COL_ICON, icon,
247                                self.model.COL_COLOR, color)
248
249                 # Mark the parent package as inactive, but make sure to
250                 # preserve error and active states
251                 i = self.tasks_to_iter[(package, None)]
252                 if self.model.get(parent, self.model.COL_ICON) != 'dialog-error':
253                     self.model.set(parent, self.model.COL_ICON, icon)
254                     if num_active == 0:
255                         self.model.set(parent, self.model.COL_COLOR, HobColors.OK)
256
257             # Clear the iters and the pids since when the task goes away the
258             # pid will no longer be used for messages
259             del self.tasks_to_iter[(package, task)]
260             del self.pids_to_task[pid]
261
262         elif isinstance(event, bb.event.BuildStarted):
263
264             self.emit("build-started")
265             self.model.prepend(None, (None,
266                                       None,
267                                       None,
268                                       "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
269                                       None,
270                                       HobColors.OK,
271                                       0))
272             if pbar:
273                 pbar.update(0, self.progress_total)
274                 pbar.set_title(bb.event.getName(event))
275
276         elif isinstance(event, bb.event.BuildCompleted):
277             failures = int (event._failures)
278             self.model.prepend(None, (None,
279                                       None,
280                                       None,
281                                       "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
282                                       None,
283                                       HobColors.OK,
284                                       0))
285
286             # Emit the appropriate signal depending on the number of failures
287             if self.buildaborted:
288                 self.emit ("build-aborted")
289             elif (failures >= 1):
290                 self.emit ("build-failed")
291             else:
292                 self.emit ("build-succeeded")
293             # Emit a generic "build-complete" signal for things wishing to
294             # handle when the build is finished
295             self.emit("build-complete")
296             # reset the all cell's icon indicator
297             self.model.close_task_refresh()
298             if pbar:
299                 pbar.set_text(event.msg)
300
301         elif isinstance(event, bb.event.DiskFull):
302             self.buildaborted = True
303
304         elif isinstance(event, bb.command.CommandFailed):
305             self.emit("log", "error", "Command execution failed: %s" % (event.error))
306             if event.error.startswith("Exited with"):
307                 # If the command fails with an exit code we're done, emit the
308                 # generic signal for the UI to notify the user
309                 self.emit("build-complete")
310                 # reset the all cell's icon indicator
311                 self.model.close_task_refresh()
312
313         elif isinstance(event, bb.event.CacheLoadStarted) and pbar:
314             pbar.set_title("Loading cache")
315             self.progress_total = event.total
316             pbar.update(0, self.progress_total)
317         elif isinstance(event, bb.event.CacheLoadProgress) and pbar:
318             pbar.update(event.current, self.progress_total)
319         elif isinstance(event, bb.event.CacheLoadCompleted) and pbar:
320             pbar.update(self.progress_total, self.progress_total)
321             pbar.hide()
322         elif isinstance(event, bb.event.ParseStarted) and pbar:
323             if event.total == 0:
324                 return
325             pbar.set_title("Processing recipes")
326             self.progress_total = event.total
327             pbar.update(0, self.progress_total)
328         elif isinstance(event, bb.event.ParseProgress) and pbar:
329             pbar.update(event.current, self.progress_total)
330         elif isinstance(event, bb.event.ParseCompleted) and pbar:
331             pbar.hide()
332         #using runqueue events as many as possible to update the progress bar
333         elif isinstance(event, bb.runqueue.runQueueTaskFailed):
334             self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode))
335         elif isinstance(event, bb.runqueue.sceneQueueTaskFailed):
336             self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \
337                                      % (event.taskid, event.taskstring, event.exitcode))
338         elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)):
339             if isinstance(event, bb.runqueue.sceneQueueTaskStarted):
340                 self.emit("log", "info", "Running setscene task %d of %d (%s)" % \
341                                          (event.stats.completed + event.stats.active + event.stats.failed + 1,
342                                           event.stats.total, event.taskstring))
343             else:
344                 if event.noexec:
345                     tasktype = 'noexec task'
346                 else:
347                     tasktype = 'task'
348                 self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \
349                                          (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1,
350                                           event.stats.total, event.taskid, event.taskstring))
351             message = {}
352             message["eventname"] = bb.event.getName(event)
353             num_of_completed = event.stats.completed + event.stats.failed
354             message["current"] = num_of_completed
355             message["total"] = event.stats.total
356             message["title"] = ""
357             message["task"] = event.taskstring
358             self.emit("task-started", message)
359         elif isinstance(event, bb.event.MultipleProviders):
360             self.emit("log", "info", "multiple providers are available for %s%s (%s)" \
361                                      % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates)))
362             self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item))
363         elif isinstance(event, bb.event.NoProvider):
364             msg = ""
365             if event._runtime:
366                 r = "R"
367             else:
368                 r = ""
369             if event._dependees:
370                 msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)\n" % (r, event._item, ", ".join(event._dependees), r)
371             else:
372                 msg = "Nothing %sPROVIDES '%s'\n" % (r, event._item)
373             if event._reasons:
374                 for reason in event._reasons:
375                     msg += ("%s\n" % reason)
376             self.emit("no-provider", msg)
377             self.emit("log", msg)
378         elif isinstance(event, bb.event.LogExecTTY):
379             icon = "dialog-warning"
380             color = HobColors.WARNING
381             if self.sequential or not parent:
382                 tree_add = self.model.append
383             else:
384                 tree_add = self.model.prepend
385             tree_add(parent,
386                      (None,
387                       package,
388                       task,
389                       event.msg,
390                       icon,
391                       color,
392                       0))
393         else:
394             if not isinstance(event, (bb.event.BuildBase,
395                                       bb.event.StampUpdate,
396                                       bb.event.ConfigParsed,
397                                       bb.event.RecipeParsed,
398                                       bb.event.RecipePreFinalise,
399                                       bb.runqueue.runQueueEvent,
400                                       bb.runqueue.runQueueExitWait,
401                                       bb.event.OperationStarted,
402                                       bb.event.OperationCompleted,
403                                       bb.event.OperationProgress)):
404                 self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error'))
405
406         return
407
408
409 def do_pastebin(text):
410     url = 'http://pastebin.com/api_public.php'
411     params = {'paste_code': text, 'paste_format': 'text'}
412
413     req = urllib2.Request(url, urllib.urlencode(params))
414     response = urllib2.urlopen(req)
415     paste_url = response.read()
416
417     return paste_url
418
419
420 class RunningBuildTreeView (gtk.TreeView):
421     __gsignals__ = {
422         "button_press_event" : "override"
423         }
424     def __init__ (self, readonly=False, hob=False):
425         gtk.TreeView.__init__ (self)
426         self.readonly = readonly
427
428         # The icon that indicates whether we're building or failed.
429         # add 'hob' flag because there has not only hob to share this code
430         if hob:
431             renderer = HobCellRendererPixbuf ()
432         else:
433             renderer = gtk.CellRendererPixbuf()
434         col = gtk.TreeViewColumn ("Status", renderer)
435         col.add_attribute (renderer, "icon-name", 4)
436         self.append_column (col)
437
438         # The message of the build.
439         # add 'hob' flag because there has not only hob to share this code
440         if hob:
441             self.message_renderer = HobWarpCellRendererText (col_number=1)
442         else:
443             self.message_renderer = gtk.CellRendererText ()
444         self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3)
445         self.message_column.add_attribute(self.message_renderer, 'background', 5)
446         self.message_renderer.set_property('editable', (not self.readonly))
447         self.append_column (self.message_column)
448
449     def do_button_press_event(self, event):
450         gtk.TreeView.do_button_press_event(self, event)
451
452         if event.button == 3:
453             selection = super(RunningBuildTreeView, self).get_selection()
454             (model, it) = selection.get_selected()
455             if it is not None:
456                 can_paste = model.get(it, model.COL_LOG)[0]
457                 if can_paste == 'pastebin':
458                     # build a simple menu with a pastebin option
459                     menu = gtk.Menu()
460                     menuitem = gtk.MenuItem("Copy")
461                     menu.append(menuitem)
462                     menuitem.connect("activate", self.clipboard_handler, (model, it))
463                     menuitem.show()
464                     menuitem = gtk.MenuItem("Send log to pastebin")
465                     menu.append(menuitem)
466                     menuitem.connect("activate", self.pastebin_handler, (model, it))
467                     menuitem.show()
468                     menu.show()
469                     menu.popup(None, None, None, event.button, event.time)
470
471     def _add_to_clipboard(self, clipping):
472         """
473         Add the contents of clipping to the system clipboard.
474         """
475         clipboard = gtk.clipboard_get()
476         clipboard.set_text(clipping)
477         clipboard.store()
478
479     def pastebin_handler(self, widget, data):
480         """
481         Send the log data to pastebin, then add the new paste url to the
482         clipboard.
483         """
484         (model, it) = data
485         paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0])
486
487         # @todo Provide visual feedback to the user that it is done and that
488         # it worked.
489         print paste_url
490
491         self._add_to_clipboard(paste_url)
492
493     def clipboard_handler(self, widget, data):
494         """
495         """
496         (model, it) = data
497         message = model.get(it, model.COL_MESSAGE)[0]
498
499         self._add_to_clipboard(message)
500
501 class BuildFailureTreeView(gtk.TreeView):
502
503     def __init__ (self):
504         gtk.TreeView.__init__(self)
505         self.set_rules_hint(False)
506         self.set_headers_visible(False)
507         self.get_selection().set_mode(gtk.SELECTION_SINGLE)
508
509         # The icon that indicates whether we're building or failed.
510         renderer = HobCellRendererPixbuf ()
511         col = gtk.TreeViewColumn ("Status", renderer)
512         col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON)
513         self.append_column (col)
514
515         # The message of the build.
516         self.message_renderer = HobWarpCellRendererText (col_number=1)
517         self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR)
518         self.append_column (self.message_column)