Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/status/builder.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/status/builder.py')
-rw-r--r--buildbot/buildbot/status/builder.py2182
1 files changed, 2182 insertions, 0 deletions
diff --git a/buildbot/buildbot/status/builder.py b/buildbot/buildbot/status/builder.py
new file mode 100644
index 0000000..97f356f
--- /dev/null
+++ b/buildbot/buildbot/status/builder.py
@@ -0,0 +1,2182 @@
+# -*- test-case-name: buildbot.test.test_status -*-
+
+from zope.interface import implements
+from twisted.python import log
+from twisted.persisted import styles
+from twisted.internet import reactor, defer, threads
+from twisted.protocols import basic
+from buildbot.process.properties import Properties
+
+import os, shutil, sys, re, urllib, itertools
+from cPickle import load, dump
+from cStringIO import StringIO
+from bz2 import BZ2File
+
+# sibling imports
+from buildbot import interfaces, util, sourcestamp
+
+SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5)
+Results = ["success", "warnings", "failure", "skipped", "exception"]
+
+
+# build processes call the following methods:
+#
+# setDefaults
+#
+# currentlyBuilding
+# currentlyIdle
+# currentlyInterlocked
+# currentlyOffline
+# currentlyWaiting
+#
+# setCurrentActivity
+# updateCurrentActivity
+# addFileToCurrentActivity
+# finishCurrentActivity
+#
+# startBuild
+# finishBuild
+
+STDOUT = interfaces.LOG_CHANNEL_STDOUT
+STDERR = interfaces.LOG_CHANNEL_STDERR
+HEADER = interfaces.LOG_CHANNEL_HEADER
+ChunkTypes = ["stdout", "stderr", "header"]
+
+class LogFileScanner(basic.NetstringReceiver):
+ def __init__(self, chunk_cb, channels=[]):
+ self.chunk_cb = chunk_cb
+ self.channels = channels
+
+ def stringReceived(self, line):
+ channel = int(line[0])
+ if not self.channels or (channel in self.channels):
+ self.chunk_cb((channel, line[1:]))
+
+class LogFileProducer:
+ """What's the plan?
+
+ the LogFile has just one FD, used for both reading and writing.
+ Each time you add an entry, fd.seek to the end and then write.
+
+ Each reader (i.e. Producer) keeps track of their own offset. The reader
+ starts by seeking to the start of the logfile, and reading forwards.
+ Between each hunk of file they yield chunks, so they must remember their
+ offset before yielding and re-seek back to that offset before reading
+ more data. When their read() returns EOF, they're finished with the first
+ phase of the reading (everything that's already been written to disk).
+
+ After EOF, the remaining data is entirely in the current entries list.
+ These entries are all of the same channel, so we can do one "".join and
+ obtain a single chunk to be sent to the listener. But since that involves
+ a yield, and more data might arrive after we give up control, we have to
+ subscribe them before yielding. We can't subscribe them any earlier,
+ otherwise they'd get data out of order.
+
+ We're using a generator in the first place so that the listener can
+ throttle us, which means they're pulling. But the subscription means
+ we're pushing. Really we're a Producer. In the first phase we can be
+ either a PullProducer or a PushProducer. In the second phase we're only a
+ PushProducer.
+
+ So the client gives a LogFileConsumer to File.subscribeConsumer . This
+ Consumer must have registerProducer(), unregisterProducer(), and
+ writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
+ except that writeChunk() takes chunks (tuples of (channel,text)) instead
+ of the normal write() which takes just text. The LogFileConsumer is
+ allowed to call stopProducing, pauseProducing, and resumeProducing on the
+ producer instance it is given. """
+
+ paused = False
+ subscribed = False
+ BUFFERSIZE = 2048
+
+ def __init__(self, logfile, consumer):
+ self.logfile = logfile
+ self.consumer = consumer
+ self.chunkGenerator = self.getChunks()
+ consumer.registerProducer(self, True)
+
+ def getChunks(self):
+ f = self.logfile.getFile()
+ offset = 0
+ chunks = []
+ p = LogFileScanner(chunks.append)
+ f.seek(offset)
+ data = f.read(self.BUFFERSIZE)
+ offset = f.tell()
+ while data:
+ p.dataReceived(data)
+ while chunks:
+ c = chunks.pop(0)
+ yield c
+ f.seek(offset)
+ data = f.read(self.BUFFERSIZE)
+ offset = f.tell()
+ del f
+
+ # now subscribe them to receive new entries
+ self.subscribed = True
+ self.logfile.watchers.append(self)
+ d = self.logfile.waitUntilFinished()
+
+ # then give them the not-yet-merged data
+ if self.logfile.runEntries:
+ channel = self.logfile.runEntries[0][0]
+ text = "".join([c[1] for c in self.logfile.runEntries])
+ yield (channel, text)
+
+ # now we've caught up to the present. Anything further will come from
+ # the logfile subscription. We add the callback *after* yielding the
+ # data from runEntries, because the logfile might have finished
+ # during the yield.
+ d.addCallback(self.logfileFinished)
+
+ def stopProducing(self):
+ # TODO: should we still call consumer.finish? probably not.
+ self.paused = True
+ self.consumer = None
+ self.done()
+
+ def done(self):
+ if self.chunkGenerator:
+ self.chunkGenerator = None # stop making chunks
+ if self.subscribed:
+ self.logfile.watchers.remove(self)
+ self.subscribed = False
+
+ def pauseProducing(self):
+ self.paused = True
+
+ def resumeProducing(self):
+ # Twisted-1.3.0 has a bug which causes hangs when resumeProducing
+ # calls transport.write (there is a recursive loop, fixed in 2.0 in
+ # t.i.abstract.FileDescriptor.doWrite by setting the producerPaused
+ # flag *before* calling resumeProducing). To work around this, we
+ # just put off the real resumeProducing for a moment. This probably
+ # has a performance hit, but I'm going to assume that the log files
+ # are not retrieved frequently enough for it to be an issue.
+
+ reactor.callLater(0, self._resumeProducing)
+
+ def _resumeProducing(self):
+ self.paused = False
+ if not self.chunkGenerator:
+ return
+ try:
+ while not self.paused:
+ chunk = self.chunkGenerator.next()
+ self.consumer.writeChunk(chunk)
+ # we exit this when the consumer says to stop, or we run out
+ # of chunks
+ except StopIteration:
+ # if the generator finished, it will have done releaseFile
+ self.chunkGenerator = None
+ # now everything goes through the subscription, and they don't get to
+ # pause anymore
+
+ def logChunk(self, build, step, logfile, channel, chunk):
+ if self.consumer:
+ self.consumer.writeChunk((channel, chunk))
+
+ def logfileFinished(self, logfile):
+ self.done()
+ if self.consumer:
+ self.consumer.unregisterProducer()
+ self.consumer.finish()
+ self.consumer = None
+
+def _tryremove(filename, timeout, retries):
+ """Try to remove a file, and if failed, try again in timeout.
+ Increases the timeout by a factor of 4, and only keeps trying for
+ another retries-amount of times.
+
+ """
+ try:
+ os.unlink(filename)
+ except OSError:
+ if retries > 0:
+ reactor.callLater(timeout, _tryremove, filename, timeout * 4,
+ retries - 1)
+ else:
+ log.msg("giving up on removing %s after over %d seconds" %
+ (filename, timeout))
+
+class LogFile:
+ """A LogFile keeps all of its contents on disk, in a non-pickle format to
+ which new entries can easily be appended. The file on disk has a name
+ like 12-log-compile-output, under the Builder's directory. The actual
+ filename is generated (before the LogFile is created) by
+ L{BuildStatus.generateLogfileName}.
+
+ Old LogFile pickles (which kept their contents in .entries) must be
+ upgraded. The L{BuilderStatus} is responsible for doing this, when it
+ loads the L{BuildStatus} into memory. The Build pickle is not modified,
+ so users who go from 0.6.5 back to 0.6.4 don't have to lose their
+ logs."""
+
+ implements(interfaces.IStatusLog, interfaces.ILogFile)
+
+ finished = False
+ length = 0
+ chunkSize = 10*1000
+ runLength = 0
+ runEntries = [] # provided so old pickled builds will getChunks() ok
+ entries = None
+ BUFFERSIZE = 2048
+ filename = None # relative to the Builder's basedir
+ openfile = None
+
+ def __init__(self, parent, name, logfilename):
+ """
+ @type parent: L{BuildStepStatus}
+ @param parent: the Step that this log is a part of
+ @type name: string
+ @param name: the name of this log, typically 'output'
+ @type logfilename: string
+ @param logfilename: the Builder-relative pathname for the saved entries
+ """
+ self.step = parent
+ self.name = name
+ self.filename = logfilename
+ fn = self.getFilename()
+ if os.path.exists(fn):
+ # the buildmaster was probably stopped abruptly, before the
+ # BuilderStatus could be saved, so BuilderStatus.nextBuildNumber
+ # is out of date, and we're overlapping with earlier builds now.
+ # Warn about it, but then overwrite the old pickle file
+ log.msg("Warning: Overwriting old serialized Build at %s" % fn)
+ self.openfile = open(fn, "w+")
+ self.runEntries = []
+ self.watchers = []
+ self.finishedWatchers = []
+
+ def getFilename(self):
+ return os.path.join(self.step.build.builder.basedir, self.filename)
+
+ def hasContents(self):
+ return os.path.exists(self.getFilename() + '.bz2') or \
+ os.path.exists(self.getFilename())
+
+ def getName(self):
+ return self.name
+
+ def getStep(self):
+ return self.step
+
+ def isFinished(self):
+ return self.finished
+ def waitUntilFinished(self):
+ if self.finished:
+ d = defer.succeed(self)
+ else:
+ d = defer.Deferred()
+ self.finishedWatchers.append(d)
+ return d
+
+ def getFile(self):
+ if self.openfile:
+ # this is the filehandle we're using to write to the log, so
+ # don't close it!
+ return self.openfile
+ # otherwise they get their own read-only handle
+ # try a compressed log first
+ try:
+ return BZ2File(self.getFilename() + ".bz2", "r")
+ except IOError:
+ pass
+ return open(self.getFilename(), "r")
+
+ def getText(self):
+ # this produces one ginormous string
+ return "".join(self.getChunks([STDOUT, STDERR], onlyText=True))
+
+ def getTextWithHeaders(self):
+ return "".join(self.getChunks(onlyText=True))
+
+ def getChunks(self, channels=[], onlyText=False):
+ # generate chunks for everything that was logged at the time we were
+ # first called, so remember how long the file was when we started.
+ # Don't read beyond that point. The current contents of
+ # self.runEntries will follow.
+
+ # this returns an iterator, which means arbitrary things could happen
+ # while we're yielding. This will faithfully deliver the log as it
+ # existed when it was started, and not return anything after that
+ # point. To use this in subscribe(catchup=True) without missing any
+ # data, you must insure that nothing will be added to the log during
+ # yield() calls.
+
+ f = self.getFile()
+ offset = 0
+ f.seek(0, 2)
+ remaining = f.tell()
+
+ leftover = None
+ if self.runEntries and (not channels or
+ (self.runEntries[0][0] in channels)):
+ leftover = (self.runEntries[0][0],
+ "".join([c[1] for c in self.runEntries]))
+
+ # freeze the state of the LogFile by passing a lot of parameters into
+ # a generator
+ return self._generateChunks(f, offset, remaining, leftover,
+ channels, onlyText)
+
+ def _generateChunks(self, f, offset, remaining, leftover,
+ channels, onlyText):
+ chunks = []
+ p = LogFileScanner(chunks.append, channels)
+ f.seek(offset)
+ data = f.read(min(remaining, self.BUFFERSIZE))
+ remaining -= len(data)
+ offset = f.tell()
+ while data:
+ p.dataReceived(data)
+ while chunks:
+ channel, text = chunks.pop(0)
+ if onlyText:
+ yield text
+ else:
+ yield (channel, text)
+ f.seek(offset)
+ data = f.read(min(remaining, self.BUFFERSIZE))
+ remaining -= len(data)
+ offset = f.tell()
+ del f
+
+ if leftover:
+ if onlyText:
+ yield leftover[1]
+ else:
+ yield leftover
+
+ def readlines(self, channel=STDOUT):
+ """Return an iterator that produces newline-terminated lines,
+ excluding header chunks."""
+ # TODO: make this memory-efficient, by turning it into a generator
+ # that retrieves chunks as necessary, like a pull-driven version of
+ # twisted.protocols.basic.LineReceiver
+ alltext = "".join(self.getChunks([channel], onlyText=True))
+ io = StringIO(alltext)
+ return io.readlines()
+
+ def subscribe(self, receiver, catchup):
+ if self.finished:
+ return
+ self.watchers.append(receiver)
+ if catchup:
+ for channel, text in self.getChunks():
+ # TODO: add logChunks(), to send over everything at once?
+ receiver.logChunk(self.step.build, self.step, self,
+ channel, text)
+
+ def unsubscribe(self, receiver):
+ if receiver in self.watchers:
+ self.watchers.remove(receiver)
+
+ def subscribeConsumer(self, consumer):
+ p = LogFileProducer(self, consumer)
+ p.resumeProducing()
+
+ # interface used by the build steps to add things to the log
+
+ def merge(self):
+ # merge all .runEntries (which are all of the same type) into a
+ # single chunk for .entries
+ if not self.runEntries:
+ return
+ channel = self.runEntries[0][0]
+ text = "".join([c[1] for c in self.runEntries])
+ assert channel < 10
+ f = self.openfile
+ f.seek(0, 2)
+ offset = 0
+ while offset < len(text):
+ size = min(len(text)-offset, self.chunkSize)
+ f.write("%d:%d" % (1 + size, channel))
+ f.write(text[offset:offset+size])
+ f.write(",")
+ offset += size
+ self.runEntries = []
+ self.runLength = 0
+
+ def addEntry(self, channel, text):
+ assert not self.finished
+ # we only add to .runEntries here. merge() is responsible for adding
+ # merged chunks to .entries
+ if self.runEntries and channel != self.runEntries[0][0]:
+ self.merge()
+ self.runEntries.append((channel, text))
+ self.runLength += len(text)
+ if self.runLength >= self.chunkSize:
+ self.merge()
+
+ for w in self.watchers:
+ w.logChunk(self.step.build, self.step, self, channel, text)
+ self.length += len(text)
+
+ def addStdout(self, text):
+ self.addEntry(STDOUT, text)
+ def addStderr(self, text):
+ self.addEntry(STDERR, text)
+ def addHeader(self, text):
+ self.addEntry(HEADER, text)
+
+ def finish(self):
+ self.merge()
+ if self.openfile:
+ # we don't do an explicit close, because there might be readers
+ # shareing the filehandle. As soon as they stop reading, the
+ # filehandle will be released and automatically closed. We will
+ # do a sync, however, to make sure the log gets saved in case of
+ # a crash.
+ self.openfile.flush()
+ os.fsync(self.openfile.fileno())
+ del self.openfile
+ self.finished = True
+ watchers = self.finishedWatchers
+ self.finishedWatchers = []
+ for w in watchers:
+ w.callback(self)
+ self.watchers = []
+
+
+ def compressLog(self):
+ compressed = self.getFilename() + ".bz2.tmp"
+ d = threads.deferToThread(self._compressLog, compressed)
+ d.addCallback(self._renameCompressedLog, compressed)
+ d.addErrback(self._cleanupFailedCompress, compressed)
+ return d
+
+ def _compressLog(self, compressed):
+ infile = self.getFile()
+ cf = BZ2File(compressed, 'w')
+ bufsize = 1024*1024
+ while True:
+ buf = infile.read(bufsize)
+ cf.write(buf)
+ if len(buf) < bufsize:
+ break
+ cf.close()
+ def _renameCompressedLog(self, rv, compressed):
+ filename = self.getFilename() + '.bz2'
+ if sys.platform == 'win32':
+ # windows cannot rename a file on top of an existing one, so
+ # fall back to delete-first. There are ways this can fail and
+ # lose the builder's history, so we avoid using it in the
+ # general (non-windows) case
+ if os.path.exists(filename):
+ os.unlink(filename)
+ os.rename(compressed, filename)
+ _tryremove(self.getFilename(), 1, 5)
+ def _cleanupFailedCompress(self, failure, compressed):
+ log.msg("failed to compress %s" % self.getFilename())
+ if os.path.exists(compressed):
+ _tryremove(compressed, 1, 5)
+ failure.trap() # reraise the failure
+
+ # persistence stuff
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ del d['step'] # filled in upon unpickling
+ del d['watchers']
+ del d['finishedWatchers']
+ d['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
+ if d.has_key('finished'):
+ del d['finished']
+ if d.has_key('openfile'):
+ del d['openfile']
+ return d
+
+ def __setstate__(self, d):
+ self.__dict__ = d
+ self.watchers = [] # probably not necessary
+ self.finishedWatchers = [] # same
+ # self.step must be filled in by our parent
+ self.finished = True
+
+ def upgrade(self, logfilename):
+ """Save our .entries to a new-style offline log file (if necessary),
+ and modify our in-memory representation to use it. The original
+ pickled LogFile (inside the pickled Build) won't be modified."""
+ self.filename = logfilename
+ if not os.path.exists(self.getFilename()):
+ self.openfile = open(self.getFilename(), "w")
+ self.finished = False
+ for channel,text in self.entries:
+ self.addEntry(channel, text)
+ self.finish() # releases self.openfile, which will be closed
+ del self.entries
+
+class HTMLLogFile:
+ implements(interfaces.IStatusLog)
+
+ filename = None
+
+ def __init__(self, parent, name, logfilename, html):
+ self.step = parent
+ self.name = name
+ self.filename = logfilename
+ self.html = html
+
+ def getName(self):
+ return self.name # set in BuildStepStatus.addLog
+ def getStep(self):
+ return self.step
+
+ def isFinished(self):
+ return True
+ def waitUntilFinished(self):
+ return defer.succeed(self)
+
+ def hasContents(self):
+ return True
+ def getText(self):
+ return self.html # looks kinda like text
+ def getTextWithHeaders(self):
+ return self.html
+ def getChunks(self):
+ return [(STDERR, self.html)]
+
+ def subscribe(self, receiver, catchup):
+ pass
+ def unsubscribe(self, receiver):
+ pass
+
+ def finish(self):
+ pass
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ del d['step']
+ return d
+
+ def upgrade(self, logfilename):
+ pass
+
+
+class Event:
+ implements(interfaces.IStatusEvent)
+
+ started = None
+ finished = None
+ text = []
+
+ # IStatusEvent methods
+ def getTimes(self):
+ return (self.started, self.finished)
+ def getText(self):
+ return self.text
+ def getLogs(self):
+ return []
+
+ def finish(self):
+ self.finished = util.now()
+
+class TestResult:
+ implements(interfaces.ITestResult)
+
+ def __init__(self, name, results, text, logs):
+ assert isinstance(name, tuple)
+ self.name = name
+ self.results = results
+ self.text = text
+ self.logs = logs
+
+ def getName(self):
+ return self.name
+
+ def getResults(self):
+ return self.results
+
+ def getText(self):
+ return self.text
+
+ def getLogs(self):
+ return self.logs
+
+
+class BuildSetStatus:
+ implements(interfaces.IBuildSetStatus)
+
+ def __init__(self, source, reason, builderNames, bsid=None):
+ self.source = source
+ self.reason = reason
+ self.builderNames = builderNames
+ self.id = bsid
+ self.successWatchers = []
+ self.finishedWatchers = []
+ self.stillHopeful = True
+ self.finished = False
+
+ def setBuildRequestStatuses(self, buildRequestStatuses):
+ self.buildRequests = buildRequestStatuses
+ def setResults(self, results):
+ # the build set succeeds only if all its component builds succeed
+ self.results = results
+ def giveUpHope(self):
+ self.stillHopeful = False
+
+
+ def notifySuccessWatchers(self):
+ for d in self.successWatchers:
+ d.callback(self)
+ self.successWatchers = []
+
+ def notifyFinishedWatchers(self):
+ self.finished = True
+ for d in self.finishedWatchers:
+ d.callback(self)
+ self.finishedWatchers = []
+
+ # methods for our clients
+
+ def getSourceStamp(self):
+ return self.source
+ def getReason(self):
+ return self.reason
+ def getResults(self):
+ return self.results
+ def getID(self):
+ return self.id
+
+ def getBuilderNames(self):
+ return self.builderNames
+ def getBuildRequests(self):
+ return self.buildRequests
+ def isFinished(self):
+ return self.finished
+
+ def waitUntilSuccess(self):
+ if self.finished or not self.stillHopeful:
+ # the deferreds have already fired
+ return defer.succeed(self)
+ d = defer.Deferred()
+ self.successWatchers.append(d)
+ return d
+
+ def waitUntilFinished(self):
+ if self.finished:
+ return defer.succeed(self)
+ d = defer.Deferred()
+ self.finishedWatchers.append(d)
+ return d
+
+class BuildRequestStatus:
+ implements(interfaces.IBuildRequestStatus)
+
+ def __init__(self, source, builderName):
+ self.source = source
+ self.builderName = builderName
+ self.builds = [] # list of BuildStatus objects
+ self.observers = []
+ self.submittedAt = None
+
+ def buildStarted(self, build):
+ self.builds.append(build)
+ for o in self.observers[:]:
+ o(build)
+
+ # methods called by our clients
+ def getSourceStamp(self):
+ return self.source
+ def getBuilderName(self):
+ return self.builderName
+ def getBuilds(self):
+ return self.builds
+
+ def subscribe(self, observer):
+ self.observers.append(observer)
+ for b in self.builds:
+ observer(b)
+ def unsubscribe(self, observer):
+ self.observers.remove(observer)
+
+ def getSubmitTime(self):
+ return self.submittedAt
+ def setSubmitTime(self, t):
+ self.submittedAt = t
+
+
+class BuildStepStatus(styles.Versioned):
+ """
+ I represent a collection of output status for a
+ L{buildbot.process.step.BuildStep}.
+
+ Statistics contain any information gleaned from a step that is
+ not in the form of a logfile. As an example, steps that run
+ tests might gather statistics about the number of passed, failed,
+ or skipped tests.
+
+ @type progress: L{buildbot.status.progress.StepProgress}
+ @cvar progress: tracks ETA for the step
+ @type text: list of strings
+ @cvar text: list of short texts that describe the command and its status
+ @type text2: list of strings
+ @cvar text2: list of short texts added to the overall build description
+ @type logs: dict of string -> L{buildbot.status.builder.LogFile}
+ @ivar logs: logs of steps
+ @type statistics: dict
+ @ivar statistics: results from running this step
+ """
+ # note that these are created when the Build is set up, before each
+ # corresponding BuildStep has started.
+ implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
+ persistenceVersion = 2
+
+ started = None
+ finished = None
+ progress = None
+ text = []
+ results = (None, [])
+ text2 = []
+ watchers = []
+ updates = {}
+ finishedWatchers = []
+ statistics = {}
+
+ def __init__(self, parent):
+ assert interfaces.IBuildStatus(parent)
+ self.build = parent
+ self.logs = []
+ self.urls = {}
+ self.watchers = []
+ self.updates = {}
+ self.finishedWatchers = []
+ self.statistics = {}
+
+ def getName(self):
+ """Returns a short string with the name of this step. This string
+ may have spaces in it."""
+ return self.name
+
+ def getBuild(self):
+ return self.build
+
+ def getTimes(self):
+ return (self.started, self.finished)
+
+ def getExpectations(self):
+ """Returns a list of tuples (name, current, target)."""
+ if not self.progress:
+ return []
+ ret = []
+ metrics = self.progress.progress.keys()
+ metrics.sort()
+ for m in metrics:
+ t = (m, self.progress.progress[m], self.progress.expectations[m])
+ ret.append(t)
+ return ret
+
+ def getLogs(self):
+ return self.logs
+
+ def getURLs(self):
+ return self.urls.copy()
+
+ def isFinished(self):
+ return (self.finished is not None)
+
+ def waitUntilFinished(self):
+ if self.finished:
+ d = defer.succeed(self)
+ else:
+ d = defer.Deferred()
+ self.finishedWatchers.append(d)
+ return d
+
+ # while the step is running, the following methods make sense.
+ # Afterwards they return None
+
+ def getETA(self):
+ if self.started is None:
+ return None # not started yet
+ if self.finished is not None:
+ return None # already finished
+ if not self.progress:
+ return None # no way to predict
+ return self.progress.remaining()
+
+ # Once you know the step has finished, the following methods are legal.
+ # Before this step has finished, they all return None.
+
+ def getText(self):
+ """Returns a list of strings which describe the step. These are
+ intended to be displayed in a narrow column. If more space is
+ available, the caller should join them together with spaces before
+ presenting them to the user."""
+ return self.text
+
+ def getResults(self):
+ """Return a tuple describing the results of the step.
+ 'result' is one of the constants in L{buildbot.status.builder}:
+ SUCCESS, WARNINGS, FAILURE, or SKIPPED.
+ 'strings' is an optional list of strings that the step wants to
+ append to the overall build's results. These strings are usually
+ more terse than the ones returned by getText(): in particular,
+ successful Steps do not usually contribute any text to the
+ overall build.
+
+ @rtype: tuple of int, list of strings
+ @returns: (result, strings)
+ """
+ return (self.results, self.text2)
+
+ def hasStatistic(self, name):
+ """Return true if this step has a value for the given statistic.
+ """
+ return self.statistics.has_key(name)
+
+ def getStatistic(self, name, default=None):
+ """Return the given statistic, if present
+ """
+ return self.statistics.get(name, default)
+
+ # subscription interface
+
+ def subscribe(self, receiver, updateInterval=10):
+ # will get logStarted, logFinished, stepETAUpdate
+ assert receiver not in self.watchers
+ self.watchers.append(receiver)
+ self.sendETAUpdate(receiver, updateInterval)
+
+ def sendETAUpdate(self, receiver, updateInterval):
+ self.updates[receiver] = None
+ # they might unsubscribe during stepETAUpdate
+ receiver.stepETAUpdate(self.build, self,
+ self.getETA(), self.getExpectations())
+ if receiver in self.watchers:
+ self.updates[receiver] = reactor.callLater(updateInterval,
+ self.sendETAUpdate,
+ receiver,
+ updateInterval)
+
+ def unsubscribe(self, receiver):
+ if receiver in self.watchers:
+ self.watchers.remove(receiver)
+ if receiver in self.updates:
+ if self.updates[receiver] is not None:
+ self.updates[receiver].cancel()
+ del self.updates[receiver]
+
+
+ # methods to be invoked by the BuildStep
+
+ def setName(self, stepname):
+ self.name = stepname
+
+ def setColor(self, color):
+ log.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color,))
+
+ def setProgress(self, stepprogress):
+ self.progress = stepprogress
+
+ def stepStarted(self):
+ self.started = util.now()
+ if self.build:
+ self.build.stepStarted(self)
+
+ def addLog(self, name):
+ assert self.started # addLog before stepStarted won't notify watchers
+ logfilename = self.build.generateLogfileName(self.name, name)
+ log = LogFile(self, name, logfilename)
+ self.logs.append(log)
+ for w in self.watchers:
+ receiver = w.logStarted(self.build, self, log)
+ if receiver:
+ log.subscribe(receiver, True)
+ d = log.waitUntilFinished()
+ d.addCallback(lambda log: log.unsubscribe(receiver))
+ d = log.waitUntilFinished()
+ d.addCallback(self.logFinished)
+ return log
+
+ def addHTMLLog(self, name, html):
+ assert self.started # addLog before stepStarted won't notify watchers
+ logfilename = self.build.generateLogfileName(self.name, name)
+ log = HTMLLogFile(self, name, logfilename, html)
+ self.logs.append(log)
+ for w in self.watchers:
+ receiver = w.logStarted(self.build, self, log)
+ # TODO: think about this: there isn't much point in letting
+ # them subscribe
+ #if receiver:
+ # log.subscribe(receiver, True)
+ w.logFinished(self.build, self, log)
+
+ def logFinished(self, log):
+ for w in self.watchers:
+ w.logFinished(self.build, self, log)
+
+ def addURL(self, name, url):
+ self.urls[name] = url
+
+ def setText(self, text):
+ self.text = text
+ for w in self.watchers:
+ w.stepTextChanged(self.build, self, text)
+ def setText2(self, text):
+ self.text2 = text
+ for w in self.watchers:
+ w.stepText2Changed(self.build, self, text)
+
+ def setStatistic(self, name, value):
+ """Set the given statistic. Usually called by subclasses.
+ """
+ self.statistics[name] = value
+
+ def stepFinished(self, results):
+ self.finished = util.now()
+ self.results = results
+ cld = [] # deferreds for log compression
+ logCompressionLimit = self.build.builder.logCompressionLimit
+ for loog in self.logs:
+ if not loog.isFinished():
+ loog.finish()
+ # if log compression is on, and it's a real LogFile,
+ # HTMLLogFiles aren't files
+ if logCompressionLimit is not False and \
+ isinstance(loog, LogFile):
+ if os.path.getsize(loog.getFilename()) > logCompressionLimit:
+ cld.append(loog.compressLog())
+
+ for r in self.updates.keys():
+ if self.updates[r] is not None:
+ self.updates[r].cancel()
+ del self.updates[r]
+
+ watchers = self.finishedWatchers
+ self.finishedWatchers = []
+ for w in watchers:
+ w.callback(self)
+ if cld:
+ return defer.DeferredList(cld)
+
+ # persistence
+
+ def __getstate__(self):
+ d = styles.Versioned.__getstate__(self)
+ del d['build'] # filled in when loading
+ if d.has_key('progress'):
+ del d['progress']
+ del d['watchers']
+ del d['finishedWatchers']
+ del d['updates']
+ return d
+
+ def __setstate__(self, d):
+ styles.Versioned.__setstate__(self, d)
+ # self.build must be filled in by our parent
+ for loog in self.logs:
+ loog.step = self
+
+ def upgradeToVersion1(self):
+ if not hasattr(self, "urls"):
+ self.urls = {}
+
+ def upgradeToVersion2(self):
+ if not hasattr(self, "statistics"):
+ self.statistics = {}
+
+
+class BuildStatus(styles.Versioned):
+ implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
+ persistenceVersion = 3
+
+ source = None
+ reason = None
+ changes = []
+ blamelist = []
+ requests = []
+ progress = None
+ started = None
+ finished = None
+ currentStep = None
+ text = []
+ results = None
+ slavename = "???"
+
+ # these lists/dicts are defined here so that unserialized instances have
+ # (empty) values. They are set in __init__ to new objects to make sure
+ # each instance gets its own copy.
+ watchers = []
+ updates = {}
+ finishedWatchers = []
+ testResults = {}
+
+ def __init__(self, parent, number):
+ """
+ @type parent: L{BuilderStatus}
+ @type number: int
+ """
+ assert interfaces.IBuilderStatus(parent)
+ self.builder = parent
+ self.number = number
+ self.watchers = []
+ self.updates = {}
+ self.finishedWatchers = []
+ self.steps = []
+ self.testResults = {}
+ self.properties = Properties()
+ self.requests = []
+
+ # IBuildStatus
+
+ def getBuilder(self):
+ """
+ @rtype: L{BuilderStatus}
+ """
+ return self.builder
+
+ def getProperty(self, propname):
+ return self.properties[propname]
+
+ def getProperties(self):
+ return self.properties
+
+ def getNumber(self):
+ return self.number
+
+ def getPreviousBuild(self):
+ if self.number == 0:
+ return None
+ return self.builder.getBuild(self.number-1)
+
+ def getSourceStamp(self, absolute=False):
+ if not absolute or not self.properties.has_key('got_revision'):
+ return self.source
+ return self.source.getAbsoluteSourceStamp(self.properties['got_revision'])
+
+ def getReason(self):
+ return self.reason
+
+ def getChanges(self):
+ return self.changes
+
+ def getRequests(self):
+ return self.requests
+
+ def getResponsibleUsers(self):
+ return self.blamelist
+
+ def getInterestedUsers(self):
+ # TODO: the Builder should add others: sheriffs, domain-owners
+ return self.blamelist + self.properties.getProperty('owners', [])
+
+ def getSteps(self):
+ """Return a list of IBuildStepStatus objects. For invariant builds
+ (those which always use the same set of Steps), this should be the
+ complete list, however some of the steps may not have started yet
+ (step.getTimes()[0] will be None). For variant builds, this may not
+ be complete (asking again later may give you more of them)."""
+ return self.steps
+
+ def getTimes(self):
+ return (self.started, self.finished)
+
+ _sentinel = [] # used as a sentinel to indicate unspecified initial_value
+ def getSummaryStatistic(self, name, summary_fn, initial_value=_sentinel):
+ """Summarize the named statistic over all steps in which it
+ exists, using combination_fn and initial_value to combine multiple
+ results into a single result. This translates to a call to Python's
+ X{reduce}::
+ return reduce(summary_fn, step_stats_list, initial_value)
+ """
+ step_stats_list = [
+ st.getStatistic(name)
+ for st in self.steps
+ if st.hasStatistic(name) ]
+ if initial_value is self._sentinel:
+ return reduce(summary_fn, step_stats_list)
+ else:
+ return reduce(summary_fn, step_stats_list, initial_value)
+
+ def isFinished(self):
+ return (self.finished is not None)
+
+ def waitUntilFinished(self):
+ if self.finished:
+ d = defer.succeed(self)
+ else:
+ d = defer.Deferred()
+ self.finishedWatchers.append(d)
+ return d
+
+ # while the build is running, the following methods make sense.
+ # Afterwards they return None
+
+ def getETA(self):
+ if self.finished is not None:
+ return None
+ if not self.progress:
+ return None
+ eta = self.progress.eta()
+ if eta is None:
+ return None
+ return eta - util.now()
+
+ def getCurrentStep(self):
+ return self.currentStep
+
+ # Once you know the build has finished, the following methods are legal.
+ # Before ths build has finished, they all return None.
+
+ def getText(self):
+ text = []
+ text.extend(self.text)
+ for s in self.steps:
+ text.extend(s.text2)
+ return text
+
+ def getResults(self):
+ return self.results
+
+ def getSlavename(self):
+ return self.slavename
+
+ def getTestResults(self):
+ return self.testResults
+
+ def getLogs(self):
+ # TODO: steps should contribute significant logs instead of this
+ # hack, which returns every log from every step. The logs should get
+ # names like "compile" and "test" instead of "compile.output"
+ logs = []
+ for s in self.steps:
+ for log in s.getLogs():
+ logs.append(log)
+ return logs
+
+ # subscription interface
+
+ def subscribe(self, receiver, updateInterval=None):
+ # will receive stepStarted and stepFinished messages
+ # and maybe buildETAUpdate
+ self.watchers.append(receiver)
+ if updateInterval is not None:
+ self.sendETAUpdate(receiver, updateInterval)
+
+ def sendETAUpdate(self, receiver, updateInterval):
+ self.updates[receiver] = None
+ ETA = self.getETA()
+ if ETA is not None:
+ receiver.buildETAUpdate(self, self.getETA())
+ # they might have unsubscribed during buildETAUpdate
+ if receiver in self.watchers:
+ self.updates[receiver] = reactor.callLater(updateInterval,
+ self.sendETAUpdate,
+ receiver,
+ updateInterval)
+
+ def unsubscribe(self, receiver):
+ if receiver in self.watchers:
+ self.watchers.remove(receiver)
+ if receiver in self.updates:
+ if self.updates[receiver] is not None:
+ self.updates[receiver].cancel()
+ del self.updates[receiver]
+
+ # methods for the base.Build to invoke
+
+ def addStepWithName(self, name):
+ """The Build is setting up, and has added a new BuildStep to its
+ list. Create a BuildStepStatus object to which it can send status
+ updates."""
+
+ s = BuildStepStatus(self)
+ s.setName(name)
+ self.steps.append(s)
+ return s
+
+ def setProperty(self, propname, value, source):
+ self.properties.setProperty(propname, value, source)
+
+ def addTestResult(self, result):
+ self.testResults[result.getName()] = result
+
+ def setSourceStamp(self, sourceStamp):
+ self.source = sourceStamp
+ self.changes = self.source.changes
+
+ def setRequests(self, requests):
+ self.requests = requests
+
+ def setReason(self, reason):
+ self.reason = reason
+ def setBlamelist(self, blamelist):
+ self.blamelist = blamelist
+ def setProgress(self, progress):
+ self.progress = progress
+
+ def buildStarted(self, build):
+ """The Build has been set up and is about to be started. It can now
+ be safely queried, so it is time to announce the new build."""
+
+ self.started = util.now()
+ # now that we're ready to report status, let the BuilderStatus tell
+ # the world about us
+ self.builder.buildStarted(self)
+
+ def setSlavename(self, slavename):
+ self.slavename = slavename
+
+ def setText(self, text):
+ assert isinstance(text, (list, tuple))
+ self.text = text
+ def setResults(self, results):
+ self.results = results
+
+ def buildFinished(self):
+ self.currentStep = None
+ self.finished = util.now()
+
+ for r in self.updates.keys():
+ if self.updates[r] is not None:
+ self.updates[r].cancel()
+ del self.updates[r]
+
+ watchers = self.finishedWatchers
+ self.finishedWatchers = []
+ for w in watchers:
+ w.callback(self)
+
+ # methods called by our BuildStepStatus children
+
+ def stepStarted(self, step):
+ self.currentStep = step
+ name = self.getBuilder().getName()
+ for w in self.watchers:
+ receiver = w.stepStarted(self, step)
+ if receiver:
+ if type(receiver) == type(()):
+ step.subscribe(receiver[0], receiver[1])
+ else:
+ step.subscribe(receiver)
+ d = step.waitUntilFinished()
+ d.addCallback(lambda step: step.unsubscribe(receiver))
+
+ step.waitUntilFinished().addCallback(self._stepFinished)
+
+ def _stepFinished(self, step):
+ results = step.getResults()
+ for w in self.watchers:
+ w.stepFinished(self, step, results)
+
+ # methods called by our BuilderStatus parent
+
+ def pruneLogs(self):
+ # this build is somewhat old: remove the build logs to save space
+ # TODO: delete logs visible through IBuildStatus.getLogs
+ for s in self.steps:
+ s.pruneLogs()
+
+ def pruneSteps(self):
+ # this build is very old: remove the build steps too
+ self.steps = []
+
+ # persistence stuff
+
+ def generateLogfileName(self, stepname, logname):
+ """Return a filename (relative to the Builder's base directory) where
+ the logfile's contents can be stored uniquely.
+
+ The base filename is made by combining our build number, the Step's
+ name, and the log's name, then removing unsuitable characters. The
+ filename is then made unique by appending _0, _1, etc, until it does
+ not collide with any other logfile.
+
+ These files are kept in the Builder's basedir (rather than a
+ per-Build subdirectory) because that makes cleanup easier: cron and
+ find will help get rid of the old logs, but the empty directories are
+ more of a hassle to remove."""
+
+ starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
+ starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
+ # now make it unique
+ unique_counter = 0
+ filename = starting_filename
+ while filename in [l.filename
+ for step in self.steps
+ for l in step.getLogs()
+ if l.filename]:
+ filename = "%s_%d" % (starting_filename, unique_counter)
+ unique_counter += 1
+ return filename
+
+ def __getstate__(self):
+ d = styles.Versioned.__getstate__(self)
+ # for now, a serialized Build is always "finished". We will never
+ # save unfinished builds.
+ if not self.finished:
+ d['finished'] = True
+ # TODO: push an "interrupted" step so it is clear that the build
+ # was interrupted. The builder will have a 'shutdown' event, but
+ # someone looking at just this build will be confused as to why
+ # the last log is truncated.
+ del d['builder'] # filled in by our parent when loading
+ del d['watchers']
+ del d['updates']
+ del d['requests']
+ del d['finishedWatchers']
+ return d
+
+ def __setstate__(self, d):
+ styles.Versioned.__setstate__(self, d)
+ # self.builder must be filled in by our parent when loading
+ for step in self.steps:
+ step.build = self
+ self.watchers = []
+ self.updates = {}
+ self.finishedWatchers = []
+
+ def upgradeToVersion1(self):
+ if hasattr(self, "sourceStamp"):
+ # the old .sourceStamp attribute wasn't actually very useful
+ maxChangeNumber, patch = self.sourceStamp
+ changes = getattr(self, 'changes', [])
+ source = sourcestamp.SourceStamp(branch=None,
+ revision=None,
+ patch=patch,
+ changes=changes)
+ self.source = source
+ self.changes = source.changes
+ del self.sourceStamp
+
+ def upgradeToVersion2(self):
+ self.properties = {}
+
+ def upgradeToVersion3(self):
+ # in version 3, self.properties became a Properties object
+ propdict = self.properties
+ self.properties = Properties()
+ self.properties.update(propdict, "Upgrade from previous version")
+
+ def upgradeLogfiles(self):
+ # upgrade any LogFiles that need it. This must occur after we've been
+ # attached to our Builder, and after we know about all LogFiles of
+ # all Steps (to get the filenames right).
+ assert self.builder
+ for s in self.steps:
+ for l in s.getLogs():
+ if l.filename:
+ pass # new-style, log contents are on disk
+ else:
+ logfilename = self.generateLogfileName(s.name, l.name)
+ # let the logfile update its .filename pointer,
+ # transferring its contents onto disk if necessary
+ l.upgrade(logfilename)
+
+ def saveYourself(self):
+ filename = os.path.join(self.builder.basedir, "%d" % self.number)
+ if os.path.isdir(filename):
+ # leftover from 0.5.0, which stored builds in directories
+ shutil.rmtree(filename, ignore_errors=True)
+ tmpfilename = filename + ".tmp"
+ try:
+ dump(self, open(tmpfilename, "wb"), -1)
+ if sys.platform == 'win32':
+ # windows cannot rename a file on top of an existing one, so
+ # fall back to delete-first. There are ways this can fail and
+ # lose the builder's history, so we avoid using it in the
+ # general (non-windows) case
+ if os.path.exists(filename):
+ os.unlink(filename)
+ os.rename(tmpfilename, filename)
+ except:
+ log.msg("unable to save build %s-#%d" % (self.builder.name,
+ self.number))
+ log.err()
+
+
+
+class BuilderStatus(styles.Versioned):
+ """I handle status information for a single process.base.Builder object.
+ That object sends status changes to me (frequently as Events), and I
+ provide them on demand to the various status recipients, like the HTML
+ waterfall display and the live status clients. It also sends build
+ summaries to me, which I log and provide to status clients who aren't
+ interested in seeing details of the individual build steps.
+
+ I am responsible for maintaining the list of historic Events and Builds,
+ pruning old ones, and loading them from / saving them to disk.
+
+ I live in the buildbot.process.base.Builder object, in the
+ .builder_status attribute.
+
+ @type category: string
+ @ivar category: user-defined category this builder belongs to; can be
+ used to filter on in status clients
+ """
+
+ implements(interfaces.IBuilderStatus, interfaces.IEventSource)
+ persistenceVersion = 1
+
+ # these limit the amount of memory we consume, as well as the size of the
+ # main Builder pickle. The Build and LogFile pickles on disk must be
+ # handled separately.
+ buildCacheSize = 30
+ buildHorizon = 100 # forget builds beyond this
+ stepHorizon = 50 # forget steps in builds beyond this
+
+ category = None
+ currentBigState = "offline" # or idle/waiting/interlocked/building
+ basedir = None # filled in by our parent
+
+ def __init__(self, buildername, category=None):
+ self.name = buildername
+ self.category = category
+
+ self.slavenames = []
+ self.events = []
+ # these three hold Events, and are used to retrieve the current
+ # state of the boxes.
+ self.lastBuildStatus = None
+ #self.currentBig = None
+ #self.currentSmall = None
+ self.currentBuilds = []
+ self.pendingBuilds = []
+ self.nextBuild = None
+ self.watchers = []
+ self.buildCache = [] # TODO: age builds out of the cache
+ self.logCompressionLimit = False # default to no compression for tests
+
+ # persistence
+
+ def __getstate__(self):
+ # when saving, don't record transient stuff like what builds are
+ # currently running, because they won't be there when we start back
+ # up. Nor do we save self.watchers, nor anything that gets set by our
+ # parent like .basedir and .status
+ d = styles.Versioned.__getstate__(self)
+ d['watchers'] = []
+ del d['buildCache']
+ for b in self.currentBuilds:
+ b.saveYourself()
+ # TODO: push a 'hey, build was interrupted' event
+ del d['currentBuilds']
+ del d['pendingBuilds']
+ del d['currentBigState']
+ del d['basedir']
+ del d['status']
+ del d['nextBuildNumber']
+ return d
+
+ def __setstate__(self, d):
+ # when loading, re-initialize the transient stuff. Remember that
+ # upgradeToVersion1 and such will be called after this finishes.
+ styles.Versioned.__setstate__(self, d)
+ self.buildCache = []
+ self.currentBuilds = []
+ self.pendingBuilds = []
+ self.watchers = []
+ self.slavenames = []
+ # self.basedir must be filled in by our parent
+ # self.status must be filled in by our parent
+
+ def upgradeToVersion1(self):
+ if hasattr(self, 'slavename'):
+ self.slavenames = [self.slavename]
+ del self.slavename
+ if hasattr(self, 'nextBuildNumber'):
+ del self.nextBuildNumber # determineNextBuildNumber chooses this
+
+ def determineNextBuildNumber(self):
+ """Scan our directory of saved BuildStatus instances to determine
+ what our self.nextBuildNumber should be. Set it one larger than the
+ highest-numbered build we discover. This is called by the top-level
+ Status object shortly after we are created or loaded from disk.
+ """
+ existing_builds = [int(f)
+ for f in os.listdir(self.basedir)
+ if re.match("^\d+$", f)]
+ if existing_builds:
+ self.nextBuildNumber = max(existing_builds) + 1
+ else:
+ self.nextBuildNumber = 0
+
+ def setLogCompressionLimit(self, lowerLimit):
+ self.logCompressionLimit = lowerLimit
+
+ def saveYourself(self):
+ for b in self.buildCache:
+ if not b.isFinished:
+ # interrupted build, need to save it anyway.
+ # BuildStatus.saveYourself will mark it as interrupted.
+ b.saveYourself()
+ filename = os.path.join(self.basedir, "builder")
+ tmpfilename = filename + ".tmp"
+ try:
+ dump(self, open(tmpfilename, "wb"), -1)
+ if sys.platform == 'win32':
+ # windows cannot rename a file on top of an existing one
+ if os.path.exists(filename):
+ os.unlink(filename)
+ os.rename(tmpfilename, filename)
+ except:
+ log.msg("unable to save builder %s" % self.name)
+ log.err()
+
+
+ # build cache management
+
+ def addBuildToCache(self, build):
+ if build in self.buildCache:
+ return
+ self.buildCache.append(build)
+ while len(self.buildCache) > self.buildCacheSize:
+ self.buildCache.pop(0)
+
+ def getBuildByNumber(self, number):
+ for b in self.currentBuilds:
+ if b.number == number:
+ return b
+ for build in self.buildCache:
+ if build.number == number:
+ return build
+ filename = os.path.join(self.basedir, "%d" % number)
+ try:
+ build = load(open(filename, "rb"))
+ styles.doUpgrade()
+ build.builder = self
+ # handle LogFiles from after 0.5.0 and before 0.6.5
+ build.upgradeLogfiles()
+ self.addBuildToCache(build)
+ return build
+ except IOError:
+ raise IndexError("no such build %d" % number)
+ except EOFError:
+ raise IndexError("corrupted build pickle %d" % number)
+
+ def prune(self):
+ return # TODO: change this to walk through the filesystem
+ # first, blow away all builds beyond our build horizon
+ self.builds = self.builds[-self.buildHorizon:]
+ # then prune steps in builds past the step horizon
+ for b in self.builds[0:-self.stepHorizon]:
+ b.pruneSteps()
+
+ # IBuilderStatus methods
+ def getName(self):
+ return self.name
+
+ def getState(self):
+ return (self.currentBigState, self.currentBuilds)
+
+ def getSlaves(self):
+ return [self.status.getSlave(name) for name in self.slavenames]
+
+ def getPendingBuilds(self):
+ return self.pendingBuilds
+
+ def getCurrentBuilds(self):
+ return self.currentBuilds
+
+ def getLastFinishedBuild(self):
+ b = self.getBuild(-1)
+ if not (b and b.isFinished()):
+ b = self.getBuild(-2)
+ return b
+
+ def getBuild(self, number):
+ if number < 0:
+ number = self.nextBuildNumber + number
+ if number < 0 or number >= self.nextBuildNumber:
+ return None
+
+ try:
+ return self.getBuildByNumber(number)
+ except IndexError:
+ return None
+
+ def getEvent(self, number):
+ try:
+ return self.events[number]
+ except IndexError:
+ return None
+
+ def generateFinishedBuilds(self, branches=[],
+ num_builds=None,
+ max_buildnum=None,
+ finished_before=None,
+ max_search=200):
+ got = 0
+ for Nb in itertools.count(1):
+ if Nb > self.nextBuildNumber:
+ break
+ if Nb > max_search:
+ break
+ build = self.getBuild(-Nb)
+ if build is None:
+ continue
+ if max_buildnum is not None:
+ if build.getNumber() > max_buildnum:
+ continue
+ if not build.isFinished():
+ continue
+ if finished_before is not None:
+ start, end = build.getTimes()
+ if end >= finished_before:
+ continue
+ if branches:
+ if build.getSourceStamp().branch not in branches:
+ continue
+ got += 1
+ yield build
+ if num_builds is not None:
+ if got >= num_builds:
+ return
+
+ def eventGenerator(self, branches=[]):
+ """This function creates a generator which will provide all of this
+ Builder's status events, starting with the most recent and
+ progressing backwards in time. """
+
+ # remember the oldest-to-earliest flow here. "next" means earlier.
+
+ # TODO: interleave build steps and self.events by timestamp.
+ # TODO: um, I think we're already doing that.
+
+ # TODO: there's probably something clever we could do here to
+ # interleave two event streams (one from self.getBuild and the other
+ # from self.getEvent), which would be simpler than this control flow
+
+ eventIndex = -1
+ e = self.getEvent(eventIndex)
+ for Nb in range(1, self.nextBuildNumber+1):
+ b = self.getBuild(-Nb)
+ if not b:
+ break
+ if branches and not b.getSourceStamp().branch in branches:
+ continue
+ steps = b.getSteps()
+ for Ns in range(1, len(steps)+1):
+ if steps[-Ns].started:
+ step_start = steps[-Ns].getTimes()[0]
+ while e is not None and e.getTimes()[0] > step_start:
+ yield e
+ eventIndex -= 1
+ e = self.getEvent(eventIndex)
+ yield steps[-Ns]
+ yield b
+ while e is not None:
+ yield e
+ eventIndex -= 1
+ e = self.getEvent(eventIndex)
+
+ def subscribe(self, receiver):
+ # will get builderChangedState, buildStarted, and buildFinished
+ self.watchers.append(receiver)
+ self.publishState(receiver)
+
+ def unsubscribe(self, receiver):
+ self.watchers.remove(receiver)
+
+ ## Builder interface (methods called by the Builder which feeds us)
+
+ def setSlavenames(self, names):
+ self.slavenames = names
+
+ def addEvent(self, text=[]):
+ # this adds a duration event. When it is done, the user should call
+ # e.finish(). They can also mangle it by modifying .text
+ e = Event()
+ e.started = util.now()
+ e.text = text
+ self.events.append(e)
+ return e # they are free to mangle it further
+
+ def addPointEvent(self, text=[]):
+ # this adds a point event, one which occurs as a single atomic
+ # instant of time.
+ e = Event()
+ e.started = util.now()
+ e.finished = 0
+ e.text = text
+ self.events.append(e)
+ return e # for consistency, but they really shouldn't touch it
+
+ def setBigState(self, state):
+ needToUpdate = state != self.currentBigState
+ self.currentBigState = state
+ if needToUpdate:
+ self.publishState()
+
+ def publishState(self, target=None):
+ state = self.currentBigState
+
+ if target is not None:
+ # unicast
+ target.builderChangedState(self.name, state)
+ return
+ for w in self.watchers:
+ try:
+ w.builderChangedState(self.name, state)
+ except:
+ log.msg("Exception caught publishing state to %r" % w)
+ log.err()
+
+ def newBuild(self):
+ """The Builder has decided to start a build, but the Build object is
+ not yet ready to report status (it has not finished creating the
+ Steps). Create a BuildStatus object that it can use."""
+ number = self.nextBuildNumber
+ self.nextBuildNumber += 1
+ # TODO: self.saveYourself(), to make sure we don't forget about the
+ # build number we've just allocated. This is not quite as important
+ # as it was before we switch to determineNextBuildNumber, but I think
+ # it may still be useful to have the new build save itself.
+ s = BuildStatus(self, number)
+ s.waitUntilFinished().addCallback(self._buildFinished)
+ return s
+
+ def addBuildRequest(self, brstatus):
+ self.pendingBuilds.append(brstatus)
+ for w in self.watchers:
+ w.requestSubmitted(brstatus)
+
+ def removeBuildRequest(self, brstatus):
+ self.pendingBuilds.remove(brstatus)
+
+ # buildStarted is called by our child BuildStatus instances
+ def buildStarted(self, s):
+ """Now the BuildStatus object is ready to go (it knows all of its
+ Steps, its ETA, etc), so it is safe to notify our watchers."""
+
+ assert s.builder is self # paranoia
+ assert s.number == self.nextBuildNumber - 1
+ assert s not in self.currentBuilds
+ self.currentBuilds.append(s)
+ self.addBuildToCache(s)
+
+ # now that the BuildStatus is prepared to answer queries, we can
+ # announce the new build to all our watchers
+
+ for w in self.watchers: # TODO: maybe do this later? callLater(0)?
+ try:
+ receiver = w.buildStarted(self.getName(), s)
+ if receiver:
+ if type(receiver) == type(()):
+ s.subscribe(receiver[0], receiver[1])
+ else:
+ s.subscribe(receiver)
+ d = s.waitUntilFinished()
+ d.addCallback(lambda s: s.unsubscribe(receiver))
+ except:
+ log.msg("Exception caught notifying %r of buildStarted event" % w)
+ log.err()
+
+ def _buildFinished(self, s):
+ assert s in self.currentBuilds
+ s.saveYourself()
+ self.currentBuilds.remove(s)
+
+ name = self.getName()
+ results = s.getResults()
+ for w in self.watchers:
+ try:
+ w.buildFinished(name, s, results)
+ except:
+ log.msg("Exception caught notifying %r of buildFinished event" % w)
+ log.err()
+
+ self.prune() # conserve disk
+
+
+ # waterfall display (history)
+
+ # I want some kind of build event that holds everything about the build:
+ # why, what changes went into it, the results of the build, itemized
+ # test results, etc. But, I do kind of need something to be inserted in
+ # the event log first, because intermixing step events and the larger
+ # build event is fraught with peril. Maybe an Event-like-thing that
+ # doesn't have a file in it but does have links. Hmm, that's exactly
+ # what it does now. The only difference would be that this event isn't
+ # pushed to the clients.
+
+ # publish to clients
+ def sendLastBuildStatus(self, client):
+ #client.newLastBuildStatus(self.lastBuildStatus)
+ pass
+ def sendCurrentActivityBigToEveryone(self):
+ for s in self.subscribers:
+ self.sendCurrentActivityBig(s)
+ def sendCurrentActivityBig(self, client):
+ state = self.currentBigState
+ if state == "offline":
+ client.currentlyOffline()
+ elif state == "idle":
+ client.currentlyIdle()
+ elif state == "building":
+ client.currentlyBuilding()
+ else:
+ log.msg("Hey, self.currentBigState is weird:", state)
+
+
+ ## HTML display interface
+
+ def getEventNumbered(self, num):
+ # deal with dropped events, pruned events
+ first = self.events[0].number
+ if first + len(self.events)-1 != self.events[-1].number:
+ log.msg(self,
+ "lost an event somewhere: [0] is %d, [%d] is %d" % \
+ (self.events[0].number,
+ len(self.events) - 1,
+ self.events[-1].number))
+ for e in self.events:
+ log.msg("e[%d]: " % e.number, e)
+ return None
+ offset = num - first
+ log.msg(self, "offset", offset)
+ try:
+ return self.events[offset]
+ except IndexError:
+ return None
+
+ ## Persistence of Status
+ def loadYourOldEvents(self):
+ if hasattr(self, "allEvents"):
+ # first time, nothing to get from file. Note that this is only if
+ # the Application gets .run() . If it gets .save()'ed, then the
+ # .allEvents attribute goes away in the initial __getstate__ and
+ # we try to load a non-existent file.
+ return
+ self.allEvents = self.loadFile("events", [])
+ if self.allEvents:
+ self.nextEventNumber = self.allEvents[-1].number + 1
+ else:
+ self.nextEventNumber = 0
+ def saveYourOldEvents(self):
+ self.saveFile("events", self.allEvents)
+
+ ## clients
+
+ def addClient(self, client):
+ if client not in self.subscribers:
+ self.subscribers.append(client)
+ self.sendLastBuildStatus(client)
+ self.sendCurrentActivityBig(client)
+ client.newEvent(self.currentSmall)
+ def removeClient(self, client):
+ if client in self.subscribers:
+ self.subscribers.remove(client)
+
+class SlaveStatus:
+ implements(interfaces.ISlaveStatus)
+
+ admin = None
+ host = None
+ connected = False
+ graceful_shutdown = False
+
+ def __init__(self, name):
+ self.name = name
+ self._lastMessageReceived = 0
+ self.runningBuilds = []
+ self.graceful_callbacks = []
+
+ def getName(self):
+ return self.name
+ def getAdmin(self):
+ return self.admin
+ def getHost(self):
+ return self.host
+ def isConnected(self):
+ return self.connected
+ def lastMessageReceived(self):
+ return self._lastMessageReceived
+ def getRunningBuilds(self):
+ return self.runningBuilds
+
+ def setAdmin(self, admin):
+ self.admin = admin
+ def setHost(self, host):
+ self.host = host
+ def setConnected(self, isConnected):
+ self.connected = isConnected
+ def setLastMessageReceived(self, when):
+ self._lastMessageReceived = when
+
+ def buildStarted(self, build):
+ self.runningBuilds.append(build)
+ def buildFinished(self, build):
+ self.runningBuilds.remove(build)
+
+ def getGraceful(self):
+ """Return the graceful shutdown flag"""
+ return self.graceful_shutdown
+ def setGraceful(self, graceful):
+ """Set the graceful shutdown flag, and notify all the watchers"""
+ self.graceful_shutdown = graceful
+ for cb in self.graceful_callbacks:
+ reactor.callLater(0, cb, graceful)
+ def addGracefulWatcher(self, watcher):
+ """Add watcher to the list of watchers to be notified when the
+ graceful shutdown flag is changed."""
+ if not watcher in self.graceful_callbacks:
+ self.graceful_callbacks.append(watcher)
+ def removeGracefulWatcher(self, watcher):
+ """Remove watcher from the list of watchers to be notified when the
+ graceful shutdown flag is changed."""
+ if watcher in self.graceful_callbacks:
+ self.graceful_callbacks.remove(watcher)
+
+class Status:
+ """
+ I represent the status of the buildmaster.
+ """
+ implements(interfaces.IStatus)
+
+ def __init__(self, botmaster, basedir):
+ """
+ @type botmaster: L{buildbot.master.BotMaster}
+ @param botmaster: the Status object uses C{.botmaster} to get at
+ both the L{buildbot.master.BuildMaster} (for
+ various buildbot-wide parameters) and the
+ actual Builders (to get at their L{BuilderStatus}
+ objects). It is not allowed to change or influence
+ anything through this reference.
+ @type basedir: string
+ @param basedir: this provides a base directory in which saved status
+ information (changes.pck, saved Build status
+ pickles) can be stored
+ """
+ self.botmaster = botmaster
+ self.basedir = basedir
+ self.watchers = []
+ self.activeBuildSets = []
+ assert os.path.isdir(basedir)
+ # compress logs bigger than 4k, a good default on linux
+ self.logCompressionLimit = 4*1024
+
+
+ # methods called by our clients
+
+ def getProjectName(self):
+ return self.botmaster.parent.projectName
+ def getProjectURL(self):
+ return self.botmaster.parent.projectURL
+ def getBuildbotURL(self):
+ return self.botmaster.parent.buildbotURL
+
+ def getURLForThing(self, thing):
+ prefix = self.getBuildbotURL()
+ if not prefix:
+ return None
+ if interfaces.IStatus.providedBy(thing):
+ return prefix
+ if interfaces.ISchedulerStatus.providedBy(thing):
+ pass
+ if interfaces.IBuilderStatus.providedBy(thing):
+ builder = thing
+ return prefix + "builders/%s" % (
+ urllib.quote(builder.getName(), safe=''),
+ )
+ if interfaces.IBuildStatus.providedBy(thing):
+ build = thing
+ builder = build.getBuilder()
+ return prefix + "builders/%s/builds/%d" % (
+ urllib.quote(builder.getName(), safe=''),
+ build.getNumber())
+ if interfaces.IBuildStepStatus.providedBy(thing):
+ step = thing
+ build = step.getBuild()
+ builder = build.getBuilder()
+ return prefix + "builders/%s/builds/%d/steps/%s" % (
+ urllib.quote(builder.getName(), safe=''),
+ build.getNumber(),
+ urllib.quote(step.getName(), safe=''))
+ # IBuildSetStatus
+ # IBuildRequestStatus
+ # ISlaveStatus
+
+ # IStatusEvent
+ if interfaces.IStatusEvent.providedBy(thing):
+ from buildbot.changes import changes
+ # TODO: this is goofy, create IChange or something
+ if isinstance(thing, changes.Change):
+ change = thing
+ return "%schanges/%d" % (prefix, change.number)
+
+ if interfaces.IStatusLog.providedBy(thing):
+ log = thing
+ step = log.getStep()
+ build = step.getBuild()
+ builder = build.getBuilder()
+
+ logs = step.getLogs()
+ for i in range(len(logs)):
+ if log is logs[i]:
+ lognum = i
+ break
+ else:
+ return None
+ return prefix + "builders/%s/builds/%d/steps/%s/logs/%d" % (
+ urllib.quote(builder.getName(), safe=''),
+ build.getNumber(),
+ urllib.quote(step.getName(), safe=''),
+ lognum)
+
+ def getChangeSources(self):
+ return list(self.botmaster.parent.change_svc)
+
+ def getChange(self, number):
+ return self.botmaster.parent.change_svc.getChangeNumbered(number)
+
+ def getSchedulers(self):
+ return self.botmaster.parent.allSchedulers()
+
+ def getBuilderNames(self, categories=None):
+ if categories == None:
+ return self.botmaster.builderNames[:] # don't let them break it
+
+ l = []
+ # respect addition order
+ for name in self.botmaster.builderNames:
+ builder = self.botmaster.builders[name]
+ if builder.builder_status.category in categories:
+ l.append(name)
+ return l
+
+ def getBuilder(self, name):
+ """
+ @rtype: L{BuilderStatus}
+ """
+ return self.botmaster.builders[name].builder_status
+
+ def getSlaveNames(self):
+ return self.botmaster.slaves.keys()
+
+ def getSlave(self, slavename):
+ return self.botmaster.slaves[slavename].slave_status
+
+ def getBuildSets(self):
+ return self.activeBuildSets[:]
+
+ def generateFinishedBuilds(self, builders=[], branches=[],
+ num_builds=None, finished_before=None,
+ max_search=200):
+
+ def want_builder(bn):
+ if builders:
+ return bn in builders
+ return True
+ builder_names = [bn
+ for bn in self.getBuilderNames()
+ if want_builder(bn)]
+
+ # 'sources' is a list of generators, one for each Builder we're
+ # using. When the generator is exhausted, it is replaced in this list
+ # with None.
+ sources = []
+ for bn in builder_names:
+ b = self.getBuilder(bn)
+ g = b.generateFinishedBuilds(branches,
+ finished_before=finished_before,
+ max_search=max_search)
+ sources.append(g)
+
+ # next_build the next build from each source
+ next_build = [None] * len(sources)
+
+ def refill():
+ for i,g in enumerate(sources):
+ if next_build[i]:
+ # already filled
+ continue
+ if not g:
+ # already exhausted
+ continue
+ try:
+ next_build[i] = g.next()
+ except StopIteration:
+ next_build[i] = None
+ sources[i] = None
+
+ got = 0
+ while True:
+ refill()
+ # find the latest build among all the candidates
+ candidates = [(i, b, b.getTimes()[1])
+ for i,b in enumerate(next_build)
+ if b is not None]
+ candidates.sort(lambda x,y: cmp(x[2], y[2]))
+ if not candidates:
+ return
+
+ # and remove it from the list
+ i, build, finshed_time = candidates[-1]
+ next_build[i] = None
+ got += 1
+ yield build
+ if num_builds is not None:
+ if got >= num_builds:
+ return
+
+ def subscribe(self, target):
+ self.watchers.append(target)
+ for name in self.botmaster.builderNames:
+ self.announceNewBuilder(target, name, self.getBuilder(name))
+ def unsubscribe(self, target):
+ self.watchers.remove(target)
+
+
+ # methods called by upstream objects
+
+ def announceNewBuilder(self, target, name, builder_status):
+ t = target.builderAdded(name, builder_status)
+ if t:
+ builder_status.subscribe(t)
+
+ def builderAdded(self, name, basedir, category=None):
+ """
+ @rtype: L{BuilderStatus}
+ """
+ filename = os.path.join(self.basedir, basedir, "builder")
+ log.msg("trying to load status pickle from %s" % filename)
+ builder_status = None
+ try:
+ builder_status = load(open(filename, "rb"))
+ styles.doUpgrade()
+ except IOError:
+ log.msg("no saved status pickle, creating a new one")
+ except:
+ log.msg("error while loading status pickle, creating a new one")
+ log.msg("error follows:")
+ log.err()
+ if not builder_status:
+ builder_status = BuilderStatus(name, category)
+ builder_status.addPointEvent(["builder", "created"])
+ log.msg("added builder %s in category %s" % (name, category))
+ # an unpickled object might not have category set from before,
+ # so set it here to make sure
+ builder_status.category = category
+ builder_status.basedir = os.path.join(self.basedir, basedir)
+ builder_status.name = name # it might have been updated
+ builder_status.status = self
+
+ if not os.path.isdir(builder_status.basedir):
+ os.makedirs(builder_status.basedir)
+ builder_status.determineNextBuildNumber()
+
+ builder_status.setBigState("offline")
+ builder_status.setLogCompressionLimit(self.logCompressionLimit)
+
+ for t in self.watchers:
+ self.announceNewBuilder(t, name, builder_status)
+
+ return builder_status
+
+ def builderRemoved(self, name):
+ for t in self.watchers:
+ t.builderRemoved(name)
+
+ def prune(self):
+ for b in self.botmaster.builders.values():
+ b.builder_status.prune()
+
+ def buildsetSubmitted(self, bss):
+ self.activeBuildSets.append(bss)
+ bss.waitUntilFinished().addCallback(self.activeBuildSets.remove)
+ for t in self.watchers:
+ t.buildsetSubmitted(bss)