Package buildbot :: Module buildslave
[frames] | no frames]

Source Code for Module buildbot.buildslave

  1  # Portions copyright Canonical Ltd. 2009 
  2   
  3  import time 
  4  from email.Message import Message 
  5  from email.Utils import formatdate 
  6  from zope.interface import implements 
  7  from twisted.python import log 
  8  from twisted.internet import defer, reactor 
  9  from twisted.application import service 
 10  import twisted.spread.pb 
 11   
 12  from buildbot.pbutil import NewCredPerspective 
 13  from buildbot.status.builder import SlaveStatus 
 14  from buildbot.status.mail import MailNotifier 
 15  from buildbot.interfaces import IBuildSlave, ILatentBuildSlave 
 16  from buildbot.process.properties import Properties 
 17   
 18  import sys 
 19  if sys.version_info[:3] < (2,4,0): 
 20      from sets import Set as set 
 21   
22 -class AbstractBuildSlave(NewCredPerspective, service.MultiService):
23 """This is the master-side representative for a remote buildbot slave. 24 There is exactly one for each slave described in the config file (the 25 c['slaves'] list). When buildbots connect in (.attach), they get a 26 reference to this instance. The BotMaster object is stashed as the 27 .botmaster attribute. The BotMaster is also our '.parent' Service. 28 29 I represent a build slave -- a remote machine capable of 30 running builds. I am instantiated by the configuration file, and can be 31 subclassed to add extra functionality.""" 32 33 implements(IBuildSlave) 34
35 - def __init__(self, name, password, max_builds=None, 36 notify_on_missing=[], missing_timeout=3600, 37 properties={}):
38 """ 39 @param name: botname this machine will supply when it connects 40 @param password: password this machine will supply when 41 it connects 42 @param max_builds: maximum number of simultaneous builds that will 43 be run concurrently on this buildslave (the 44 default is None for no limit) 45 @param properties: properties that will be applied to builds run on 46 this slave 47 @type properties: dictionary 48 """ 49 service.MultiService.__init__(self) 50 self.slavename = name 51 self.password = password 52 self.botmaster = None # no buildmaster yet 53 self.slave_status = SlaveStatus(name) 54 self.slave = None # a RemoteReference to the Bot, when connected 55 self.slave_commands = None 56 self.slavebuilders = {} 57 self.max_builds = max_builds 58 59 self.properties = Properties() 60 self.properties.update(properties, "BuildSlave") 61 self.properties.setProperty("slavename", name, "BuildSlave") 62 63 self.lastMessageReceived = 0 64 if isinstance(notify_on_missing, str): 65 notify_on_missing = [notify_on_missing] 66 self.notify_on_missing = notify_on_missing 67 for i in notify_on_missing: 68 assert isinstance(i, str) 69 self.missing_timeout = missing_timeout 70 self.missing_timer = None
71
72 - def update(self, new):
73 """ 74 Given a new BuildSlave, configure this one identically. Because 75 BuildSlave objects are remotely referenced, we can't replace them 76 without disconnecting the slave, yet there's no reason to do that. 77 """ 78 # the reconfiguration logic should guarantee this: 79 assert self.slavename == new.slavename 80 assert self.password == new.password 81 assert self.__class__ == new.__class__ 82 self.max_builds = new.max_builds
83
84 - def __repr__(self):
85 if self.botmaster: 86 builders = self.botmaster.getBuildersForSlave(self.slavename) 87 return "<%s '%s', current builders: %s>" % \ 88 (self.__class__.__name__, self.slavename, 89 ','.join(map(lambda b: b.name, builders))) 90 else: 91 return "<%s '%s', (no builders yet)>" % \ 92 (self.__class__.__name__, self.slavename)
93
94 - def setBotmaster(self, botmaster):
95 assert not self.botmaster, "BuildSlave already has a botmaster" 96 self.botmaster = botmaster 97 self.startMissingTimer()
98
99 - def stopMissingTimer(self):
100 if self.missing_timer: 101 self.missing_timer.cancel() 102 self.missing_timer = None
103
104 - def startMissingTimer(self):
105 if self.notify_on_missing and self.missing_timeout and self.parent: 106 self.stopMissingTimer() # in case it's already running 107 self.missing_timer = reactor.callLater(self.missing_timeout, 108 self._missing_timer_fired)
109
110 - def _missing_timer_fired(self):
111 self.missing_timer = None 112 # notify people, but only if we're still in the config 113 if not self.parent: 114 return 115 116 buildmaster = self.botmaster.parent 117 status = buildmaster.getStatus() 118 text = "The Buildbot working for '%s'\n" % status.getProjectName() 119 text += ("has noticed that the buildslave named %s went away\n" % 120 self.slavename) 121 text += "\n" 122 text += ("It last disconnected at %s (buildmaster-local time)\n" % 123 time.ctime(time.time() - self.missing_timeout)) # approx 124 text += "\n" 125 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n" 126 text += "was '%s'.\n" % self.slave_status.getAdmin() 127 text += "\n" 128 text += "Sincerely,\n" 129 text += " The Buildbot\n" 130 text += " %s\n" % status.getProjectURL() 131 subject = "Buildbot: buildslave %s was lost" % self.slavename 132 return self._mail_missing_message(subject, text)
133 134
135 - def updateSlave(self):
136 """Called to add or remove builders after the slave has connected. 137 138 @return: a Deferred that indicates when an attached slave has 139 accepted the new builders and/or released the old ones.""" 140 if self.slave: 141 return self.sendBuilderList() 142 else: 143 return defer.succeed(None)
144
145 - def updateSlaveStatus(self, buildStarted=None, buildFinished=None):
146 if buildStarted: 147 self.slave_status.buildStarted(buildStarted) 148 if buildFinished: 149 self.slave_status.buildFinished(buildFinished)
150
151 - def attached(self, bot):
152 """This is called when the slave connects. 153 154 @return: a Deferred that fires with a suitable pb.IPerspective to 155 give to the slave (i.e. 'self')""" 156 157 if self.slave: 158 # uh-oh, we've got a duplicate slave. The most likely 159 # explanation is that the slave is behind a slow link, thinks we 160 # went away, and has attempted to reconnect, so we've got two 161 # "connections" from the same slave, but the previous one is 162 # stale. Give the new one precedence. 163 log.msg("duplicate slave %s replacing old one" % self.slavename) 164 165 # just in case we've got two identically-configured slaves, 166 # report the IP addresses of both so someone can resolve the 167 # squabble 168 tport = self.slave.broker.transport 169 log.msg("old slave was connected from", tport.getPeer()) 170 log.msg("new slave is from", bot.broker.transport.getPeer()) 171 d = self.disconnect() 172 else: 173 d = defer.succeed(None) 174 # now we go through a sequence of calls, gathering information, then 175 # tell the Botmaster that it can finally give this slave to all the 176 # Builders that care about it. 177 178 # we accumulate slave information in this 'state' dictionary, then 179 # set it atomically if we make it far enough through the process 180 state = {} 181 182 # Reset graceful shutdown status 183 self.slave_status.setGraceful(False) 184 # We want to know when the graceful shutdown flag changes 185 self.slave_status.addGracefulWatcher(self._gracefulChanged) 186 187 def _log_attachment_on_slave(res): 188 d1 = bot.callRemote("print", "attached") 189 d1.addErrback(lambda why: None) 190 return d1
191 d.addCallback(_log_attachment_on_slave) 192 193 def _get_info(res): 194 d1 = bot.callRemote("getSlaveInfo") 195 def _got_info(info): 196 log.msg("Got slaveinfo from '%s'" % self.slavename) 197 # TODO: info{} might have other keys 198 state["admin"] = info.get("admin") 199 state["host"] = info.get("host") 200 state["access_uri"] = info.get("access_uri", None)
201 def _info_unavailable(why): 202 # maybe an old slave, doesn't implement remote_getSlaveInfo 203 log.msg("BuildSlave.info_unavailable") 204 log.err(why) 205 d1.addCallbacks(_got_info, _info_unavailable) 206 return d1 207 d.addCallback(_get_info) 208 209 def _get_version(res): 210 d1 = bot.callRemote("getVersion") 211 def _got_version(version): 212 state["version"] = version 213 def _version_unavailable(why): 214 # probably an old slave 215 log.msg("BuildSlave.version_unavailable") 216 log.err(why) 217 d1.addCallbacks(_got_version, _version_unavailable) 218 d.addCallback(_get_version) 219 220 def _get_commands(res): 221 d1 = bot.callRemote("getCommands") 222 def _got_commands(commands): 223 state["slave_commands"] = commands 224 def _commands_unavailable(why): 225 # probably an old slave 226 log.msg("BuildSlave._commands_unavailable") 227 if why.check(AttributeError): 228 return 229 log.err(why) 230 d1.addCallbacks(_got_commands, _commands_unavailable) 231 return d1 232 d.addCallback(_get_commands) 233 234 def _accept_slave(res): 235 self.slave_status.setAdmin(state.get("admin")) 236 self.slave_status.setHost(state.get("host")) 237 self.slave_status.setAccessURI(state.get("access_uri")) 238 self.slave_status.setVersion(state.get("version")) 239 self.slave_status.setConnected(True) 240 self.slave_commands = state.get("slave_commands") 241 self.slave = bot 242 log.msg("bot attached") 243 self.messageReceivedFromSlave() 244 self.stopMissingTimer() 245 self.botmaster.parent.status.slaveConnected(self.slavename) 246 247 return self.updateSlave() 248 d.addCallback(_accept_slave) 249 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck()) 250 251 # Finally, the slave gets a reference to this BuildSlave. They 252 # receive this later, after we've started using them. 253 d.addCallback(lambda res: self) 254 return d 255
256 - def messageReceivedFromSlave(self):
257 now = time.time() 258 self.lastMessageReceived = now 259 self.slave_status.setLastMessageReceived(now)
260
261 - def detached(self, mind):
262 self.slave = None 263 self.slave_status.removeGracefulWatcher(self._gracefulChanged) 264 self.slave_status.setConnected(False) 265 log.msg("BuildSlave.detached(%s)" % self.slavename) 266 self.botmaster.parent.status.slaveDisconnected(self.slavename)
267
268 - def disconnect(self):
269 """Forcibly disconnect the slave. 270 271 This severs the TCP connection and returns a Deferred that will fire 272 (with None) when the connection is probably gone. 273 274 If the slave is still alive, they will probably try to reconnect 275 again in a moment. 276 277 This is called in two circumstances. The first is when a slave is 278 removed from the config file. In this case, when they try to 279 reconnect, they will be rejected as an unknown slave. The second is 280 when we wind up with two connections for the same slave, in which 281 case we disconnect the older connection. 282 """ 283 284 if not self.slave: 285 return defer.succeed(None) 286 log.msg("disconnecting old slave %s now" % self.slavename) 287 # When this Deferred fires, we'll be ready to accept the new slave 288 return self._disconnect(self.slave)
289
290 - def _disconnect(self, slave):
291 # all kinds of teardown will happen as a result of 292 # loseConnection(), but it happens after a reactor iteration or 293 # two. Hook the actual disconnect so we can know when it is safe 294 # to connect the new slave. We have to wait one additional 295 # iteration (with callLater(0)) to make sure the *other* 296 # notifyOnDisconnect handlers have had a chance to run. 297 d = defer.Deferred() 298 299 # notifyOnDisconnect runs the callback with one argument, the 300 # RemoteReference being disconnected. 301 def _disconnected(rref): 302 reactor.callLater(0, d.callback, None)
303 slave.notifyOnDisconnect(_disconnected) 304 tport = slave.broker.transport 305 # this is the polite way to request that a socket be closed 306 tport.loseConnection() 307 try: 308 # but really we don't want to wait for the transmit queue to 309 # drain. The remote end is unlikely to ACK the data, so we'd 310 # probably have to wait for a (20-minute) TCP timeout. 311 #tport._closeSocket() 312 # however, doing _closeSocket (whether before or after 313 # loseConnection) somehow prevents the notifyOnDisconnect 314 # handlers from being run. Bummer. 315 tport.offset = 0 316 tport.dataBuffer = "" 317 except: 318 # however, these hacks are pretty internal, so don't blow up if 319 # they fail or are unavailable 320 log.msg("failed to accelerate the shutdown process") 321 pass 322 log.msg("waiting for slave to finish disconnecting") 323 324 return d 325
326 - def sendBuilderList(self):
327 our_builders = self.botmaster.getBuildersForSlave(self.slavename) 328 blist = [(b.name, b.slavebuilddir) for b in our_builders] 329 d = self.slave.callRemote("setBuilderList", blist) 330 return d
331
332 - def perspective_keepalive(self):
333 pass
334
335 - def addSlaveBuilder(self, sb):
336 self.slavebuilders[sb.builder_name] = sb
337
338 - def removeSlaveBuilder(self, sb):
339 try: 340 del self.slavebuilders[sb.builder_name] 341 except KeyError: 342 pass
343
344 - def canStartBuild(self):
345 """ 346 I am called when a build is requested to see if this buildslave 347 can start a build. This function can be used to limit overall 348 concurrency on the buildslave. 349 """ 350 # If we're waiting to shutdown gracefully, then we shouldn't 351 # accept any new jobs. 352 if self.slave_status.getGraceful(): 353 return False 354 355 if self.max_builds: 356 active_builders = [sb for sb in self.slavebuilders.values() 357 if sb.isBusy()] 358 if len(active_builders) >= self.max_builds: 359 return False 360 return True
361
362 - def _mail_missing_message(self, subject, text):
363 # first, see if we have a MailNotifier we can use. This gives us a 364 # fromaddr and a relayhost. 365 buildmaster = self.botmaster.parent 366 for st in buildmaster.statusTargets: 367 if isinstance(st, MailNotifier): 368 break 369 else: 370 # if not, they get a default MailNotifier, which always uses SMTP 371 # to localhost and uses a dummy fromaddr of "buildbot". 372 log.msg("buildslave-missing msg using default MailNotifier") 373 st = MailNotifier("buildbot") 374 # now construct the mail 375 376 m = Message() 377 m.set_payload(text) 378 m['Date'] = formatdate(localtime=True) 379 m['Subject'] = subject 380 m['From'] = st.fromaddr 381 recipients = self.notify_on_missing 382 m['To'] = ", ".join(recipients) 383 d = st.sendMessage(m, recipients) 384 # return the Deferred for testing purposes 385 return d
386
387 - def _gracefulChanged(self, graceful):
388 """This is called when our graceful shutdown setting changes""" 389 if graceful: 390 active_builders = [sb for sb in self.slavebuilders.values() 391 if sb.isBusy()] 392 if len(active_builders) == 0: 393 # Shut down! 394 self.shutdown()
395
396 - def shutdown(self):
397 """Shutdown the slave""" 398 # Look for a builder with a remote reference to the client side 399 # slave. If we can find one, then call "shutdown" on the remote 400 # builder, which will cause the slave buildbot process to exit. 401 d = None 402 for b in self.slavebuilders.values(): 403 if b.remote: 404 d = b.remote.callRemote("shutdown") 405 break 406 407 if d: 408 log.msg("Shutting down slave: %s" % self.slavename) 409 # The remote shutdown call will not complete successfully since the 410 # buildbot process exits almost immediately after getting the 411 # shutdown request. 412 # Here we look at the reason why the remote call failed, and if 413 # it's because the connection was lost, that means the slave 414 # shutdown as expected. 415 def _errback(why): 416 if why.check(twisted.spread.pb.PBConnectionLost): 417 log.msg("Lost connection to %s" % self.slavename) 418 else: 419 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
420 d.addErrback(_errback) 421 return d 422 log.err("Couldn't find remote builder to shut down slave") 423 return defer.succeed(None) 424
425 -class BuildSlave(AbstractBuildSlave):
426
427 - def sendBuilderList(self):
428 d = AbstractBuildSlave.sendBuilderList(self) 429 def _sent(slist): 430 dl = [] 431 for name, remote in slist.items(): 432 # use get() since we might have changed our mind since then 433 b = self.botmaster.builders.get(name) 434 if b: 435 d1 = b.attached(self, remote, self.slave_commands) 436 dl.append(d1) 437 return defer.DeferredList(dl)
438 def _set_failed(why): 439 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) 440 log.err(why)
441 # TODO: hang up on them?, without setBuilderList we can't use 442 # them 443 d.addCallbacks(_sent, _set_failed) 444 return d 445
446 - def detached(self, mind):
447 AbstractBuildSlave.detached(self, mind) 448 self.botmaster.slaveLost(self) 449 self.startMissingTimer()
450
451 - def buildFinished(self, sb):
452 """This is called when a build on this slave is finished.""" 453 # If we're gracefully shutting down, and we have no more active 454 # builders, then it's safe to disconnect 455 if self.slave_status.getGraceful(): 456 active_builders = [sb for sb in self.slavebuilders.values() 457 if sb.isBusy()] 458 if len(active_builders) == 0: 459 # Shut down! 460 return self.shutdown() 461 return defer.succeed(None)
462
463 -class AbstractLatentBuildSlave(AbstractBuildSlave):
464 """A build slave that will start up a slave instance when needed. 465 466 To use, subclass and implement start_instance and stop_instance. 467 468 See ec2buildslave.py for a concrete example. Also see the stub example in 469 test/test_slaves.py. 470 """ 471 472 implements(ILatentBuildSlave) 473 474 substantiated = False 475 substantiation_deferred = None 476 build_wait_timer = None 477 _start_result = _shutdown_callback_handle = None 478
479 - def __init__(self, name, password, max_builds=None, 480 notify_on_missing=[], missing_timeout=60*20, 481 build_wait_timeout=60*10, 482 properties={}):
483 AbstractBuildSlave.__init__( 484 self, name, password, max_builds, notify_on_missing, 485 missing_timeout, properties) 486 self.building = set() 487 self.build_wait_timeout = build_wait_timeout
488
489 - def start_instance(self):
490 # responsible for starting instance that will try to connect with 491 # this master. Should return deferred. Problems should use an 492 # errback. 493 raise NotImplementedError
494
495 - def stop_instance(self, fast=False):
496 # responsible for shutting down instance. 497 raise NotImplementedError
498
499 - def substantiate(self, sb):
500 if self.substantiated: 501 self._clearBuildWaitTimer() 502 self._setBuildWaitTimer() 503 return defer.succeed(self) 504 if self.substantiation_deferred is None: 505 if self.parent and not self.missing_timer: 506 # start timer. if timer times out, fail deferred 507 self.missing_timer = reactor.callLater( 508 self.missing_timeout, 509 self._substantiation_failed, defer.TimeoutError()) 510 self.substantiation_deferred = defer.Deferred() 511 if self.slave is None: 512 self._substantiate() # start up instance 513 # else: we're waiting for an old one to detach. the _substantiate 514 # will be done in ``detached`` below. 515 return self.substantiation_deferred
516
517 - def _substantiate(self):
518 # register event trigger 519 d = self.start_instance() 520 self._shutdown_callback_handle = reactor.addSystemEventTrigger( 521 'before', 'shutdown', self._soft_disconnect, fast=True) 522 def stash_reply(result): 523 self._start_result = result
524 def clean_up(failure): 525 if self.missing_timer is not None: 526 self.missing_timer.cancel() 527 self._substantiation_failed(failure) 528 if self._shutdown_callback_handle is not None: 529 handle = self._shutdown_callback_handle 530 del self._shutdown_callback_handle 531 reactor.removeSystemEventTrigger(handle) 532 return failure
533 d.addCallbacks(stash_reply, clean_up) 534 return d 535
536 - def attached(self, bot):
537 if self.substantiation_deferred is None: 538 msg = 'Slave %s received connection while not trying to ' \ 539 'substantiate. Disconnecting.' % (self.slavename,) 540 log.msg(msg) 541 self._disconnect(bot) 542 return defer.fail(RuntimeError(msg)) 543 return AbstractBuildSlave.attached(self, bot)
544
545 - def detached(self, mind):
546 AbstractBuildSlave.detached(self, mind) 547 if self.substantiation_deferred is not None: 548 self._substantiate()
549
550 - def _substantiation_failed(self, failure):
551 d = self.substantiation_deferred 552 self.substantiation_deferred = None 553 self.missing_timer = None 554 d.errback(failure) 555 self.insubstantiate() 556 # notify people, but only if we're still in the config 557 if not self.parent or not self.notify_on_missing: 558 return 559 560 buildmaster = self.botmaster.parent 561 status = buildmaster.getStatus() 562 text = "The Buildbot working for '%s'\n" % status.getProjectName() 563 text += ("has noticed that the latent buildslave named %s \n" % 564 self.slavename) 565 text += "never substantiated after a request\n" 566 text += "\n" 567 text += ("The request was made at %s (buildmaster-local time)\n" % 568 time.ctime(time.time() - self.missing_timeout)) # approx 569 text += "\n" 570 text += "Sincerely,\n" 571 text += " The Buildbot\n" 572 text += " %s\n" % status.getProjectURL() 573 subject = "Buildbot: buildslave %s never substantiated" % self.slavename 574 return self._mail_missing_message(subject, text)
575
576 - def buildStarted(self, sb):
577 assert self.substantiated 578 self._clearBuildWaitTimer() 579 self.building.add(sb.builder_name)
580
581 - def buildFinished(self, sb):
582 self.building.remove(sb.builder_name) 583 if not self.building: 584 self._setBuildWaitTimer()
585
586 - def _clearBuildWaitTimer(self):
587 if self.build_wait_timer is not None: 588 if self.build_wait_timer.active(): 589 self.build_wait_timer.cancel() 590 self.build_wait_timer = None
591
592 - def _setBuildWaitTimer(self):
593 self._clearBuildWaitTimer() 594 self.build_wait_timer = reactor.callLater( 595 self.build_wait_timeout, self._soft_disconnect)
596
597 - def insubstantiate(self, fast=False):
598 self._clearBuildWaitTimer() 599 d = self.stop_instance(fast) 600 if self._shutdown_callback_handle is not None: 601 handle = self._shutdown_callback_handle 602 del self._shutdown_callback_handle 603 reactor.removeSystemEventTrigger(handle) 604 self.substantiated = False 605 self.building.clear() # just to be sure 606 return d
607
608 - def _soft_disconnect(self, fast=False):
609 d = AbstractBuildSlave.disconnect(self) 610 if self.slave is not None: 611 # this could be called when the slave needs to shut down, such as 612 # in BotMaster.removeSlave, *or* when a new slave requests a 613 # connection when we already have a slave. It's not clear what to 614 # do in the second case: this shouldn't happen, and if it 615 # does...if it's a latent slave, shutting down will probably kill 616 # something we want...but we can't know what the status is. So, 617 # here, we just do what should be appropriate for the first case, 618 # and put our heads in the sand for the second, at least for now. 619 # The best solution to the odd situation is removing it as a 620 # possibilty: make the master in charge of connecting to the 621 # slave, rather than vice versa. TODO. 622 d = defer.DeferredList([d, self.insubstantiate(fast)]) 623 else: 624 if self.substantiation_deferred is not None: 625 # unlike the previous block, we don't expect this situation when 626 # ``attached`` calls ``disconnect``, only when we get a simple 627 # request to "go away". 628 self.substantiation_deferred.errback() 629 self.substantiation_deferred = None 630 if self.missing_timer: 631 self.missing_timer.cancel() 632 self.missing_timer = None 633 self.stop_instance() 634 return d
635
636 - def disconnect(self):
637 d = self._soft_disconnect() 638 # this removes the slave from all builders. It won't come back 639 # without a restart (or maybe a sighup) 640 self.botmaster.slaveLost(self)
641
642 - def stopService(self):
643 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self) 644 if self.slave is not None: 645 d = self._soft_disconnect() 646 res = defer.DeferredList([res, d]) 647 return res
648
649 - def updateSlave(self):
650 """Called to add or remove builders after the slave has connected. 651 652 Also called after botmaster's builders are initially set. 653 654 @return: a Deferred that indicates when an attached slave has 655 accepted the new builders and/or released the old ones.""" 656 for b in self.botmaster.getBuildersForSlave(self.slavename): 657 if b.name not in self.slavebuilders: 658 b.addLatentSlave(self) 659 return AbstractBuildSlave.updateSlave(self)
660
661 - def sendBuilderList(self):
662 d = AbstractBuildSlave.sendBuilderList(self) 663 def _sent(slist): 664 dl = [] 665 for name, remote in slist.items(): 666 # use get() since we might have changed our mind since then. 667 # we're checking on the builder in addition to the 668 # slavebuilders out of a bit of paranoia. 669 b = self.botmaster.builders.get(name) 670 sb = self.slavebuilders.get(name) 671 if b and sb: 672 d1 = sb.attached(self, remote, self.slave_commands) 673 dl.append(d1) 674 return defer.DeferredList(dl)
675 def _set_failed(why): 676 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) 677 log.err(why) 678 # TODO: hang up on them?, without setBuilderList we can't use 679 # them 680 if self.substantiation_deferred: 681 self.substantiation_deferred.errback() 682 self.substantiation_deferred = None 683 if self.missing_timer: 684 self.missing_timer.cancel() 685 self.missing_timer = None 686 # TODO: maybe log? send an email? 687 return why 688 d.addCallbacks(_sent, _set_failed) 689 def _substantiated(res): 690 self.substantiated = True 691 if self.substantiation_deferred: 692 d = self.substantiation_deferred 693 del self.substantiation_deferred 694 res = self._start_result 695 del self._start_result 696 d.callback(res) 697 # note that the missing_timer is already handled within 698 # ``attached`` 699 if not self.building: 700 self._setBuildWaitTimer() 701 d.addCallback(_substantiated) 702 return d 703