Changelog update.
[debian_buildbot.git] / master / buildbot / buildslave.py
blobec31de78994c3059e3b25fc760f536bce51dfedc
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.
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.
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.
14 # Portions Copyright Buildbot Team Members
15 # Portions Copyright Canonical Ltd. 2009
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
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
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.
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."""
43 implements(IBuildSlave)
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
75 self.properties = Properties()
76 self.properties.update(properties, "BuildSlave")
77 self.properties.setProperty("slavename", name, "BuildSlave")
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
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
103 self.properties.updateFromProperties(new.properties)
105 if self.botmaster:
106 self.updateLocks()
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)
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]
128 def locksAvailable(self):
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
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
140 def acquireLocks(self):
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.
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
156 def releaseLocks(self):
158 I am called to release any locks after a build has finished
160 log.msg("releaseLocks(%s): %s" % (self, self.locks))
161 for lock, access in self.locks:
162 lock.release(self, access)
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()
170 def stopMissingTimer(self):
171 if self.missing_timer:
172 self.missing_timer.cancel()
173 self.missing_timer = None
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)
181 def recordConnectTime(self):
182 if self.slave_status:
183 self.slave_status.recordConnectTime()
185 def isConnected(self):
186 return self.slave
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
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)
213 def updateSlave(self):
214 """Called to add or remove builders after the slave has connected.
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)
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)
229 def attached(self, bot):
230 """This is called when the slave connects.
232 @return: a Deferred that fires when the attachment is complete
235 # the botmaster should ensure this.
236 assert not self.isConnected()
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.
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 = {}
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)
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)
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)
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)
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)
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)
318 return self.updateSlave()
319 d.addCallback(_accept_slave)
320 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck())
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
327 def messageReceivedFromSlave(self):
328 now = time.time()
329 self.lastMessageReceived = now
330 self.slave_status.setLastMessageReceived(now)
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)
339 def disconnect(self):
340 """Forcibly disconnect the slave.
342 This severs the TCP connection and returns a Deferred that will fire
343 (with None) when the connection is probably gone.
345 If the slave is still alive, they will probably try to reconnect
346 again in a moment.
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.
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)
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()
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")
394 return d
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
402 def perspective_keepalive(self):
403 pass
405 def perspective_shutdown(self):
406 log.msg("slave %s wants to shut down" % self.slavename)
407 self.slave_status.setGraceful(True)
409 def addSlaveBuilder(self, sb):
410 self.slavebuilders[sb.builder_name] = sb
412 def removeSlaveBuilder(self, sb):
413 try:
414 del self.slavebuilders[sb.builder_name]
415 except KeyError:
416 pass
418 def canStartBuild(self):
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.
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
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
435 if not self.locksAvailable():
436 return False
438 return True
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
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
465 def _gracefulChanged(self, graceful):
466 """This is called when our graceful shutdown setting changes"""
467 self.maybeShutdown()
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
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
492 wfd = defer.waitForDeferred(new_way())
493 yield wfd
494 if wfd.getResult():
495 return # done!
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
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()
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')
540 class BuildSlave(AbstractBuildSlave):
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
561 def detached(self, mind):
562 AbstractBuildSlave.detached(self, mind)
563 self.botmaster.slaveLost(self)
564 self.startMissingTimer()
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)
573 class AbstractLatentBuildSlave(AbstractBuildSlave):
574 """A build slave that will start up a slave instance when needed.
576 To use, subclass and implement start_instance and stop_instance.
578 See ec2buildslave.py for a concrete example. Also see the stub example in
579 test/test_slaves.py.
582 implements(ILatentBuildSlave)
584 substantiated = False
585 substantiation_deferred = None
586 build_wait_timer = None
587 _shutdown_callback_handle = None
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
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
605 def stop_instance(self, fast=False):
606 # responsible for shutting down instance.
607 raise NotImplementedError
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
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
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)
660 def detached(self, mind):
661 AbstractBuildSlave.detached(self, mind)
662 if self.substantiation_deferred is not None:
663 self._substantiate()
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
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)
692 def buildStarted(self, sb):
693 assert self.substantiated
694 self._clearBuildWaitTimer()
695 self.building.add(sb.builder_name)
697 def buildFinished(self, sb):
698 self.building.remove(sb.builder_name)
699 if not self.building:
700 self._setBuildWaitTimer()
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
708 def _setBuildWaitTimer(self):
709 self._clearBuildWaitTimer()
710 self.build_wait_timer = reactor.callLater(
711 self.build_wait_timeout, self._soft_disconnect)
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
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
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)
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
766 def updateSlave(self):
767 """Called to add or remove builders after the slave has connected.
769 Also called after botmaster's builders are initially set.
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)
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