runningbuild.py 24 KB

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