OLD | NEW |
| (Empty) |
1 # -*- test-case-name: buildbot.test.test_status -*- | |
2 | |
3 from twisted.spread import pb | |
4 from twisted.python import components, log as twlog | |
5 from twisted.internet import reactor | |
6 from twisted.application import strports | |
7 from twisted.cred import portal, checkers | |
8 | |
9 from buildbot import interfaces | |
10 from zope.interface import Interface, implements | |
11 from buildbot.status import builder, base | |
12 from buildbot.changes import changes | |
13 | |
14 class IRemote(Interface): | |
15 pass | |
16 | |
17 def makeRemote(obj): | |
18 # we want IRemote(None) to be None, but you can't really do that with | |
19 # adapters, so we fake it | |
20 if obj is None: | |
21 return None | |
22 return IRemote(obj) | |
23 | |
24 | |
25 class RemoteBuildSet(pb.Referenceable): | |
26 def __init__(self, buildset): | |
27 self.b = buildset | |
28 | |
29 def remote_getSourceStamp(self): | |
30 return self.b.getSourceStamp() | |
31 | |
32 def remote_getReason(self): | |
33 return self.b.getReason() | |
34 | |
35 def remote_getID(self): | |
36 return self.b.getID() | |
37 | |
38 def remote_getBuilderNames(self): | |
39 return self.b.getBuilderNames() | |
40 | |
41 def remote_getBuildRequests(self): | |
42 """Returns a list of (builderName, BuildRequest) tuples.""" | |
43 return [(br.getBuilderName(), IRemote(br)) | |
44 for br in self.b.getBuildRequests()] | |
45 | |
46 def remote_isFinished(self): | |
47 return self.b.isFinished() | |
48 | |
49 def remote_waitUntilSuccess(self): | |
50 d = self.b.waitUntilSuccess() | |
51 d.addCallback(lambda res: self) | |
52 return d | |
53 | |
54 def remote_waitUntilFinished(self): | |
55 d = self.b.waitUntilFinished() | |
56 d.addCallback(lambda res: self) | |
57 return d | |
58 | |
59 def remote_getResults(self): | |
60 return self.b.getResults() | |
61 | |
62 components.registerAdapter(RemoteBuildSet, | |
63 interfaces.IBuildSetStatus, IRemote) | |
64 | |
65 | |
66 class RemoteBuilder(pb.Referenceable): | |
67 def __init__(self, builder): | |
68 self.b = builder | |
69 | |
70 def remote_getName(self): | |
71 return self.b.getName() | |
72 | |
73 def remote_getCategory(self): | |
74 return self.b.getCategory() | |
75 | |
76 def remote_getState(self): | |
77 state, builds = self.b.getState() | |
78 return (state, | |
79 None, # TODO: remove leftover ETA | |
80 [makeRemote(b) for b in builds]) | |
81 | |
82 def remote_getSlaves(self): | |
83 return [IRemote(s) for s in self.b.getSlaves()] | |
84 | |
85 def remote_getLastFinishedBuild(self): | |
86 return makeRemote(self.b.getLastFinishedBuild()) | |
87 | |
88 def remote_getCurrentBuilds(self): | |
89 return [IRemote(b) for b in self.b.getCurrentBuilds()] | |
90 | |
91 def remote_getBuild(self, number): | |
92 return makeRemote(self.b.getBuild(number)) | |
93 | |
94 def remote_getEvent(self, number): | |
95 return IRemote(self.b.getEvent(number)) | |
96 | |
97 components.registerAdapter(RemoteBuilder, | |
98 interfaces.IBuilderStatus, IRemote) | |
99 | |
100 | |
101 class RemoteBuildRequest(pb.Referenceable): | |
102 def __init__(self, buildreq): | |
103 self.b = buildreq | |
104 self.observers = [] | |
105 | |
106 def remote_getSourceStamp(self): | |
107 return self.b.getSourceStamp() | |
108 | |
109 def remote_getBuilderName(self): | |
110 return self.b.getBuilderName() | |
111 | |
112 def remote_subscribe(self, observer): | |
113 """The observer's remote_newbuild method will be called (with two | |
114 arguments: the RemoteBuild object, and our builderName) for each new | |
115 Build that is created to handle this BuildRequest.""" | |
116 self.observers.append(observer) | |
117 def send(bs): | |
118 d = observer.callRemote("newbuild", | |
119 IRemote(bs), self.b.getBuilderName()) | |
120 d.addErrback(lambda err: None) | |
121 reactor.callLater(0, self.b.subscribe, send) | |
122 | |
123 def remote_unsubscribe(self, observer): | |
124 # PB (well, at least oldpb) doesn't re-use RemoteReference instances, | |
125 # so sending the same object across the wire twice will result in two | |
126 # separate objects that compare as equal ('a is not b' and 'a == b'). | |
127 # That means we can't use a simple 'self.observers.remove(observer)' | |
128 # here. | |
129 for o in self.observers: | |
130 if o == observer: | |
131 self.observers.remove(o) | |
132 | |
133 components.registerAdapter(RemoteBuildRequest, | |
134 interfaces.IBuildRequestStatus, IRemote) | |
135 | |
136 class RemoteBuild(pb.Referenceable): | |
137 def __init__(self, build): | |
138 self.b = build | |
139 self.observers = [] | |
140 | |
141 def remote_getBuilderName(self): | |
142 return self.b.getBuilder().getName() | |
143 | |
144 def remote_getNumber(self): | |
145 return self.b.getNumber() | |
146 | |
147 def remote_getReason(self): | |
148 return self.b.getReason() | |
149 | |
150 def remote_getChanges(self): | |
151 return [IRemote(c) for c in self.b.getChanges()] | |
152 | |
153 def remote_getResponsibleUsers(self): | |
154 return self.b.getResponsibleUsers() | |
155 | |
156 def remote_getSteps(self): | |
157 return [IRemote(s) for s in self.b.getSteps()] | |
158 | |
159 def remote_getTimes(self): | |
160 return self.b.getTimes() | |
161 | |
162 def remote_isFinished(self): | |
163 return self.b.isFinished() | |
164 | |
165 def remote_waitUntilFinished(self): | |
166 # the Deferred returned by callRemote() will fire when this build is | |
167 # finished | |
168 d = self.b.waitUntilFinished() | |
169 d.addCallback(lambda res: self) | |
170 return d | |
171 | |
172 def remote_getETA(self): | |
173 return self.b.getETA() | |
174 | |
175 def remote_getCurrentStep(self): | |
176 return makeRemote(self.b.getCurrentStep()) | |
177 | |
178 def remote_getText(self): | |
179 return self.b.getText() | |
180 | |
181 def remote_getResults(self): | |
182 return self.b.getResults() | |
183 | |
184 def remote_getLogs(self): | |
185 logs = {} | |
186 for name,log in self.b.getLogs().items(): | |
187 logs[name] = IRemote(log) | |
188 return logs | |
189 | |
190 def remote_subscribe(self, observer, updateInterval=None): | |
191 """The observer will have remote_stepStarted(buildername, build, | |
192 stepname, step), remote_stepFinished(buildername, build, stepname, | |
193 step, results), and maybe remote_buildETAUpdate(buildername, build, | |
194 eta)) messages sent to it.""" | |
195 self.observers.append(observer) | |
196 s = BuildSubscriber(observer) | |
197 self.b.subscribe(s, updateInterval) | |
198 | |
199 def remote_unsubscribe(self, observer): | |
200 # TODO: is the observer automatically unsubscribed when the build | |
201 # finishes? Or are they responsible for unsubscribing themselves | |
202 # anyway? How do we avoid a race condition here? | |
203 for o in self.observers: | |
204 if o == observer: | |
205 self.observers.remove(o) | |
206 | |
207 | |
208 components.registerAdapter(RemoteBuild, | |
209 interfaces.IBuildStatus, IRemote) | |
210 | |
211 class BuildSubscriber: | |
212 def __init__(self, observer): | |
213 self.observer = observer | |
214 | |
215 def buildETAUpdate(self, build, eta): | |
216 self.observer.callRemote("buildETAUpdate", | |
217 build.getBuilder().getName(), | |
218 IRemote(build), | |
219 eta) | |
220 | |
221 def stepStarted(self, build, step): | |
222 self.observer.callRemote("stepStarted", | |
223 build.getBuilder().getName(), | |
224 IRemote(build), | |
225 step.getName(), IRemote(step)) | |
226 return None | |
227 | |
228 def stepFinished(self, build, step, results): | |
229 self.observer.callRemote("stepFinished", | |
230 build.getBuilder().getName(), | |
231 IRemote(build), | |
232 step.getName(), IRemote(step), | |
233 results) | |
234 | |
235 | |
236 class RemoteBuildStep(pb.Referenceable): | |
237 def __init__(self, step): | |
238 self.s = step | |
239 | |
240 def remote_getName(self): | |
241 return self.s.getName() | |
242 | |
243 def remote_getBuild(self): | |
244 return IRemote(self.s.getBuild()) | |
245 | |
246 def remote_getTimes(self): | |
247 return self.s.getTimes() | |
248 | |
249 def remote_getExpectations(self): | |
250 return self.s.getExpectations() | |
251 | |
252 def remote_getLogs(self): | |
253 logs = {} | |
254 for log in self.s.getLogs(): | |
255 logs[log.getName()] = IRemote(log) | |
256 return logs | |
257 | |
258 def remote_isFinished(self): | |
259 return self.s.isFinished() | |
260 | |
261 def remote_waitUntilFinished(self): | |
262 return self.s.waitUntilFinished() # returns a Deferred | |
263 | |
264 def remote_getETA(self): | |
265 return self.s.getETA() | |
266 | |
267 def remote_getText(self): | |
268 return self.s.getText() | |
269 | |
270 def remote_getResults(self): | |
271 return self.s.getResults() | |
272 | |
273 components.registerAdapter(RemoteBuildStep, | |
274 interfaces.IBuildStepStatus, IRemote) | |
275 | |
276 class RemoteSlave: | |
277 def __init__(self, slave): | |
278 self.s = slave | |
279 | |
280 def remote_getName(self): | |
281 return self.s.getName() | |
282 def remote_getAdmin(self): | |
283 return self.s.getAdmin() | |
284 def remote_getHost(self): | |
285 return self.s.getHost() | |
286 def remote_isConnected(self): | |
287 return self.s.isConnected() | |
288 | |
289 components.registerAdapter(RemoteSlave, | |
290 interfaces.ISlaveStatus, IRemote) | |
291 | |
292 class RemoteEvent: | |
293 def __init__(self, event): | |
294 self.e = event | |
295 | |
296 def remote_getTimes(self): | |
297 return self.s.getTimes() | |
298 def remote_getText(self): | |
299 return self.s.getText() | |
300 | |
301 components.registerAdapter(RemoteEvent, | |
302 interfaces.IStatusEvent, IRemote) | |
303 | |
304 class RemoteLog(pb.Referenceable): | |
305 def __init__(self, log): | |
306 self.l = log | |
307 | |
308 def remote_getName(self): | |
309 return self.l.getName() | |
310 | |
311 def remote_isFinished(self): | |
312 return self.l.isFinished() | |
313 def remote_waitUntilFinished(self): | |
314 d = self.l.waitUntilFinished() | |
315 d.addCallback(lambda res: self) | |
316 return d | |
317 | |
318 def remote_getText(self): | |
319 return self.l.getText() | |
320 def remote_getTextWithHeaders(self): | |
321 return self.l.getTextWithHeaders() | |
322 def remote_getChunks(self): | |
323 return self.l.getChunks() | |
324 # TODO: subscription interface | |
325 | |
326 components.registerAdapter(RemoteLog, builder.LogFile, IRemote) | |
327 # TODO: something similar for builder.HTMLLogfile ? | |
328 | |
329 class RemoteChange: | |
330 def __init__(self, change): | |
331 self.c = change | |
332 | |
333 def getWho(self): | |
334 return self.c.who | |
335 def getFiles(self): | |
336 return self.c.files | |
337 def getComments(self): | |
338 return self.c.comments | |
339 | |
340 components.registerAdapter(RemoteChange, changes.Change, IRemote) | |
341 | |
342 | |
343 class StatusClientPerspective(base.StatusReceiverPerspective): | |
344 | |
345 subscribed = None | |
346 client = None | |
347 | |
348 def __init__(self, status): | |
349 self.status = status # the IStatus | |
350 self.subscribed_to_builders = [] # Builders to which we're subscribed | |
351 self.subscribed_to = [] # everything else we're subscribed to | |
352 | |
353 def __getstate__(self): | |
354 d = self.__dict__.copy() | |
355 d['client'] = None | |
356 return d | |
357 | |
358 def attached(self, mind): | |
359 #twlog.msg("StatusClientPerspective.attached") | |
360 return self | |
361 | |
362 def detached(self, mind): | |
363 twlog.msg("PB client detached") | |
364 self.client = None | |
365 for name in self.subscribed_to_builders: | |
366 twlog.msg(" unsubscribing from Builder(%s)" % name) | |
367 self.status.getBuilder(name).unsubscribe(self) | |
368 for s in self.subscribed_to: | |
369 twlog.msg(" unsubscribe from %s" % s) | |
370 s.unsubscribe(self) | |
371 self.subscribed = None | |
372 | |
373 def perspective_subscribe(self, mode, interval, target): | |
374 """The remote client wishes to subscribe to some set of events. | |
375 'target' will be sent remote messages when these events happen. | |
376 'mode' indicates which events are desired: it is a string with one | |
377 of the following values: | |
378 | |
379 'builders': builderAdded, builderRemoved | |
380 'builds': those plus builderChangedState, buildStarted, buildFinished | |
381 'steps': all those plus buildETAUpdate, stepStarted, stepFinished | |
382 'logs': all those plus stepETAUpdate, logStarted, logFinished | |
383 'full': all those plus logChunk (with the log contents) | |
384 | |
385 | |
386 Messages are defined by buildbot.interfaces.IStatusReceiver . | |
387 'interval' is used to specify how frequently ETAUpdate messages | |
388 should be sent. | |
389 | |
390 Raising or lowering the subscription level will take effect starting | |
391 with the next build or step.""" | |
392 | |
393 assert mode in ("builders", "builds", "steps", "logs", "full") | |
394 assert target | |
395 twlog.msg("PB subscribe(%s)" % mode) | |
396 | |
397 self.client = target | |
398 self.subscribed = mode | |
399 self.interval = interval | |
400 self.subscribed_to.append(self.status) | |
401 # wait a moment before subscribing, so the new-builder messages | |
402 # won't appear before this remote method finishes | |
403 reactor.callLater(0, self.status.subscribe, self) | |
404 return None | |
405 | |
406 def perspective_unsubscribe(self): | |
407 twlog.msg("PB unsubscribe") | |
408 self.status.unsubscribe(self) | |
409 self.subscribed_to.remove(self.status) | |
410 self.client = None | |
411 | |
412 def perspective_getBuildSets(self): | |
413 """This returns tuples of (buildset, bsid), because that is much more | |
414 convenient for tryclient.""" | |
415 return [(IRemote(s), s.getID()) for s in self.status.getBuildSets()] | |
416 | |
417 def perspective_getBuilderNames(self): | |
418 return self.status.getBuilderNames() | |
419 | |
420 def perspective_getBuilder(self, name): | |
421 b = self.status.getBuilder(name) | |
422 return IRemote(b) | |
423 | |
424 def perspective_getSlave(self, name): | |
425 s = self.status.getSlave(name) | |
426 return IRemote(s) | |
427 | |
428 def perspective_ping(self): | |
429 """Ping method to allow pb clients to validate their connections.""" | |
430 return "pong" | |
431 | |
432 # IStatusReceiver methods, invoked if we've subscribed | |
433 | |
434 # mode >= builder | |
435 def builderAdded(self, name, builder): | |
436 self.client.callRemote("builderAdded", name, IRemote(builder)) | |
437 if self.subscribed in ("builds", "steps", "logs", "full"): | |
438 self.subscribed_to_builders.append(name) | |
439 return self | |
440 return None | |
441 | |
442 def builderChangedState(self, name, state): | |
443 self.client.callRemote("builderChangedState", name, state, None) | |
444 # TODO: remove leftover ETA argument | |
445 | |
446 def builderRemoved(self, name): | |
447 if name in self.subscribed_to_builders: | |
448 self.subscribed_to_builders.remove(name) | |
449 self.client.callRemote("builderRemoved", name) | |
450 | |
451 def buildsetSubmitted(self, buildset): | |
452 # TODO: deliver to client, somehow | |
453 pass | |
454 | |
455 # mode >= builds | |
456 def buildStarted(self, name, build): | |
457 self.client.callRemote("buildStarted", name, IRemote(build)) | |
458 if self.subscribed in ("steps", "logs", "full"): | |
459 self.subscribed_to.append(build) | |
460 return (self, self.interval) | |
461 return None | |
462 | |
463 def buildFinished(self, name, build, results): | |
464 if build in self.subscribed_to: | |
465 # we might have joined during the build | |
466 self.subscribed_to.remove(build) | |
467 self.client.callRemote("buildFinished", | |
468 name, IRemote(build), results) | |
469 | |
470 # mode >= steps | |
471 def buildETAUpdate(self, build, eta): | |
472 self.client.callRemote("buildETAUpdate", | |
473 build.getBuilder().getName(), IRemote(build), | |
474 eta) | |
475 | |
476 def stepStarted(self, build, step): | |
477 # we add some information here so the client doesn't have to do an | |
478 # extra round-trip | |
479 self.client.callRemote("stepStarted", | |
480 build.getBuilder().getName(), IRemote(build), | |
481 step.getName(), IRemote(step)) | |
482 if self.subscribed in ("logs", "full"): | |
483 self.subscribed_to.append(step) | |
484 return (self, self.interval) | |
485 return None | |
486 | |
487 def stepFinished(self, build, step, results): | |
488 self.client.callRemote("stepFinished", | |
489 build.getBuilder().getName(), IRemote(build), | |
490 step.getName(), IRemote(step), | |
491 results) | |
492 if step in self.subscribed_to: | |
493 # eventually (through some new subscription method) we could | |
494 # join in the middle of the step | |
495 self.subscribed_to.remove(step) | |
496 | |
497 # mode >= logs | |
498 def stepETAUpdate(self, build, step, ETA, expectations): | |
499 self.client.callRemote("stepETAUpdate", | |
500 build.getBuilder().getName(), IRemote(build), | |
501 step.getName(), IRemote(step), | |
502 ETA, expectations) | |
503 | |
504 def logStarted(self, build, step, log): | |
505 # TODO: make the HTMLLog adapter | |
506 rlog = IRemote(log, None) | |
507 if not rlog: | |
508 print "hey, couldn't adapt %s to IRemote" % log | |
509 self.client.callRemote("logStarted", | |
510 build.getBuilder().getName(), IRemote(build), | |
511 step.getName(), IRemote(step), | |
512 log.getName(), IRemote(log, None)) | |
513 if self.subscribed in ("full",): | |
514 self.subscribed_to.append(log) | |
515 return self | |
516 return None | |
517 | |
518 def logFinished(self, build, step, log): | |
519 self.client.callRemote("logFinished", | |
520 build.getBuilder().getName(), IRemote(build), | |
521 step.getName(), IRemote(step), | |
522 log.getName(), IRemote(log, None)) | |
523 if log in self.subscribed_to: | |
524 self.subscribed_to.remove(log) | |
525 | |
526 # mode >= full | |
527 def logChunk(self, build, step, log, channel, text): | |
528 self.client.callRemote("logChunk", | |
529 build.getBuilder().getName(), IRemote(build), | |
530 step.getName(), IRemote(step), | |
531 log.getName(), IRemote(log), | |
532 channel, text) | |
533 | |
534 | |
535 class PBListener(base.StatusReceiverMultiService): | |
536 """I am a listener for PB-based status clients.""" | |
537 | |
538 compare_attrs = ["port", "cred"] | |
539 implements(portal.IRealm) | |
540 | |
541 def __init__(self, port, user="statusClient", passwd="clientpw"): | |
542 base.StatusReceiverMultiService.__init__(self) | |
543 if type(port) is int: | |
544 port = "tcp:%d" % port | |
545 self.port = port | |
546 self.cred = (user, passwd) | |
547 p = portal.Portal(self) | |
548 c = checkers.InMemoryUsernamePasswordDatabaseDontUse() | |
549 c.addUser(user, passwd) | |
550 p.registerChecker(c) | |
551 f = pb.PBServerFactory(p) | |
552 s = strports.service(port, f) | |
553 s.setServiceParent(self) | |
554 | |
555 def setServiceParent(self, parent): | |
556 base.StatusReceiverMultiService.setServiceParent(self, parent) | |
557 self.setup() | |
558 | |
559 def setup(self): | |
560 self.status = self.parent.getStatus() | |
561 | |
562 def requestAvatar(self, avatarID, mind, interface): | |
563 assert interface == pb.IPerspective | |
564 p = StatusClientPerspective(self.status) | |
565 p.attached(mind) # perhaps .callLater(0) ? | |
566 return (pb.IPerspective, p, | |
567 lambda p=p,mind=mind: p.detached(mind)) | |
OLD | NEW |