3 # Imports to initialise twisted reactor and pygtk
8 from twisted.internet import gtk2reactor
12 gnome.init('gbuildbotclient', '0.1')
17 import gobject, gtk.glade
18 from twisted.internet import reactor
19 from twisted.spread import pb
20 from twisted.cred import credentials
21 from twisted.python import log
23 # A class to manage the remote connection to the buildbot server
32 def connect(self, host, port, username, password):
34 cf = pb.PBClientFactory()
35 creds = credentials.UsernamePassword(username, password)
38 reactor.connectTCP(host, port, cf)
40 d.addCallbacks(self.connected, lambda arg: log.err(arg))
42 log.msg('connecting to %s:%d as %s' % (host, port, username))
46 def connected(self, ref):
50 # Remote reference is a StatusClientPerspective object.
53 self.remote.notifyOnDisconnect(self.disconnected)
55 def disconnected(self, ref):
57 log.msg('disconnected')
61 def subscribe(self, mode, interval, target):
62 return self.remote.callRemote('subscribe', mode, interval, target)
66 class App(pb.Referenceable):
68 # Model/View column identities
70 COL_BUILDER = 0 # PYOBJECT: RemoteBuilder objecr
71 COL_BUILDER_NAME = 1 # str: Name of builder
72 COL_BUILDER_STATE = 2 # str: State of builder
73 COL_BUILDER_BUILD_STATUS = 3 # str: State of current or last build
75 # Subscription modes. Each mode includes events of previous mode.
77 MODE_BUILDERS = 'builders' # builderAdded, builderRemoved
78 MODE_BUILDS = 'builds' # builderChangedState, buildStarted, buildFinished
79 MODE_STEPS = 'steps' # buildETAUpdate, stepStarted, stepFinished
80 MODE_LOGS = 'logs' # stepETAUpdate, logStarted, logFinished
81 MODE_FULL = 'full' # logChunk
85 Results = ["success", "warnings", "failure", "skipped", "exception"]
87 def __init__(self, host, port, username, password, updateInterval = 5):
89 # Initialise buildbot client
91 self.client = BuildbotClient()
93 # Initialise GUI stuff
95 self.xml = gtk.glade.XML('gbuildbotclient.glade')
97 self.win = self.xml.get_widget('toplevel')
98 self.win.connect('destroy', gtk.main_quit)
100 self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, str, str, str)
102 self.model.set_sort_column_id(self.COL_BUILDER_NAME, gtk.SORT_ASCENDING)
104 view = gtk.TreeView(self.model)
106 def SortableTreeViewColumn(name, text):
107 c = gtk.TreeViewColumn(name, gtk.CellRendererText(), text = text)
108 c.set_clickable(True)
109 c.set_resizable(True)
110 c.set_sort_column_id(text)
114 SortableTreeViewColumn('Name', self.COL_BUILDER_NAME))
117 SortableTreeViewColumn('State', self.COL_BUILDER_STATE))
120 SortableTreeViewColumn(
121 'Build Status', self.COL_BUILDER_BUILD_STATUS))
125 scrolledwindow = self.xml.get_widget('builders_scrolledwindow')
126 scrolledwindow.add_with_viewport(view)
127 scrolledwindow.show()
131 # Subscribe to build events to update main window
133 d = self.client.connect(host, port, username, password)
137 d = self.client.subscribe(self.MODE_STEPS, 5, self)
139 d.addCallbacks(lambda arg: sys.stdout.write("ok\n"),
140 lambda *args: sys.stdout.write("error: %s\n" % args))
142 d.addCallback(subscribe)
144 # Callbacks for subscription mode >= MODE_BUILDERS
146 def remote_builderAdded(self, buildername, builder):
147 """Called by the PB server when a builder has been added to the
148 buildbot. The buildername parameter is the name of the build
149 as a string, and builder is a RemoteBuilder object."""
151 log.msg('added builder "%s"' % buildername)
153 # Add builder to model
155 iter = self.model.append()
157 self.model.set_value(iter, self.COL_BUILDER, builder)
158 self.model.set_value(iter, self.COL_BUILDER_NAME, buildername)
160 def remote_builderRemoved(self, buildername):
161 """Called by the PB server when a builder has been removed from the
164 log.msg('removed builder "%s"' % buildername)
166 # Remove builder from model
168 def delBuilder(model, path, iter, user_data):
169 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
170 self.model.remove(iter)
172 self.model.foreach(delBuilder, None)
174 # Callbacks for subscription mode >= MODE_BUILDS
176 def remote_buildStarted(self, buildername, build):
177 """Called by the PB server when a builder has started a build. The
178 buildername parameter is the name of the build as a string,
179 and build is a RemoteBuild object."""
181 log.msg('remote_buildStarted')
183 # Update build status
185 def updateStatus(model, path, iter, user_data):
186 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
187 build.callRemote('getNumber').addCallback(
189 model.set_value(iter,
190 self.COL_BUILDER_BUILD_STATUS,
191 'started build %d' % num))
193 self.model.foreach(updateStatus, None)
195 def remote_builderChangedState(self, buildername, statename, eta):
196 """Called by the PB server when a builder has changed state. The
197 buildername parameter is the name of the build, state is a
198 description of the state of the build, and eta is the
199 estimated time in seconds until the completion of the build."""
201 log.msg('builder "%s" changed state to "%s"' % (buildername, statename))
203 # Reflect state change in model
205 def updateState(model, path, iter, user_data):
206 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
207 model.set_value(iter, self.COL_BUILDER_STATE, statename)
209 self.model.foreach(updateState, None)
211 def remote_buildFinished(self, buildername, build, result):
212 """Called by the PB server when a build has finished. Buildername is
213 the name of the build, build a RemoteBuild object, and result an
214 integer exit code being the result of the build."""
216 log.msg('remote_buildFinished')
218 # Update build status
220 def updateStatus(model, path, iter, user_data):
221 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
222 build.callRemote('getNumber').addCallback(
224 model.set_value(iter,
225 self.COL_BUILDER_BUILD_STATUS,
226 'finished build %d' % num))
228 self.model.foreach(updateStatus, None)
230 # Callbacks for subscription mode >= MODE_STEPS
232 def remote_stepStarted(self, buildername, build, stepname, step):
233 """Called by the PB server when a step in a build is started. The
234 buildername parameter is the name of the build, build is a
235 RemoteBuild object, stepname the name of the step, and step a
236 RemoteBuildStep object."""
238 log.msg('step "%s" started for "%s"' % (stepname, buildername))
240 # Update build status
242 def updateStatus(model, path, iter, user_data):
243 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
244 self.model.set_value(iter, self.COL_BUILDER_BUILD_STATUS,
245 'Started step "%s"' % stepname)
247 self.model.foreach(updateStatus, None)
249 def remote_stepFinished(self, buildername, build, stepname, step, results):
250 """Called by the PB server when a step in a build has finished. The
251 buildername parameter is the name of the build, build is a
252 remote build object, stepname the name of the step, step a
253 RemoteBuildStep object, and results is a tuple of result code
254 and a list of optional strings the step wants to append to the
255 overall build results."""
257 log.msg('step "%s" finished for "%s"' % (stepname, buildername))
259 # Update build status
261 def updateStatus(model, path, iter, user_data):
262 if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
263 self.model.set_value(iter, self.COL_BUILDER_BUILD_STATUS,
264 'Finished step "%s"' % stepname)
266 self.model.foreach(updateStatus, None)
268 def remote_buildETAUpdate(self, buildername, build, eta):
269 """Called by the PB server to update the ETA for the overall build.
270 The buidlername parameter is the name of he build, build is a
271 RemoteBuild object, and ETA the estimated time to completion
272 of the overall build."""
274 log.msg('"%s" to finish build in %s seconds' % (buildername, eta))
276 def remote_stepETAUpdate(self, buildername, build, stepname, step,
278 """Called by the PB server to update the ETA for a step in a build.
279 The buidlername parameter is the name of the build, build a
280 RemoteBuild object, stepname the name of the step, step a
281 RemoteBuildStep object, eta the estimated time to completion o
282 the step, and expectations a tuple of (name, current, target)
283 where the value of current approaches target."""
285 log.msg('"%s" to finish step %s in %s seconds' % (buildername, stepname, eta))
287 def remote_logStarted(self, buildername, build, stepname, step,
289 """Called when the log for a build step is started."""
293 def remote_logFinished(self, buildername, build, stepname, step,
295 """Called when the log for a bild step is finished."""
301 if __name__ == '__main__':
303 # Command line parsing
305 from optparse import OptionParser
307 optparser = OptionParser(
308 usage = '%prog HOSTNAME:PORT -u USER -p PASS')
310 optparser.add_option('-u', '--user', dest = 'user',
311 action = 'store', type = 'string',
312 help = 'user to connect as')
314 optparser.add_option('-p', '--password', dest = 'password',
315 action = 'store', type = 'string',
316 help = 'password to connect user as')
318 (opts, argv) = optparser.parse_args()
321 optparser.print_usage()
324 if opts.user is None:
325 print 'Username required'
326 optparser.print_usage()
329 if opts.password is None:
330 print 'Password required'
331 optparser.print_usage()
334 host, port = string.split(argv[0], ':')
338 log.startLogging(sys.stdout)
339 app = App(host, int(port), opts.user, opts.password)