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

Source Code for Module buildbot.buildslave

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