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