compress old changelog
[buildbot.git] / buildbot / process / builder.py
blobe0beb7c54753b4c17cc6b6ad8d40b02ff955b046
2 import random, weakref
3 from zope.interface import implements
4 from twisted.python import log, components
5 from twisted.spread import pb
6 from twisted.internet import reactor, defer
8 from buildbot import interfaces
9 from buildbot.status.progress import Expectations
10 from buildbot.util import now
11 from buildbot.process import base
13 (ATTACHING, # slave attached, still checking hostinfo/etc
14 IDLE, # idle, available for use
15 PINGING, # build about to start, making sure it is still alive
16 BUILDING, # build is running
17 ) = range(4)
19 class SlaveBuilder(pb.Referenceable):
20 """I am the master-side representative for one of the
21 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
22 buildbot. When a remote builder connects, I query it for command versions
23 and then make it available to any Builds that are ready to run. """
25 def __init__(self):
26 self.ping_watchers = []
27 self.state = ATTACHING
28 self.remote = None
29 self.slave = None
30 self.builder_name = None
32 def __repr__(self):
33 r = "<SlaveBuilder"
34 if self.builder_name:
35 r += " builder=%s" % self.builder_name
36 if self.slave:
37 r += " slave=%s" % self.slave.slavename
38 r += ">"
39 return r
41 def setBuilder(self, b):
42 self.builder = b
43 self.builder_name = b.name
45 def getSlaveCommandVersion(self, command, oldversion=None):
46 if self.remoteCommands is None:
47 # the slave is 0.5.0 or earlier
48 return oldversion
49 return self.remoteCommands.get(command)
51 def isAvailable(self):
52 # if this SlaveBuilder is busy, then it's definitely not available
53 if self.isBusy():
54 return False
56 # otherwise, check in with the BuildSlave
57 if self.slave:
58 return self.slave.canStartBuild()
60 # no slave? not very available.
61 return False
63 def isBusy(self):
64 return self.state != IDLE
66 def attached(self, slave, remote, commands):
67 """
68 @type slave: L{buildbot.buildslave.BuildSlave}
69 @param slave: the BuildSlave that represents the buildslave as a
70 whole
71 @type remote: L{twisted.spread.pb.RemoteReference}
72 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
73 @type commands: dict: string -> string, or None
74 @param commands: provides the slave's version of each RemoteCommand
75 """
76 self.slave = slave
77 self.remote = remote
78 self.remoteCommands = commands # maps command name to version
79 self.slave.addSlaveBuilder(self)
80 log.msg("Buildslave %s attached to %s" % (slave.slavename,
81 self.builder_name))
82 d = self.remote.callRemote("setMaster", self)
83 d.addErrback(self._attachFailure, "Builder.setMaster")
84 d.addCallback(self._attached2)
85 return d
87 def _attached2(self, res):
88 d = self.remote.callRemote("print", "attached")
89 d.addErrback(self._attachFailure, "Builder.print 'attached'")
90 d.addCallback(self._attached3)
91 return d
93 def _attached3(self, res):
94 # now we say they're really attached
95 self.state = IDLE
96 return self
98 def _attachFailure(self, why, where):
99 assert isinstance(where, str)
100 log.msg(where)
101 log.err(why)
102 return why
104 def detached(self):
105 log.msg("Buildslave %s detached from %s" % (self.slave.slavename,
106 self.builder_name))
107 if self.slave:
108 self.slave.removeSlaveBuilder(self)
109 self.slave = None
110 self.remote = None
111 self.remoteCommands = None
113 def buildStarted(self):
114 self.state = BUILDING
116 def buildFinished(self):
117 self.state = IDLE
118 reactor.callLater(0, self.builder.botmaster.maybeStartAllBuilds)
120 def ping(self, timeout, status=None):
121 """Ping the slave to make sure it is still there. Returns a Deferred
122 that fires with True if it is.
124 @param status: if you point this at a BuilderStatus, a 'pinging'
125 event will be pushed.
128 self.state = PINGING
129 newping = not self.ping_watchers
130 d = defer.Deferred()
131 self.ping_watchers.append(d)
132 if newping:
133 if status:
134 event = status.addEvent(["pinging"], "yellow")
135 d2 = defer.Deferred()
136 d2.addCallback(self._pong_status, event)
137 self.ping_watchers.insert(0, d2)
138 # I think it will make the tests run smoother if the status
139 # is updated before the ping completes
140 Ping().ping(self.remote, timeout).addCallback(self._pong)
142 return d
144 def _pong(self, res):
145 watchers, self.ping_watchers = self.ping_watchers, []
146 for d in watchers:
147 d.callback(res)
149 def _pong_status(self, res, event):
150 if res:
151 event.text = ["ping", "success"]
152 event.color = "green"
153 else:
154 event.text = ["ping", "failed"]
155 event.color = "red"
156 event.finish()
158 class Ping:
159 running = False
160 timer = None
162 def ping(self, remote, timeout):
163 assert not self.running
164 self.running = True
165 log.msg("sending ping")
166 self.d = defer.Deferred()
167 # TODO: add a distinct 'ping' command on the slave.. using 'print'
168 # for this purpose is kind of silly.
169 remote.callRemote("print", "ping").addCallbacks(self._pong,
170 self._ping_failed,
171 errbackArgs=(remote,))
173 # We use either our own timeout or the (long) TCP timeout to detect
174 # silently-missing slaves. This might happen because of a NAT
175 # timeout or a routing loop. If the slave just shuts down (and we
176 # somehow missed the FIN), we should get a "connection refused"
177 # message.
178 self.timer = reactor.callLater(timeout, self._ping_timeout, remote)
179 return self.d
181 def _ping_timeout(self, remote):
182 log.msg("ping timeout")
183 # force the BuildSlave to disconnect, since this indicates that
184 # the bot is unreachable.
185 del self.timer
186 remote.broker.transport.loseConnection()
187 # the forcibly-lost connection will now cause the ping to fail
189 def _stopTimer(self):
190 if not self.running:
191 return
192 self.running = False
194 if self.timer:
195 self.timer.cancel()
196 del self.timer
198 def _pong(self, res):
199 log.msg("ping finished: success")
200 self._stopTimer()
201 self.d.callback(True)
203 def _ping_failed(self, res, remote):
204 log.msg("ping finished: failure")
205 self._stopTimer()
206 # the slave has some sort of internal error, disconnect them. If we
207 # don't, we'll requeue a build and ping them again right away,
208 # creating a nasty loop.
209 remote.broker.transport.loseConnection()
210 # TODO: except, if they actually did manage to get this far, they'll
211 # probably reconnect right away, and we'll do this game again. Maybe
212 # it would be better to leave them in the PINGING state.
213 self.d.callback(False)
216 class Builder(pb.Referenceable):
217 """I manage all Builds of a given type.
219 Each Builder is created by an entry in the config file (the c['builders']
220 list), with a number of parameters.
222 One of these parameters is the L{buildbot.process.factory.BuildFactory}
223 object that is associated with this Builder. The factory is responsible
224 for creating new L{Build<buildbot.process.base.Build>} objects. Each
225 Build object defines when and how the build is performed, so a new
226 Factory or Builder should be defined to control this behavior.
228 The Builder holds on to a number of L{base.BuildRequest} objects in a
229 list named C{.buildable}. Incoming BuildRequest objects will be added to
230 this list, or (if possible) merged into an existing request. When a slave
231 becomes available, I will use my C{BuildFactory} to turn the request into
232 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
233 goes into C{.building} while it runs. Once the build finishes, I will
234 discard it.
236 I maintain a list of available SlaveBuilders, one for each connected
237 slave that the C{slavenames} parameter says we can use. Some of these
238 will be idle, some of them will be busy running builds for me. If there
239 are multiple slaves, I can run multiple builds at once.
241 I also manage forced builds, progress expectation (ETA) management, and
242 some status delivery chores.
244 I am persisted in C{BASEDIR/BUILDERNAME/builder}, so I can remember how
245 long a build usually takes to run (in my C{expectations} attribute). This
246 pickle also includes the L{buildbot.status.builder.BuilderStatus} object,
247 which remembers the set of historic builds.
249 @type buildable: list of L{buildbot.process.base.BuildRequest}
250 @ivar buildable: BuildRequests that are ready to build, but which are
251 waiting for a buildslave to be available.
253 @type building: list of L{buildbot.process.base.Build}
254 @ivar building: Builds that are actively running
256 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
257 @ivar slaves: the slaves currently available for building
260 expectations = None # this is created the first time we get a good build
261 START_BUILD_TIMEOUT = 10
262 CHOOSE_SLAVES_RANDOMLY = True # disabled for determinism during tests
264 def __init__(self, setup, builder_status):
266 @type setup: dict
267 @param setup: builder setup data, as stored in
268 BuildmasterConfig['builders']. Contains name,
269 slavename(s), builddir, factory, locks.
270 @type builder_status: L{buildbot.status.builder.BuilderStatus}
272 self.name = setup['name']
273 self.slavenames = []
274 if setup.has_key('slavename'):
275 self.slavenames.append(setup['slavename'])
276 if setup.has_key('slavenames'):
277 self.slavenames.extend(setup['slavenames'])
278 self.builddir = setup['builddir']
279 self.buildFactory = setup['factory']
280 self.locks = setup.get("locks", [])
281 if setup.has_key('periodicBuildTime'):
282 raise ValueError("periodicBuildTime can no longer be defined as"
283 " part of the Builder: use scheduler.Periodic"
284 " instead")
286 # build/wannabuild slots: Build objects move along this sequence
287 self.buildable = []
288 self.building = []
289 # old_building holds active builds that were stolen from a predecessor
290 self.old_building = weakref.WeakKeyDictionary()
292 # buildslaves which have connected but which are not yet available.
293 # These are always in the ATTACHING state.
294 self.attaching_slaves = []
296 # buildslaves at our disposal. Each SlaveBuilder instance has a
297 # .state that is IDLE, PINGING, or BUILDING. "PINGING" is used when a
298 # Build is about to start, to make sure that they're still alive.
299 self.slaves = []
301 self.builder_status = builder_status
302 self.builder_status.setSlavenames(self.slavenames)
304 # for testing, to help synchronize tests
305 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
306 'idle': []}
308 def setBotmaster(self, botmaster):
309 self.botmaster = botmaster
311 def compareToSetup(self, setup):
312 diffs = []
313 setup_slavenames = []
314 if setup.has_key('slavename'):
315 setup_slavenames.append(setup['slavename'])
316 setup_slavenames.extend(setup.get('slavenames', []))
317 if setup_slavenames != self.slavenames:
318 diffs.append('slavenames changed from %s to %s' \
319 % (self.slavenames, setup_slavenames))
320 if setup['builddir'] != self.builddir:
321 diffs.append('builddir changed from %s to %s' \
322 % (self.builddir, setup['builddir']))
323 if setup['factory'] != self.buildFactory: # compare objects
324 diffs.append('factory changed')
325 oldlocks = [(lock.__class__, lock.name)
326 for lock in setup.get('locks',[])]
327 newlocks = [(lock.__class__, lock.name)
328 for lock in self.locks]
329 if oldlocks != newlocks:
330 diffs.append('locks changed from %s to %s' % (oldlocks, newlocks))
331 return diffs
333 def __repr__(self):
334 return "<Builder '%s' at %d>" % (self.name, id(self))
337 def submitBuildRequest(self, req):
338 req.submittedAt = now()
339 self.buildable.append(req)
340 req.requestSubmitted(self)
341 self.builder_status.addBuildRequest(req.status)
342 self.maybeStartBuild()
344 def cancelBuildRequest(self, req):
345 if req in self.buildable:
346 self.buildable.remove(req)
347 self.builder_status.removeBuildRequest(req.status)
348 return True
349 return False
351 def __getstate__(self):
352 d = self.__dict__.copy()
353 # TODO: note that d['buildable'] can contain Deferreds
354 del d['building'] # TODO: move these back to .buildable?
355 del d['slaves']
356 return d
358 def __setstate__(self, d):
359 self.__dict__ = d
360 self.building = []
361 self.slaves = []
363 def consumeTheSoulOfYourPredecessor(self, old):
364 """Suck the brain out of an old Builder.
366 This takes all the runtime state from an existing Builder and moves
367 it into ourselves. This is used when a Builder is changed in the
368 master.cfg file: the new Builder has a different factory, but we want
369 all the builds that were queued for the old one to get processed by
370 the new one. Any builds which are already running will keep running.
371 The new Builder will get as many of the old SlaveBuilder objects as
372 it wants."""
374 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
375 (self, old))
376 # we claim all the pending builds, removing them from the old
377 # Builder's queue. This insures that the old Builder will not start
378 # any new work.
379 log.msg(" stealing %s buildrequests" % len(old.buildable))
380 self.buildable.extend(old.buildable)
381 old.buildable = []
383 # old.building (i.e. builds which are still running) is not migrated
384 # directly: it keeps track of builds which were in progress in the
385 # old Builder. When those builds finish, the old Builder will be
386 # notified, not us. However, since the old SlaveBuilder will point to
387 # us, it is our maybeStartBuild() that will be triggered.
388 if old.building:
389 self.builder_status.setBigState("building")
390 # however, we do grab a weakref to the active builds, so that our
391 # BuilderControl can see them and stop them. We use a weakref because
392 # we aren't the one to get notified, so there isn't a convenient
393 # place to remove it from self.building .
394 for b in old.building:
395 self.old_building[b] = None
396 for b in old.old_building:
397 self.old_building[b] = None
399 # Our set of slavenames may be different. Steal any of the old
400 # buildslaves that we want to keep using.
401 for sb in old.slaves[:]:
402 if sb.slave.slavename in self.slavenames:
403 log.msg(" stealing buildslave %s" % sb)
404 self.slaves.append(sb)
405 old.slaves.remove(sb)
406 sb.setBuilder(self)
408 # old.attaching_slaves:
409 # these SlaveBuilders are waiting on a sequence of calls:
410 # remote.setMaster and remote.print . When these two complete,
411 # old._attached will be fired, which will add a 'connect' event to
412 # the builder_status and try to start a build. However, we've pulled
413 # everything out of the old builder's queue, so it will have no work
414 # to do. The outstanding remote.setMaster/print call will be holding
415 # the last reference to the old builder, so it will disappear just
416 # after that response comes back.
418 # The BotMaster will ask the slave to re-set their list of Builders
419 # shortly after this function returns, which will cause our
420 # attached() method to be fired with a bunch of references to remote
421 # SlaveBuilders, some of which we already have (by stealing them
422 # from the old Builder), some of which will be new. The new ones
423 # will be re-attached.
425 # Therefore, we don't need to do anything about old.attaching_slaves
427 return # all done
429 def getBuild(self, number):
430 for b in self.building:
431 if b.build_status.number == number:
432 return b
433 for b in self.old_building.keys():
434 if b.build_status.number == number:
435 return b
436 return None
438 def fireTestEvent(self, name, fire_with=None):
439 if fire_with is None:
440 fire_with = self
441 watchers = self.watchers[name]
442 self.watchers[name] = []
443 for w in watchers:
444 reactor.callLater(0, w.callback, fire_with)
446 def attached(self, slave, remote, commands):
447 """This is invoked by the BuildSlave when the self.slavename bot
448 registers their builder.
450 @type slave: L{buildbot.buildslave.BuildSlave}
451 @param slave: the BuildSlave that represents the buildslave as a whole
452 @type remote: L{twisted.spread.pb.RemoteReference}
453 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
454 @type commands: dict: string -> string, or None
455 @param commands: provides the slave's version of each RemoteCommand
457 @rtype: L{twisted.internet.defer.Deferred}
458 @return: a Deferred that fires (with 'self') when the slave-side
459 builder is fully attached and ready to accept commands.
461 for s in self.attaching_slaves + self.slaves:
462 if s.slave == slave:
463 # already attached to them. This is fairly common, since
464 # attached() gets called each time we receive the builder
465 # list from the slave, and we ask for it each time we add or
466 # remove a builder. So if the slave is hosting builders
467 # A,B,C, and the config file changes A, we'll remove A and
468 # re-add it, triggering two builder-list requests, getting
469 # two redundant calls to attached() for B, and another two
470 # for C.
472 # Therefore, when we see that we're already attached, we can
473 # just ignore it. TODO: build a diagram of the state
474 # transitions here, I'm concerned about sb.attached() failing
475 # and leaving sb.state stuck at 'ATTACHING', and about
476 # the detached() message arriving while there's some
477 # transition pending such that the response to the transition
478 # re-vivifies sb
479 return defer.succeed(self)
481 sb = SlaveBuilder()
482 sb.setBuilder(self)
483 self.attaching_slaves.append(sb)
484 d = sb.attached(slave, remote, commands)
485 d.addCallback(self._attached)
486 d.addErrback(self._not_attached, slave)
487 return d
489 def _attached(self, sb):
490 # TODO: make this .addSlaveEvent(slave.slavename, ['connect']) ?
491 self.builder_status.addPointEvent(['connect', sb.slave.slavename])
492 self.attaching_slaves.remove(sb)
493 self.slaves.append(sb)
494 reactor.callLater(0, self.maybeStartBuild)
496 self.fireTestEvent('attach')
497 return self
499 def _not_attached(self, why, slave):
500 # already log.err'ed by SlaveBuilder._attachFailure
501 # TODO: make this .addSlaveEvent?
502 # TODO: remove from self.slaves (except that detached() should get
503 # run first, right?)
504 self.builder_status.addPointEvent(['failed', 'connect',
505 slave.slave.slavename])
506 # TODO: add an HTMLLogFile of the exception
507 self.fireTestEvent('attach', why)
509 def detached(self, slave):
510 """This is called when the connection to the bot is lost."""
511 log.msg("%s.detached" % self, slave.slavename)
512 for sb in self.attaching_slaves + self.slaves:
513 if sb.slave == slave:
514 break
515 else:
516 log.msg("WEIRD: Builder.detached(%s) (%s)"
517 " not in attaching_slaves(%s)"
518 " or slaves(%s)" % (slave, slave.slavename,
519 self.attaching_slaves,
520 self.slaves))
521 return
522 if sb.state == BUILDING:
523 # the Build's .lostRemote method (invoked by a notifyOnDisconnect
524 # handler) will cause the Build to be stopped, probably right
525 # after the notifyOnDisconnect that invoked us finishes running.
527 # TODO: should failover to a new Build
528 #self.retryBuild(sb.build)
529 pass
531 if sb in self.attaching_slaves:
532 self.attaching_slaves.remove(sb)
533 if sb in self.slaves:
534 self.slaves.remove(sb)
536 # TODO: make this .addSlaveEvent?
537 self.builder_status.addPointEvent(['disconnect', slave.slavename])
538 sb.detached() # inform the SlaveBuilder that their slave went away
539 self.updateBigStatus()
540 self.fireTestEvent('detach')
541 if not self.slaves:
542 self.fireTestEvent('detach_all')
544 def updateBigStatus(self):
545 if not self.slaves:
546 self.builder_status.setBigState("offline")
547 elif self.building:
548 self.builder_status.setBigState("building")
549 else:
550 self.builder_status.setBigState("idle")
551 self.fireTestEvent('idle')
553 def maybeStartBuild(self):
554 log.msg("maybeStartBuild %s: %s %s" %
555 (self, self.buildable, self.slaves))
556 if not self.buildable:
557 self.updateBigStatus()
558 return # nothing to do
560 # pick an idle slave
561 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
562 if not available_slaves:
563 log.msg("%s: want to start build, but we don't have a remote"
564 % self)
565 self.updateBigStatus()
566 return
567 if self.CHOOSE_SLAVES_RANDOMLY:
568 sb = random.choice(available_slaves)
569 else:
570 sb = available_slaves[0]
572 # there is something to build, and there is a slave on which to build
573 # it. Grab the oldest request, see if we can merge it with anything
574 # else.
575 req = self.buildable.pop(0)
576 self.builder_status.removeBuildRequest(req.status)
577 mergers = []
578 for br in self.buildable[:]:
579 if req.canBeMergedWith(br):
580 self.buildable.remove(br)
581 self.builder_status.removeBuildRequest(br.status)
582 mergers.append(br)
583 requests = [req] + mergers
585 # Create a new build from our build factory and set ourself as the
586 # builder.
587 build = self.buildFactory.newBuild(requests)
588 build.setBuilder(self)
589 build.setLocks(self.locks)
591 # start it
592 self.startBuild(build, sb)
594 def startBuild(self, build, sb):
595 """Start a build on the given slave.
596 @param build: the L{base.Build} to start
597 @param sb: the L{SlaveBuilder} which will host this build
599 @return: a Deferred which fires with a
600 L{buildbot.interfaces.IBuildControl} that can be used to stop the
601 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
602 watch the Build as it runs. """
604 self.building.append(build)
605 self.updateBigStatus()
607 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
608 # ping the slave to make sure they're still there. If they're fallen
609 # off the map (due to a NAT timeout or something), this will fail in
610 # a couple of minutes, depending upon the TCP timeout. TODO: consider
611 # making this time out faster, or at least characterize the likely
612 # duration.
613 d = sb.ping(self.START_BUILD_TIMEOUT)
614 d.addCallback(self._startBuild_1, build, sb)
615 return d
617 def _startBuild_1(self, res, build, sb):
618 if not res:
619 return self._startBuildFailed("slave ping failed", build, sb)
620 # The buildslave is ready to go. sb.buildStarted() sets its state to
621 # BUILDING (so we won't try to use it for any other builds). This
622 # gets set back to IDLE by the Build itself when it finishes.
623 sb.buildStarted()
624 d = sb.remote.callRemote("startBuild")
625 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
626 callbackArgs=(build,sb), errbackArgs=(build,sb))
627 return d
629 def _startBuild_2(self, res, build, sb):
630 # create the BuildStatus object that goes with the Build
631 bs = self.builder_status.newBuild()
633 # start the build. This will first set up the steps, then tell the
634 # BuildStatus that it has started, which will announce it to the
635 # world (through our BuilderStatus object, which is its parent).
636 # Finally it will start the actual build process.
637 d = build.startBuild(bs, self.expectations, sb)
638 d.addCallback(self.buildFinished, sb)
639 d.addErrback(log.err) # this shouldn't happen. if it does, the slave
640 # will be wedged
641 for req in build.requests:
642 req.buildStarted(build, bs)
643 return build # this is the IBuildControl
645 def _startBuildFailed(self, why, build, sb):
646 # put the build back on the buildable list
647 log.msg("I tried to tell the slave that the build %s started, but "
648 "remote_startBuild failed: %s" % (build, why))
649 # release the slave. This will queue a call to maybeStartBuild, which
650 # will fire after other notifyOnDisconnect handlers have marked the
651 # slave as disconnected (so we don't try to use it again).
652 sb.buildFinished()
654 log.msg("re-queueing the BuildRequest")
655 self.building.remove(build)
656 for req in build.requests:
657 self.buildable.insert(0, req) # the interrupted build gets first
658 # priority
659 self.builder_status.addBuildRequest(req.status)
662 def buildFinished(self, build, sb):
663 """This is called when the Build has finished (either success or
664 failure). Any exceptions during the build are reported with
665 results=FAILURE, not with an errback."""
667 # by the time we get here, the Build has already released the slave
668 # (which queues a call to maybeStartBuild)
670 self.building.remove(build)
671 for req in build.requests:
672 req.finished(build.build_status)
674 def setExpectations(self, progress):
675 """Mark the build as successful and update expectations for the next
676 build. Only call this when the build did not fail in any way that
677 would invalidate the time expectations generated by it. (if the
678 compile failed and thus terminated early, we can't use the last
679 build to predict how long the next one will take).
681 if self.expectations:
682 self.expectations.update(progress)
683 else:
684 # the first time we get a good build, create our Expectations
685 # based upon its results
686 self.expectations = Expectations(progress)
687 log.msg("new expectations: %s seconds" % \
688 self.expectations.expectedBuildTime())
690 def shutdownSlave(self):
691 if self.remote:
692 self.remote.callRemote("shutdown")
695 class BuilderControl(components.Adapter):
696 implements(interfaces.IBuilderControl)
698 def requestBuild(self, req):
699 """Submit a BuildRequest to this Builder."""
700 self.original.submitBuildRequest(req)
702 def requestBuildSoon(self, req):
703 """Submit a BuildRequest like requestBuild, but raise a
704 L{buildbot.interfaces.NoSlaveError} if no slaves are currently
705 available, so it cannot be used to queue a BuildRequest in the hopes
706 that a slave will eventually connect. This method is appropriate for
707 use by things like the web-page 'Force Build' button."""
708 if not self.original.slaves:
709 raise interfaces.NoSlaveError
710 self.requestBuild(req)
712 def resubmitBuild(self, bs, reason="<rebuild, no reason given>"):
713 if not bs.isFinished():
714 return
716 ss = bs.getSourceStamp(absolute=True)
717 req = base.BuildRequest(reason, ss, self.original.name)
718 self.requestBuild(req)
720 def getPendingBuilds(self):
721 # return IBuildRequestControl objects
722 raise NotImplementedError
724 def getBuild(self, number):
725 return self.original.getBuild(number)
727 def ping(self, timeout=30):
728 if not self.original.slaves:
729 self.original.builder_status.addPointEvent(["ping", "no slave"],
730 "red")
731 return defer.succeed(False) # interfaces.NoSlaveError
732 dl = []
733 for s in self.original.slaves:
734 dl.append(s.ping(timeout, self.original.builder_status))
735 d = defer.DeferredList(dl)
736 d.addCallback(self._gatherPingResults)
737 return d
739 def _gatherPingResults(self, res):
740 for ignored,success in res:
741 if not success:
742 return False
743 return True
745 components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl)