diff options
Diffstat (limited to 'buildbot/contrib/bb_applet.py')
-rwxr-xr-x | buildbot/contrib/bb_applet.py | 413 |
1 files changed, 413 insertions, 0 deletions
diff --git a/buildbot/contrib/bb_applet.py b/buildbot/contrib/bb_applet.py new file mode 100755 index 0000000..8430a2f --- /dev/null +++ b/buildbot/contrib/bb_applet.py @@ -0,0 +1,413 @@ +#! /usr/bin/python + +# This is a Gnome-2 panel applet that uses the +# buildbot.status.client.PBListener interface to display a terse summary of +# the buildmaster. It displays one column per builder, with a box on top for +# the status of the most recent build (red, green, or orange), and a somewhat +# smaller box on the bottom for the current state of the builder (white for +# idle, yellow for building, red for offline). There are tooltips available +# to tell you which box is which. + +# Edit the line at the beginning of the MyApplet class to fill in the host +# and portnumber of your buildmaster's PBListener status port. Eventually +# this will move into a preferences dialog, but first we must create a +# preferences dialog. + +# See the notes at the end for installation hints and support files (you +# cannot simply run this script from the shell). You must create a bonobo +# .server file that points to this script, and put the .server file somewhere +# that bonobo will look for it. Only then will this applet appear in the +# panel's "Add Applet" menu. + +# Note: These applets are run in an environment that throws away stdout and +# stderr. Any logging must be done with syslog or explicitly to a file. +# Exceptions are particularly annoying in such an environment. + +# -Brian Warner, warner@lothar.com + +if 0: + import sys + dpipe = open("/tmp/applet.log", "a", 1) + sys.stdout = dpipe + sys.stderr = dpipe + print "starting" + +from twisted.internet import gtk2reactor +gtk2reactor.install() + +import gtk +import gnomeapplet + +# preferences are not yet implemented +MENU = """ +<popup name="button3"> + <menuitem name="Connect" verb="Connect" label="Connect" + pixtype="stock" pixname="gtk-refresh"/> + <menuitem name="Disconnect" verb="Disconnect" label="Disconnect" + pixtype="stock" pixname="gtk-stop"/> + <menuitem name="Prefs" verb="Props" label="_Preferences..." + pixtype="stock" pixname="gtk-properties"/> +</popup> +""" + +from twisted.spread import pb +from twisted.cred import credentials + +# sigh, these constants should cross the wire as strings, not integers +SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5) +Results = ["success", "warnings", "failure", "skipped", "exception"] + + +class Box: + + def __init__(self, buildername, hbox, tips, size, hslice): + self.buildername = buildername + self.hbox = hbox + self.tips = tips + self.state = "idle" + self.eta = None + self.last_results = None + self.last_text = None + self.size = size + self.hslice = hslice + + def create(self): + self.vbox = gtk.VBox(False) + l = gtk.Label(".") + self.current_box = box = gtk.EventBox() + # these size requests are somewhat non-deterministic. I think it + # depends upon how large label is, or how much space was already + # consumed when the box is added. + self.current_box.set_size_request(self.hslice, self.size * 0.75) + box.add(l) + self.vbox.pack_end(box) + self.current_box.modify_bg(gtk.STATE_NORMAL, + gtk.gdk.color_parse("gray50")) + + l2 = gtk.Label(".") + self.last_box = gtk.EventBox() + self.current_box.set_size_request(self.hslice, self.size * 0.25) + self.last_box.add(l2) + self.vbox.pack_end(self.last_box, True, True) + self.vbox.show_all() + self.hbox.pack_start(self.vbox, True, True) + + def remove(self): + self.hbox.remove(self.box) + + def set_state(self, state): + self.state = state + self.update() + + def set_eta(self, eta): + self.eta = eta + self.update() + + def set_last_build_results(self, results): + self.last_results = results + self.update() + + def set_last_build_text(self, text): + self.last_text = text + self.update() + + def update(self): + currentmap = {"offline": "red", + "idle": "white", + "waiting": "yellow", + "interlocked": "yellow", + "building": "yellow", + } + color = currentmap[self.state] + self.current_box.modify_bg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(color)) + lastmap = {None: "gray50", + SUCCESS: "green", + WARNINGS: "orange", + FAILURE: "red", + EXCEPTION: "purple", + } + last_color = lastmap[self.last_results] + self.last_box.modify_bg(gtk.STATE_NORMAL, + gtk.gdk.color_parse(last_color)) + current_tip = "%s:\n%s" % (self.buildername, self.state) + if self.eta is not None: + current_tip += " (ETA=%ds)" % self.eta + self.tips.set_tip(self.current_box, current_tip) + last_tip = "%s:\n" % self.buildername + if self.last_text: + last_tip += "\n".join(self.last_text) + else: + last_tip += "no builds" + self.tips.set_tip(self.last_box, last_tip) + + +class MyApplet(pb.Referenceable): + # CHANGE THIS TO POINT TO YOUR BUILDMASTER + buildmaster = "buildmaster.example.org", 12345 + filled = None + + def __init__(self, container): + self.applet = container + self.size = container.get_size() + self.hslice = self.size / 4 + container.set_size_request(self.size, self.size) + self.fill_nut() + verbs = [("Props", self.menu_preferences), + ("Connect", self.menu_connect), + ("Disconnect", self.menu_disconnect), + ] + container.setup_menu(MENU, verbs) + self.boxes = {} + self.connect() + + def fill(self, what): + if self.filled: + self.applet.remove(self.filled) + self.filled = None + self.applet.add(what) + self.filled = what + self.applet.show_all() + + def fill_nut(self): + i = gtk.Image() + i.set_from_file("/tmp/nut32.png") + self.fill(i) + + def fill_hbox(self): + self.hbox = gtk.HBox(True) + self.fill(self.hbox) + + def connect(self): + host, port = self.buildmaster + cf = pb.PBClientFactory() + creds = credentials.UsernamePassword("statusClient", "clientpw") + d = cf.login(creds) + reactor.connectTCP(host, port, cf) + d.addCallback(self.connected) + return d + + def connected(self, ref): + print "connected" + ref.notifyOnDisconnect(self.disconnected) + self.remote = ref + self.remote.callRemote("subscribe", "steps", 5, self) + self.fill_hbox() + self.tips = gtk.Tooltips() + self.tips.enable() + + def disconnect(self): + self.remote.broker.transport.loseConnection() + + def disconnected(self, *args): + print "disconnected" + self.fill_nut() + + def remote_builderAdded(self, buildername, builder): + print "builderAdded", buildername + box = Box(buildername, self.hbox, self.tips, self.size, self.hslice) + self.boxes[buildername] = box + box.create() + self.applet.set_size_request(self.hslice * len(self.boxes), + self.size) + d = builder.callRemote("getLastFinishedBuild") + + def _got(build): + if build: + d1 = build.callRemote("getResults") + d1.addCallback(box.set_last_build_results) + d2 = build.callRemote("getText") + d2.addCallback(box.set_last_build_text) + d.addCallback(_got) + + def remote_builderRemoved(self, buildername): + self.boxes[buildername].remove() + del self.boxes[buildername] + self.applet.set_size_request(self.hslice * len(self.boxes), + self.size) + + def remote_builderChangedState(self, buildername, state, eta): + self.boxes[buildername].set_state(state) + self.boxes[buildername].set_eta(eta) + print "change", buildername, state, eta + + def remote_buildStarted(self, buildername, build): + print "buildStarted", buildername + + def remote_buildFinished(self, buildername, build, results): + print "buildFinished", results + box = self.boxes[buildername] + box.set_eta(None) + d1 = build.callRemote("getResults") + d1.addCallback(box.set_last_build_results) + d2 = build.callRemote("getText") + d2.addCallback(box.set_last_build_text) + + def remote_buildETAUpdate(self, buildername, build, eta): + self.boxes[buildername].set_eta(eta) + print "ETA", buildername, eta + + def remote_stepStarted(self, buildername, build, stepname, step): + print "stepStarted", buildername, stepname + + def remote_stepFinished(self, buildername, build, stepname, step, results): + pass + + def menu_preferences(self, event, data=None): + print "prefs!" + p = Prefs(self) + p.create() + + def set_buildmaster(self, buildmaster): + host, port = buildmaster.split(":") + self.buildmaster = host, int(port) + self.disconnect() + reactor.callLater(0.5, self.connect) + + def menu_connect(self, event, data=None): + self.connect() + + def menu_disconnect(self, event, data=None): + self.disconnect() + + +class Prefs: + + def __init__(self, parent): + self.parent = parent + + def create(self): + self.w = w = gtk.Window() + v = gtk.VBox() + h = gtk.HBox() + h.pack_start(gtk.Label("buildmaster (host:port) : ")) + self.buildmaster_entry = b = gtk.Entry() + if self.parent.buildmaster: + host, port = self.parent.buildmaster + b.set_text("%s:%d" % (host, port)) + h.pack_start(b) + v.add(h) + + b = gtk.Button("Ok") + b.connect("clicked", self.done) + v.add(b) + + w.add(v) + w.show_all() + + def done(self, widget): + buildmaster = self.buildmaster_entry.get_text() + self.parent.set_buildmaster(buildmaster) + self.w.unmap() + + +def factory(applet, iid): + MyApplet(applet) + applet.show_all() + return True + + +from twisted.internet import reactor + +# instead of reactor.run(), we do the following: +reactor.startRunning() +reactor.simulate() +gnomeapplet.bonobo_factory("OAFIID:GNOME_Buildbot_Factory", + gnomeapplet.Applet.__gtype__, + "buildbot", "0", factory) + +# code ends here: bonobo_factory runs gtk.mainloop() internally and +# doesn't return until the program ends + +# SUPPORTING FILES: + +# save the following as ~/lib/bonobo/servers/bb_applet.server, and update all +# the pathnames to match your system +bb_applet_server = """ +<oaf_info> + +<oaf_server iid="OAFIID:GNOME_Buildbot_Factory" + type="exe" + location="/home/warner/stuff/buildbot-trunk/contrib/bb_applet.py"> + + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:Bonobo/GenericFactory:1.0"/> + <item value="IDL:Bonobo/Unknown:1.0"/> + </oaf_attribute> + <oaf_attribute name="name" type="string" value="Buildbot Factory"/> + <oaf_attribute name="description" type="string" value="Test"/> +</oaf_server> + +<oaf_server iid="OAFIID:GNOME_Buildbot" + type="factory" + location="OAFIID:GNOME_Buildbot_Factory"> + + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0"/> + <item value="IDL:Bonobo/Control:1.0"/> + <item value="IDL:Bonobo/Unknown:1.0"/> + </oaf_attribute> + <oaf_attribute name="name" type="string" value="Buildbot"/> + <oaf_attribute name="description" type="string" + value="Watch Buildbot status" + /> + <oaf_attribute name="panel:category" type="string" value="Utility"/> + <oaf_attribute name="panel:icon" type="string" + value="/home/warner/stuff/buildbot-trunk/doc/hexnut32.png" + /> + +</oaf_server> + +</oaf_info> +""" + +# a quick rundown on the Gnome2 applet scheme (probably wrong: there are +# better docs out there that you should be following instead) +# http://www.pycage.de/howto_bonobo.html describes a lot of +# the base Bonobo stuff. +# http://www.daa.com.au/pipermail/pygtk/2002-September/003393.html + +# bb_applet.server must be in your $BONOBO_ACTIVATION_PATH . I use +# ~/lib/bonobo/servers . This environment variable is read by +# bonobo-activation-server, so it must be set before you start any Gnome +# stuff. I set it in ~/.bash_profile . You can also put it in +# /usr/lib/bonobo/servers/ , which is probably on the default +# $BONOBO_ACTIVATION_PATH, so you won't have to update anything. + +# It is safest to put this in place before bonobo-activation-server is +# started, which may mean before any Gnome program is running. It may or may +# not detect bb_applet.server if it is installed afterwards.. there seem to +# be hooks, some of which involve FAM, but I never managed to make them work. +# The file must have a name that ends in .server or it will be ignored. + +# The .server file registers two OAF ids and tells the activation-server how +# to create those objects. The first is the GNOME_Buildbot_Factory, and is +# created by running the bb_applet.py script. The second is the +# GNOME_Buildbot applet itself, and is created by asking the +# GNOME_Buildbot_Factory to make it. + +# gnome-panel's "Add To Panel" menu will gather all the OAF ids that claim +# to implement the "IDL:GNOME/Vertigo/PanelAppletShell:1.0" in its +# "repo_ids" attribute. The sub-menu is determined by the "panel:category" +# attribute. The icon comes from "panel:icon", the text displayed in the +# menu comes from "name", the text in the tool-tip comes from "description". + +# The factory() function is called when a new applet is created. It receives +# a container that should be populated with the actual applet contents (in +# this case a Button). + +# If you're hacking on the code, just modify bb_applet.py and then kill -9 +# the running applet: the panel will ask you if you'd like to re-load the +# applet, and when you say 'yes', bb_applet.py will be re-executed. Note that +# 'kill PID' won't work because the program is sitting in C code, and SIGINT +# isn't delivered until after it surfaces to python, which will be never. + +# Running bb_applet.py by itself will result in a factory instance being +# created and then sitting around forever waiting for the activation-server +# to ask it to make an applet. This isn't very useful. + +# The "location" filename in bb_applet.server must point to bb_applet.py, and +# bb_applet.py must be executable. + +# Enjoy! +# -Brian Warner |