| 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 |