#! /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 = """ """ 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 = """ """ # 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