blob: 8ce4c4c61da4a636acacfb841a4e7a4b1569b214 [file] [log] [blame]
#! /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() #@UndefinedVariable
import gtk #@UnresolvedImport
import gnomeapplet #@UnresolvedImport
# 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