Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/contrib/bb_applet.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/contrib/bb_applet.py')
-rwxr-xr-xbuildbot/contrib/bb_applet.py413
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