WebStatus: yes create public_html/ at startup, otherwise we get internal server error...
[buildbot.git] / contrib / bb_applet.py
blob1abdc3d160129877b94ab276e671a33c23ba9ffe
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"]
60 class Box:
61 def __init__(self, buildername, hbox, tips, size, hslice):
62 self.buildername = buildername
63 self.hbox = hbox
64 self.tips = tips
65 self.state = "idle"
66 self.eta = None
67 self.last_results = None
68 self.last_text = None
69 self.size = size
70 self.hslice = hslice
72 def create(self):
73 self.vbox = gtk.VBox(False)
74 l = gtk.Label(".")
75 self.current_box = box = gtk.EventBox()
76 # these size requests are somewhat non-deterministic. I think it
77 # depends upon how large label is, or how much space was already
78 # consumed when the box is added.
79 self.current_box.set_size_request(self.hslice, self.size * 0.75)
80 box.add(l)
81 self.vbox.pack_end(box)
82 self.current_box.modify_bg(gtk.STATE_NORMAL,
83 gtk.gdk.color_parse("gray50"))
85 l2 = gtk.Label(".")
86 self.last_box = gtk.EventBox()
87 self.current_box.set_size_request(self.hslice, self.size * 0.25)
88 self.last_box.add(l2)
89 self.vbox.pack_end(self.last_box, True, True)
90 self.vbox.show_all()
91 self.hbox.pack_start(self.vbox, True, True)
93 def remove(self):
94 self.hbox.remove(self.box)
96 def set_state(self, state):
97 self.state = state
98 self.update()
99 def set_eta(self, eta):
100 self.eta = eta
101 self.update()
102 def set_last_build_results(self, results):
103 self.last_results = results
104 self.update()
105 def set_last_build_text(self, text):
106 self.last_text = text
107 self.update()
109 def update(self):
110 currentmap = {"offline": "red",
111 "idle": "white",
112 "waiting": "yellow",
113 "interlocked": "yellow",
114 "building": "yellow",}
115 color = currentmap[self.state]
116 self.current_box.modify_bg(gtk.STATE_NORMAL,
117 gtk.gdk.color_parse(color))
118 lastmap = {None: "gray50",
119 SUCCESS: "green",
120 WARNINGS: "orange",
121 FAILURE: "red",
122 EXCEPTION: "purple",
124 last_color = lastmap[self.last_results]
125 self.last_box.modify_bg(gtk.STATE_NORMAL,
126 gtk.gdk.color_parse(last_color))
127 current_tip = "%s:\n%s" % (self.buildername, self.state)
128 if self.eta is not None:
129 current_tip += " (ETA=%ds)" % self.eta
130 self.tips.set_tip(self.current_box, current_tip)
131 last_tip = "%s:\n" % self.buildername
132 if self.last_text:
133 last_tip += "\n".join(self.last_text)
134 else:
135 last_tip += "no builds"
136 self.tips.set_tip(self.last_box, last_tip)
140 class MyApplet(pb.Referenceable):
141 # CHANGE THIS TO POINT TO YOUR BUILDMASTER
142 buildmaster = "buildmaster.example.org", 12345
143 filled = None
145 def __init__(self, container):
146 self.applet = container
147 self.size = container.get_size()
148 self.hslice = self.size / 4
149 container.set_size_request(self.size, self.size)
150 self.fill_nut()
151 verbs = [ ("Props", self.menu_preferences),
152 ("Connect", self.menu_connect),
153 ("Disconnect", self.menu_disconnect),
155 container.setup_menu(MENU, verbs)
156 self.boxes = {}
157 self.connect()
159 def fill(self, what):
160 if self.filled:
161 self.applet.remove(self.filled)
162 self.filled = None
163 self.applet.add(what)
164 self.filled = what
165 self.applet.show_all()
167 def fill_nut(self):
168 i = gtk.Image()
169 i.set_from_file("/tmp/nut32.png")
170 self.fill(i)
172 def fill_hbox(self):
173 self.hbox = gtk.HBox(True)
174 self.fill(self.hbox)
176 def connect(self):
177 host, port = self.buildmaster
178 cf = pb.PBClientFactory()
179 creds = credentials.UsernamePassword("statusClient", "clientpw")
180 d = cf.login(creds)
181 reactor.connectTCP(host, port, cf)
182 d.addCallback(self.connected)
183 return d
184 def connected(self, ref):
185 print "connected"
186 ref.notifyOnDisconnect(self.disconnected)
187 self.remote = ref
188 self.remote.callRemote("subscribe", "steps", 5, self)
189 self.fill_hbox()
190 self.tips = gtk.Tooltips()
191 self.tips.enable()
193 def disconnect(self):
194 self.remote.broker.transport.loseConnection()
196 def disconnected(self, *args):
197 print "disconnected"
198 self.fill_nut()
200 def remote_builderAdded(self, buildername, builder):
201 print "builderAdded", buildername
202 box = Box(buildername, self.hbox, self.tips, self.size, self.hslice)
203 self.boxes[buildername] = box
204 box.create()
205 self.applet.set_size_request(self.hslice * len(self.boxes),
206 self.size)
207 d = builder.callRemote("getLastFinishedBuild")
208 def _got(build):
209 if build:
210 d1 = build.callRemote("getResults")
211 d1.addCallback(box.set_last_build_results)
212 d2 = build.callRemote("getText")
213 d2.addCallback(box.set_last_build_text)
214 d.addCallback(_got)
217 def remote_builderRemoved(self, buildername):
218 self.boxes[buildername].remove()
219 del self.boxes[buildername]
220 self.applet.set_size_request(self.hslice * len(self.boxes),
221 self.size)
223 def remote_builderChangedState(self, buildername, state, eta):
224 self.boxes[buildername].set_state(state)
225 self.boxes[buildername].set_eta(eta)
226 print "change", buildername, state, eta
228 def remote_buildStarted(self, buildername, build):
229 print "buildStarted", buildername
231 def remote_buildFinished(self, buildername, build, results):
232 print "buildFinished", results
233 box = self.boxes[buildername]
234 box.set_eta(None)
235 d1 = build.callRemote("getResults")
236 d1.addCallback(box.set_last_build_results)
237 d2 = build.callRemote("getText")
238 d2.addCallback(box.set_last_build_text)
240 def remote_buildETAUpdate(self, buildername, build, eta):
241 self.boxes[buildername].set_eta(eta)
242 print "ETA", buildername, eta
244 def remote_stepStarted(self, buildername, build, stepname, step):
245 print "stepStarted", buildername, stepname
247 def remote_stepFinished(self, buildername, build, stepname, step, results):
248 pass
250 def menu_preferences(self, event, data=None):
251 print "prefs!"
252 p = Prefs(self)
253 p.create()
255 def set_buildmaster(self, buildmaster):
256 host, port = buildmaster.split(":")
257 self.buildmaster = host, int(port)
258 self.disconnect()
259 reactor.callLater(0.5, self.connect)
261 def menu_connect(self, event, data=None):
262 self.connect()
264 def menu_disconnect(self, event, data=None):
265 self.disconnect()
268 class Prefs:
269 def __init__(self, parent):
270 self.parent = parent
272 def create(self):
273 self.w = w = gtk.Window()
274 v = gtk.VBox()
275 h = gtk.HBox()
276 h.pack_start(gtk.Label("buildmaster (host:port) : "))
277 self.buildmaster_entry = b = gtk.Entry()
278 if self.parent.buildmaster:
279 host, port = self.parent.buildmaster
280 b.set_text("%s:%d" % (host, port))
281 h.pack_start(b)
282 v.add(h)
284 b = gtk.Button("Ok")
285 b.connect("clicked", self.done)
286 v.add(b)
288 w.add(v)
289 w.show_all()
290 def done(self, widget):
291 buildmaster = self.buildmaster_entry.get_text()
292 self.parent.set_buildmaster(buildmaster)
293 self.w.unmap()
297 def factory(applet, iid):
298 MyApplet(applet)
299 applet.show_all()
300 return True
303 from twisted.internet import reactor
305 # instead of reactor.run(), we do the following:
306 reactor.startRunning()
307 reactor.simulate()
308 gnomeapplet.bonobo_factory("OAFIID:GNOME_Buildbot_Factory",
309 gnomeapplet.Applet.__gtype__,
310 "buildbot", "0", factory)
312 # code ends here: bonobo_factory runs gtk.mainloop() internally and
313 # doesn't return until the program ends
316 # SUPPORTING FILES:
318 # save the following as ~/lib/bonobo/servers/bb_applet.server, and update all
319 # the pathnames to match your system
320 bb_applet_server = """
321 <oaf_info>
323 <oaf_server iid="OAFIID:GNOME_Buildbot_Factory"
324 type="exe"
325 location="/home/warner/stuff/buildbot-trunk/contrib/bb_applet.py">
327 <oaf_attribute name="repo_ids" type="stringv">
328 <item value="IDL:Bonobo/GenericFactory:1.0"/>
329 <item value="IDL:Bonobo/Unknown:1.0"/>
330 </oaf_attribute>
331 <oaf_attribute name="name" type="string" value="Buildbot Factory"/>
332 <oaf_attribute name="description" type="string" value="Test"/>
333 </oaf_server>
335 <oaf_server iid="OAFIID:GNOME_Buildbot"
336 type="factory"
337 location="OAFIID:GNOME_Buildbot_Factory">
339 <oaf_attribute name="repo_ids" type="stringv">
340 <item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0"/>
341 <item value="IDL:Bonobo/Control:1.0"/>
342 <item value="IDL:Bonobo/Unknown:1.0"/>
343 </oaf_attribute>
344 <oaf_attribute name="name" type="string" value="Buildbot"/>
345 <oaf_attribute name="description" type="string"
346 value="Watch Buildbot status"
348 <oaf_attribute name="panel:category" type="string" value="Utility"/>
349 <oaf_attribute name="panel:icon" type="string"
350 value="/home/warner/stuff/buildbot-trunk/doc/hexnut32.png"
353 </oaf_server>
355 </oaf_info>
358 # a quick rundown on the Gnome2 applet scheme (probably wrong: there are
359 # better docs out there that you should be following instead)
360 # http://www.pycage.de/howto_bonobo.html describes a lot of
361 # the base Bonobo stuff.
362 # http://www.daa.com.au/pipermail/pygtk/2002-September/003393.html
364 # bb_applet.server must be in your $BONOBO_ACTIVATION_PATH . I use
365 # ~/lib/bonobo/servers . This environment variable is read by
366 # bonobo-activation-server, so it must be set before you start any Gnome
367 # stuff. I set it in ~/.bash_profile . You can also put it in
368 # /usr/lib/bonobo/servers/ , which is probably on the default
369 # $BONOBO_ACTIVATION_PATH, so you won't have to update anything.
371 # It is safest to put this in place before bonobo-activation-server is
372 # started, which may mean before any Gnome program is running. It may or may
373 # not detect bb_applet.server if it is installed afterwards.. there seem to
374 # be hooks, some of which involve FAM, but I never managed to make them work.
375 # The file must have a name that ends in .server or it will be ignored.
377 # The .server file registers two OAF ids and tells the activation-server how
378 # to create those objects. The first is the GNOME_Buildbot_Factory, and is
379 # created by running the bb_applet.py script. The second is the
380 # GNOME_Buildbot applet itself, and is created by asking the
381 # GNOME_Buildbot_Factory to make it.
383 # gnome-panel's "Add To Panel" menu will gather all the OAF ids that claim
384 # to implement the "IDL:GNOME/Vertigo/PanelAppletShell:1.0" in its
385 # "repo_ids" attribute. The sub-menu is determined by the "panel:category"
386 # attribute. The icon comes from "panel:icon", the text displayed in the
387 # menu comes from "name", the text in the tool-tip comes from "description".
389 # The factory() function is called when a new applet is created. It receives
390 # a container that should be populated with the actual applet contents (in
391 # this case a Button).
393 # If you're hacking on the code, just modify bb_applet.py and then kill -9
394 # the running applet: the panel will ask you if you'd like to re-load the
395 # applet, and when you say 'yes', bb_applet.py will be re-executed. Note that
396 # 'kill PID' won't work because the program is sitting in C code, and SIGINT
397 # isn't delivered until after it surfaces to python, which will be never.
399 # Running bb_applet.py by itself will result in a factory instance being
400 # created and then sitting around forever waiting for the activation-server
401 # to ask it to make an applet. This isn't very useful.
403 # The "location" filename in bb_applet.server must point to bb_applet.py, and
404 # bb_applet.py must be executable.
406 # Enjoy!
407 # -Brian Warner