1 # -*- test-case-name: buildbot.test.test_status -*-
3 from __future__
import generators
5 from twisted
.python
import log
6 from twisted
.persisted
import styles
7 from twisted
.internet
import reactor
, defer
8 from twisted
.protocols
import basic
10 import os
, shutil
, sys
, re
, urllib
18 from buildbot
import interfaces
, util
, sourcestamp
19 from buildbot
.twcompat
import implements
, providedBy
21 SUCCESS
, WARNINGS
, FAILURE
, SKIPPED
, EXCEPTION
= range(5)
22 Results
= ["success", "warnings", "failure", "skipped", "exception"]
25 # build processes call the following methods:
31 # currentlyInterlocked
36 # updateCurrentActivity
37 # addFileToCurrentActivity
38 # finishCurrentActivity
43 STDOUT
= interfaces
.LOG_CHANNEL_STDOUT
44 STDERR
= interfaces
.LOG_CHANNEL_STDERR
45 HEADER
= interfaces
.LOG_CHANNEL_HEADER
46 ChunkTypes
= ["stdout", "stderr", "header"]
48 class LogFileScanner(basic
.NetstringReceiver
):
49 def __init__(self
, chunk_cb
, channels
=[]):
50 self
.chunk_cb
= chunk_cb
51 self
.channels
= channels
53 def stringReceived(self
, line
):
54 channel
= int(line
[0])
55 if not self
.channels
or (channel
in self
.channels
):
56 self
.chunk_cb((channel
, line
[1:]))
58 class LogFileProducer
:
61 the LogFile has just one FD, used for both reading and writing.
62 Each time you add an entry, fd.seek to the end and then write.
64 Each reader (i.e. Producer) keeps track of their own offset. The reader
65 starts by seeking to the start of the logfile, and reading forwards.
66 Between each hunk of file they yield chunks, so they must remember their
67 offset before yielding and re-seek back to that offset before reading
68 more data. When their read() returns EOF, they're finished with the first
69 phase of the reading (everything that's already been written to disk).
71 After EOF, the remaining data is entirely in the current entries list.
72 These entries are all of the same channel, so we can do one "".join and
73 obtain a single chunk to be sent to the listener. But since that involves
74 a yield, and more data might arrive after we give up control, we have to
75 subscribe them before yielding. We can't subscribe them any earlier,
76 otherwise they'd get data out of order.
78 We're using a generator in the first place so that the listener can
79 throttle us, which means they're pulling. But the subscription means
80 we're pushing. Really we're a Producer. In the first phase we can be
81 either a PullProducer or a PushProducer. In the second phase we're only a
84 So the client gives a LogFileConsumer to File.subscribeConsumer . This
85 Consumer must have registerProducer(), unregisterProducer(), and
86 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
87 except that writeChunk() takes chunks (tuples of (channel,text)) instead
88 of the normal write() which takes just text. The LogFileConsumer is
89 allowed to call stopProducing, pauseProducing, and resumeProducing on the
90 producer instance it is given. """
96 def __init__(self
, logfile
, consumer
):
97 self
.logfile
= logfile
98 self
.consumer
= consumer
99 self
.chunkGenerator
= self
.getChunks()
100 consumer
.registerProducer(self
, True)
103 f
= self
.logfile
.getFile()
106 p
= LogFileScanner(chunks
.append
)
108 data
= f
.read(self
.BUFFERSIZE
)
116 data
= f
.read(self
.BUFFERSIZE
)
120 # now subscribe them to receive new entries
121 self
.subscribed
= True
122 self
.logfile
.watchers
.append(self
)
123 d
= self
.logfile
.waitUntilFinished()
125 # then give them the not-yet-merged data
126 if self
.logfile
.runEntries
:
127 channel
= self
.logfile
.runEntries
[0][0]
128 text
= "".join([c
[1] for c
in self
.logfile
.runEntries
])
129 yield (channel
, text
)
131 # now we've caught up to the present. Anything further will come from
132 # the logfile subscription. We add the callback *after* yielding the
133 # data from runEntries, because the logfile might have finished
135 d
.addCallback(self
.logfileFinished
)
137 def stopProducing(self
):
138 # TODO: should we still call consumer.finish? probably not.
144 if self
.chunkGenerator
:
145 self
.chunkGenerator
= None # stop making chunks
147 self
.logfile
.watchers
.remove(self
)
148 self
.subscribed
= False
150 def pauseProducing(self
):
153 def resumeProducing(self
):
154 # Twisted-1.3.0 has a bug which causes hangs when resumeProducing
155 # calls transport.write (there is a recursive loop, fixed in 2.0 in
156 # t.i.abstract.FileDescriptor.doWrite by setting the producerPaused
157 # flag *before* calling resumeProducing). To work around this, we
158 # just put off the real resumeProducing for a moment. This probably
159 # has a performance hit, but I'm going to assume that the log files
160 # are not retrieved frequently enough for it to be an issue.
162 reactor
.callLater(0, self
._resumeProducing
)
164 def _resumeProducing(self
):
166 if not self
.chunkGenerator
:
169 while not self
.paused
:
170 chunk
= self
.chunkGenerator
.next()
171 self
.consumer
.writeChunk(chunk
)
172 # we exit this when the consumer says to stop, or we run out
174 except StopIteration:
175 # if the generator finished, it will have done releaseFile
176 self
.chunkGenerator
= None
177 # now everything goes through the subscription, and they don't get to
180 def logChunk(self
, build
, step
, logfile
, channel
, chunk
):
182 self
.consumer
.writeChunk((channel
, chunk
))
184 def logfileFinished(self
, logfile
):
187 self
.consumer
.unregisterProducer()
188 self
.consumer
.finish()
192 """A LogFile keeps all of its contents on disk, in a non-pickle format to
193 which new entries can easily be appended. The file on disk has a name
194 like 12-log-compile-output, under the Builder's directory. The actual
195 filename is generated (before the LogFile is created) by
196 L{BuildStatus.generateLogfileName}.
198 Old LogFile pickles (which kept their contents in .entries) must be
199 upgraded. The L{BuilderStatus} is responsible for doing this, when it
200 loads the L{BuildStatus} into memory. The Build pickle is not modified,
201 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
205 implements(interfaces
.IStatusLog
, interfaces
.ILogFile
)
207 __implements__
= (interfaces
.IStatusLog
, interfaces
.ILogFile
)
213 runEntries
= [] # provided so old pickled builds will getChunks() ok
216 filename
= None # relative to the Builder's basedir
219 def __init__(self
, parent
, name
, logfilename
):
221 @type parent: L{BuildStepStatus}
222 @param parent: the Step that this log is a part of
224 @param name: the name of this log, typically 'output'
225 @type logfilename: string
226 @param logfilename: the Builder-relative pathname for the saved entries
230 self
.filename
= logfilename
231 fn
= self
.getFilename()
232 if os
.path
.exists(fn
):
233 # the buildmaster was probably stopped abruptly, before the
234 # BuilderStatus could be saved, so BuilderStatus.nextBuildNumber
235 # is out of date, and we're overlapping with earlier builds now.
236 # Warn about it, but then overwrite the old pickle file
237 log
.msg("Warning: Overwriting old serialized Build at %s" % fn
)
238 self
.openfile
= open(fn
, "w+")
241 self
.finishedWatchers
= []
243 def getFilename(self
):
244 return os
.path
.join(self
.step
.build
.builder
.basedir
, self
.filename
)
246 def hasContents(self
):
247 return os
.path
.exists(self
.getFilename())
255 def isFinished(self
):
257 def waitUntilFinished(self
):
259 d
= defer
.succeed(self
)
262 self
.finishedWatchers
.append(d
)
267 # this is the filehandle we're using to write to the log, so
270 # otherwise they get their own read-only handle
271 return open(self
.getFilename(), "r")
274 # this produces one ginormous string
275 return "".join(self
.getChunks([STDOUT
, STDERR
], onlyText
=True))
277 def getTextWithHeaders(self
):
278 return "".join(self
.getChunks(onlyText
=True))
280 def getChunks(self
, channels
=[], onlyText
=False):
281 # generate chunks for everything that was logged at the time we were
282 # first called, so remember how long the file was when we started.
283 # Don't read beyond that point. The current contents of
284 # self.runEntries will follow.
286 # this returns an iterator, which means arbitrary things could happen
287 # while we're yielding. This will faithfully deliver the log as it
288 # existed when it was started, and not return anything after that
289 # point. To use this in subscribe(catchup=True) without missing any
290 # data, you must insure that nothing will be added to the log during
299 if self
.runEntries
and (not channels
or
300 (self
.runEntries
[0][0] in channels
)):
301 leftover
= (self
.runEntries
[0][0],
302 "".join([c
[1] for c
in self
.runEntries
]))
304 # freeze the state of the LogFile by passing a lot of parameters into
306 return self
._generateChunks
(f
, offset
, remaining
, leftover
,
309 def _generateChunks(self
, f
, offset
, remaining
, leftover
,
312 p
= LogFileScanner(chunks
.append
, channels
)
314 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
315 remaining
-= len(data
)
320 channel
, text
= chunks
.pop(0)
324 yield (channel
, text
)
326 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
327 remaining
-= len(data
)
337 def subscribe(self
, receiver
, catchup
):
340 self
.watchers
.append(receiver
)
342 for channel
, text
in self
.getChunks():
343 # TODO: add logChunks(), to send over everything at once?
344 receiver
.logChunk(self
.step
.build
, self
.step
, self
,
347 def unsubscribe(self
, receiver
):
348 if receiver
in self
.watchers
:
349 self
.watchers
.remove(receiver
)
351 def subscribeConsumer(self
, consumer
):
352 p
= LogFileProducer(self
, consumer
)
355 # interface used by the build steps to add things to the log
358 # merge all .runEntries (which are all of the same type) into a
359 # single chunk for .entries
360 if not self
.runEntries
:
362 channel
= self
.runEntries
[0][0]
363 text
= "".join([c
[1] for c
in self
.runEntries
])
368 while offset
< len(text
):
369 size
= min(len(text
)-offset
, self
.chunkSize
)
370 f
.write("%d:%d" % (1 + size
, channel
))
371 f
.write(text
[offset
:offset
+size
])
377 def addEntry(self
, channel
, text
):
378 assert not self
.finished
379 # we only add to .runEntries here. merge() is responsible for adding
380 # merged chunks to .entries
381 if self
.runEntries
and channel
!= self
.runEntries
[0][0]:
383 self
.runEntries
.append((channel
, text
))
384 self
.runLength
+= len(text
)
385 if self
.runLength
>= self
.chunkSize
:
388 for w
in self
.watchers
:
389 w
.logChunk(self
.step
.build
, self
.step
, self
, channel
, text
)
390 self
.length
+= len(text
)
392 def addStdout(self
, text
):
393 self
.addEntry(STDOUT
, text
)
394 def addStderr(self
, text
):
395 self
.addEntry(STDERR
, text
)
396 def addHeader(self
, text
):
397 self
.addEntry(HEADER
, text
)
402 # we don't do an explicit close, because there might be readers
403 # shareing the filehandle. As soon as they stop reading, the
404 # filehandle will be released and automatically closed. We will
405 # do a sync, however, to make sure the log gets saved in case of
407 os
.fsync(self
.openfile
.fileno())
410 watchers
= self
.finishedWatchers
411 self
.finishedWatchers
= []
416 def __getstate__(self
):
417 d
= self
.__dict
__.copy()
418 del d
['step'] # filled in upon unpickling
420 del d
['finishedWatchers']
421 d
['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
422 if d
.has_key('finished'):
424 if d
.has_key('openfile'):
428 def __setstate__(self
, d
):
430 self
.watchers
= [] # probably not necessary
431 self
.finishedWatchers
= [] # same
432 # self.step must be filled in by our parent
435 def upgrade(self
, logfilename
):
436 """Save our .entries to a new-style offline log file (if necessary),
437 and modify our in-memory representation to use it. The original
438 pickled LogFile (inside the pickled Build) won't be modified."""
439 self
.filename
= logfilename
440 if not os
.path
.exists(self
.getFilename()):
441 self
.openfile
= open(self
.getFilename(), "w")
442 self
.finished
= False
443 for channel
,text
in self
.entries
:
444 self
.addEntry(channel
, text
)
445 self
.finish() # releases self.openfile, which will be closed
451 implements(interfaces
.IStatusLog
)
453 __implements__
= interfaces
.IStatusLog
,
457 def __init__(self
, parent
, name
, logfilename
, html
):
460 self
.filename
= logfilename
464 return self
.name
# set in BuildStepStatus.addLog
468 def isFinished(self
):
470 def waitUntilFinished(self
):
471 return defer
.succeed(self
)
473 def hasContents(self
):
476 return self
.html
# looks kinda like text
477 def getTextWithHeaders(self
):
480 return [(STDERR
, self
.html
)]
482 def subscribe(self
, receiver
, catchup
):
484 def unsubscribe(self
, receiver
):
490 def __getstate__(self
):
491 d
= self
.__dict
__.copy()
495 def upgrade(self
, logfilename
):
501 implements(interfaces
.IStatusEvent
)
503 __implements__
= interfaces
.IStatusEvent
,
510 # IStatusEvent methods
512 return (self
.started
, self
.finished
)
521 self
.finished
= util
.now()
525 implements(interfaces
.ITestResult
)
527 __implements__
= interfaces
.ITestResult
,
529 def __init__(self
, name
, results
, text
, logs
):
530 assert isinstance(name
, tuple)
532 self
.results
= results
539 def getResults(self
):
549 class BuildSetStatus
:
551 implements(interfaces
.IBuildSetStatus
)
553 __implements__
= interfaces
.IBuildSetStatus
,
555 def __init__(self
, source
, reason
, builderNames
, bsid
=None):
558 self
.builderNames
= builderNames
560 self
.successWatchers
= []
561 self
.finishedWatchers
= []
562 self
.stillHopeful
= True
563 self
.finished
= False
565 def setBuildRequestStatuses(self
, buildRequestStatuses
):
566 self
.buildRequests
= buildRequestStatuses
567 def setResults(self
, results
):
568 # the build set succeeds only if all its component builds succeed
569 self
.results
= results
570 def giveUpHope(self
):
571 self
.stillHopeful
= False
574 def notifySuccessWatchers(self
):
575 for d
in self
.successWatchers
:
577 self
.successWatchers
= []
579 def notifyFinishedWatchers(self
):
581 for d
in self
.finishedWatchers
:
583 self
.finishedWatchers
= []
585 # methods for our clients
587 def getSourceStamp(self
):
591 def getResults(self
):
596 def getBuilderNames(self
):
597 return self
.builderNames
598 def getBuildRequests(self
):
599 return self
.buildRequests
600 def isFinished(self
):
603 def waitUntilSuccess(self
):
604 if self
.finished
or not self
.stillHopeful
:
605 # the deferreds have already fired
606 return defer
.succeed(self
)
608 self
.successWatchers
.append(d
)
611 def waitUntilFinished(self
):
613 return defer
.succeed(self
)
615 self
.finishedWatchers
.append(d
)
618 class BuildRequestStatus
:
620 implements(interfaces
.IBuildRequestStatus
)
622 __implements__
= interfaces
.IBuildRequestStatus
,
624 def __init__(self
, source
, builderName
):
626 self
.builderName
= builderName
627 self
.builds
= [] # list of BuildStatus objects
630 def buildStarted(self
, build
):
631 self
.builds
.append(build
)
632 for o
in self
.observers
[:]:
635 # methods called by our clients
636 def getSourceStamp(self
):
638 def getBuilderName(self
):
639 return self
.builderName
643 def subscribe(self
, observer
):
644 self
.observers
.append(observer
)
645 for b
in self
.builds
:
647 def unsubscribe(self
, observer
):
648 self
.observers
.remove(observer
)
651 class BuildStepStatus(styles
.Versioned
):
653 I represent a collection of output status for a
654 L{buildbot.process.step.BuildStep}.
657 @cvar color: color that this step feels best represents its
658 current mood. yellow,green,red,orange are the
659 most likely choices, although purple indicates
661 @type progress: L{buildbot.status.progress.StepProgress}
662 @cvar progress: tracks ETA for the step
663 @type text: list of strings
664 @cvar text: list of short texts that describe the command and its status
665 @type text2: list of strings
666 @cvar text2: list of short texts added to the overall build description
667 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
668 @ivar logs: logs of steps
670 # note that these are created when the Build is set up, before each
671 # corresponding BuildStep has started.
673 implements(interfaces
.IBuildStepStatus
, interfaces
.IStatusEvent
)
675 __implements__
= interfaces
.IBuildStepStatus
, interfaces
.IStatusEvent
676 persistenceVersion
= 1
687 finishedWatchers
= []
689 def __init__(self
, parent
):
690 assert interfaces
.IBuildStatus(parent
)
696 self
.finishedWatchers
= []
699 """Returns a short string with the name of this step. This string
700 may have spaces in it."""
707 return (self
.started
, self
.finished
)
709 def getExpectations(self
):
710 """Returns a list of tuples (name, current, target)."""
711 if not self
.progress
:
714 metrics
= self
.progress
.progress
.keys()
717 t
= (m
, self
.progress
.progress
[m
], self
.progress
.expectations
[m
])
725 return self
.urls
.copy()
727 def isFinished(self
):
728 return (self
.finished
is not None)
730 def waitUntilFinished(self
):
732 d
= defer
.succeed(self
)
735 self
.finishedWatchers
.append(d
)
738 # while the step is running, the following methods make sense.
739 # Afterwards they return None
742 if self
.started
is None:
743 return None # not started yet
744 if self
.finished
is not None:
745 return None # already finished
746 if not self
.progress
:
747 return None # no way to predict
748 return self
.progress
.remaining()
750 # Once you know the step has finished, the following methods are legal.
751 # Before this step has finished, they all return None.
754 """Returns a list of strings which describe the step. These are
755 intended to be displayed in a narrow column. If more space is
756 available, the caller should join them together with spaces before
757 presenting them to the user."""
761 """Returns a single string with the color that should be used to
762 display this step. 'green', 'orange', 'red', 'yellow' and 'purple'
763 are the most likely ones."""
766 def getResults(self
):
767 """Return a tuple describing the results of the step.
768 'result' is one of the constants in L{buildbot.status.builder}:
769 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
770 'strings' is an optional list of strings that the step wants to
771 append to the overall build's results. These strings are usually
772 more terse than the ones returned by getText(): in particular,
773 successful Steps do not usually contribute any text to the
776 @rtype: tuple of int, list of strings
777 @returns: (result, strings)
779 return (self
.results
, self
.text2
)
781 # subscription interface
783 def subscribe(self
, receiver
, updateInterval
=10):
784 # will get logStarted, logFinished, stepETAUpdate
785 assert receiver
not in self
.watchers
786 self
.watchers
.append(receiver
)
787 self
.sendETAUpdate(receiver
, updateInterval
)
789 def sendETAUpdate(self
, receiver
, updateInterval
):
790 self
.updates
[receiver
] = None
791 # they might unsubscribe during stepETAUpdate
792 receiver
.stepETAUpdate(self
.build
, self
,
793 self
.getETA(), self
.getExpectations())
794 if receiver
in self
.watchers
:
795 self
.updates
[receiver
] = reactor
.callLater(updateInterval
,
800 def unsubscribe(self
, receiver
):
801 if receiver
in self
.watchers
:
802 self
.watchers
.remove(receiver
)
803 if receiver
in self
.updates
:
804 if self
.updates
[receiver
] is not None:
805 self
.updates
[receiver
].cancel()
806 del self
.updates
[receiver
]
809 # methods to be invoked by the BuildStep
811 def setName(self
, stepname
):
814 def setProgress(self
, stepprogress
):
815 self
.progress
= stepprogress
817 def stepStarted(self
):
818 self
.started
= util
.now()
820 self
.build
.stepStarted(self
)
822 def addLog(self
, name
):
823 assert self
.started
# addLog before stepStarted won't notify watchers
824 logfilename
= self
.build
.generateLogfileName(self
.name
, name
)
825 log
= LogFile(self
, name
, logfilename
)
826 self
.logs
.append(log
)
827 for w
in self
.watchers
:
828 receiver
= w
.logStarted(self
.build
, self
, log
)
830 log
.subscribe(receiver
, True)
831 d
= log
.waitUntilFinished()
832 d
.addCallback(lambda log
: log
.unsubscribe(receiver
))
833 d
= log
.waitUntilFinished()
834 d
.addCallback(self
.logFinished
)
837 def addHTMLLog(self
, name
, html
):
838 assert self
.started
# addLog before stepStarted won't notify watchers
839 logfilename
= self
.build
.generateLogfileName(self
.name
, name
)
840 log
= HTMLLogFile(self
, name
, logfilename
, html
)
841 self
.logs
.append(log
)
842 for w
in self
.watchers
:
843 receiver
= w
.logStarted(self
.build
, self
, log
)
844 # TODO: think about this: there isn't much point in letting
847 # log.subscribe(receiver, True)
848 w
.logFinished(self
.build
, self
, log
)
850 def logFinished(self
, log
):
851 for w
in self
.watchers
:
852 w
.logFinished(self
.build
, self
, log
)
854 def addURL(self
, name
, url
):
855 self
.urls
[name
] = url
857 def setColor(self
, color
):
859 def setText(self
, text
):
861 def setText2(self
, text
):
864 def stepFinished(self
, results
):
865 self
.finished
= util
.now()
866 self
.results
= results
867 for loog
in self
.logs
:
868 if not loog
.isFinished():
871 for r
in self
.updates
.keys():
872 if self
.updates
[r
] is not None:
873 self
.updates
[r
].cancel()
876 watchers
= self
.finishedWatchers
877 self
.finishedWatchers
= []
883 def __getstate__(self
):
884 d
= styles
.Versioned
.__getstate
__(self
)
885 del d
['build'] # filled in when loading
886 if d
.has_key('progress'):
889 del d
['finishedWatchers']
893 def __setstate__(self
, d
):
894 styles
.Versioned
.__setstate
__(self
, d
)
895 # self.build must be filled in by our parent
896 for loog
in self
.logs
:
899 def upgradeToVersion1(self
):
900 if not hasattr(self
, "urls"):
904 class BuildStatus(styles
.Versioned
):
906 implements(interfaces
.IBuildStatus
, interfaces
.IStatusEvent
)
908 __implements__
= interfaces
.IBuildStatus
, interfaces
.IStatusEvent
909 persistenceVersion
= 2
924 # these lists/dicts are defined here so that unserialized instances have
925 # (empty) values. They are set in __init__ to new objects to make sure
926 # each instance gets its own copy.
929 finishedWatchers
= []
932 def __init__(self
, parent
, number
):
934 @type parent: L{BuilderStatus}
937 assert interfaces
.IBuilderStatus(parent
)
938 self
.builder
= parent
942 self
.finishedWatchers
= []
944 self
.testResults
= {}
949 def getBuilder(self
):
951 @rtype: L{BuilderStatus}
955 def getProperty(self
, propname
):
956 return self
.properties
[propname
]
961 def getPreviousBuild(self
):
964 return self
.builder
.getBuild(self
.number
-1)
966 def getSourceStamp(self
):
967 return (self
.source
.branch
, self
.source
.revision
, self
.source
.patch
)
972 def getChanges(self
):
975 def getResponsibleUsers(self
):
976 return self
.blamelist
978 def getInterestedUsers(self
):
979 # TODO: the Builder should add others: sheriffs, domain-owners
980 return self
.blamelist
983 """Return a list of IBuildStepStatus objects. For invariant builds
984 (those which always use the same set of Steps), this should be the
985 complete list, however some of the steps may not have started yet
986 (step.getTimes()[0] will be None). For variant builds, this may not
987 be complete (asking again later may give you more of them)."""
991 return (self
.started
, self
.finished
)
993 def isFinished(self
):
994 return (self
.finished
is not None)
996 def waitUntilFinished(self
):
998 d
= defer
.succeed(self
)
1000 d
= defer
.Deferred()
1001 self
.finishedWatchers
.append(d
)
1004 # while the build is running, the following methods make sense.
1005 # Afterwards they return None
1008 if self
.finished
is not None:
1010 if not self
.progress
:
1012 eta
= self
.progress
.eta()
1015 return eta
- util
.now()
1017 def getCurrentStep(self
):
1018 return self
.currentStep
1020 # Once you know the build has finished, the following methods are legal.
1021 # Before ths build has finished, they all return None.
1025 text
.extend(self
.text
)
1026 for s
in self
.steps
:
1027 text
.extend(s
.text2
)
1033 def getResults(self
):
1036 def getSlavename(self
):
1037 return self
.slavename
1039 def getTestResults(self
):
1040 return self
.testResults
1043 # TODO: steps should contribute significant logs instead of this
1044 # hack, which returns every log from every step. The logs should get
1045 # names like "compile" and "test" instead of "compile.output"
1047 for s
in self
.steps
:
1048 for log
in s
.getLogs():
1052 # subscription interface
1054 def subscribe(self
, receiver
, updateInterval
=None):
1055 # will receive stepStarted and stepFinished messages
1056 # and maybe buildETAUpdate
1057 self
.watchers
.append(receiver
)
1058 if updateInterval
is not None:
1059 self
.sendETAUpdate(receiver
, updateInterval
)
1061 def sendETAUpdate(self
, receiver
, updateInterval
):
1062 self
.updates
[receiver
] = None
1065 receiver
.buildETAUpdate(self
, self
.getETA())
1066 # they might have unsubscribed during buildETAUpdate
1067 if receiver
in self
.watchers
:
1068 self
.updates
[receiver
] = reactor
.callLater(updateInterval
,
1073 def unsubscribe(self
, receiver
):
1074 if receiver
in self
.watchers
:
1075 self
.watchers
.remove(receiver
)
1076 if receiver
in self
.updates
:
1077 if self
.updates
[receiver
] is not None:
1078 self
.updates
[receiver
].cancel()
1079 del self
.updates
[receiver
]
1081 # methods for the base.Build to invoke
1083 def addStepWithName(self
, name
):
1084 """The Build is setting up, and has added a new BuildStep to its
1085 list. Create a BuildStepStatus object to which it can send status
1088 s
= BuildStepStatus(self
)
1090 self
.steps
.append(s
)
1093 def setProperty(self
, propname
, value
):
1094 self
.properties
[propname
] = value
1096 def addTestResult(self
, result
):
1097 self
.testResults
[result
.getName()] = result
1099 def setSourceStamp(self
, sourceStamp
):
1100 self
.source
= sourceStamp
1101 self
.changes
= self
.source
.changes
1103 def setReason(self
, reason
):
1104 self
.reason
= reason
1105 def setBlamelist(self
, blamelist
):
1106 self
.blamelist
= blamelist
1107 def setProgress(self
, progress
):
1108 self
.progress
= progress
1110 def buildStarted(self
, build
):
1111 """The Build has been set up and is about to be started. It can now
1112 be safely queried, so it is time to announce the new build."""
1114 self
.started
= util
.now()
1115 # now that we're ready to report status, let the BuilderStatus tell
1116 # the world about us
1117 self
.builder
.buildStarted(self
)
1119 def setSlavename(self
, slavename
):
1120 self
.slavename
= slavename
1122 def setText(self
, text
):
1123 assert isinstance(text
, (list, tuple))
1125 def setColor(self
, color
):
1127 def setResults(self
, results
):
1128 self
.results
= results
1130 def buildFinished(self
):
1131 self
.currentStep
= None
1132 self
.finished
= util
.now()
1134 for r
in self
.updates
.keys():
1135 if self
.updates
[r
] is not None:
1136 self
.updates
[r
].cancel()
1139 watchers
= self
.finishedWatchers
1140 self
.finishedWatchers
= []
1144 # methods called by our BuildStepStatus children
1146 def stepStarted(self
, step
):
1147 self
.currentStep
= step
1148 name
= self
.getBuilder().getName()
1149 for w
in self
.watchers
:
1150 receiver
= w
.stepStarted(self
, step
)
1152 if type(receiver
) == type(()):
1153 step
.subscribe(receiver
[0], receiver
[1])
1155 step
.subscribe(receiver
)
1156 d
= step
.waitUntilFinished()
1157 d
.addCallback(lambda step
: step
.unsubscribe(receiver
))
1159 step
.waitUntilFinished().addCallback(self
._stepFinished
)
1161 def _stepFinished(self
, step
):
1162 results
= step
.getResults()
1163 for w
in self
.watchers
:
1164 w
.stepFinished(self
, step
, results
)
1166 # methods called by our BuilderStatus parent
1168 def pruneLogs(self
):
1169 # this build is somewhat old: remove the build logs to save space
1170 # TODO: delete logs visible through IBuildStatus.getLogs
1171 for s
in self
.steps
:
1174 def pruneSteps(self
):
1175 # this build is very old: remove the build steps too
1180 def generateLogfileName(self
, stepname
, logname
):
1181 """Return a filename (relative to the Builder's base directory) where
1182 the logfile's contents can be stored uniquely.
1184 The base filename is made by combining our build number, the Step's
1185 name, and the log's name, then removing unsuitable characters. The
1186 filename is then made unique by appending _0, _1, etc, until it does
1187 not collide with any other logfile.
1189 These files are kept in the Builder's basedir (rather than a
1190 per-Build subdirectory) because that makes cleanup easier: cron and
1191 find will help get rid of the old logs, but the empty directories are
1192 more of a hassle to remove."""
1194 starting_filename
= "%d-log-%s-%s" % (self
.number
, stepname
, logname
)
1195 starting_filename
= re
.sub(r
'[^\w\.\-]', '_', starting_filename
)
1196 # now make it unique
1198 filename
= starting_filename
1199 while filename
in [l
.filename
1200 for step
in self
.steps
1201 for l
in step
.getLogs()
1203 filename
= "%s_%d" % (starting_filename
, unique_counter
)
1207 def __getstate__(self
):
1208 d
= styles
.Versioned
.__getstate
__(self
)
1209 # for now, a serialized Build is always "finished". We will never
1210 # save unfinished builds.
1211 if not self
.finished
:
1212 d
['finished'] = True
1213 # TODO: push an "interrupted" step so it is clear that the build
1214 # was interrupted. The builder will have a 'shutdown' event, but
1215 # someone looking at just this build will be confused as to why
1216 # the last log is truncated.
1217 del d
['builder'] # filled in by our parent when loading
1220 del d
['finishedWatchers']
1223 def __setstate__(self
, d
):
1224 styles
.Versioned
.__setstate
__(self
, d
)
1225 # self.builder must be filled in by our parent when loading
1226 for step
in self
.steps
:
1230 self
.finishedWatchers
= []
1232 def upgradeToVersion1(self
):
1233 if hasattr(self
, "sourceStamp"):
1234 # the old .sourceStamp attribute wasn't actually very useful
1235 maxChangeNumber
, patch
= self
.sourceStamp
1236 changes
= getattr(self
, 'changes', [])
1237 source
= sourcestamp
.SourceStamp(branch
=None,
1241 self
.source
= source
1242 self
.changes
= source
.changes
1243 del self
.sourceStamp
1245 def upgradeToVersion2(self
):
1246 self
.properties
= {}
1248 def upgradeLogfiles(self
):
1249 # upgrade any LogFiles that need it. This must occur after we've been
1250 # attached to our Builder, and after we know about all LogFiles of
1251 # all Steps (to get the filenames right).
1253 for s
in self
.steps
:
1254 for l
in s
.getLogs():
1256 pass # new-style, log contents are on disk
1258 logfilename
= self
.generateLogfileName(s
.name
, l
.name
)
1259 # let the logfile update its .filename pointer,
1260 # transferring its contents onto disk if necessary
1261 l
.upgrade(logfilename
)
1263 def saveYourself(self
):
1264 filename
= os
.path
.join(self
.builder
.basedir
, "%d" % self
.number
)
1265 if os
.path
.isdir(filename
):
1266 # leftover from 0.5.0, which stored builds in directories
1267 shutil
.rmtree(filename
, ignore_errors
=True)
1268 tmpfilename
= filename
+ ".tmp"
1270 pickle
.dump(self
, open(tmpfilename
, "wb"), -1)
1271 if sys
.platform
== 'win32':
1272 # windows cannot rename a file on top of an existing one, so
1273 # fall back to delete-first. There are ways this can fail and
1274 # lose the builder's history, so we avoid using it in the
1275 # general (non-windows) case
1276 if os
.path
.exists(filename
):
1278 os
.rename(tmpfilename
, filename
)
1280 log
.msg("unable to save build %s-#%d" % (self
.builder
.name
,
1286 class BuilderStatus(styles
.Versioned
):
1287 """I handle status information for a single process.base.Builder object.
1288 That object sends status changes to me (frequently as Events), and I
1289 provide them on demand to the various status recipients, like the HTML
1290 waterfall display and the live status clients. It also sends build
1291 summaries to me, which I log and provide to status clients who aren't
1292 interested in seeing details of the individual build steps.
1294 I am responsible for maintaining the list of historic Events and Builds,
1295 pruning old ones, and loading them from / saving them to disk.
1297 I live in the buildbot.process.base.Builder object, in the .statusbag
1300 @type category: string
1301 @ivar category: user-defined category this builder belongs to; can be
1302 used to filter on in status clients
1306 implements(interfaces
.IBuilderStatus
)
1308 __implements__
= interfaces
.IBuilderStatus
,
1309 persistenceVersion
= 1
1311 # these limit the amount of memory we consume, as well as the size of the
1312 # main Builder pickle. The Build and LogFile pickles on disk must be
1313 # handled separately.
1315 buildHorizon
= 100 # forget builds beyond this
1316 stepHorizon
= 50 # forget steps in builds beyond this
1319 currentBigState
= "offline" # or idle/waiting/interlocked/building
1320 basedir
= None # filled in by our parent
1322 def __init__(self
, buildername
, category
=None):
1323 self
.name
= buildername
1324 self
.category
= category
1326 self
.slavenames
= []
1328 # these three hold Events, and are used to retrieve the current
1329 # state of the boxes.
1330 self
.lastBuildStatus
= None
1331 #self.currentBig = None
1332 #self.currentSmall = None
1333 self
.currentBuilds
= []
1334 self
.pendingBuilds
= []
1335 self
.nextBuild
= None
1337 self
.buildCache
= [] # TODO: age builds out of the cache
1341 def __getstate__(self
):
1342 # when saving, don't record transient stuff like what builds are
1343 # currently running, because they won't be there when we start back
1344 # up. Nor do we save self.watchers, nor anything that gets set by our
1345 # parent like .basedir and .status
1346 d
= styles
.Versioned
.__getstate
__(self
)
1349 for b
in self
.currentBuilds
:
1351 # TODO: push a 'hey, build was interrupted' event
1352 del d
['currentBuilds']
1353 del d
['pendingBuilds']
1354 del d
['currentBigState']
1357 del d
['nextBuildNumber']
1360 def __setstate__(self
, d
):
1361 # when loading, re-initialize the transient stuff. Remember that
1362 # upgradeToVersion1 and such will be called after this finishes.
1363 styles
.Versioned
.__setstate
__(self
, d
)
1364 self
.buildCache
= []
1365 self
.currentBuilds
= []
1366 self
.pendingBuilds
= []
1368 self
.slavenames
= []
1369 # self.basedir must be filled in by our parent
1370 # self.status must be filled in by our parent
1372 def upgradeToVersion1(self
):
1373 if hasattr(self
, 'slavename'):
1374 self
.slavenames
= [self
.slavename
]
1376 if hasattr(self
, 'nextBuildNumber'):
1377 del self
.nextBuildNumber
# determineNextBuildNumber chooses this
1379 def determineNextBuildNumber(self
):
1380 """Scan our directory of saved BuildStatus instances to determine
1381 what our self.nextBuildNumber should be. Set it one larger than the
1382 highest-numbered build we discover. This is called by the top-level
1383 Status object shortly after we are created or loaded from disk.
1385 existing_builds
= [int(f
)
1386 for f
in os
.listdir(self
.basedir
)
1387 if re
.match("^\d+$", f
)]
1389 self
.nextBuildNumber
= max(existing_builds
) + 1
1391 self
.nextBuildNumber
= 0
1393 def saveYourself(self
):
1394 for b
in self
.buildCache
:
1395 if not b
.isFinished
:
1396 # interrupted build, need to save it anyway.
1397 # BuildStatus.saveYourself will mark it as interrupted.
1399 filename
= os
.path
.join(self
.basedir
, "builder")
1400 tmpfilename
= filename
+ ".tmp"
1402 pickle
.dump(self
, open(tmpfilename
, "wb"), -1)
1403 if sys
.platform
== 'win32':
1404 # windows cannot rename a file on top of an existing one
1405 if os
.path
.exists(filename
):
1407 os
.rename(tmpfilename
, filename
)
1409 log
.msg("unable to save builder %s" % self
.name
)
1413 # build cache management
1415 def addBuildToCache(self
, build
):
1416 if build
in self
.buildCache
:
1418 self
.buildCache
.append(build
)
1419 while len(self
.buildCache
) > self
.buildCacheSize
:
1420 self
.buildCache
.pop(0)
1422 def getBuildByNumber(self
, number
):
1423 for b
in self
.currentBuilds
:
1424 if b
.number
== number
:
1426 for build
in self
.buildCache
:
1427 if build
.number
== number
:
1429 filename
= os
.path
.join(self
.basedir
, "%d" % number
)
1431 build
= pickle
.load(open(filename
, "rb"))
1433 build
.builder
= self
1434 # handle LogFiles from after 0.5.0 and before 0.6.5
1435 build
.upgradeLogfiles()
1436 self
.addBuildToCache(build
)
1439 raise IndexError("no such build %d" % number
)
1441 raise IndexError("corrupted build pickle %d" % number
)
1444 return # TODO: change this to walk through the filesystem
1445 # first, blow away all builds beyond our build horizon
1446 self
.builds
= self
.builds
[-self
.buildHorizon
:]
1447 # then prune steps in builds past the step horizon
1448 for b
in self
.builds
[0:-self
.stepHorizon
]:
1451 # IBuilderStatus methods
1456 return (self
.currentBigState
, self
.currentBuilds
)
1458 def getSlaves(self
):
1459 return [self
.status
.getSlave(name
) for name
in self
.slavenames
]
1461 def getPendingBuilds(self
):
1462 return self
.pendingBuilds
1464 def getCurrentBuilds(self
):
1465 return self
.currentBuilds
1467 def getLastFinishedBuild(self
):
1468 b
= self
.getBuild(-1)
1469 if not (b
and b
.isFinished()):
1470 b
= self
.getBuild(-2)
1473 def getBuild(self
, number
):
1475 number
= self
.nextBuildNumber
+ number
1476 if number
< 0 or number
>= self
.nextBuildNumber
:
1480 return self
.getBuildByNumber(number
)
1484 def getEvent(self
, number
):
1486 return self
.events
[number
]
1490 def eventGenerator(self
):
1491 """This function creates a generator which will provide all of this
1492 Builder's status events, starting with the most recent and
1493 progressing backwards in time. """
1495 # remember the oldest-to-earliest flow here. "next" means earlier.
1497 # TODO: interleave build steps and self.events by timestamp
1500 e
= self
.getEvent(eventIndex
)
1501 for Nb
in range(1, self
.nextBuildNumber
+1):
1502 b
= self
.getBuild(-Nb
)
1505 steps
= b
.getSteps()
1506 for Ns
in range(1, len(steps
)+1):
1507 if steps
[-Ns
].started
:
1508 step_start
= steps
[-Ns
].getTimes()[0]
1509 while e
is not None and e
.getTimes()[0] > step_start
:
1512 e
= self
.getEvent(eventIndex
)
1515 while e
is not None:
1518 e
= self
.getEvent(eventIndex
)
1520 def subscribe(self
, receiver
):
1521 # will get builderChangedState, buildStarted, and buildFinished
1522 self
.watchers
.append(receiver
)
1523 self
.publishState(receiver
)
1525 def unsubscribe(self
, receiver
):
1526 self
.watchers
.remove(receiver
)
1528 ## Builder interface (methods called by the Builder which feeds us)
1530 def setSlavenames(self
, names
):
1531 self
.slavenames
= names
1533 def addEvent(self
, text
=[], color
=None):
1534 # this adds a duration event. When it is done, the user should call
1535 # e.finish(). They can also mangle it by modifying .text and .color
1537 e
.started
= util
.now()
1540 self
.events
.append(e
)
1541 return e
# they are free to mangle it further
1543 def addPointEvent(self
, text
=[], color
=None):
1544 # this adds a point event, one which occurs as a single atomic
1547 e
.started
= util
.now()
1551 self
.events
.append(e
)
1552 return e
# for consistency, but they really shouldn't touch it
1554 def setBigState(self
, state
):
1555 needToUpdate
= state
!= self
.currentBigState
1556 self
.currentBigState
= state
1560 def publishState(self
, target
=None):
1561 state
= self
.currentBigState
1563 if target
is not None:
1565 target
.builderChangedState(self
.name
, state
)
1567 for w
in self
.watchers
:
1568 w
.builderChangedState(self
.name
, state
)
1571 """The Builder has decided to start a build, but the Build object is
1572 not yet ready to report status (it has not finished creating the
1573 Steps). Create a BuildStatus object that it can use."""
1574 number
= self
.nextBuildNumber
1575 self
.nextBuildNumber
+= 1
1576 # TODO: self.saveYourself(), to make sure we don't forget about the
1577 # build number we've just allocated. This is not quite as important
1578 # as it was before we switch to determineNextBuildNumber, but I think
1579 # it may still be useful to have the new build save itself.
1580 s
= BuildStatus(self
, number
)
1581 s
.waitUntilFinished().addCallback(self
._buildFinished
)
1584 def addBuildRequest(self
, brstatus
):
1585 self
.pendingBuilds
.append(brstatus
)
1586 def removeBuildRequest(self
, brstatus
):
1587 self
.pendingBuilds
.remove(brstatus
)
1589 # buildStarted is called by our child BuildStatus instances
1590 def buildStarted(self
, s
):
1591 """Now the BuildStatus object is ready to go (it knows all of its
1592 Steps, its ETA, etc), so it is safe to notify our watchers."""
1594 assert s
.builder
is self
# paranoia
1595 assert s
.number
== self
.nextBuildNumber
- 1
1596 assert s
not in self
.currentBuilds
1597 self
.currentBuilds
.append(s
)
1598 self
.addBuildToCache(s
)
1600 # now that the BuildStatus is prepared to answer queries, we can
1601 # announce the new build to all our watchers
1603 for w
in self
.watchers
: # TODO: maybe do this later? callLater(0)?
1604 receiver
= w
.buildStarted(self
.getName(), s
)
1606 if type(receiver
) == type(()):
1607 s
.subscribe(receiver
[0], receiver
[1])
1609 s
.subscribe(receiver
)
1610 d
= s
.waitUntilFinished()
1611 d
.addCallback(lambda s
: s
.unsubscribe(receiver
))
1614 def _buildFinished(self
, s
):
1615 assert s
in self
.currentBuilds
1617 self
.currentBuilds
.remove(s
)
1619 name
= self
.getName()
1620 results
= s
.getResults()
1621 for w
in self
.watchers
:
1622 w
.buildFinished(name
, s
, results
)
1624 self
.prune() # conserve disk
1627 # waterfall display (history)
1629 # I want some kind of build event that holds everything about the build:
1630 # why, what changes went into it, the results of the build, itemized
1631 # test results, etc. But, I do kind of need something to be inserted in
1632 # the event log first, because intermixing step events and the larger
1633 # build event is fraught with peril. Maybe an Event-like-thing that
1634 # doesn't have a file in it but does have links. Hmm, that's exactly
1635 # what it does now. The only difference would be that this event isn't
1636 # pushed to the clients.
1638 # publish to clients
1639 def sendLastBuildStatus(self
, client
):
1640 #client.newLastBuildStatus(self.lastBuildStatus)
1642 def sendCurrentActivityBigToEveryone(self
):
1643 for s
in self
.subscribers
:
1644 self
.sendCurrentActivityBig(s
)
1645 def sendCurrentActivityBig(self
, client
):
1646 state
= self
.currentBigState
1647 if state
== "offline":
1648 client
.currentlyOffline()
1649 elif state
== "idle":
1650 client
.currentlyIdle()
1651 elif state
== "building":
1652 client
.currentlyBuilding()
1654 log
.msg("Hey, self.currentBigState is weird:", state
)
1657 ## HTML display interface
1659 def getEventNumbered(self
, num
):
1660 # deal with dropped events, pruned events
1661 first
= self
.events
[0].number
1662 if first
+ len(self
.events
)-1 != self
.events
[-1].number
:
1664 "lost an event somewhere: [0] is %d, [%d] is %d" % \
1665 (self
.events
[0].number
,
1666 len(self
.events
) - 1,
1667 self
.events
[-1].number
))
1668 for e
in self
.events
:
1669 log
.msg("e[%d]: " % e
.number
, e
)
1671 offset
= num
- first
1672 log
.msg(self
, "offset", offset
)
1674 return self
.events
[offset
]
1678 ## Persistence of Status
1679 def loadYourOldEvents(self
):
1680 if hasattr(self
, "allEvents"):
1681 # first time, nothing to get from file. Note that this is only if
1682 # the Application gets .run() . If it gets .save()'ed, then the
1683 # .allEvents attribute goes away in the initial __getstate__ and
1684 # we try to load a non-existent file.
1686 self
.allEvents
= self
.loadFile("events", [])
1688 self
.nextEventNumber
= self
.allEvents
[-1].number
+ 1
1690 self
.nextEventNumber
= 0
1691 def saveYourOldEvents(self
):
1692 self
.saveFile("events", self
.allEvents
)
1696 def addClient(self
, client
):
1697 if client
not in self
.subscribers
:
1698 self
.subscribers
.append(client
)
1699 self
.sendLastBuildStatus(client
)
1700 self
.sendCurrentActivityBig(client
)
1701 client
.newEvent(self
.currentSmall
)
1702 def removeClient(self
, client
):
1703 if client
in self
.subscribers
:
1704 self
.subscribers
.remove(client
)
1708 implements(interfaces
.ISlaveStatus
)
1710 __implements__
= interfaces
.ISlaveStatus
,
1716 def __init__(self
, name
):
1725 def isConnected(self
):
1726 return self
.connected
1730 I represent the status of the buildmaster.
1733 implements(interfaces
.IStatus
)
1735 __implements__
= interfaces
.IStatus
,
1737 def __init__(self
, botmaster
, basedir
):
1739 @type botmaster: L{buildbot.master.BotMaster}
1740 @param botmaster: the Status object uses C{.botmaster} to get at
1741 both the L{buildbot.master.BuildMaster} (for
1742 various buildbot-wide parameters) and the
1743 actual Builders (to get at their L{BuilderStatus}
1744 objects). It is not allowed to change or influence
1745 anything through this reference.
1746 @type basedir: string
1747 @param basedir: this provides a base directory in which saved status
1748 information (changes.pck, saved Build status
1749 pickles) can be stored
1751 self
.botmaster
= botmaster
1752 self
.basedir
= basedir
1754 self
.activeBuildSets
= []
1755 assert os
.path
.isdir(basedir
)
1758 # methods called by our clients
1760 def getProjectName(self
):
1761 return self
.botmaster
.parent
.projectName
1762 def getProjectURL(self
):
1763 return self
.botmaster
.parent
.projectURL
1764 def getBuildbotURL(self
):
1765 return self
.botmaster
.parent
.buildbotURL
1767 def getURLForThing(self
, thing
):
1768 prefix
= self
.getBuildbotURL()
1771 if providedBy(thing
, interfaces
.IStatus
):
1773 if providedBy(thing
, interfaces
.ISchedulerStatus
):
1775 if providedBy(thing
, interfaces
.IBuilderStatus
):
1777 return prefix
+ urllib
.quote(builder
.getName(), safe
='')
1778 if providedBy(thing
, interfaces
.IBuildStatus
):
1780 builder
= build
.getBuilder()
1781 return "%s%s/builds/%d" % (
1783 urllib
.quote(builder
.getName(), safe
=''),
1785 if providedBy(thing
, interfaces
.IBuildStepStatus
):
1787 build
= step
.getBuild()
1788 builder
= build
.getBuilder()
1789 return "%s%s/builds/%d/%s" % (
1791 urllib
.quote(builder
.getName(), safe
=''),
1793 "step-" + urllib
.quote(step
.getName(), safe
=''))
1795 # IBuildRequestStatus
1799 if providedBy(thing
, interfaces
.IStatusEvent
):
1800 from buildbot
.changes
import changes
1801 # TODO: this is goofy, create IChange or something
1802 if isinstance(thing
, changes
.Change
):
1804 return "%schanges/%d" % (prefix
, change
.number
)
1806 if providedBy(thing
, interfaces
.IStatusLog
):
1808 step
= log
.getStep()
1809 build
= step
.getBuild()
1810 builder
= build
.getBuilder()
1812 logs
= step
.getLogs()
1813 for i
in range(len(logs
)):
1819 return "%s%s/builds/%d/%s/%d" % (
1821 urllib
.quote(builder
.getName(), safe
=''),
1823 "step-" + urllib
.quote(step
.getName(), safe
=''),
1827 def getSchedulers(self
):
1828 return self
.botmaster
.parent
.allSchedulers()
1830 def getBuilderNames(self
, categories
=None):
1831 if categories
== None:
1832 return self
.botmaster
.builderNames
[:] # don't let them break it
1835 # respect addition order
1836 for name
in self
.botmaster
.builderNames
:
1837 builder
= self
.botmaster
.builders
[name
]
1838 if builder
.builder_status
.category
in categories
:
1842 def getBuilder(self
, name
):
1844 @rtype: L{BuilderStatus}
1846 return self
.botmaster
.builders
[name
].builder_status
1848 def getSlave(self
, slavename
):
1849 return self
.botmaster
.slaves
[slavename
].slave_status
1851 def getBuildSets(self
):
1852 return self
.activeBuildSets
[:]
1854 def subscribe(self
, target
):
1855 self
.watchers
.append(target
)
1856 for name
in self
.botmaster
.builderNames
:
1857 self
.announceNewBuilder(target
, name
, self
.getBuilder(name
))
1858 def unsubscribe(self
, target
):
1859 self
.watchers
.remove(target
)
1862 # methods called by upstream objects
1864 def announceNewBuilder(self
, target
, name
, builder_status
):
1865 t
= target
.builderAdded(name
, builder_status
)
1867 builder_status
.subscribe(t
)
1869 def builderAdded(self
, name
, basedir
, category
=None):
1871 @rtype: L{BuilderStatus}
1873 filename
= os
.path
.join(self
.basedir
, basedir
, "builder")
1874 log
.msg("trying to load status pickle from %s" % filename
)
1875 builder_status
= None
1877 builder_status
= pickle
.load(open(filename
, "rb"))
1880 log
.msg("no saved status pickle, creating a new one")
1882 log
.msg("error while loading status pickle, creating a new one")
1883 log
.msg("error follows:")
1885 if not builder_status
:
1886 builder_status
= BuilderStatus(name
, category
)
1887 builder_status
.addPointEvent(["builder", "created"])
1888 log
.msg("added builder %s in category %s" % (name
, category
))
1889 # an unpickled object might not have category set from before,
1890 # so set it here to make sure
1891 builder_status
.category
= category
1892 builder_status
.basedir
= os
.path
.join(self
.basedir
, basedir
)
1893 builder_status
.name
= name
# it might have been updated
1894 builder_status
.status
= self
1896 if not os
.path
.isdir(builder_status
.basedir
):
1897 os
.mkdir(builder_status
.basedir
)
1898 builder_status
.determineNextBuildNumber()
1900 builder_status
.setBigState("offline")
1902 for t
in self
.watchers
:
1903 self
.announceNewBuilder(t
, name
, builder_status
)
1905 return builder_status
1907 def builderRemoved(self
, name
):
1908 for t
in self
.watchers
:
1909 t
.builderRemoved(name
)
1912 for b
in self
.botmaster
.builders
.values():
1913 b
.builder_status
.prune()
1915 def buildsetSubmitted(self
, bss
):
1916 self
.activeBuildSets
.append(bss
)
1917 bss
.waitUntilFinished().addCallback(self
.activeBuildSets
.remove
)
1918 for t
in self
.watchers
:
1919 t
.buildsetSubmitted(bss
)