which included commits to RCS files with non-trunk default branches. git-svn-id: svn://10.0.0.236/trunk@238587 18797224-902f-48f8-a5cc-f745e15eee43
409 lines
14 KiB
Python
Executable File
409 lines
14 KiB
Python
Executable File
#! /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
|
|
|