blob: 3ce4590c1621ff369ac8ea7896d1341e1baa4059 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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
21import gtk
22import gobject
23import gtk.glade
24import threading
25import urllib2
26import os
27import contextlib
28
29from bb.ui.crumbs.buildmanager import BuildManager, BuildConfiguration
30from bb.ui.crumbs.buildmanager import BuildManagerTreeView
31
32from bb.ui.crumbs.runningbuild import RunningBuild, RunningBuildTreeView
33
34# The metadata loader is used by the BuildSetupDialog to download the
35# available options to populate the dialog
36class MetaDataLoader(gobject.GObject):
37 """ This class provides the mechanism for loading the metadata (the
38 fetching and parsing) from a given URL. The metadata encompasses details
39 on what machines are available. The distribution and images available for
40 the machine and the the uris to use for building the given machine."""
41 __gsignals__ = {
42 'success' : (gobject.SIGNAL_RUN_LAST,
43 gobject.TYPE_NONE,
44 ()),
45 'error' : (gobject.SIGNAL_RUN_LAST,
46 gobject.TYPE_NONE,
47 (gobject.TYPE_STRING,))
48 }
49
50 # We use these little helper functions to ensure that we take the gdk lock
51 # when emitting the signal. These functions are called as idles (so that
52 # they happen in the gtk / main thread's main loop.
53 def emit_error_signal (self, remark):
54 gtk.gdk.threads_enter()
55 self.emit ("error", remark)
56 gtk.gdk.threads_leave()
57
58 def emit_success_signal (self):
59 gtk.gdk.threads_enter()
60 self.emit ("success")
61 gtk.gdk.threads_leave()
62
63 def __init__ (self):
64 gobject.GObject.__init__ (self)
65
66 class LoaderThread(threading.Thread):
67 """ This class provides an asynchronous loader for the metadata (by
68 using threads and signals). This is useful since the metadata may be
69 at a remote URL."""
70 class LoaderImportException (Exception):
71 pass
72
73 def __init__(self, loader, url):
74 threading.Thread.__init__ (self)
75 self.url = url
76 self.loader = loader
77
78 def run (self):
79 result = {}
80 try:
81 with contextlib.closing (urllib2.urlopen (self.url)) as f:
82 # Parse the metadata format. The format is....
83 # <machine>;<default distro>|<distro>...;<default image>|<image>...;<type##url>|...
84 for line in f:
85 components = line.split(";")
86 if (len (components) < 4):
87 raise MetaDataLoader.LoaderThread.LoaderImportException
88 machine = components[0]
89 distros = components[1].split("|")
90 images = components[2].split("|")
91 urls = components[3].split("|")
92
93 result[machine] = (distros, images, urls)
94
95 # Create an object representing this *potential*
96 # configuration. It can become concrete if the machine, distro
97 # and image are all chosen in the UI
98 configuration = BuildConfiguration()
99 configuration.metadata_url = self.url
100 configuration.machine_options = result
101 self.loader.configuration = configuration
102
103 # Emit that we've actually got a configuration
104 gobject.idle_add (MetaDataLoader.emit_success_signal,
105 self.loader)
106
107 except MetaDataLoader.LoaderThread.LoaderImportException as e:
108 gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
109 "Repository metadata corrupt")
110 except Exception as e:
111 gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
112 "Unable to download repository metadata")
113 print(e)
114
115 def try_fetch_from_url (self, url):
116 # Try and download the metadata. Firing a signal if successful
117 thread = MetaDataLoader.LoaderThread(self, url)
118 thread.start()
119
120class BuildSetupDialog (gtk.Dialog):
121 RESPONSE_BUILD = 1
122
123 # A little helper method that just sets the states on the widgets based on
124 # whether we've got good metadata or not.
125 def set_configurable (self, configurable):
126 if (self.configurable == configurable):
127 return
128
129 self.configurable = configurable
130 for widget in self.conf_widgets:
131 widget.set_sensitive (configurable)
132
133 if not configurable:
134 self.machine_combo.set_active (-1)
135 self.distribution_combo.set_active (-1)
136 self.image_combo.set_active (-1)
137
138 # GTK widget callbacks
139 def refresh_button_clicked (self, button):
140 # Refresh button clicked.
141
142 url = self.location_entry.get_chars (0, -1)
143 self.loader.try_fetch_from_url(url)
144
145 def repository_entry_editable_changed (self, entry):
146 if (len (entry.get_chars (0, -1)) > 0):
147 self.refresh_button.set_sensitive (True)
148 else:
149 self.refresh_button.set_sensitive (False)
150 self.clear_status_message()
151
152 # If we were previously configurable we are no longer since the
153 # location entry has been changed
154 self.set_configurable (False)
155
156 def machine_combo_changed (self, combobox):
157 active_iter = combobox.get_active_iter()
158
159 if not active_iter:
160 return
161
162 model = combobox.get_model()
163
164 if model:
165 chosen_machine = model.get (active_iter, 0)[0]
166
167 (distros_model, images_model) = \
168 self.loader.configuration.get_distro_and_images_models (chosen_machine)
169
170 self.distribution_combo.set_model (distros_model)
171 self.image_combo.set_model (images_model)
172
173 # Callbacks from the loader
174 def loader_success_cb (self, loader):
175 self.status_image.set_from_icon_name ("info",
176 gtk.ICON_SIZE_BUTTON)
177 self.status_image.show()
178 self.status_label.set_label ("Repository metadata successfully downloaded")
179
180 # Set the models on the combo boxes based on the models generated from
181 # the configuration that the loader has created
182
183 # We just need to set the machine here, that then determines the
184 # distro and image options. Cunning huh? :-)
185
186 self.configuration = self.loader.configuration
187 model = self.configuration.get_machines_model ()
188 self.machine_combo.set_model (model)
189
190 self.set_configurable (True)
191
192 def loader_error_cb (self, loader, message):
193 self.status_image.set_from_icon_name ("error",
194 gtk.ICON_SIZE_BUTTON)
195 self.status_image.show()
196 self.status_label.set_text ("Error downloading repository metadata")
197 for widget in self.conf_widgets:
198 widget.set_sensitive (False)
199
200 def clear_status_message (self):
201 self.status_image.hide()
202 self.status_label.set_label (
203 """<i>Enter the repository location and press _Refresh</i>""")
204
205 def __init__ (self):
206 gtk.Dialog.__init__ (self)
207
208 # Cancel
209 self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
210
211 # Build
212 button = gtk.Button ("_Build", None, True)
213 image = gtk.Image ()
214 image.set_from_stock (gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
215 button.set_image (image)
216 self.add_action_widget (button, BuildSetupDialog.RESPONSE_BUILD)
217 button.show_all ()
218
219 # Pull in *just* the table from the Glade XML data.
220 gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
221 root = "build_table")
222 table = gxml.get_widget ("build_table")
223 self.vbox.pack_start (table, True, False, 0)
224
225 # Grab all the widgets that we need to turn on/off when we refresh...
226 self.conf_widgets = []
227 self.conf_widgets += [gxml.get_widget ("machine_label")]
228 self.conf_widgets += [gxml.get_widget ("distribution_label")]
229 self.conf_widgets += [gxml.get_widget ("image_label")]
230 self.conf_widgets += [gxml.get_widget ("machine_combo")]
231 self.conf_widgets += [gxml.get_widget ("distribution_combo")]
232 self.conf_widgets += [gxml.get_widget ("image_combo")]
233
234 # Grab the status widgets
235 self.status_image = gxml.get_widget ("status_image")
236 self.status_label = gxml.get_widget ("status_label")
237
238 # Grab the refresh button and connect to the clicked signal
239 self.refresh_button = gxml.get_widget ("refresh_button")
240 self.refresh_button.connect ("clicked", self.refresh_button_clicked)
241
242 # Grab the location entry and connect to editable::changed
243 self.location_entry = gxml.get_widget ("location_entry")
244 self.location_entry.connect ("changed",
245 self.repository_entry_editable_changed)
246
247 # Grab the machine combo and hook onto the changed signal. This then
248 # allows us to populate the distro and image combos
249 self.machine_combo = gxml.get_widget ("machine_combo")
250 self.machine_combo.connect ("changed", self.machine_combo_changed)
251
252 # Setup the combo
253 cell = gtk.CellRendererText()
254 self.machine_combo.pack_start(cell, True)
255 self.machine_combo.add_attribute(cell, 'text', 0)
256
257 # Grab the distro and image combos. We need these to populate with
258 # models once the machine is chosen
259 self.distribution_combo = gxml.get_widget ("distribution_combo")
260 cell = gtk.CellRendererText()
261 self.distribution_combo.pack_start(cell, True)
262 self.distribution_combo.add_attribute(cell, 'text', 0)
263
264 self.image_combo = gxml.get_widget ("image_combo")
265 cell = gtk.CellRendererText()
266 self.image_combo.pack_start(cell, True)
267 self.image_combo.add_attribute(cell, 'text', 0)
268
269 # Put the default descriptive text in the status box
270 self.clear_status_message()
271
272 # Mark as non-configurable, this is just greys out the widgets the
273 # user can't yet use
274 self.configurable = False
275 self.set_configurable(False)
276
277 # Show the table
278 table.show_all ()
279
280 # The loader and some signals connected to it to update the status
281 # area
282 self.loader = MetaDataLoader()
283 self.loader.connect ("success", self.loader_success_cb)
284 self.loader.connect ("error", self.loader_error_cb)
285
286 def update_configuration (self):
287 """ A poorly named function but it updates the internal configuration
288 from the widgets. This can make that configuration concrete and can
289 thus be used for building """
290 # Extract the chosen machine from the combo
291 model = self.machine_combo.get_model()
292 active_iter = self.machine_combo.get_active_iter()
293 if (active_iter):
294 self.configuration.machine = model.get(active_iter, 0)[0]
295
296 # Extract the chosen distro from the combo
297 model = self.distribution_combo.get_model()
298 active_iter = self.distribution_combo.get_active_iter()
299 if (active_iter):
300 self.configuration.distro = model.get(active_iter, 0)[0]
301
302 # Extract the chosen image from the combo
303 model = self.image_combo.get_model()
304 active_iter = self.image_combo.get_active_iter()
305 if (active_iter):
306 self.configuration.image = model.get(active_iter, 0)[0]
307
308# This function operates to pull events out from the event queue and then push
309# them into the RunningBuild (which then drives the RunningBuild which then
310# pushes through and updates the progress tree view.)
311#
312# TODO: Should be a method on the RunningBuild class
313def event_handle_timeout (eventHandler, build):
314 # Consume as many messages as we can ...
315 event = eventHandler.getEvent()
316 while event:
317 build.handle_event (event)
318 event = eventHandler.getEvent()
319 return True
320
321class MainWindow (gtk.Window):
322
323 # Callback that gets fired when the user hits a button in the
324 # BuildSetupDialog.
325 def build_dialog_box_response_cb (self, dialog, response_id):
326 conf = None
327 if (response_id == BuildSetupDialog.RESPONSE_BUILD):
328 dialog.update_configuration()
329 print(dialog.configuration.machine, dialog.configuration.distro, \
330 dialog.configuration.image)
331 conf = dialog.configuration
332
333 dialog.destroy()
334
335 if conf:
336 self.manager.do_build (conf)
337
338 def build_button_clicked_cb (self, button):
339 dialog = BuildSetupDialog ()
340
341 # For some unknown reason Dialog.run causes nice little deadlocks ... :-(
342 dialog.connect ("response", self.build_dialog_box_response_cb)
343 dialog.show()
344
345 def __init__ (self):
346 gtk.Window.__init__ (self)
347
348 # Pull in *just* the main vbox from the Glade XML data and then pack
349 # that inside the window
350 gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
351 root = "main_window_vbox")
352 vbox = gxml.get_widget ("main_window_vbox")
353 self.add (vbox)
354
355 # Create the tree views for the build manager view and the progress view
356 self.build_manager_view = BuildManagerTreeView()
357 self.running_build_view = RunningBuildTreeView()
358
359 # Grab the scrolled windows that we put the tree views into
360 self.results_scrolledwindow = gxml.get_widget ("results_scrolledwindow")
361 self.progress_scrolledwindow = gxml.get_widget ("progress_scrolledwindow")
362
363 # Put the tree views inside ...
364 self.results_scrolledwindow.add (self.build_manager_view)
365 self.progress_scrolledwindow.add (self.running_build_view)
366
367 # Hook up the build button...
368 self.build_button = gxml.get_widget ("main_toolbutton_build")
369 self.build_button.connect ("clicked", self.build_button_clicked_cb)
370
371# I'm not very happy about the current ownership of the RunningBuild. I have
372# my suspicions that this object should be held by the BuildManager since we
373# care about the signals in the manager
374
375def running_build_succeeded_cb (running_build, manager):
376 # Notify the manager that a build has succeeded. This is necessary as part
377 # of the 'hack' that we use for making the row in the model / view
378 # representing the ongoing build change into a row representing the
379 # completed build. Since we know only one build can be running a time then
380 # we can handle this.
381
382 # FIXME: Refactor all this so that the RunningBuild is owned by the
383 # BuildManager. It can then hook onto the signals directly and drive
384 # interesting things it cares about.
385 manager.notify_build_succeeded ()
386 print("build succeeded")
387
388def running_build_failed_cb (running_build, manager):
389 # As above
390 print("build failed")
391 manager.notify_build_failed ()
392
393def main (server, eventHandler):
394 # Initialise threading...
395 gobject.threads_init()
396 gtk.gdk.threads_init()
397
398 main_window = MainWindow ()
399 main_window.show_all ()
400
401 # Set up the build manager stuff in general
402 builds_dir = os.path.join (os.getcwd(), "results")
403 manager = BuildManager (server, builds_dir)
404 main_window.build_manager_view.set_model (manager.model)
405
406 # Do the running build setup
407 running_build = RunningBuild ()
408 main_window.running_build_view.set_model (running_build.model)
409 running_build.connect ("build-succeeded", running_build_succeeded_cb,
410 manager)
411 running_build.connect ("build-failed", running_build_failed_cb, manager)
412
413 # We need to save the manager into the MainWindow so that the toolbar
414 # button can use it.
415 # FIXME: Refactor ?
416 main_window.manager = manager
417
418 # Use a timeout function for probing the event queue to find out if we
419 # have a message waiting for us.
420 gobject.timeout_add (200,
421 event_handle_timeout,
422 eventHandler,
423 running_build)
424
425 gtk.main()