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
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
30 dpipe
= open("/tmp/applet.log", "a", 1)
35 from twisted
.internet
import gtk2reactor
36 gtk2reactor
.install() #@UndefinedVariable
38 import gtk
#@UnresolvedImport
39 import gnomeapplet
#@UnresolvedImport
41 # preferences are not yet implemented
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"/>
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"]
63 def __init__(self
, buildername
, hbox
, tips
, size
, hslice
):
64 self
.buildername
= buildername
69 self
.last_results
= None
75 self
.vbox
= gtk
.VBox(False)
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)
83 self
.vbox
.pack_end(box
)
84 self
.current_box
.modify_bg(gtk
.STATE_NORMAL
,
85 gtk
.gdk
.color_parse("gray50"))
88 self
.last_box
= gtk
.EventBox()
89 self
.current_box
.set_size_request(self
.hslice
, self
.size
* 0.25)
91 self
.vbox
.pack_end(self
.last_box
, True, True)
93 self
.hbox
.pack_start(self
.vbox
, True, True)
96 self
.hbox
.remove(self
.box
)
98 def set_state(self
, state
):
102 def set_eta(self
, eta
):
106 def set_last_build_results(self
, results
):
107 self
.last_results
= results
110 def set_last_build_text(self
, text
):
111 self
.last_text
= text
115 currentmap
= {"offline": "red",
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",
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
139 last_tip
+= "\n".join(self
.last_text
)
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
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
)
156 verbs
= [("Props", self
.menu_preferences
),
157 ("Connect", self
.menu_connect
),
158 ("Disconnect", self
.menu_disconnect
),
160 container
.setup_menu(MENU
, verbs
)
164 def fill(self
, what
):
166 self
.applet
.remove(self
.filled
)
168 self
.applet
.add(what
)
170 self
.applet
.show_all()
174 i
.set_from_file("/tmp/nut32.png")
178 self
.hbox
= gtk
.HBox(True)
182 host
, port
= self
.buildmaster
183 cf
= pb
.PBClientFactory()
184 creds
= credentials
.UsernamePassword("statusClient", "clientpw")
186 reactor
.connectTCP(host
, port
, cf
)
187 d
.addCallback(self
.connected
)
190 def connected(self
, ref
):
192 ref
.notifyOnDisconnect(self
.disconnected
)
194 self
.remote
.callRemote("subscribe", "steps", 5, self
)
196 self
.tips
= gtk
.Tooltips()
199 def disconnect(self
):
200 self
.remote
.broker
.transport
.loseConnection()
202 def disconnected(self
, *args
):
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
211 self
.applet
.set_size_request(self
.hslice
* len(self
.boxes
),
213 d
= builder
.callRemote("getLastFinishedBuild")
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
)
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
),
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
]
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
):
256 def menu_preferences(self
, event
, data
=None):
261 def set_buildmaster(self
, buildmaster
):
262 host
, port
= buildmaster
.split(":")
263 self
.buildmaster
= host
, int(port
)
265 reactor
.callLater(0.5, self
.connect
)
267 def menu_connect(self
, event
, data
=None):
270 def menu_disconnect(self
, event
, data
=None):
276 def __init__(self
, parent
):
280 self
.w
= w
= gtk
.Window()
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
))
292 b
.connect("clicked", self
.done
)
298 def done(self
, widget
):
299 buildmaster
= self
.buildmaster_entry
.get_text()
300 self
.parent
.set_buildmaster(buildmaster
)
304 def factory(applet
, iid
):
310 from twisted
.internet
import reactor
312 # instead of reactor.run(), we do the following:
313 reactor
.startRunning()
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
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
= """
329 <oaf_server iid="OAFIID:GNOME_Buildbot_Factory"
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"/>
337 <oaf_attribute name="name" type="string" value="Buildbot Factory"/>
338 <oaf_attribute name="description" type="string" value="Test"/>
341 <oaf_server iid="OAFIID:GNOME_Buildbot"
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"/>
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"
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.