OLD | NEW |
| (Empty) |
1 | |
2 from twisted.internet import gtk2reactor | |
3 gtk2reactor.install() | |
4 | |
5 import sys, time | |
6 | |
7 import pygtk | |
8 pygtk.require("2.0") | |
9 import gobject, gtk | |
10 assert(gtk.Window) # in gtk1 it's gtk.GtkWindow | |
11 | |
12 from twisted.spread import pb | |
13 | |
14 #from buildbot.clients.base import Builder, Client | |
15 from buildbot.clients.base import TextClient | |
16 from buildbot.util import now | |
17 | |
18 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION | |
19 | |
20 ''' | |
21 class Pane: | |
22 def __init__(self): | |
23 pass | |
24 | |
25 class OneRow(Pane): | |
26 """This is a one-row status bar. It has one square per Builder, and that | |
27 square is either red, yellow, or green. """ | |
28 | |
29 def __init__(self): | |
30 Pane.__init__(self) | |
31 self.widget = gtk.VBox(gtk.FALSE, 2) | |
32 self.nameBox = gtk.HBox(gtk.TRUE) | |
33 self.statusBox = gtk.HBox(gtk.TRUE) | |
34 self.widget.add(self.nameBox) | |
35 self.widget.add(self.statusBox) | |
36 self.widget.show_all() | |
37 self.builders = [] | |
38 | |
39 def getWidget(self): | |
40 return self.widget | |
41 def addBuilder(self, builder): | |
42 print "OneRow.addBuilder" | |
43 # todo: ordering. Should follow the order in which they were added | |
44 # to the original BotMaster | |
45 self.builders.append(builder) | |
46 # add the name to the left column, and a label (with background) to | |
47 # the right | |
48 name = gtk.Label(builder.name) | |
49 status = gtk.Label('??') | |
50 status.set_size_request(64,64) | |
51 box = gtk.EventBox() | |
52 box.add(status) | |
53 name.show() | |
54 box.show_all() | |
55 self.nameBox.add(name) | |
56 self.statusBox.add(box) | |
57 builder.haveSomeWidgets([name, status, box]) | |
58 | |
59 class R2Builder(Builder): | |
60 def start(self): | |
61 self.nameSquare.set_text(self.name) | |
62 self.statusSquare.set_text("???") | |
63 self.subscribe() | |
64 def haveSomeWidgets(self, widgets): | |
65 self.nameSquare, self.statusSquare, self.statusBox = widgets | |
66 | |
67 def remote_newLastBuildStatus(self, event): | |
68 color = None | |
69 if event: | |
70 text = "\n".join(event.text) | |
71 color = event.color | |
72 else: | |
73 text = "none" | |
74 self.statusSquare.set_text(text) | |
75 if color: | |
76 print "color", color | |
77 self.statusBox.modify_bg(gtk.STATE_NORMAL, | |
78 gtk.gdk.color_parse(color)) | |
79 | |
80 def remote_currentlyOffline(self): | |
81 self.statusSquare.set_text("offline") | |
82 def remote_currentlyIdle(self): | |
83 self.statusSquare.set_text("idle") | |
84 def remote_currentlyWaiting(self, seconds): | |
85 self.statusSquare.set_text("waiting") | |
86 def remote_currentlyInterlocked(self): | |
87 self.statusSquare.set_text("interlocked") | |
88 def remote_currentlyBuilding(self, eta): | |
89 self.statusSquare.set_text("building") | |
90 | |
91 | |
92 class CompactRow(Pane): | |
93 def __init__(self): | |
94 Pane.__init__(self) | |
95 self.widget = gtk.VBox(gtk.FALSE, 3) | |
96 self.nameBox = gtk.HBox(gtk.TRUE, 2) | |
97 self.lastBuildBox = gtk.HBox(gtk.TRUE, 2) | |
98 self.statusBox = gtk.HBox(gtk.TRUE, 2) | |
99 self.widget.add(self.nameBox) | |
100 self.widget.add(self.lastBuildBox) | |
101 self.widget.add(self.statusBox) | |
102 self.widget.show_all() | |
103 self.builders = [] | |
104 | |
105 def getWidget(self): | |
106 return self.widget | |
107 | |
108 def addBuilder(self, builder): | |
109 self.builders.append(builder) | |
110 | |
111 name = gtk.Label(builder.name) | |
112 name.show() | |
113 self.nameBox.add(name) | |
114 | |
115 last = gtk.Label('??') | |
116 last.set_size_request(64,64) | |
117 lastbox = gtk.EventBox() | |
118 lastbox.add(last) | |
119 lastbox.show_all() | |
120 self.lastBuildBox.add(lastbox) | |
121 | |
122 status = gtk.Label('??') | |
123 status.set_size_request(64,64) | |
124 statusbox = gtk.EventBox() | |
125 statusbox.add(status) | |
126 statusbox.show_all() | |
127 self.statusBox.add(statusbox) | |
128 | |
129 builder.haveSomeWidgets([name, last, lastbox, status, statusbox]) | |
130 | |
131 def removeBuilder(self, name, builder): | |
132 self.nameBox.remove(builder.nameSquare) | |
133 self.lastBuildBox.remove(builder.lastBuildBox) | |
134 self.statusBox.remove(builder.statusBox) | |
135 self.builders.remove(builder) | |
136 | |
137 class CompactBuilder(Builder): | |
138 def setup(self): | |
139 self.timer = None | |
140 self.text = [] | |
141 self.eta = None | |
142 def start(self): | |
143 self.nameSquare.set_text(self.name) | |
144 self.statusSquare.set_text("???") | |
145 self.subscribe() | |
146 def haveSomeWidgets(self, widgets): | |
147 (self.nameSquare, | |
148 self.lastBuildSquare, self.lastBuildBox, | |
149 self.statusSquare, self.statusBox) = widgets | |
150 | |
151 def remote_currentlyOffline(self): | |
152 self.eta = None | |
153 self.stopTimer() | |
154 self.statusSquare.set_text("offline") | |
155 self.statusBox.modify_bg(gtk.STATE_NORMAL, | |
156 gtk.gdk.color_parse("red")) | |
157 def remote_currentlyIdle(self): | |
158 self.eta = None | |
159 self.stopTimer() | |
160 self.statusSquare.set_text("idle") | |
161 def remote_currentlyWaiting(self, seconds): | |
162 self.nextBuild = now() + seconds | |
163 self.startTimer(self.updateWaiting) | |
164 def remote_currentlyInterlocked(self): | |
165 self.stopTimer() | |
166 self.statusSquare.set_text("interlocked") | |
167 def startTimer(self, func): | |
168 # the func must clear self.timer and return gtk.FALSE when the event | |
169 # has arrived | |
170 self.stopTimer() | |
171 self.timer = gtk.timeout_add(1000, func) | |
172 func() | |
173 def stopTimer(self): | |
174 if self.timer: | |
175 gtk.timeout_remove(self.timer) | |
176 self.timer = None | |
177 def updateWaiting(self): | |
178 when = self.nextBuild | |
179 if now() < when: | |
180 next = time.strftime("%H:%M:%S", time.localtime(when)) | |
181 secs = "[%d seconds]" % (when - now()) | |
182 self.statusSquare.set_text("waiting\n%s\n%s" % (next, secs)) | |
183 return gtk.TRUE # restart timer | |
184 else: | |
185 # done | |
186 self.statusSquare.set_text("waiting\n[RSN]") | |
187 self.timer = None | |
188 return gtk.FALSE | |
189 | |
190 def remote_currentlyBuilding(self, eta): | |
191 self.stopTimer() | |
192 self.statusSquare.set_text("building") | |
193 if eta: | |
194 d = eta.callRemote("subscribe", self, 5) | |
195 | |
196 def remote_newLastBuildStatus(self, event): | |
197 color = None | |
198 if event: | |
199 text = "\n".join(event.text) | |
200 color = event.color | |
201 else: | |
202 text = "none" | |
203 if not color: color = "gray" | |
204 self.lastBuildSquare.set_text(text) | |
205 self.lastBuildBox.modify_bg(gtk.STATE_NORMAL, | |
206 gtk.gdk.color_parse(color)) | |
207 | |
208 def remote_newEvent(self, event): | |
209 assert(event.__class__ == GtkUpdatingEvent) | |
210 self.current = event | |
211 event.builder = self | |
212 self.text = event.text | |
213 if not self.text: self.text = ["idle"] | |
214 self.eta = None | |
215 self.stopTimer() | |
216 self.updateText() | |
217 color = event.color | |
218 if not color: color = "gray" | |
219 self.statusBox.modify_bg(gtk.STATE_NORMAL, | |
220 gtk.gdk.color_parse(color)) | |
221 | |
222 def updateCurrent(self): | |
223 text = self.current.text | |
224 if text: | |
225 self.text = text | |
226 self.updateText() | |
227 color = self.current.color | |
228 if color: | |
229 self.statusBox.modify_bg(gtk.STATE_NORMAL, | |
230 gtk.gdk.color_parse(color)) | |
231 def updateText(self): | |
232 etatext = [] | |
233 if self.eta: | |
234 etatext = [time.strftime("%H:%M:%S", time.localtime(self.eta))] | |
235 if now() > self.eta: | |
236 etatext += ["RSN"] | |
237 else: | |
238 seconds = self.eta - now() | |
239 etatext += ["[%d secs]" % seconds] | |
240 text = "\n".join(self.text + etatext) | |
241 self.statusSquare.set_text(text) | |
242 def updateTextTimer(self): | |
243 self.updateText() | |
244 return gtk.TRUE # restart timer | |
245 | |
246 def remote_progress(self, seconds): | |
247 if seconds == None: | |
248 self.eta = None | |
249 else: | |
250 self.eta = now() + seconds | |
251 self.startTimer(self.updateTextTimer) | |
252 self.updateText() | |
253 def remote_finished(self, eta): | |
254 self.eta = None | |
255 self.stopTimer() | |
256 self.updateText() | |
257 eta.callRemote("unsubscribe", self) | |
258 ''' | |
259 | |
260 class Box: | |
261 def __init__(self, text="?"): | |
262 self.text = text | |
263 self.box = gtk.EventBox() | |
264 self.label = gtk.Label(text) | |
265 self.box.add(self.label) | |
266 self.box.set_size_request(64,64) | |
267 self.timer = None | |
268 | |
269 def getBox(self): | |
270 return self.box | |
271 | |
272 def setText(self, text): | |
273 self.text = text | |
274 self.label.set_text(text) | |
275 | |
276 def setColor(self, color): | |
277 if not color: | |
278 return | |
279 self.box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) | |
280 | |
281 def setETA(self, eta): | |
282 if eta: | |
283 self.when = now() + eta | |
284 self.startTimer() | |
285 else: | |
286 self.stopTimer() | |
287 | |
288 def startTimer(self): | |
289 self.stopTimer() | |
290 self.timer = gobject.timeout_add(1000, self.update) | |
291 self.update() | |
292 | |
293 def stopTimer(self): | |
294 if self.timer: | |
295 gobject.source_remove(self.timer) | |
296 self.timer = None | |
297 self.label.set_text(self.text) | |
298 | |
299 def update(self): | |
300 if now() < self.when: | |
301 next = time.strftime("%H:%M:%S", time.localtime(self.when)) | |
302 secs = "[%d secs]" % (self.when - now()) | |
303 self.label.set_text("%s\n%s\n%s" % (self.text, next, secs)) | |
304 return True # restart timer | |
305 else: | |
306 # done | |
307 self.label.set_text("%s\n[soon]\n[overdue]" % (self.text,)) | |
308 self.timer = None | |
309 return False | |
310 | |
311 | |
312 | |
313 class ThreeRowBuilder: | |
314 def __init__(self, name, ref): | |
315 self.name = name | |
316 | |
317 self.last = Box() | |
318 self.current = Box() | |
319 self.step = Box("idle") | |
320 self.step.setColor("white") | |
321 | |
322 self.ref = ref | |
323 | |
324 def getBoxes(self): | |
325 return self.last.getBox(), self.current.getBox(), self.step.getBox() | |
326 | |
327 def getLastBuild(self): | |
328 d = self.ref.callRemote("getLastFinishedBuild") | |
329 d.addCallback(self.gotLastBuild) | |
330 def gotLastBuild(self, build): | |
331 if build: | |
332 build.callRemote("getText").addCallback(self.gotLastText) | |
333 build.callRemote("getResults").addCallback(self.gotLastResult) | |
334 | |
335 def gotLastText(self, text): | |
336 print "Got text", text | |
337 self.last.setText("\n".join(text)) | |
338 | |
339 def gotLastResult(self, result): | |
340 colormap = {SUCCESS: 'green', | |
341 FAILURE: 'red', | |
342 WARNINGS: 'orange', | |
343 EXCEPTION: 'purple', | |
344 } | |
345 self.last.setColor(colormap[result]) | |
346 | |
347 def getState(self): | |
348 self.ref.callRemote("getState").addCallback(self.gotState) | |
349 def gotState(self, res): | |
350 state, ETA, builds = res | |
351 # state is one of: offline, idle, waiting, interlocked, building | |
352 # TODO: ETA is going away, you have to look inside the builds to get | |
353 # that value | |
354 currentmap = {"offline": "red", | |
355 "idle": "white", | |
356 "waiting": "yellow", | |
357 "interlocked": "yellow", | |
358 "building": "yellow",} | |
359 text = state | |
360 self.current.setColor(currentmap[state]) | |
361 if ETA is not None: | |
362 text += "\nETA=%s secs" % ETA | |
363 self.current.setText(state) | |
364 | |
365 def buildStarted(self, build): | |
366 print "[%s] buildStarted" % (self.name,) | |
367 self.current.setColor("yellow") | |
368 | |
369 def buildFinished(self, build, results): | |
370 print "[%s] buildFinished: %s" % (self.name, results) | |
371 self.gotLastBuild(build) | |
372 self.current.setColor("white") | |
373 self.current.stopTimer() | |
374 | |
375 def buildETAUpdate(self, eta): | |
376 print "[%s] buildETAUpdate: %s" % (self.name, eta) | |
377 self.current.setETA(eta) | |
378 | |
379 | |
380 def stepStarted(self, stepname, step): | |
381 print "[%s] stepStarted: %s" % (self.name, stepname) | |
382 self.step.setText(stepname) | |
383 self.step.setColor("yellow") | |
384 def stepFinished(self, stepname, step, results): | |
385 print "[%s] stepFinished: %s %s" % (self.name, stepname, results) | |
386 self.step.setText("idle") | |
387 self.step.setColor("white") | |
388 self.step.stopTimer() | |
389 def stepETAUpdate(self, stepname, eta): | |
390 print "[%s] stepETAUpdate: %s %s" % (self.name, stepname, eta) | |
391 self.step.setETA(eta) | |
392 | |
393 | |
394 class ThreeRowClient(pb.Referenceable): | |
395 def __init__(self, window): | |
396 self.window = window | |
397 self.buildernames = [] | |
398 self.builders = {} | |
399 | |
400 def connected(self, ref): | |
401 print "connected" | |
402 self.ref = ref | |
403 self.pane = gtk.VBox(False, 2) | |
404 self.table = gtk.Table(1+3, 1) | |
405 self.pane.add(self.table) | |
406 self.window.vb.add(self.pane) | |
407 self.pane.show_all() | |
408 ref.callRemote("subscribe", "logs", 5, self) | |
409 | |
410 def removeTable(self): | |
411 for child in self.table.get_children(): | |
412 self.table.remove(child) | |
413 self.pane.remove(self.table) | |
414 | |
415 def makeTable(self): | |
416 columns = len(self.builders) | |
417 self.table = gtk.Table(2, columns) | |
418 self.pane.add(self.table) | |
419 for i in range(len(self.buildernames)): | |
420 name = self.buildernames[i] | |
421 b = self.builders[name] | |
422 last,current,step = b.getBoxes() | |
423 self.table.attach(gtk.Label(name), i, i+1, 0, 1) | |
424 self.table.attach(last, i, i+1, 1, 2, | |
425 xpadding=1, ypadding=1) | |
426 self.table.attach(current, i, i+1, 2, 3, | |
427 xpadding=1, ypadding=1) | |
428 self.table.attach(step, i, i+1, 3, 4, | |
429 xpadding=1, ypadding=1) | |
430 self.table.show_all() | |
431 | |
432 def rebuildTable(self): | |
433 self.removeTable() | |
434 self.makeTable() | |
435 | |
436 def remote_builderAdded(self, buildername, builder): | |
437 print "builderAdded", buildername | |
438 assert buildername not in self.buildernames | |
439 self.buildernames.append(buildername) | |
440 | |
441 b = ThreeRowBuilder(buildername, builder) | |
442 self.builders[buildername] = b | |
443 self.rebuildTable() | |
444 b.getLastBuild() | |
445 b.getState() | |
446 | |
447 def remote_builderRemoved(self, buildername): | |
448 del self.builders[buildername] | |
449 self.buildernames.remove(buildername) | |
450 self.rebuildTable() | |
451 | |
452 def remote_builderChangedState(self, name, state, eta): | |
453 self.builders[name].gotState((state, eta, None)) | |
454 def remote_buildStarted(self, name, build): | |
455 self.builders[name].buildStarted(build) | |
456 def remote_buildFinished(self, name, build, results): | |
457 self.builders[name].buildFinished(build, results) | |
458 | |
459 def remote_buildETAUpdate(self, name, build, eta): | |
460 self.builders[name].buildETAUpdate(eta) | |
461 def remote_stepStarted(self, name, build, stepname, step): | |
462 self.builders[name].stepStarted(stepname, step) | |
463 def remote_stepFinished(self, name, build, stepname, step, results): | |
464 self.builders[name].stepFinished(stepname, step, results) | |
465 | |
466 def remote_stepETAUpdate(self, name, build, stepname, step, | |
467 eta, expectations): | |
468 # expectations is a list of (metricname, current_value, | |
469 # expected_value) tuples, so that we could show individual progress | |
470 # meters for each metric | |
471 self.builders[name].stepETAUpdate(stepname, eta) | |
472 | |
473 def remote_logStarted(self, buildername, build, stepname, step, | |
474 logname, log): | |
475 pass | |
476 | |
477 def remote_logFinished(self, buildername, build, stepname, step, | |
478 logname, log): | |
479 pass | |
480 | |
481 | |
482 class GtkClient(TextClient): | |
483 ClientClass = ThreeRowClient | |
484 | |
485 def __init__(self, master): | |
486 self.master = master | |
487 | |
488 w = gtk.Window() | |
489 self.w = w | |
490 #w.set_size_request(64,64) | |
491 w.connect('destroy', lambda win: gtk.main_quit()) | |
492 self.vb = gtk.VBox(False, 2) | |
493 self.status = gtk.Label("unconnected") | |
494 self.vb.add(self.status) | |
495 self.listener = self.ClientClass(self) | |
496 w.add(self.vb) | |
497 w.show_all() | |
498 | |
499 def connected(self, ref): | |
500 self.status.set_text("connected") | |
501 TextClient.connected(self, ref) | |
502 | |
503 """ | |
504 def addBuilder(self, name, builder): | |
505 Client.addBuilder(self, name, builder) | |
506 self.pane.addBuilder(builder) | |
507 def removeBuilder(self, name): | |
508 self.pane.removeBuilder(name, self.builders[name]) | |
509 Client.removeBuilder(self, name) | |
510 | |
511 def startConnecting(self, master): | |
512 self.master = master | |
513 Client.startConnecting(self, master) | |
514 self.status.set_text("connecting to %s.." % master) | |
515 def connected(self, remote): | |
516 Client.connected(self, remote) | |
517 self.status.set_text(self.master) | |
518 remote.notifyOnDisconnect(self.disconnected) | |
519 def disconnected(self, remote): | |
520 self.status.set_text("disconnected, will retry") | |
521 """ | |
522 | |
523 def main(): | |
524 master = "localhost:8007" | |
525 if len(sys.argv) > 1: | |
526 master = sys.argv[1] | |
527 c = GtkClient(master) | |
528 c.run() | |
529 | |
530 if __name__ == '__main__': | |
531 main() | |
532 | |
OLD | NEW |