Start of command line buildbot status client.
[tpot/bbremote.git] / gbuildbotclient
1 #!/usr/bin/python
2
3 # Imports to initialise twisted reactor and pygtk
4
5 import pygtk
6 pygtk.require('2.0')
7
8 from twisted.internet import gtk2reactor, defer
9 gtk2reactor.install()
10
11 import gnome.ui
12 gnome.init('gbuildbotclient', '0.1')
13
14 # Misc other imports
15
16 import sys, string
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
22 from bbclient import BuildbotClient, CommandLineOptions
23
24 # Application class
25
26 class App(pb.Referenceable):
27
28     # Model/View column identities
29
30     COL_BUILDER = 0               # PYOBJECT: RemoteBuilder objecr
31     COL_BUILDER_NAME = 1          # str: Name of builder
32     COL_BUILDER_STATE = 2         # str: State of builder
33     COL_BUILDER_BUILD_STATUS = 3  # str: State of current or last build
34
35     COL_BUILDER_BUILD_ETA_TEXT = 4      # str: Time remaining for build
36     COL_BUILDER_BUILD_ETA_PERCENT = 5   # int: Percent complete for build
37     COL_BUILDER_BUILD_ETA_TOTAL = 6     # int: Total estimated time for build
38
39     # Subscription modes.  Each mode includes events of previous mode.
40
41     MODE_BUILDERS = 'builders' # builderAdded, builderRemoved
42     MODE_BUILDS = 'builds'  # builderChangedState, buildStarted, buildFinished
43     MODE_STEPS = 'steps'    # buildETAUpdate, stepStarted, stepFinished
44     MODE_LOGS = 'logs'      # stepETAUpdate, logStarted, logFinished
45     MODE_FULL = 'full'      # logChunk
46
47     # Results constants
48
49     Results = ["success", "warnings", "failure", "skipped", "exception"]
50
51     def __init__(self, host, port, username, password, updateInterval = 5):
52
53         # Initialise buildbot client
54
55         self.client = BuildbotClient()
56
57         # Initialise GUI stuff
58
59         self.xml = gtk.glade.XML('gbuildbotclient.glade')
60
61         self.win = self.xml.get_widget('toplevel')
62         self.win.connect('destroy', gtk.main_quit)
63
64         self.model = gtk.ListStore(
65             gobject.TYPE_PYOBJECT, str, str, str, str, int, int)
66
67         self.model.set_sort_column_id(
68             self.COL_BUILDER_STATE, gtk.SORT_ASCENDING)
69
70         view = gtk.TreeView(self.model)
71
72         def SortableTreeViewColumn(name, text):
73             c = gtk.TreeViewColumn(name, gtk.CellRendererText(), text = text)
74             c.set_clickable(True)
75             c.set_resizable(True)
76             c.set_sort_column_id(text)
77             c.set_expand(True)
78             return c
79
80         view.append_column(
81             SortableTreeViewColumn('Name', self.COL_BUILDER_NAME))
82
83         view.append_column(
84             SortableTreeViewColumn('State', self.COL_BUILDER_STATE))
85
86         view.append_column(
87             SortableTreeViewColumn(
88                 'Build Status', self.COL_BUILDER_BUILD_STATUS))
89
90         col = gtk.TreeViewColumn('Build ETA',
91                                  gtk.CellRendererProgress(), 
92                                  text = self.COL_BUILDER_BUILD_ETA_TEXT,
93                                  value = self.COL_BUILDER_BUILD_ETA_PERCENT)
94
95         col.set_clickable(True)
96         col.set_resizable(True)
97         col.set_sort_column_id(self.COL_BUILDER_BUILD_ETA_PERCENT)
98         col.set_expand(True)
99
100         view.append_column(col)
101
102         view.show()
103
104         scrolledwindow = self.xml.get_widget('builders_scrolledwindow')
105         scrolledwindow.add_with_viewport(view)
106         scrolledwindow.show()
107
108         self.win.show()
109
110         # Subscribe to build events to update main window
111
112         d = self.client.connect(host, port, username, password)
113
114         def subscribe(arg):
115
116             d = self.client.subscribe(self.MODE_STEPS, 5, self)
117
118             d.addErrback(
119                 lambda *args: sys.stdout.write("error: %s\n" % args))
120
121         d.addCallback(subscribe)
122
123     # Callbacks for subscription mode >= MODE_BUILDERS
124
125     def remote_builderAdded(self, buildername, builder):
126         """Called by the PB server when a builder has been added to the
127         buildbot.  The buildername parameter is the name of the build
128         as a string, and builder is a RemoteBuilder object."""
129
130         # Add builder to model
131
132         iter = self.model.append()
133
134         self.model.set_value(iter, self.COL_BUILDER, builder)
135         self.model.set_value(iter, self.COL_BUILDER_NAME, buildername)
136         self.model.set_value(iter, self.COL_BUILDER_BUILD_ETA_TEXT, 'n/a')
137
138     def remote_builderRemoved(self, buildername):
139         """Called by the PB server when a builder has been removed from the
140         buildbot."""
141
142         # Remove builder from model
143
144         def delBuilder(model, path, iter, user_data):
145             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
146                 self.model.remove(iter)
147
148         self.model.foreach(delBuilder, None)
149
150     # Callbacks for subscription mode >= MODE_BUILDS
151
152     def remote_buildStarted(self, buildername, build):
153         """Called by the PB server when a builder has started a build. The
154         buildername parameter is the name of the build as a string,
155         and build is a RemoteBuild object."""
156
157         # Update build status
158
159         def updateStatus(model, path, iter, user_data):
160             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
161                 build.callRemote('getNumber').addCallback(
162                     lambda num:
163                         model.set_value(iter, 
164                                         self.COL_BUILDER_BUILD_STATUS,
165                                         'Started build %d' % num))
166
167         self.model.foreach(updateStatus, None)
168
169     def remote_builderChangedState(self, buildername, statename, eta):
170         """Called by the PB server when a builder has changed state.  The
171         buildername parameter is the name of the build, state is a
172         description of the state of the build, and eta is the
173         estimated time in seconds until the completion of the build."""
174
175         # Reflect state change in model
176
177         def updateState(model, path, iter, user_data):
178
179             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
180
181                 # Update state
182
183                 model.set_value(iter, self.COL_BUILDER_STATE, statename)
184
185                 # Update status
186
187                 if statename == 'building':
188
189                     model.set_value(
190                         iter, self.COL_BUILDER_BUILD_STATUS, 'Unknown')
191
192                     model.set_value(
193                         iter, self.COL_BUILDER_BUILD_ETA_TEXT, 'Unknown')
194
195         self.model.foreach(updateState, None)
196
197     def remote_buildFinished(self, buildername, build, result):
198         """Called by the PB server when a build has finished.  Buildername is
199         the name of the build, build a RemoteBuild object, and result an
200         integer exit code being the result of the build."""
201
202         # Update build status
203
204         def updateStatus(model, path, iter, user_data):
205
206             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
207
208                 # Update build status
209
210                 dl = defer.DeferredList([build.callRemote('getNumber'),
211                                          build.callRemote('getResults')])
212
213                 dl.addCallback(
214                     lambda arg: 
215                         model.set_value(iter, 
216                                         self.COL_BUILDER_BUILD_STATUS,
217                                         'Finished build %d: %s' % 
218                                         (arg[0][1], self.Results[arg[1][1]])))
219
220                 # Reset progress indicators
221
222                 model.set_value(iter, self.COL_BUILDER_BUILD_ETA_TOTAL, 0)
223                 model.set_value(iter, self.COL_BUILDER_BUILD_ETA_PERCENT, 0)
224                 model.set_value(iter, self.COL_BUILDER_BUILD_ETA_TEXT, 'n/a')
225
226         self.model.foreach(updateStatus, None)
227
228     # Callbacks for subscription mode >= MODE_STEPS
229
230     def remote_stepStarted(self, buildername, build, stepname, step):
231         """Called by the PB server when a step in a build is started.  The
232         buildername parameter is the name of the build, build is a
233         RemoteBuild object, stepname the name of the step, and step a
234         RemoteBuildStep object."""
235
236         # Update build status
237
238         def updateStatus(model, path, iter, user_data):
239             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
240                 model.set_value(iter, self.COL_BUILDER_BUILD_STATUS,
241                                 'Started step "%s"' % stepname)
242
243         self.model.foreach(updateStatus, None)
244
245     def remote_stepFinished(self, buildername, build, stepname, step, results):
246         """Called by the PB server when a step in a build has finished.  The
247         buildername parameter is the name of the build, build is a
248         remote build object, stepname the name of the step, step a
249         RemoteBuildStep object, and results is a tuple of result code
250         and a list of optional strings the step wants to append to the
251         overall build results."""
252
253         # Update build status
254
255         def updateStatus(model, path, iter, user_data):
256             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
257                 self.model.set_value(iter, self.COL_BUILDER_BUILD_STATUS,
258                                      'Finished step "%s"' % stepname)
259
260         self.model.foreach(updateStatus, None)
261
262     def remote_buildETAUpdate(self, buildername, build, eta):
263         """Called by the PB server to update the ETA for the overall build.
264         The buidlername parameter is the name of he build, build is a
265         RemoteBuild object, and ETA the estimated time to completion
266         of the overall build."""
267
268         # Update build progress 
269
270         def updateProgress(model, path, iter, user_data):
271
272             if model.get_value(iter, self.COL_BUILDER_NAME) == buildername:
273                 
274                 min = int(eta/60)
275                 sec = int(eta - min*60)
276
277                 model.set_value(
278                     iter, self.COL_BUILDER_BUILD_ETA_TEXT, 
279                     '%d:%02d remaining' % (min, sec))
280
281                 total = model.get_value(iter, self.COL_BUILDER_BUILD_ETA_TOTAL)
282
283                 if total == 0:
284                     model.set_value(iter, self.COL_BUILDER_BUILD_ETA_TOTAL, eta)
285                     total = eta
286
287                 model.set_value(
288                     iter, self.COL_BUILDER_BUILD_ETA_PERCENT, 
289                     100 * (total - eta) / total)
290
291         self.model.foreach(updateProgress, None)        
292
293     def remote_stepETAUpdate(self, buildername, build, stepname, step,
294                              eta, expectations):
295         """Called by the PB server to update the ETA for a step in a build.
296         The buidlername parameter is the name of the build, build a
297         RemoteBuild object, stepname the name of the step, step a
298         RemoteBuildStep object, eta the estimated time to completion o
299         the step, and expectations a tuple of (name, current, target)
300         where the value of current approaches target.""" 
301
302         pass
303
304     def remote_logStarted(self, buildername, build, stepname, step,
305                           logname, remotelog):
306         """Called when the log for a build step is started."""
307         
308         pass
309
310     def remote_logFinished(self, buildername, build, stepname, step,
311                            logname, remotelog):
312         """Called when the log for a bild step is finished."""
313
314         pass
315
316 # Main function
317
318 if __name__ == '__main__':
319
320     # Command line parsing
321
322     host, port, user, password = CommandLineOptions()
323
324     # Start application
325
326     log.startLogging(sys.stdout)
327     app = App(host, port, user, password)
328
329     reactor.run()