Fix status where change object's comments field is None
[buildbot.git] / contrib / bb_applet.py
blob8430a2f817556458446ef353dd71b26569c0cb3d
1 #! /usr/bin/python
3 # This is a Gnome-2 panel applet that uses the
4 # buildbot.status.client.PBListener interface to display a terse summary of
5 # the buildmaster. It displays one column per builder, with a box on top for
6 # the status of the most recent build (red, green, or orange), and a somewhat
7 # smaller box on the bottom for the current state of the builder (white for
8 # idle, yellow for building, red for offline). There are tooltips available
9 # to tell you which box is which.
11 # Edit the line at the beginning of the MyApplet class to fill in the host
12 # and portnumber of your buildmaster's PBListener status port. Eventually
13 # this will move into a preferences dialog, but first we must create a
14 # preferences dialog.
16 # See the notes at the end for installation hints and support files (you
17 # cannot simply run this script from the shell). You must create a bonobo
18 # .server file that points to this script, and put the .server file somewhere
19 # that bonobo will look for it. Only then will this applet appear in the
20 # panel's "Add Applet" menu.
22 # Note: These applets are run in an environment that throws away stdout and
23 # stderr. Any logging must be done with syslog or explicitly to a file.
24 # Exceptions are particularly annoying in such an environment.
26 # -Brian Warner, warner@lothar.com
28 if 0:
29 import sys
30 dpipe = open("/tmp/applet.log", "a", 1)
31 sys.stdout = dpipe
32 sys.stderr = dpipe
33 print "starting"
35 from twisted.internet import gtk2reactor
36 gtk2reactor.install()
38 import gtk
39 import gnomeapplet
41 # preferences are not yet implemented
42 MENU = """
43 <popup name="button3">
44 <menuitem name="Connect" verb="Connect" label="Connect"
45 pixtype="stock" pixname="gtk-refresh"/>
46 <menuitem name="Disconnect" verb="Disconnect" label="Disconnect"
47 pixtype="stock" pixname="gtk-stop"/>
48 <menuitem name="Prefs" verb="Props" label="_Preferences..."
49 pixtype="stock" pixname="gtk-properties"/>
50 </popup>
51 """
53 from twisted.spread import pb
54 from twisted.cred import credentials
56 # sigh, these constants should cross the wire as strings, not integers
57 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5)
58 Results = ["success", "warnings", "failure", "skipped", "exception"]
61 class Box:
63 def __init__(self, buildername, hbox, tips, size, hslice):
64 self.buildername = buildername
65 self.hbox = hbox
66 self.tips = tips
67 self.state = "idle"
68 self.eta = None
69 self.last_results = None
70 self.last_text = None
71 self.size = size
72 self.hslice = hslice
74 def create(self):
75 self.vbox = gtk.VBox(False)
76 l = gtk.Label(".")
77 self.current_box = box = gtk.EventBox()
78 # these size requests are somewhat non-deterministic. I think it
79 # depends upon how large label is, or how much space was already
80 # consumed when the box is added.
81 self.current_box.set_size_request(self.hslice, self.size * 0.75)
82 box.add(l)
83 self.vbox.pack_end(box)
84 self.current_box.modify_bg(gtk.STATE_NORMAL,
85 gtk.gdk.color_parse("gray50"))
87 l2 = gtk.Label(".")
88 self.last_box = gtk.EventBox()
89 self.current_box.set_size_request(self.hslice, self.size * 0.25)
90 self.last_box.add(l2)
91 self.vbox.pack_end(self.last_box, True, True)
92 self.vbox.show_all()
93 self.hbox.pack_start(self.vbox, True, True)
95 def remove(self):
96 self.hbox.remove(self.box)
98 def set_state(self, state):
99 self.state = state
100 self.update()
102 def set_eta(self, eta):
103 self.eta = eta
104 self.update()
106 def set_last_build_results(self, results):
107 self.last_results = results
108 self.update()
110 def set_last_build_text(self, text):
111 self.last_text = text
112 self.update()
114 def update(self):
115 currentmap = {"offline": "red",
116 "idle": "white",
117 "waiting": "yellow",
118 "interlocked": "yellow",
119 "building": "yellow",
121 color = currentmap[self.state]
122 self.current_box.modify_bg(gtk.STATE_NORMAL,
123 gtk.gdk.color_parse(color))
124 lastmap = {None: "gray50",
125 SUCCESS: "green",
126 WARNINGS: "orange",
127 FAILURE: "red",
128 EXCEPTION: "purple",
130 last_color = lastmap[self.last_results]
131 self.last_box.modify_bg(gtk.STATE_NORMAL,
132 gtk.gdk.color_parse(last_color))
133 current_tip = "%s:\n%s" % (self.buildername, self.state)
134 if self.eta is not None:
135 current_tip += " (ETA=%ds)" % self.eta
136 self.tips.set_tip(self.current_box, current_tip)
137 last_tip = "%s:\n" % self.buildername
138 if self.last_text:
139 last_tip += "\n".join(self.last_text)
140 else:
141 last_tip += "no builds"
142 self.tips.set_tip(self.last_box, last_tip)
145 class MyApplet(pb.Referenceable):
146 # CHANGE THIS TO POINT TO YOUR BUILDMASTER
147 buildmaster = "buildmaster.example.org", 12345
148 filled = None
150 def __init__(self, container):
151 self.applet = container
152 self.size = container.get_size()
153 self.hslice = self.size / 4
154 container.set_size_request(self.size, self.size)
155 self.fill_nut()
156 verbs = [("Props", self.menu_preferences),
157 ("Connect", self.menu_connect),
158 ("Disconnect", self.menu_disconnect),
160 container.setup_menu(MENU, verbs)
161 self.boxes = {}
162 self.connect()
164 def fill(self, what):
165 if self.filled:
166 self.applet.remove(self.filled)
167 self.filled = None
168 self.applet.add(what)
169 self.filled = what
170 self.applet.show_all()
172 def fill_nut(self):
173 i = gtk.Image()
174 i.set_from_file("/tmp/nut32.png")
175 self.fill(i)
177 def fill_hbox(self):
178 self.hbox = gtk.HBox(True)
179 self.fill(self.hbox)
181 def connect(self):
182 host, port = self.buildmaster
183 cf = pb.PBClientFactory()
184 creds = credentials.UsernamePassword("statusClient", "clientpw")
185 d = cf.login(creds)
186 reactor.connectTCP(host, port, cf)
187 d.addCallback(self.connected)
188 return d
190 def connected(self, ref):
191 print "connected"
192 ref.notifyOnDisconnect(self.disconnected)
193 self.remote = ref
194 self.remote.callRemote("subscribe", "steps", 5, self)
195 self.fill_hbox()
196 self.tips = gtk.Tooltips()
197 self.tips.enable()
199 def disconnect(self):
200 self.remote.broker.transport.loseConnection()
202 def disconnected(self, *args):
203 print "disconnected"
204 self.fill_nut()
206 def remote_builderAdded(self, buildername, builder):
207 print "builderAdded", buildername
208 box = Box(buildername, self.hbox, self.tips, self.size, self.hslice)
209 self.boxes[buildername] = box
210 box.create()
211 self.applet.set_size_request(self.hslice * len(self.boxes),
212 self.size)
213 d = builder.callRemote("getLastFinishedBuild")
215 def _got(build):
216 if build:
217 d1 = build.callRemote("getResults")
218 d1.addCallback(box.set_last_build_results)
219 d2 = build.callRemote("getText")
220 d2.addCallback(box.set_last_build_text)
221 d.addCallback(_got)
223 def remote_builderRemoved(self, buildername):
224 self.boxes[buildername].remove()
225 del self.boxes[buildername]
226 self.applet.set_size_request(self.hslice * len(self.boxes),
227 self.size)
229 def remote_builderChangedState(self, buildername, state, eta):
230 self.boxes[buildername].set_state(state)
231 self.boxes[buildername].set_eta(eta)
232 print "change", buildername, state, eta
234 def remote_buildStarted(self, buildername, build):
235 print "buildStarted", buildername
237 def remote_buildFinished(self, buildername, build, results):
238 print "buildFinished", results
239 box = self.boxes[buildername]
240 box.set_eta(None)
241 d1 = build.callRemote("getResults")
242 d1.addCallback(box.set_last_build_results)
243 d2 = build.callRemote("getText")
244 d2.addCallback(box.set_last_build_text)
246 def remote_buildETAUpdate(self, buildername, build, eta):
247 self.boxes[buildername].set_eta(eta)
248 print "ETA", buildername, eta
250 def remote_stepStarted(self, buildername, build, stepname, step):
251 print "stepStarted", buildername, stepname
253 def remote_stepFinished(self, buildername, build, stepname, step, results):
254 pass
256 def menu_preferences(self, event, data=None):
257 print "prefs!"
258 p = Prefs(self)
259 p.create()
261 def set_buildmaster(self, buildmaster):
262 host, port = buildmaster.split(":")
263 self.buildmaster = host, int(port)
264 self.disconnect()
265 reactor.callLater(0.5, self.connect)
267 def menu_connect(self, event, data=None):
268 self.connect()
270 def menu_disconnect(self, event, data=None):
271 self.disconnect()
274 class Prefs:
276 def __init__(self, parent):
277 self.parent = parent
279 def create(self):
280 self.w = w = gtk.Window()
281 v = gtk.VBox()
282 h = gtk.HBox()
283 h.pack_start(gtk.Label("buildmaster (host:port) : "))
284 self.buildmaster_entry = b = gtk.Entry()
285 if self.parent.buildmaster:
286 host, port = self.parent.buildmaster
287 b.set_text("%s:%d" % (host, port))
288 h.pack_start(b)
289 v.add(h)
291 b = gtk.Button("Ok")
292 b.connect("clicked", self.done)
293 v.add(b)
295 w.add(v)
296 w.show_all()
298 def done(self, widget):
299 buildmaster = self.buildmaster_entry.get_text()
300 self.parent.set_buildmaster(buildmaster)
301 self.w.unmap()
304 def factory(applet, iid):
305 MyApplet(applet)
306 applet.show_all()
307 return True
310 from twisted.internet import reactor
312 # instead of reactor.run(), we do the following:
313 reactor.startRunning()
314 reactor.simulate()
315 gnomeapplet.bonobo_factory("OAFIID:GNOME_Buildbot_Factory",
316 gnomeapplet.Applet.__gtype__,
317 "buildbot", "0", factory)
319 # code ends here: bonobo_factory runs gtk.mainloop() internally and
320 # doesn't return until the program ends
322 # SUPPORTING FILES:
324 # save the following as ~/lib/bonobo/servers/bb_applet.server, and update all
325 # the pathnames to match your system
326 bb_applet_server = """
327 <oaf_info>
329 <oaf_server iid="OAFIID:GNOME_Buildbot_Factory"
330 type="exe"
331 location="/home/warner/stuff/buildbot-trunk/contrib/bb_applet.py">
333 <oaf_attribute name="repo_ids" type="stringv">
334 <item value="IDL:Bonobo/GenericFactory:1.0"/>
335 <item value="IDL:Bonobo/Unknown:1.0"/>
336 </oaf_attribute>
337 <oaf_attribute name="name" type="string" value="Buildbot Factory"/>
338 <oaf_attribute name="description" type="string" value="Test"/>
339 </oaf_server>
341 <oaf_server iid="OAFIID:GNOME_Buildbot"
342 type="factory"
343 location="OAFIID:GNOME_Buildbot_Factory">
345 <oaf_attribute name="repo_ids" type="stringv">
346 <item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0"/>
347 <item value="IDL:Bonobo/Control:1.0"/>
348 <item value="IDL:Bonobo/Unknown:1.0"/>
349 </oaf_attribute>
350 <oaf_attribute name="name" type="string" value="Buildbot"/>
351 <oaf_attribute name="description" type="string"
352 value="Watch Buildbot status"
354 <oaf_attribute name="panel:category" type="string" value="Utility"/>
355 <oaf_attribute name="panel:icon" type="string"
356 value="/home/warner/stuff/buildbot-trunk/doc/hexnut32.png"
359 </oaf_server>
361 </oaf_info>
364 # a quick rundown on the Gnome2 applet scheme (probably wrong: there are
365 # better docs out there that you should be following instead)
366 # http://www.pycage.de/howto_bonobo.html describes a lot of
367 # the base Bonobo stuff.
368 # http://www.daa.com.au/pipermail/pygtk/2002-September/003393.html
370 # bb_applet.server must be in your $BONOBO_ACTIVATION_PATH . I use
371 # ~/lib/bonobo/servers . This environment variable is read by
372 # bonobo-activation-server, so it must be set before you start any Gnome
373 # stuff. I set it in ~/.bash_profile . You can also put it in
374 # /usr/lib/bonobo/servers/ , which is probably on the default
375 # $BONOBO_ACTIVATION_PATH, so you won't have to update anything.
377 # It is safest to put this in place before bonobo-activation-server is
378 # started, which may mean before any Gnome program is running. It may or may
379 # not detect bb_applet.server if it is installed afterwards.. there seem to
380 # be hooks, some of which involve FAM, but I never managed to make them work.
381 # The file must have a name that ends in .server or it will be ignored.
383 # The .server file registers two OAF ids and tells the activation-server how
384 # to create those objects. The first is the GNOME_Buildbot_Factory, and is
385 # created by running the bb_applet.py script. The second is the
386 # GNOME_Buildbot applet itself, and is created by asking the
387 # GNOME_Buildbot_Factory to make it.
389 # gnome-panel's "Add To Panel" menu will gather all the OAF ids that claim
390 # to implement the "IDL:GNOME/Vertigo/PanelAppletShell:1.0" in its
391 # "repo_ids" attribute. The sub-menu is determined by the "panel:category"
392 # attribute. The icon comes from "panel:icon", the text displayed in the
393 # menu comes from "name", the text in the tool-tip comes from "description".
395 # The factory() function is called when a new applet is created. It receives
396 # a container that should be populated with the actual applet contents (in
397 # this case a Button).
399 # If you're hacking on the code, just modify bb_applet.py and then kill -9
400 # the running applet: the panel will ask you if you'd like to re-load the
401 # applet, and when you say 'yes', bb_applet.py will be re-executed. Note that
402 # 'kill PID' won't work because the program is sitting in C code, and SIGINT
403 # isn't delivered until after it surfaces to python, which will be never.
405 # Running bb_applet.py by itself will result in a factory instance being
406 # created and then sitting around forever waiting for the activation-server
407 # to ask it to make an applet. This isn't very useful.
409 # The "location" filename in bb_applet.server must point to bb_applet.py, and
410 # bb_applet.py must be executable.
412 # Enjoy!
413 # -Brian Warner