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
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"]
61 def __init__(self
, buildername
, hbox
, tips
, size
, hslice
):
62 self
.buildername
= buildername
67 self
.last_results
= None
73 self
.vbox
= gtk
.VBox(False)
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)
81 self
.vbox
.pack_end(box
)
82 self
.current_box
.modify_bg(gtk
.STATE_NORMAL
,
83 gtk
.gdk
.color_parse("gray50"))
86 self
.last_box
= gtk
.EventBox()
87 self
.current_box
.set_size_request(self
.hslice
, self
.size
* 0.25)
89 self
.vbox
.pack_end(self
.last_box
, True, True)
91 self
.hbox
.pack_start(self
.vbox
, True, True)
94 self
.hbox
.remove(self
.box
)
96 def set_state(self
, state
):
99 def set_eta(self
, eta
):
102 def set_last_build_results(self
, results
):
103 self
.last_results
= results
105 def set_last_build_text(self
, text
):
106 self
.last_text
= text
110 currentmap
= {"offline": "red",
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",
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
133 last_tip
+= "\n".join(self
.last_text
)
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
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
)
151 verbs
= [ ("Props", self
.menu_preferences
),
152 ("Connect", self
.menu_connect
),
153 ("Disconnect", self
.menu_disconnect
),
155 container
.setup_menu(MENU
, verbs
)
159 def fill(self
, what
):
161 self
.applet
.remove(self
.filled
)
163 self
.applet
.add(what
)
165 self
.applet
.show_all()
169 i
.set_from_file("/tmp/nut32.png")
173 self
.hbox
= gtk
.HBox(True)
177 host
, port
= self
.buildmaster
178 cf
= pb
.PBClientFactory()
179 creds
= credentials
.UsernamePassword("statusClient", "clientpw")
181 reactor
.connectTCP(host
, port
, cf
)
182 d
.addCallback(self
.connected
)
184 def connected(self
, ref
):
186 ref
.notifyOnDisconnect(self
.disconnected
)
188 self
.remote
.callRemote("subscribe", "steps", 5, self
)
190 self
.tips
= gtk
.Tooltips()
193 def disconnect(self
):
194 self
.remote
.broker
.transport
.loseConnection()
196 def disconnected(self
, *args
):
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
205 self
.applet
.set_size_request(self
.hslice
* len(self
.boxes
),
207 d
= builder
.callRemote("getLastFinishedBuild")
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
)
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
),
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
]
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
):
250 def menu_preferences(self
, event
, data
=None):
255 def set_buildmaster(self
, buildmaster
):
256 host
, port
= buildmaster
.split(":")
257 self
.buildmaster
= host
, int(port
)
259 reactor
.callLater(0.5, self
.connect
)
261 def menu_connect(self
, event
, data
=None):
264 def menu_disconnect(self
, event
, data
=None):
269 def __init__(self
, parent
):
273 self
.w
= w
= gtk
.Window()
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
))
285 b
.connect("clicked", self
.done
)
290 def done(self
, widget
):
291 buildmaster
= self
.buildmaster_entry
.get_text()
292 self
.parent
.set_buildmaster(buildmaster
)
297 def factory(applet
, iid
):
303 from twisted
.internet
import reactor
305 # instead of reactor.run(), we do the following:
306 reactor
.startRunning()
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
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
= """
323 <oaf_server iid="OAFIID:GNOME_Buildbot_Factory"
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"/>
331 <oaf_attribute name="name" type="string" value="Buildbot Factory"/>
332 <oaf_attribute name="description" type="string" value="Test"/>
335 <oaf_server iid="OAFIID:GNOME_Buildbot"
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"/>
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"
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.