#!/usr/bin/python # Imports to initialise twisted reactor and pygtk import pygtk pygtk.require('2.0') from twisted.internet import gtk2reactor gtk2reactor.install() import gnome.ui gnome.init('gbuildbotclient', '0.1') # Misc other imports import sys, string import gobject, gtk.glade from twisted.internet import reactor from twisted.spread import pb from twisted.cred import credentials from twisted.python import log # A class to manage the remote connection to the buildbot server class BuildbotClient: def __init__(self): self.builders = {} self.remote = None def connect(self, host, port, username, password): cf = pb.PBClientFactory() creds = credentials.UsernamePassword(username, password) d = cf.login(creds) reactor.connectTCP(host, port, cf) d.addCallbacks(self.connected, lambda arg: log.err(arg)) log.msg('connecting to %s:%d as %s' % (host, port, username)) return d def connected(self, ref): log.msg('connected') # Remote reference is a StatusClientPerspective object. self.remote = ref self.remote.notifyOnDisconnect(self.disconnected) def disconnected(self, ref): log.msg('disconnected') ref = None def subscribe(self, mode, interval, target): return self.remote.callRemote('subscribe', mode, interval, target) # Application class class App(pb.Referenceable): # Model/View column identities COL_BUILDER = 0 # PYOBJECT: RemoteBuilder objecr COL_BUILDER_NAME = 1 # str: Name of builder COL_BUILDER_STATE = 2 # str: State of builder # Subscription modes. Each mode includes events of previous mode. MODE_BUILDERS = 'builders' # builderAdded, builderRemoved MODE_BUILDS = 'builds' # builderChangedState, buildStarted, buildFinished MODE_STEPS = 'steps' # buildETAUpdate, stepStarted, stepFinished MODE_LOGS = 'logs' # stepETAUpdate, logStarted, logFinished MODE_FULL = 'full' # logChunk # Results constants Results = ["success", "warnings", "failure", "skipped", "exception"] def __init__(self, host, port, username, password, updateInterval = 5): # Initialise buildbot client self.client = BuildbotClient() # Initialise GUI stuff self.xml = gtk.glade.XML('gbuildbotclient.glade') self.win = self.xml.get_widget('toplevel') self.win.connect('destroy', gtk.main_quit) self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, str, str) self.model.set_sort_column_id(self.COL_BUILDER_NAME, gtk.SORT_ASCENDING) view = gtk.TreeView(self.model) def SortableTreeViewColumn(name, text): c = gtk.TreeViewColumn(name, gtk.CellRendererText(), text = text) c.set_clickable(True) c.set_resizable(True) c.set_sort_column_id(text) return c view.append_column( SortableTreeViewColumn('Name', self.COL_BUILDER_NAME)) view.append_column( SortableTreeViewColumn('State', self.COL_BUILDER_STATE)) view.show() scrolledwindow = self.xml.get_widget('builders_scrolledwindow') scrolledwindow.add_with_viewport(view) scrolledwindow.show() self.win.show() # Subscribe to build events to update main window d = self.client.connect(host, port, username, password) def subscribe(arg): d = self.client.subscribe(self.MODE_BUILDS, 5, self) d.addCallbacks(lambda arg: sys.stdout.write("ok\n"), lambda *args: sys.stdout.write("error: %s\n" % args)) d.addCallback(subscribe) # Callbacks for subscription mode >= MODE_BUILDERS def remote_builderAdded(self, buildername, builder): """Called by the PB server when a builder has been added to the buildbot. The buildername parameter is the name of the build as a string, and builder is a RemoteBuilder object.""" log.msg('added builder "%s"' % buildername) # Add builder to model iter = self.model.append() self.model.set_value(iter, self.COL_BUILDER, builder) self.model.set_value(iter, self.COL_BUILDER_NAME, buildername) def remote_builderRemoved(self, buildername): """Called by the PB server when a builder has been removed from the buildbot.""" log.msg('removed builder "%s"' % buildername) # Remove builder from model def delBuilder(model, path, iter, user_data): if model.get_value(iter, self.COL_BUILDER_NAME) == buildername: self.model.remove(iter) self.model.foreach(delBuilder, None) # Subscription mode >= builds def remote_buildStarted(self, buildername, build): """Called by the PB server when a builder has started a build. The buildername parameter is the name of the build as a string, and build is a RemoteBuild object.""" log.msg('remote_buildStarted') def remote_builderChangedState(self, buildername, statename, eta): """Called by the PB server when a builder has changed state. The buildername parameter is the name of the build, state is a description of the state of the build, and eta is the estimated time in seconds until the completion of the build.""" log.msg('builder "%s" changed state to "%s"' % (buildername, statename)) # Reflect state change in model def updateState(model, path, iter, user_data): if model.get_value(iter, self.COL_BUILDER_NAME) == buildername: model.set_value(iter, self.COL_BUILDER_STATE, statename) self.model.foreach(updateState, None) def remote_buildFinished(self, buildername, build, result): """Called by the PB server when a build has finished. Buildername is the name of the build, build a RemoteBuild object, and result an integer exit code being the result of the build.""" log.msg('remote_buildFinished') # Main function if __name__ == '__main__': # Command line parsing from optparse import OptionParser optparser = OptionParser( usage = '%prog HOSTNAME:PORT -u USER -p PASS') optparser.add_option('-u', '--user', dest = 'user', action = 'store', type = 'string', help = 'user to connect as') optparser.add_option('-p', '--password', dest = 'password', action = 'store', type = 'string', help = 'password to connect user as') (opts, argv) = optparser.parse_args() if len(argv) != 1: optparser.print_usage() sys.exit(1) if opts.user is None: print 'Username required' optparser.print_usage() sys.exit(1) if opts.password is None: print 'Password required' optparser.print_usage() sys.exit(1) host, port = string.split(argv[0], ':') # Start application log.startLogging(sys.stdout) app = App(host, int(port), opts.user, opts.password) reactor.run()