1 # -*- test-case-name: buildbot.test.test_status -*-
3 from zope
.interface
import implements
4 from twisted
.python
import log
5 from twisted
.persisted
import styles
6 from twisted
.internet
import reactor
, defer
, threads
7 from twisted
.protocols
import basic
8 from buildbot
.process
.properties
import Properties
11 import os
, shutil
, sys
, re
, urllib
, itertools
12 from cPickle
import load
, dump
13 from cStringIO
import StringIO
14 from bz2
import BZ2File
17 from buildbot
import interfaces
, util
, sourcestamp
19 SUCCESS
, WARNINGS
, FAILURE
, SKIPPED
, EXCEPTION
= range(5)
20 Results
= ["success", "warnings", "failure", "skipped", "exception"]
23 # build processes call the following methods:
29 # currentlyInterlocked
34 # updateCurrentActivity
35 # addFileToCurrentActivity
36 # finishCurrentActivity
41 STDOUT
= interfaces
.LOG_CHANNEL_STDOUT
42 STDERR
= interfaces
.LOG_CHANNEL_STDERR
43 HEADER
= interfaces
.LOG_CHANNEL_HEADER
44 ChunkTypes
= ["stdout", "stderr", "header"]
46 class LogFileScanner(basic
.NetstringReceiver
):
47 def __init__(self
, chunk_cb
, channels
=[]):
48 self
.chunk_cb
= chunk_cb
49 self
.channels
= channels
51 def stringReceived(self
, line
):
52 channel
= int(line
[0])
53 if not self
.channels
or (channel
in self
.channels
):
54 self
.chunk_cb((channel
, line
[1:]))
56 class LogFileProducer
:
59 the LogFile has just one FD, used for both reading and writing.
60 Each time you add an entry, fd.seek to the end and then write.
62 Each reader (i.e. Producer) keeps track of their own offset. The reader
63 starts by seeking to the start of the logfile, and reading forwards.
64 Between each hunk of file they yield chunks, so they must remember their
65 offset before yielding and re-seek back to that offset before reading
66 more data. When their read() returns EOF, they're finished with the first
67 phase of the reading (everything that's already been written to disk).
69 After EOF, the remaining data is entirely in the current entries list.
70 These entries are all of the same channel, so we can do one "".join and
71 obtain a single chunk to be sent to the listener. But since that involves
72 a yield, and more data might arrive after we give up control, we have to
73 subscribe them before yielding. We can't subscribe them any earlier,
74 otherwise they'd get data out of order.
76 We're using a generator in the first place so that the listener can
77 throttle us, which means they're pulling. But the subscription means
78 we're pushing. Really we're a Producer. In the first phase we can be
79 either a PullProducer or a PushProducer. In the second phase we're only a
82 So the client gives a LogFileConsumer to File.subscribeConsumer . This
83 Consumer must have registerProducer(), unregisterProducer(), and
84 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
85 except that writeChunk() takes chunks (tuples of (channel,text)) instead
86 of the normal write() which takes just text. The LogFileConsumer is
87 allowed to call stopProducing, pauseProducing, and resumeProducing on the
88 producer instance it is given. """
94 def __init__(self
, logfile
, consumer
):
95 self
.logfile
= logfile
96 self
.consumer
= consumer
97 self
.chunkGenerator
= self
.getChunks()
98 consumer
.registerProducer(self
, True)
101 f
= self
.logfile
.getFile()
104 p
= LogFileScanner(chunks
.append
)
106 data
= f
.read(self
.BUFFERSIZE
)
114 data
= f
.read(self
.BUFFERSIZE
)
118 # now subscribe them to receive new entries
119 self
.subscribed
= True
120 self
.logfile
.watchers
.append(self
)
121 d
= self
.logfile
.waitUntilFinished()
123 # then give them the not-yet-merged data
124 if self
.logfile
.runEntries
:
125 channel
= self
.logfile
.runEntries
[0][0]
126 text
= "".join([c
[1] for c
in self
.logfile
.runEntries
])
127 yield (channel
, text
)
129 # now we've caught up to the present. Anything further will come from
130 # the logfile subscription. We add the callback *after* yielding the
131 # data from runEntries, because the logfile might have finished
133 d
.addCallback(self
.logfileFinished
)
135 def stopProducing(self
):
136 # TODO: should we still call consumer.finish? probably not.
142 if self
.chunkGenerator
:
143 self
.chunkGenerator
= None # stop making chunks
145 self
.logfile
.watchers
.remove(self
)
146 self
.subscribed
= False
148 def pauseProducing(self
):
151 def resumeProducing(self
):
152 # Twisted-1.3.0 has a bug which causes hangs when resumeProducing
153 # calls transport.write (there is a recursive loop, fixed in 2.0 in
154 # t.i.abstract.FileDescriptor.doWrite by setting the producerPaused
155 # flag *before* calling resumeProducing). To work around this, we
156 # just put off the real resumeProducing for a moment. This probably
157 # has a performance hit, but I'm going to assume that the log files
158 # are not retrieved frequently enough for it to be an issue.
160 reactor
.callLater(0, self
._resumeProducing
)
162 def _resumeProducing(self
):
164 if not self
.chunkGenerator
:
167 while not self
.paused
:
168 chunk
= self
.chunkGenerator
.next()
169 self
.consumer
.writeChunk(chunk
)
170 # we exit this when the consumer says to stop, or we run out
172 except StopIteration:
173 # if the generator finished, it will have done releaseFile
174 self
.chunkGenerator
= None
175 # now everything goes through the subscription, and they don't get to
178 def logChunk(self
, build
, step
, logfile
, channel
, chunk
):
180 self
.consumer
.writeChunk((channel
, chunk
))
182 def logfileFinished(self
, logfile
):
185 self
.consumer
.unregisterProducer()
186 self
.consumer
.finish()
189 def _tryremove(filename
, timeout
, retries
):
190 """Try to remove a file, and if failed, try again in timeout.
191 Increases the timeout by a factor of 4, and only keeps trying for
192 another retries-amount of times.
199 reactor
.callLater(timeout
, _tryremove
, filename
, timeout
* 4,
202 log
.msg("giving up on removing %s after over %d seconds" %
206 """A LogFile keeps all of its contents on disk, in a non-pickle format to
207 which new entries can easily be appended. The file on disk has a name
208 like 12-log-compile-output, under the Builder's directory. The actual
209 filename is generated (before the LogFile is created) by
210 L{BuildStatus.generateLogfileName}.
212 Old LogFile pickles (which kept their contents in .entries) must be
213 upgraded. The L{BuilderStatus} is responsible for doing this, when it
214 loads the L{BuildStatus} into memory. The Build pickle is not modified,
215 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
218 implements(interfaces
.IStatusLog
, interfaces
.ILogFile
)
224 runEntries
= [] # provided so old pickled builds will getChunks() ok
227 filename
= None # relative to the Builder's basedir
230 def __init__(self
, parent
, name
, logfilename
):
232 @type parent: L{BuildStepStatus}
233 @param parent: the Step that this log is a part of
235 @param name: the name of this log, typically 'output'
236 @type logfilename: string
237 @param logfilename: the Builder-relative pathname for the saved entries
241 self
.filename
= logfilename
242 fn
= self
.getFilename()
243 if os
.path
.exists(fn
):
244 # the buildmaster was probably stopped abruptly, before the
245 # BuilderStatus could be saved, so BuilderStatus.nextBuildNumber
246 # is out of date, and we're overlapping with earlier builds now.
247 # Warn about it, but then overwrite the old pickle file
248 log
.msg("Warning: Overwriting old serialized Build at %s" % fn
)
249 self
.openfile
= open(fn
, "w+")
252 self
.finishedWatchers
= []
254 def getFilename(self
):
255 return os
.path
.join(self
.step
.build
.builder
.basedir
, self
.filename
)
257 def hasContents(self
):
258 return os
.path
.exists(self
.getFilename() + '.bz2') or \
259 os
.path
.exists(self
.getFilename())
267 def isFinished(self
):
269 def waitUntilFinished(self
):
271 d
= defer
.succeed(self
)
274 self
.finishedWatchers
.append(d
)
279 # this is the filehandle we're using to write to the log, so
282 # otherwise they get their own read-only handle
283 # try a compressed log first
285 return BZ2File(self
.getFilename() + ".bz2", "r")
288 return open(self
.getFilename(), "r")
291 # this produces one ginormous string
292 return "".join(self
.getChunks([STDOUT
, STDERR
], onlyText
=True))
294 def getTextWithHeaders(self
):
295 return "".join(self
.getChunks(onlyText
=True))
297 def getChunks(self
, channels
=[], onlyText
=False):
298 # generate chunks for everything that was logged at the time we were
299 # first called, so remember how long the file was when we started.
300 # Don't read beyond that point. The current contents of
301 # self.runEntries will follow.
303 # this returns an iterator, which means arbitrary things could happen
304 # while we're yielding. This will faithfully deliver the log as it
305 # existed when it was started, and not return anything after that
306 # point. To use this in subscribe(catchup=True) without missing any
307 # data, you must insure that nothing will be added to the log during
316 if self
.runEntries
and (not channels
or
317 (self
.runEntries
[0][0] in channels
)):
318 leftover
= (self
.runEntries
[0][0],
319 "".join([c
[1] for c
in self
.runEntries
]))
321 # freeze the state of the LogFile by passing a lot of parameters into
323 return self
._generateChunks
(f
, offset
, remaining
, leftover
,
326 def _generateChunks(self
, f
, offset
, remaining
, leftover
,
329 p
= LogFileScanner(chunks
.append
, channels
)
331 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
332 remaining
-= len(data
)
337 channel
, text
= chunks
.pop(0)
341 yield (channel
, text
)
343 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
344 remaining
-= len(data
)
354 def readlines(self
, channel
=STDOUT
):
355 """Return an iterator that produces newline-terminated lines,
356 excluding header chunks."""
357 # TODO: make this memory-efficient, by turning it into a generator
358 # that retrieves chunks as necessary, like a pull-driven version of
359 # twisted.protocols.basic.LineReceiver
360 alltext
= "".join(self
.getChunks([channel
], onlyText
=True))
361 io
= StringIO(alltext
)
362 return io
.readlines()
364 def subscribe(self
, receiver
, catchup
):
367 self
.watchers
.append(receiver
)
369 for channel
, text
in self
.getChunks():
370 # TODO: add logChunks(), to send over everything at once?
371 receiver
.logChunk(self
.step
.build
, self
.step
, self
,
374 def unsubscribe(self
, receiver
):
375 if receiver
in self
.watchers
:
376 self
.watchers
.remove(receiver
)
378 def subscribeConsumer(self
, consumer
):
379 p
= LogFileProducer(self
, consumer
)
382 # interface used by the build steps to add things to the log
385 # merge all .runEntries (which are all of the same type) into a
386 # single chunk for .entries
387 if not self
.runEntries
:
389 channel
= self
.runEntries
[0][0]
390 text
= "".join([c
[1] for c
in self
.runEntries
])
395 while offset
< len(text
):
396 size
= min(len(text
)-offset
, self
.chunkSize
)
397 f
.write("%d:%d" % (1 + size
, channel
))
398 f
.write(text
[offset
:offset
+size
])
404 def addEntry(self
, channel
, text
):
405 assert not self
.finished
406 # we only add to .runEntries here. merge() is responsible for adding
407 # merged chunks to .entries
408 if self
.runEntries
and channel
!= self
.runEntries
[0][0]:
410 self
.runEntries
.append((channel
, text
))
411 self
.runLength
+= len(text
)
412 if self
.runLength
>= self
.chunkSize
:
415 for w
in self
.watchers
:
416 w
.logChunk(self
.step
.build
, self
.step
, self
, channel
, text
)
417 self
.length
+= len(text
)
419 def addStdout(self
, text
):
420 self
.addEntry(STDOUT
, text
)
421 def addStderr(self
, text
):
422 self
.addEntry(STDERR
, text
)
423 def addHeader(self
, text
):
424 self
.addEntry(HEADER
, text
)
429 # we don't do an explicit close, because there might be readers
430 # shareing the filehandle. As soon as they stop reading, the
431 # filehandle will be released and automatically closed. We will
432 # do a sync, however, to make sure the log gets saved in case of
434 self
.openfile
.flush()
435 os
.fsync(self
.openfile
.fileno())
438 watchers
= self
.finishedWatchers
439 self
.finishedWatchers
= []
445 def compressLog(self
):
446 compressed
= self
.getFilename() + ".bz2.tmp"
447 d
= threads
.deferToThread(self
._compressLog
, compressed
)
448 d
.addCallback(self
._renameCompressedLog
, compressed
)
449 d
.addErrback(self
._cleanupFailedCompress
, compressed
)
452 def _compressLog(self
, compressed
):
453 infile
= self
.getFile()
454 cf
= BZ2File(compressed
, 'w')
457 buf
= infile
.read(bufsize
)
459 if len(buf
) < bufsize
:
462 def _renameCompressedLog(self
, rv
, compressed
):
463 filename
= self
.getFilename() + '.bz2'
464 if sys
.platform
== 'win32':
465 # windows cannot rename a file on top of an existing one, so
466 # fall back to delete-first. There are ways this can fail and
467 # lose the builder's history, so we avoid using it in the
468 # general (non-windows) case
469 if os
.path
.exists(filename
):
471 os
.rename(compressed
, filename
)
472 _tryremove(self
.getFilename(), 1, 5)
473 def _cleanupFailedCompress(self
, failure
, compressed
):
474 log
.msg("failed to compress %s" % self
.getFilename())
475 if os
.path
.exists(compressed
):
476 _tryremove(compressed
, 1, 5)
477 failure
.trap() # reraise the failure
480 def __getstate__(self
):
481 d
= self
.__dict
__.copy()
482 del d
['step'] # filled in upon unpickling
484 del d
['finishedWatchers']
485 d
['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
486 if d
.has_key('finished'):
488 if d
.has_key('openfile'):
492 def __setstate__(self
, d
):
494 self
.watchers
= [] # probably not necessary
495 self
.finishedWatchers
= [] # same
496 # self.step must be filled in by our parent
499 def upgrade(self
, logfilename
):
500 """Save our .entries to a new-style offline log file (if necessary),
501 and modify our in-memory representation to use it. The original
502 pickled LogFile (inside the pickled Build) won't be modified."""
503 self
.filename
= logfilename
504 if not os
.path
.exists(self
.getFilename()):
505 self
.openfile
= open(self
.getFilename(), "w")
506 self
.finished
= False
507 for channel
,text
in self
.entries
:
508 self
.addEntry(channel
, text
)
509 self
.finish() # releases self.openfile, which will be closed
513 implements(interfaces
.IStatusLog
)
517 def __init__(self
, parent
, name
, logfilename
, html
):
520 self
.filename
= logfilename
524 return self
.name
# set in BuildStepStatus.addLog
528 def isFinished(self
):
530 def waitUntilFinished(self
):
531 return defer
.succeed(self
)
533 def hasContents(self
):
536 return self
.html
# looks kinda like text
537 def getTextWithHeaders(self
):
540 return [(STDERR
, self
.html
)]
542 def subscribe(self
, receiver
, catchup
):
544 def unsubscribe(self
, receiver
):
550 def __getstate__(self
):
551 d
= self
.__dict
__.copy()
555 def upgrade(self
, logfilename
):
560 implements(interfaces
.IStatusEvent
)
566 # IStatusEvent methods
568 return (self
.started
, self
.finished
)
575 self
.finished
= util
.now()
578 implements(interfaces
.ITestResult
)
580 def __init__(self
, name
, results
, text
, logs
):
581 assert isinstance(name
, tuple)
583 self
.results
= results
590 def getResults(self
):
600 class BuildSetStatus
:
601 implements(interfaces
.IBuildSetStatus
)
603 def __init__(self
, source
, reason
, builderNames
, bsid
=None):
606 self
.builderNames
= builderNames
608 self
.successWatchers
= []
609 self
.finishedWatchers
= []
610 self
.stillHopeful
= True
611 self
.finished
= False
613 def setBuildRequestStatuses(self
, buildRequestStatuses
):
614 self
.buildRequests
= buildRequestStatuses
615 def setResults(self
, results
):
616 # the build set succeeds only if all its component builds succeed
617 self
.results
= results
618 def giveUpHope(self
):
619 self
.stillHopeful
= False
622 def notifySuccessWatchers(self
):
623 for d
in self
.successWatchers
:
625 self
.successWatchers
= []
627 def notifyFinishedWatchers(self
):
629 for d
in self
.finishedWatchers
:
631 self
.finishedWatchers
= []
633 # methods for our clients
635 def getSourceStamp(self
):
639 def getResults(self
):
644 def getBuilderNames(self
):
645 return self
.builderNames
646 def getBuildRequests(self
):
647 return self
.buildRequests
648 def isFinished(self
):
651 def waitUntilSuccess(self
):
652 if self
.finished
or not self
.stillHopeful
:
653 # the deferreds have already fired
654 return defer
.succeed(self
)
656 self
.successWatchers
.append(d
)
659 def waitUntilFinished(self
):
661 return defer
.succeed(self
)
663 self
.finishedWatchers
.append(d
)
666 class BuildRequestStatus
:
667 implements(interfaces
.IBuildRequestStatus
)
669 def __init__(self
, source
, builderName
):
671 self
.builderName
= builderName
672 self
.builds
= [] # list of BuildStatus objects
674 self
.submittedAt
= None
676 def buildStarted(self
, build
):
677 self
.builds
.append(build
)
678 for o
in self
.observers
[:]:
681 # methods called by our clients
682 def getSourceStamp(self
):
684 def getBuilderName(self
):
685 return self
.builderName
689 def subscribe(self
, observer
):
690 self
.observers
.append(observer
)
691 for b
in self
.builds
:
693 def unsubscribe(self
, observer
):
694 self
.observers
.remove(observer
)
696 def getSubmitTime(self
):
697 return self
.submittedAt
698 def setSubmitTime(self
, t
):
702 class BuildStepStatus(styles
.Versioned
):
704 I represent a collection of output status for a
705 L{buildbot.process.step.BuildStep}.
707 Statistics contain any information gleaned from a step that is
708 not in the form of a logfile. As an example, steps that run
709 tests might gather statistics about the number of passed, failed,
712 @type progress: L{buildbot.status.progress.StepProgress}
713 @cvar progress: tracks ETA for the step
714 @type text: list of strings
715 @cvar text: list of short texts that describe the command and its status
716 @type text2: list of strings
717 @cvar text2: list of short texts added to the overall build description
718 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
719 @ivar logs: logs of steps
720 @type statistics: dict
721 @ivar statistics: results from running this step
723 # note that these are created when the Build is set up, before each
724 # corresponding BuildStep has started.
725 implements(interfaces
.IBuildStepStatus
, interfaces
.IStatusEvent
)
726 persistenceVersion
= 2
736 finishedWatchers
= []
739 def __init__(self
, parent
):
740 assert interfaces
.IBuildStatus(parent
)
746 self
.finishedWatchers
= []
750 """Returns a short string with the name of this step. This string
751 may have spaces in it."""
758 return (self
.started
, self
.finished
)
760 def getExpectations(self
):
761 """Returns a list of tuples (name, current, target)."""
762 if not self
.progress
:
765 metrics
= self
.progress
.progress
.keys()
768 t
= (m
, self
.progress
.progress
[m
], self
.progress
.expectations
[m
])
776 return self
.urls
.copy()
778 def isFinished(self
):
779 return (self
.finished
is not None)
781 def waitUntilFinished(self
):
783 d
= defer
.succeed(self
)
786 self
.finishedWatchers
.append(d
)
789 # while the step is running, the following methods make sense.
790 # Afterwards they return None
793 if self
.started
is None:
794 return None # not started yet
795 if self
.finished
is not None:
796 return None # already finished
797 if not self
.progress
:
798 return None # no way to predict
799 return self
.progress
.remaining()
801 # Once you know the step has finished, the following methods are legal.
802 # Before this step has finished, they all return None.
805 """Returns a list of strings which describe the step. These are
806 intended to be displayed in a narrow column. If more space is
807 available, the caller should join them together with spaces before
808 presenting them to the user."""
811 def getResults(self
):
812 """Return a tuple describing the results of the step.
813 'result' is one of the constants in L{buildbot.status.builder}:
814 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
815 'strings' is an optional list of strings that the step wants to
816 append to the overall build's results. These strings are usually
817 more terse than the ones returned by getText(): in particular,
818 successful Steps do not usually contribute any text to the
821 @rtype: tuple of int, list of strings
822 @returns: (result, strings)
824 return (self
.results
, self
.text2
)
826 def hasStatistic(self
, name
):
827 """Return true if this step has a value for the given statistic.
829 return self
.statistics
.has_key(name
)
831 def getStatistic(self
, name
, default
=None):
832 """Return the given statistic, if present
834 return self
.statistics
.get(name
, default
)
836 # subscription interface
838 def subscribe(self
, receiver
, updateInterval
=10):
839 # will get logStarted, logFinished, stepETAUpdate
840 assert receiver
not in self
.watchers
841 self
.watchers
.append(receiver
)
842 self
.sendETAUpdate(receiver
, updateInterval
)
844 def sendETAUpdate(self
, receiver
, updateInterval
):
845 self
.updates
[receiver
] = None
846 # they might unsubscribe during stepETAUpdate
847 receiver
.stepETAUpdate(self
.build
, self
,
848 self
.getETA(), self
.getExpectations())
849 if receiver
in self
.watchers
:
850 self
.updates
[receiver
] = reactor
.callLater(updateInterval
,
855 def unsubscribe(self
, receiver
):
856 if receiver
in self
.watchers
:
857 self
.watchers
.remove(receiver
)
858 if receiver
in self
.updates
:
859 if self
.updates
[receiver
] is not None:
860 self
.updates
[receiver
].cancel()
861 del self
.updates
[receiver
]
864 # methods to be invoked by the BuildStep
866 def setName(self
, stepname
):
869 def setColor(self
, color
):
870 log
.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color
,))
872 def setProgress(self
, stepprogress
):
873 self
.progress
= stepprogress
875 def stepStarted(self
):
876 self
.started
= util
.now()
878 self
.build
.stepStarted(self
)
880 def addLog(self
, name
):
881 assert self
.started
# addLog before stepStarted won't notify watchers
882 logfilename
= self
.build
.generateLogfileName(self
.name
, name
)
883 log
= LogFile(self
, name
, logfilename
)
884 self
.logs
.append(log
)
885 for w
in self
.watchers
:
886 receiver
= w
.logStarted(self
.build
, self
, log
)
888 log
.subscribe(receiver
, True)
889 d
= log
.waitUntilFinished()
890 d
.addCallback(lambda log
: log
.unsubscribe(receiver
))
891 d
= log
.waitUntilFinished()
892 d
.addCallback(self
.logFinished
)
895 def addHTMLLog(self
, name
, html
):
896 assert self
.started
# addLog before stepStarted won't notify watchers
897 logfilename
= self
.build
.generateLogfileName(self
.name
, name
)
898 log
= HTMLLogFile(self
, name
, logfilename
, html
)
899 self
.logs
.append(log
)
900 for w
in self
.watchers
:
901 receiver
= w
.logStarted(self
.build
, self
, log
)
902 # TODO: think about this: there isn't much point in letting
905 # log.subscribe(receiver, True)
906 w
.logFinished(self
.build
, self
, log
)
908 def logFinished(self
, log
):
909 for w
in self
.watchers
:
910 w
.logFinished(self
.build
, self
, log
)
912 def addURL(self
, name
, url
):
913 self
.urls
[name
] = url
915 def setText(self
, text
):
917 for w
in self
.watchers
:
918 w
.stepTextChanged(self
.build
, self
, text
)
919 def setText2(self
, text
):
921 for w
in self
.watchers
:
922 w
.stepText2Changed(self
.build
, self
, text
)
924 def setStatistic(self
, name
, value
):
925 """Set the given statistic. Usually called by subclasses.
927 self
.statistics
[name
] = value
929 def stepFinished(self
, results
):
930 self
.finished
= util
.now()
931 self
.results
= results
932 cld
= [] # deferreds for log compression
933 logCompressionLimit
= self
.build
.builder
.logCompressionLimit
934 for loog
in self
.logs
:
935 if not loog
.isFinished():
937 # if log compression is on, and it's a real LogFile,
938 # HTMLLogFiles aren't files
939 if logCompressionLimit
is not False and \
940 isinstance(loog
, LogFile
):
941 if os
.path
.getsize(loog
.getFilename()) > logCompressionLimit
:
942 cld
.append(loog
.compressLog())
944 for r
in self
.updates
.keys():
945 if self
.updates
[r
] is not None:
946 self
.updates
[r
].cancel()
949 watchers
= self
.finishedWatchers
950 self
.finishedWatchers
= []
954 return defer
.DeferredList(cld
)
958 def __getstate__(self
):
959 d
= styles
.Versioned
.__getstate
__(self
)
960 del d
['build'] # filled in when loading
961 if d
.has_key('progress'):
964 del d
['finishedWatchers']
968 def __setstate__(self
, d
):
969 styles
.Versioned
.__setstate
__(self
, d
)
970 # self.build must be filled in by our parent
971 for loog
in self
.logs
:
974 def upgradeToVersion1(self
):
975 if not hasattr(self
, "urls"):
978 def upgradeToVersion2(self
):
979 if not hasattr(self
, "statistics"):
983 class BuildStatus(styles
.Versioned
):
984 implements(interfaces
.IBuildStatus
, interfaces
.IStatusEvent
)
985 persistenceVersion
= 3
1000 # these lists/dicts are defined here so that unserialized instances have
1001 # (empty) values. They are set in __init__ to new objects to make sure
1002 # each instance gets its own copy.
1005 finishedWatchers
= []
1008 def __init__(self
, parent
, number
):
1010 @type parent: L{BuilderStatus}
1013 assert interfaces
.IBuilderStatus(parent
)
1014 self
.builder
= parent
1015 self
.number
= number
1018 self
.finishedWatchers
= []
1020 self
.testResults
= {}
1021 self
.properties
= Properties()
1025 return "<%s #%s>" % (self
.__class
__.__name
__, self
.number
)
1029 def getBuilder(self
):
1031 @rtype: L{BuilderStatus}
1035 def getProperty(self
, propname
):
1036 return self
.properties
[propname
]
1038 def getProperties(self
):
1039 return self
.properties
1041 def getNumber(self
):
1044 def getPreviousBuild(self
):
1045 if self
.number
== 0:
1047 return self
.builder
.getBuild(self
.number
-1)
1049 def getSourceStamp(self
, absolute
=False):
1050 if not absolute
or not self
.properties
.has_key('got_revision'):
1052 return self
.source
.getAbsoluteSourceStamp(self
.properties
['got_revision'])
1054 def getReason(self
):
1057 def getChanges(self
):
1060 def getRequests(self
):
1061 return self
.requests
1063 def getResponsibleUsers(self
):
1064 return self
.blamelist
1066 def getInterestedUsers(self
):
1067 # TODO: the Builder should add others: sheriffs, domain-owners
1068 return self
.blamelist
+ self
.properties
.getProperty('owners', [])
1071 """Return a list of IBuildStepStatus objects. For invariant builds
1072 (those which always use the same set of Steps), this should be the
1073 complete list, however some of the steps may not have started yet
1074 (step.getTimes()[0] will be None). For variant builds, this may not
1075 be complete (asking again later may give you more of them)."""
1079 return (self
.started
, self
.finished
)
1081 _sentinel
= [] # used as a sentinel to indicate unspecified initial_value
1082 def getSummaryStatistic(self
, name
, summary_fn
, initial_value
=_sentinel
):
1083 """Summarize the named statistic over all steps in which it
1084 exists, using combination_fn and initial_value to combine multiple
1085 results into a single result. This translates to a call to Python's
1087 return reduce(summary_fn, step_stats_list, initial_value)
1090 st
.getStatistic(name
)
1091 for st
in self
.steps
1092 if st
.hasStatistic(name
) ]
1093 if initial_value
is self
._sentinel
:
1094 return reduce(summary_fn
, step_stats_list
)
1096 return reduce(summary_fn
, step_stats_list
, initial_value
)
1098 def isFinished(self
):
1099 return (self
.finished
is not None)
1101 def waitUntilFinished(self
):
1103 d
= defer
.succeed(self
)
1105 d
= defer
.Deferred()
1106 self
.finishedWatchers
.append(d
)
1109 # while the build is running, the following methods make sense.
1110 # Afterwards they return None
1113 if self
.finished
is not None:
1115 if not self
.progress
:
1117 eta
= self
.progress
.eta()
1120 return eta
- util
.now()
1122 def getCurrentStep(self
):
1123 return self
.currentStep
1125 # Once you know the build has finished, the following methods are legal.
1126 # Before ths build has finished, they all return None.
1130 text
.extend(self
.text
)
1131 for s
in self
.steps
:
1132 text
.extend(s
.text2
)
1135 def getResults(self
):
1138 def getSlavename(self
):
1139 return self
.slavename
1141 def getTestResults(self
):
1142 return self
.testResults
1145 # TODO: steps should contribute significant logs instead of this
1146 # hack, which returns every log from every step. The logs should get
1147 # names like "compile" and "test" instead of "compile.output"
1149 for s
in self
.steps
:
1150 for log
in s
.getLogs():
1154 # subscription interface
1156 def subscribe(self
, receiver
, updateInterval
=None):
1157 # will receive stepStarted and stepFinished messages
1158 # and maybe buildETAUpdate
1159 self
.watchers
.append(receiver
)
1160 if updateInterval
is not None:
1161 self
.sendETAUpdate(receiver
, updateInterval
)
1163 def sendETAUpdate(self
, receiver
, updateInterval
):
1164 self
.updates
[receiver
] = None
1167 receiver
.buildETAUpdate(self
, self
.getETA())
1168 # they might have unsubscribed during buildETAUpdate
1169 if receiver
in self
.watchers
:
1170 self
.updates
[receiver
] = reactor
.callLater(updateInterval
,
1175 def unsubscribe(self
, receiver
):
1176 if receiver
in self
.watchers
:
1177 self
.watchers
.remove(receiver
)
1178 if receiver
in self
.updates
:
1179 if self
.updates
[receiver
] is not None:
1180 self
.updates
[receiver
].cancel()
1181 del self
.updates
[receiver
]
1183 # methods for the base.Build to invoke
1185 def addStepWithName(self
, name
):
1186 """The Build is setting up, and has added a new BuildStep to its
1187 list. Create a BuildStepStatus object to which it can send status
1190 s
= BuildStepStatus(self
)
1192 self
.steps
.append(s
)
1195 def setProperty(self
, propname
, value
, source
):
1196 self
.properties
.setProperty(propname
, value
, source
)
1198 def addTestResult(self
, result
):
1199 self
.testResults
[result
.getName()] = result
1201 def setSourceStamp(self
, sourceStamp
):
1202 self
.source
= sourceStamp
1203 self
.changes
= self
.source
.changes
1205 def setRequests(self
, requests
):
1206 self
.requests
= requests
1208 def setReason(self
, reason
):
1209 self
.reason
= reason
1210 def setBlamelist(self
, blamelist
):
1211 self
.blamelist
= blamelist
1212 def setProgress(self
, progress
):
1213 self
.progress
= progress
1215 def buildStarted(self
, build
):
1216 """The Build has been set up and is about to be started. It can now
1217 be safely queried, so it is time to announce the new build."""
1219 self
.started
= util
.now()
1220 # now that we're ready to report status, let the BuilderStatus tell
1221 # the world about us
1222 self
.builder
.buildStarted(self
)
1224 def setSlavename(self
, slavename
):
1225 self
.slavename
= slavename
1227 def setText(self
, text
):
1228 assert isinstance(text
, (list, tuple))
1230 def setResults(self
, results
):
1231 self
.results
= results
1233 def buildFinished(self
):
1234 self
.currentStep
= None
1235 self
.finished
= util
.now()
1237 for r
in self
.updates
.keys():
1238 if self
.updates
[r
] is not None:
1239 self
.updates
[r
].cancel()
1242 watchers
= self
.finishedWatchers
1243 self
.finishedWatchers
= []
1247 # methods called by our BuildStepStatus children
1249 def stepStarted(self
, step
):
1250 self
.currentStep
= step
1251 name
= self
.getBuilder().getName()
1252 for w
in self
.watchers
:
1253 receiver
= w
.stepStarted(self
, step
)
1255 if type(receiver
) == type(()):
1256 step
.subscribe(receiver
[0], receiver
[1])
1258 step
.subscribe(receiver
)
1259 d
= step
.waitUntilFinished()
1260 d
.addCallback(lambda step
: step
.unsubscribe(receiver
))
1262 step
.waitUntilFinished().addCallback(self
._stepFinished
)
1264 def _stepFinished(self
, step
):
1265 results
= step
.getResults()
1266 for w
in self
.watchers
:
1267 w
.stepFinished(self
, step
, results
)
1269 # methods called by our BuilderStatus parent
1271 def pruneLogs(self
):
1272 # this build is somewhat old: remove the build logs to save space
1273 # TODO: delete logs visible through IBuildStatus.getLogs
1274 for s
in self
.steps
:
1277 def pruneSteps(self
):
1278 # this build is very old: remove the build steps too
1283 def generateLogfileName(self
, stepname
, logname
):
1284 """Return a filename (relative to the Builder's base directory) where
1285 the logfile's contents can be stored uniquely.
1287 The base filename is made by combining our build number, the Step's
1288 name, and the log's name, then removing unsuitable characters. The
1289 filename is then made unique by appending _0, _1, etc, until it does
1290 not collide with any other logfile.
1292 These files are kept in the Builder's basedir (rather than a
1293 per-Build subdirectory) because that makes cleanup easier: cron and
1294 find will help get rid of the old logs, but the empty directories are
1295 more of a hassle to remove."""
1297 starting_filename
= "%d-log-%s-%s" % (self
.number
, stepname
, logname
)
1298 starting_filename
= re
.sub(r
'[^\w\.\-]', '_', starting_filename
)
1299 # now make it unique
1301 filename
= starting_filename
1302 while filename
in [l
.filename
1303 for step
in self
.steps
1304 for l
in step
.getLogs()
1306 filename
= "%s_%d" % (starting_filename
, unique_counter
)
1310 def __getstate__(self
):
1311 d
= styles
.Versioned
.__getstate
__(self
)
1312 # for now, a serialized Build is always "finished". We will never
1313 # save unfinished builds.
1314 if not self
.finished
:
1315 d
['finished'] = True
1316 # TODO: push an "interrupted" step so it is clear that the build
1317 # was interrupted. The builder will have a 'shutdown' event, but
1318 # someone looking at just this build will be confused as to why
1319 # the last log is truncated.
1320 del d
['builder'] # filled in by our parent when loading
1324 del d
['finishedWatchers']
1327 def __setstate__(self
, d
):
1328 styles
.Versioned
.__setstate
__(self
, d
)
1329 # self.builder must be filled in by our parent when loading
1330 for step
in self
.steps
:
1334 self
.finishedWatchers
= []
1336 def upgradeToVersion1(self
):
1337 if hasattr(self
, "sourceStamp"):
1338 # the old .sourceStamp attribute wasn't actually very useful
1339 maxChangeNumber
, patch
= self
.sourceStamp
1340 changes
= getattr(self
, 'changes', [])
1341 source
= sourcestamp
.SourceStamp(branch
=None,
1345 self
.source
= source
1346 self
.changes
= source
.changes
1347 del self
.sourceStamp
1349 def upgradeToVersion2(self
):
1350 self
.properties
= {}
1352 def upgradeToVersion3(self
):
1353 # in version 3, self.properties became a Properties object
1354 propdict
= self
.properties
1355 self
.properties
= Properties()
1356 self
.properties
.update(propdict
, "Upgrade from previous version")
1358 def upgradeLogfiles(self
):
1359 # upgrade any LogFiles that need it. This must occur after we've been
1360 # attached to our Builder, and after we know about all LogFiles of
1361 # all Steps (to get the filenames right).
1363 for s
in self
.steps
:
1364 for l
in s
.getLogs():
1366 pass # new-style, log contents are on disk
1368 logfilename
= self
.generateLogfileName(s
.name
, l
.name
)
1369 # let the logfile update its .filename pointer,
1370 # transferring its contents onto disk if necessary
1371 l
.upgrade(logfilename
)
1373 def saveYourself(self
):
1374 filename
= os
.path
.join(self
.builder
.basedir
, "%d" % self
.number
)
1375 if os
.path
.isdir(filename
):
1376 # leftover from 0.5.0, which stored builds in directories
1377 shutil
.rmtree(filename
, ignore_errors
=True)
1378 tmpfilename
= filename
+ ".tmp"
1380 dump(self
, open(tmpfilename
, "wb"), -1)
1381 if sys
.platform
== 'win32':
1382 # windows cannot rename a file on top of an existing one, so
1383 # fall back to delete-first. There are ways this can fail and
1384 # lose the builder's history, so we avoid using it in the
1385 # general (non-windows) case
1386 if os
.path
.exists(filename
):
1388 os
.rename(tmpfilename
, filename
)
1390 log
.msg("unable to save build %s-#%d" % (self
.builder
.name
,
1396 class BuilderStatus(styles
.Versioned
):
1397 """I handle status information for a single process.base.Builder object.
1398 That object sends status changes to me (frequently as Events), and I
1399 provide them on demand to the various status recipients, like the HTML
1400 waterfall display and the live status clients. It also sends build
1401 summaries to me, which I log and provide to status clients who aren't
1402 interested in seeing details of the individual build steps.
1404 I am responsible for maintaining the list of historic Events and Builds,
1405 pruning old ones, and loading them from / saving them to disk.
1407 I live in the buildbot.process.base.Builder object, in the
1408 .builder_status attribute.
1410 @type category: string
1411 @ivar category: user-defined category this builder belongs to; can be
1412 used to filter on in status clients
1415 implements(interfaces
.IBuilderStatus
, interfaces
.IEventSource
)
1416 persistenceVersion
= 1
1418 # these limit the amount of memory we consume, as well as the size of the
1419 # main Builder pickle. The Build and LogFile pickles on disk must be
1420 # handled separately.
1422 buildHorizon
= 100 # forget builds beyond this
1423 stepHorizon
= 50 # forget steps in builds beyond this
1424 eventHorizon
= 50 # forget events beyond this
1427 currentBigState
= "offline" # or idle/waiting/interlocked/building
1428 basedir
= None # filled in by our parent
1430 def __init__(self
, buildername
, category
=None):
1431 self
.name
= buildername
1432 self
.category
= category
1434 self
.slavenames
= []
1436 # these three hold Events, and are used to retrieve the current
1437 # state of the boxes.
1438 self
.lastBuildStatus
= None
1439 #self.currentBig = None
1440 #self.currentSmall = None
1441 self
.currentBuilds
= []
1442 self
.pendingBuilds
= []
1443 self
.nextBuild
= None
1445 self
.buildCache
= weakref
.WeakValueDictionary()
1446 self
.buildCache_LRU
= []
1447 self
.logCompressionLimit
= False # default to no compression for tests
1451 def __getstate__(self
):
1452 # when saving, don't record transient stuff like what builds are
1453 # currently running, because they won't be there when we start back
1454 # up. Nor do we save self.watchers, nor anything that gets set by our
1455 # parent like .basedir and .status
1456 d
= styles
.Versioned
.__getstate
__(self
)
1459 del d
['buildCache_LRU']
1460 for b
in self
.currentBuilds
:
1462 # TODO: push a 'hey, build was interrupted' event
1463 del d
['currentBuilds']
1464 del d
['pendingBuilds']
1465 del d
['currentBigState']
1468 del d
['nextBuildNumber']
1471 def __setstate__(self
, d
):
1472 # when loading, re-initialize the transient stuff. Remember that
1473 # upgradeToVersion1 and such will be called after this finishes.
1474 styles
.Versioned
.__setstate
__(self
, d
)
1475 self
.buildCache
= weakref
.WeakValueDictionary()
1476 self
.buildCache_LRU
= []
1477 self
.currentBuilds
= []
1478 self
.pendingBuilds
= []
1480 self
.slavenames
= []
1481 # self.basedir must be filled in by our parent
1482 # self.status must be filled in by our parent
1484 def upgradeToVersion1(self
):
1485 if hasattr(self
, 'slavename'):
1486 self
.slavenames
= [self
.slavename
]
1488 if hasattr(self
, 'nextBuildNumber'):
1489 del self
.nextBuildNumber
# determineNextBuildNumber chooses this
1491 def determineNextBuildNumber(self
):
1492 """Scan our directory of saved BuildStatus instances to determine
1493 what our self.nextBuildNumber should be. Set it one larger than the
1494 highest-numbered build we discover. This is called by the top-level
1495 Status object shortly after we are created or loaded from disk.
1497 existing_builds
= [int(f
)
1498 for f
in os
.listdir(self
.basedir
)
1499 if re
.match("^\d+$", f
)]
1501 self
.nextBuildNumber
= max(existing_builds
) + 1
1503 self
.nextBuildNumber
= 0
1505 def setLogCompressionLimit(self
, lowerLimit
):
1506 self
.logCompressionLimit
= lowerLimit
1508 def saveYourself(self
):
1509 for b
in self
.currentBuilds
:
1510 if not b
.isFinished
:
1511 # interrupted build, need to save it anyway.
1512 # BuildStatus.saveYourself will mark it as interrupted.
1514 filename
= os
.path
.join(self
.basedir
, "builder")
1515 tmpfilename
= filename
+ ".tmp"
1517 dump(self
, open(tmpfilename
, "wb"), -1)
1518 if sys
.platform
== 'win32':
1519 # windows cannot rename a file on top of an existing one
1520 if os
.path
.exists(filename
):
1522 os
.rename(tmpfilename
, filename
)
1524 log
.msg("unable to save builder %s" % self
.name
)
1528 # build cache management
1530 def touchBuildCache(self
, build
):
1531 self
.buildCache
[build
.number
] = build
1532 if build
in self
.buildCache_LRU
:
1533 self
.buildCache_LRU
.remove(build
)
1534 self
.buildCache_LRU
= self
.buildCache_LRU
[-(self
.buildCacheSize
-1):] + [ build
]
1537 def getBuildByNumber(self
, number
):
1538 # first look in currentBuilds
1539 for b
in self
.currentBuilds
:
1540 if b
.number
== number
:
1541 return self
.touchBuildCache(b
)
1543 # then in the buildCache
1544 if number
in self
.buildCache
:
1545 return self
.touchBuildCache(self
.buildCache
[number
])
1547 # then fall back to loading it from disk
1548 filename
= os
.path
.join(self
.basedir
, "%d" % number
)
1550 log
.msg("Loading builder %s's build %d from on-disk pickle"
1551 % (self
.name
, number
))
1552 build
= load(open(filename
, "rb"))
1554 build
.builder
= self
1555 # handle LogFiles from after 0.5.0 and before 0.6.5
1556 build
.upgradeLogfiles()
1557 return self
.touchBuildCache(build
)
1559 raise IndexError("no such build %d" % number
)
1561 raise IndexError("corrupted build pickle %d" % number
)
1564 return # TODO: change this to walk through the filesystem
1565 # first, blow away all builds beyond our build horizon
1566 self
.builds
= self
.builds
[-self
.buildHorizon
:]
1567 # then prune steps in builds past the step horizon
1568 for b
in self
.builds
[0:-self
.stepHorizon
]:
1571 def pruneEvents(self
):
1572 self
.events
= self
.events
[-self
.eventHorizon
:]
1574 # IBuilderStatus methods
1579 return (self
.currentBigState
, self
.currentBuilds
)
1581 def getSlaves(self
):
1582 return [self
.status
.getSlave(name
) for name
in self
.slavenames
]
1584 def getPendingBuilds(self
):
1585 return self
.pendingBuilds
1587 def getCurrentBuilds(self
):
1588 return self
.currentBuilds
1590 def getLastFinishedBuild(self
):
1591 b
= self
.getBuild(-1)
1592 if not (b
and b
.isFinished()):
1593 b
= self
.getBuild(-2)
1596 def getBuild(self
, number
):
1598 number
= self
.nextBuildNumber
+ number
1599 if number
< 0 or number
>= self
.nextBuildNumber
:
1603 return self
.getBuildByNumber(number
)
1607 def getEvent(self
, number
):
1609 return self
.events
[number
]
1613 def generateFinishedBuilds(self
, branches
=[],
1616 finished_before
=None,
1619 for Nb
in itertools
.count(1):
1620 if Nb
> self
.nextBuildNumber
:
1624 build
= self
.getBuild(-Nb
)
1627 if max_buildnum
is not None:
1628 if build
.getNumber() > max_buildnum
:
1630 if not build
.isFinished():
1632 if finished_before
is not None:
1633 start
, end
= build
.getTimes()
1634 if end
>= finished_before
:
1637 if build
.getSourceStamp().branch
not in branches
:
1641 if num_builds
is not None:
1642 if got
>= num_builds
:
1645 def eventGenerator(self
, branches
=[]):
1646 """This function creates a generator which will provide all of this
1647 Builder's status events, starting with the most recent and
1648 progressing backwards in time. """
1650 # remember the oldest-to-earliest flow here. "next" means earlier.
1652 # TODO: interleave build steps and self.events by timestamp.
1653 # TODO: um, I think we're already doing that.
1655 # TODO: there's probably something clever we could do here to
1656 # interleave two event streams (one from self.getBuild and the other
1657 # from self.getEvent), which would be simpler than this control flow
1660 e
= self
.getEvent(eventIndex
)
1661 for Nb
in range(1, self
.nextBuildNumber
+1):
1662 b
= self
.getBuild(-Nb
)
1665 if branches
and not b
.getSourceStamp().branch
in branches
:
1667 steps
= b
.getSteps()
1668 for Ns
in range(1, len(steps
)+1):
1669 if steps
[-Ns
].started
:
1670 step_start
= steps
[-Ns
].getTimes()[0]
1671 while e
is not None and e
.getTimes()[0] > step_start
:
1674 e
= self
.getEvent(eventIndex
)
1677 while e
is not None:
1680 e
= self
.getEvent(eventIndex
)
1682 def subscribe(self
, receiver
):
1683 # will get builderChangedState, buildStarted, and buildFinished
1684 self
.watchers
.append(receiver
)
1685 self
.publishState(receiver
)
1687 def unsubscribe(self
, receiver
):
1688 self
.watchers
.remove(receiver
)
1690 ## Builder interface (methods called by the Builder which feeds us)
1692 def setSlavenames(self
, names
):
1693 self
.slavenames
= names
1695 def addEvent(self
, text
=[]):
1696 # this adds a duration event. When it is done, the user should call
1697 # e.finish(). They can also mangle it by modifying .text
1699 e
.started
= util
.now()
1701 self
.events
.append(e
)
1703 return e
# they are free to mangle it further
1705 def addPointEvent(self
, text
=[]):
1706 # this adds a point event, one which occurs as a single atomic
1709 e
.started
= util
.now()
1712 self
.events
.append(e
)
1714 return e
# for consistency, but they really shouldn't touch it
1716 def setBigState(self
, state
):
1717 needToUpdate
= state
!= self
.currentBigState
1718 self
.currentBigState
= state
1722 def publishState(self
, target
=None):
1723 state
= self
.currentBigState
1725 if target
is not None:
1727 target
.builderChangedState(self
.name
, state
)
1729 for w
in self
.watchers
:
1731 w
.builderChangedState(self
.name
, state
)
1733 log
.msg("Exception caught publishing state to %r" % w
)
1737 """The Builder has decided to start a build, but the Build object is
1738 not yet ready to report status (it has not finished creating the
1739 Steps). Create a BuildStatus object that it can use."""
1740 number
= self
.nextBuildNumber
1741 self
.nextBuildNumber
+= 1
1742 # TODO: self.saveYourself(), to make sure we don't forget about the
1743 # build number we've just allocated. This is not quite as important
1744 # as it was before we switch to determineNextBuildNumber, but I think
1745 # it may still be useful to have the new build save itself.
1746 s
= BuildStatus(self
, number
)
1747 s
.waitUntilFinished().addCallback(self
._buildFinished
)
1750 def addBuildRequest(self
, brstatus
):
1751 self
.pendingBuilds
.append(brstatus
)
1752 for w
in self
.watchers
:
1753 w
.requestSubmitted(brstatus
)
1755 def removeBuildRequest(self
, brstatus
):
1756 self
.pendingBuilds
.remove(brstatus
)
1758 # buildStarted is called by our child BuildStatus instances
1759 def buildStarted(self
, s
):
1760 """Now the BuildStatus object is ready to go (it knows all of its
1761 Steps, its ETA, etc), so it is safe to notify our watchers."""
1763 assert s
.builder
is self
# paranoia
1764 assert s
.number
== self
.nextBuildNumber
- 1
1765 assert s
not in self
.currentBuilds
1766 self
.currentBuilds
.append(s
)
1767 self
.touchBuildCache(s
)
1769 # now that the BuildStatus is prepared to answer queries, we can
1770 # announce the new build to all our watchers
1772 for w
in self
.watchers
: # TODO: maybe do this later? callLater(0)?
1774 receiver
= w
.buildStarted(self
.getName(), s
)
1776 if type(receiver
) == type(()):
1777 s
.subscribe(receiver
[0], receiver
[1])
1779 s
.subscribe(receiver
)
1780 d
= s
.waitUntilFinished()
1781 d
.addCallback(lambda s
: s
.unsubscribe(receiver
))
1783 log
.msg("Exception caught notifying %r of buildStarted event" % w
)
1786 def _buildFinished(self
, s
):
1787 assert s
in self
.currentBuilds
1789 self
.currentBuilds
.remove(s
)
1791 name
= self
.getName()
1792 results
= s
.getResults()
1793 for w
in self
.watchers
:
1795 w
.buildFinished(name
, s
, results
)
1797 log
.msg("Exception caught notifying %r of buildFinished event" % w
)
1800 self
.prune() # conserve disk
1803 # waterfall display (history)
1805 # I want some kind of build event that holds everything about the build:
1806 # why, what changes went into it, the results of the build, itemized
1807 # test results, etc. But, I do kind of need something to be inserted in
1808 # the event log first, because intermixing step events and the larger
1809 # build event is fraught with peril. Maybe an Event-like-thing that
1810 # doesn't have a file in it but does have links. Hmm, that's exactly
1811 # what it does now. The only difference would be that this event isn't
1812 # pushed to the clients.
1814 # publish to clients
1815 def sendLastBuildStatus(self
, client
):
1816 #client.newLastBuildStatus(self.lastBuildStatus)
1818 def sendCurrentActivityBigToEveryone(self
):
1819 for s
in self
.subscribers
:
1820 self
.sendCurrentActivityBig(s
)
1821 def sendCurrentActivityBig(self
, client
):
1822 state
= self
.currentBigState
1823 if state
== "offline":
1824 client
.currentlyOffline()
1825 elif state
== "idle":
1826 client
.currentlyIdle()
1827 elif state
== "building":
1828 client
.currentlyBuilding()
1830 log
.msg("Hey, self.currentBigState is weird:", state
)
1833 ## HTML display interface
1835 def getEventNumbered(self
, num
):
1836 # deal with dropped events, pruned events
1837 first
= self
.events
[0].number
1838 if first
+ len(self
.events
)-1 != self
.events
[-1].number
:
1840 "lost an event somewhere: [0] is %d, [%d] is %d" % \
1841 (self
.events
[0].number
,
1842 len(self
.events
) - 1,
1843 self
.events
[-1].number
))
1844 for e
in self
.events
:
1845 log
.msg("e[%d]: " % e
.number
, e
)
1847 offset
= num
- first
1848 log
.msg(self
, "offset", offset
)
1850 return self
.events
[offset
]
1854 ## Persistence of Status
1855 def loadYourOldEvents(self
):
1856 if hasattr(self
, "allEvents"):
1857 # first time, nothing to get from file. Note that this is only if
1858 # the Application gets .run() . If it gets .save()'ed, then the
1859 # .allEvents attribute goes away in the initial __getstate__ and
1860 # we try to load a non-existent file.
1862 self
.allEvents
= self
.loadFile("events", [])
1864 self
.nextEventNumber
= self
.allEvents
[-1].number
+ 1
1866 self
.nextEventNumber
= 0
1867 def saveYourOldEvents(self
):
1868 self
.saveFile("events", self
.allEvents
)
1872 def addClient(self
, client
):
1873 if client
not in self
.subscribers
:
1874 self
.subscribers
.append(client
)
1875 self
.sendLastBuildStatus(client
)
1876 self
.sendCurrentActivityBig(client
)
1877 client
.newEvent(self
.currentSmall
)
1878 def removeClient(self
, client
):
1879 if client
in self
.subscribers
:
1880 self
.subscribers
.remove(client
)
1883 implements(interfaces
.ISlaveStatus
)
1888 graceful_shutdown
= False
1890 def __init__(self
, name
):
1892 self
._lastMessageReceived
= 0
1893 self
.runningBuilds
= []
1894 self
.graceful_callbacks
= []
1902 def isConnected(self
):
1903 return self
.connected
1904 def lastMessageReceived(self
):
1905 return self
._lastMessageReceived
1906 def getRunningBuilds(self
):
1907 return self
.runningBuilds
1909 def setAdmin(self
, admin
):
1911 def setHost(self
, host
):
1913 def setConnected(self
, isConnected
):
1914 self
.connected
= isConnected
1915 def setLastMessageReceived(self
, when
):
1916 self
._lastMessageReceived
= when
1918 def buildStarted(self
, build
):
1919 self
.runningBuilds
.append(build
)
1920 def buildFinished(self
, build
):
1921 self
.runningBuilds
.remove(build
)
1923 def getGraceful(self
):
1924 """Return the graceful shutdown flag"""
1925 return self
.graceful_shutdown
1926 def setGraceful(self
, graceful
):
1927 """Set the graceful shutdown flag, and notify all the watchers"""
1928 self
.graceful_shutdown
= graceful
1929 for cb
in self
.graceful_callbacks
:
1930 reactor
.callLater(0, cb
, graceful
)
1931 def addGracefulWatcher(self
, watcher
):
1932 """Add watcher to the list of watchers to be notified when the
1933 graceful shutdown flag is changed."""
1934 if not watcher
in self
.graceful_callbacks
:
1935 self
.graceful_callbacks
.append(watcher
)
1936 def removeGracefulWatcher(self
, watcher
):
1937 """Remove watcher from the list of watchers to be notified when the
1938 graceful shutdown flag is changed."""
1939 if watcher
in self
.graceful_callbacks
:
1940 self
.graceful_callbacks
.remove(watcher
)
1944 I represent the status of the buildmaster.
1946 implements(interfaces
.IStatus
)
1948 def __init__(self
, botmaster
, basedir
):
1950 @type botmaster: L{buildbot.master.BotMaster}
1951 @param botmaster: the Status object uses C{.botmaster} to get at
1952 both the L{buildbot.master.BuildMaster} (for
1953 various buildbot-wide parameters) and the
1954 actual Builders (to get at their L{BuilderStatus}
1955 objects). It is not allowed to change or influence
1956 anything through this reference.
1957 @type basedir: string
1958 @param basedir: this provides a base directory in which saved status
1959 information (changes.pck, saved Build status
1960 pickles) can be stored
1962 self
.botmaster
= botmaster
1963 self
.basedir
= basedir
1965 self
.activeBuildSets
= []
1966 assert os
.path
.isdir(basedir
)
1967 # compress logs bigger than 4k, a good default on linux
1968 self
.logCompressionLimit
= 4*1024
1971 # methods called by our clients
1973 def getProjectName(self
):
1974 return self
.botmaster
.parent
.projectName
1975 def getProjectURL(self
):
1976 return self
.botmaster
.parent
.projectURL
1977 def getBuildbotURL(self
):
1978 return self
.botmaster
.parent
.buildbotURL
1980 def getURLForThing(self
, thing
):
1981 prefix
= self
.getBuildbotURL()
1984 if interfaces
.IStatus
.providedBy(thing
):
1986 if interfaces
.ISchedulerStatus
.providedBy(thing
):
1988 if interfaces
.IBuilderStatus
.providedBy(thing
):
1990 return prefix
+ "builders/%s" % (
1991 urllib
.quote(builder
.getName(), safe
=''),
1993 if interfaces
.IBuildStatus
.providedBy(thing
):
1995 builder
= build
.getBuilder()
1996 return prefix
+ "builders/%s/builds/%d" % (
1997 urllib
.quote(builder
.getName(), safe
=''),
1999 if interfaces
.IBuildStepStatus
.providedBy(thing
):
2001 build
= step
.getBuild()
2002 builder
= build
.getBuilder()
2003 return prefix
+ "builders/%s/builds/%d/steps/%s" % (
2004 urllib
.quote(builder
.getName(), safe
=''),
2006 urllib
.quote(step
.getName(), safe
=''))
2008 # IBuildRequestStatus
2012 if interfaces
.IStatusEvent
.providedBy(thing
):
2013 from buildbot
.changes
import changes
2014 # TODO: this is goofy, create IChange or something
2015 if isinstance(thing
, changes
.Change
):
2017 return "%schanges/%d" % (prefix
, change
.number
)
2019 if interfaces
.IStatusLog
.providedBy(thing
):
2021 step
= log
.getStep()
2022 build
= step
.getBuild()
2023 builder
= build
.getBuilder()
2025 logs
= step
.getLogs()
2026 for i
in range(len(logs
)):
2032 return prefix
+ "builders/%s/builds/%d/steps/%s/logs/%d" % (
2033 urllib
.quote(builder
.getName(), safe
=''),
2035 urllib
.quote(step
.getName(), safe
=''),
2038 def getChangeSources(self
):
2039 return list(self
.botmaster
.parent
.change_svc
)
2041 def getChange(self
, number
):
2042 return self
.botmaster
.parent
.change_svc
.getChangeNumbered(number
)
2044 def getSchedulers(self
):
2045 return self
.botmaster
.parent
.allSchedulers()
2047 def getBuilderNames(self
, categories
=None):
2048 if categories
== None:
2049 return self
.botmaster
.builderNames
[:] # don't let them break it
2052 # respect addition order
2053 for name
in self
.botmaster
.builderNames
:
2054 builder
= self
.botmaster
.builders
[name
]
2055 if builder
.builder_status
.category
in categories
:
2059 def getBuilder(self
, name
):
2061 @rtype: L{BuilderStatus}
2063 return self
.botmaster
.builders
[name
].builder_status
2065 def getSlaveNames(self
):
2066 return self
.botmaster
.slaves
.keys()
2068 def getSlave(self
, slavename
):
2069 return self
.botmaster
.slaves
[slavename
].slave_status
2071 def getBuildSets(self
):
2072 return self
.activeBuildSets
[:]
2074 def generateFinishedBuilds(self
, builders
=[], branches
=[],
2075 num_builds
=None, finished_before
=None,
2078 def want_builder(bn
):
2080 return bn
in builders
2083 for bn
in self
.getBuilderNames()
2084 if want_builder(bn
)]
2086 # 'sources' is a list of generators, one for each Builder we're
2087 # using. When the generator is exhausted, it is replaced in this list
2090 for bn
in builder_names
:
2091 b
= self
.getBuilder(bn
)
2092 g
= b
.generateFinishedBuilds(branches
,
2093 finished_before
=finished_before
,
2094 max_search
=max_search
)
2097 # next_build the next build from each source
2098 next_build
= [None] * len(sources
)
2101 for i
,g
in enumerate(sources
):
2109 next_build
[i
] = g
.next()
2110 except StopIteration:
2111 next_build
[i
] = None
2117 # find the latest build among all the candidates
2118 candidates
= [(i
, b
, b
.getTimes()[1])
2119 for i
,b
in enumerate(next_build
)
2121 candidates
.sort(lambda x
,y
: cmp(x
[2], y
[2]))
2125 # and remove it from the list
2126 i
, build
, finshed_time
= candidates
[-1]
2127 next_build
[i
] = None
2130 if num_builds
is not None:
2131 if got
>= num_builds
:
2134 def subscribe(self
, target
):
2135 self
.watchers
.append(target
)
2136 for name
in self
.botmaster
.builderNames
:
2137 self
.announceNewBuilder(target
, name
, self
.getBuilder(name
))
2138 def unsubscribe(self
, target
):
2139 self
.watchers
.remove(target
)
2142 # methods called by upstream objects
2144 def announceNewBuilder(self
, target
, name
, builder_status
):
2145 t
= target
.builderAdded(name
, builder_status
)
2147 builder_status
.subscribe(t
)
2149 def builderAdded(self
, name
, basedir
, category
=None):
2151 @rtype: L{BuilderStatus}
2153 filename
= os
.path
.join(self
.basedir
, basedir
, "builder")
2154 log
.msg("trying to load status pickle from %s" % filename
)
2155 builder_status
= None
2157 builder_status
= load(open(filename
, "rb"))
2160 log
.msg("no saved status pickle, creating a new one")
2162 log
.msg("error while loading status pickle, creating a new one")
2163 log
.msg("error follows:")
2165 if not builder_status
:
2166 builder_status
= BuilderStatus(name
, category
)
2167 builder_status
.addPointEvent(["builder", "created"])
2168 log
.msg("added builder %s in category %s" % (name
, category
))
2169 # an unpickled object might not have category set from before,
2170 # so set it here to make sure
2171 builder_status
.category
= category
2172 builder_status
.basedir
= os
.path
.join(self
.basedir
, basedir
)
2173 builder_status
.name
= name
# it might have been updated
2174 builder_status
.status
= self
2176 if not os
.path
.isdir(builder_status
.basedir
):
2177 os
.makedirs(builder_status
.basedir
)
2178 builder_status
.determineNextBuildNumber()
2180 builder_status
.setBigState("offline")
2181 builder_status
.setLogCompressionLimit(self
.logCompressionLimit
)
2183 for t
in self
.watchers
:
2184 self
.announceNewBuilder(t
, name
, builder_status
)
2186 return builder_status
2188 def builderRemoved(self
, name
):
2189 for t
in self
.watchers
:
2190 t
.builderRemoved(name
)
2193 for b
in self
.botmaster
.builders
.values():
2194 b
.builder_status
.prune()
2196 def buildsetSubmitted(self
, bss
):
2197 self
.activeBuildSets
.append(bss
)
2198 bss
.waitUntilFinished().addCallback(self
.activeBuildSets
.remove
)
2199 for t
in self
.watchers
:
2200 t
.buildsetSubmitted(bss
)