status/builder.py: update comment
[buildbot.git] / buildbot / status / builder.py
blob656089cc73ccf5029fd4e39b5a944f70b9f70cd0
1 # -*- test-case-name: buildbot.test.test_status -*-
3 from __future__ import generators
5 from twisted.python import log
6 from twisted.persisted import styles
7 from twisted.internet import reactor, defer
8 from twisted.protocols import basic
10 import os, shutil, sys, re, urllib
11 try:
12 import cPickle
13 pickle = cPickle
14 except ImportError:
15 import pickle
16 try:
17 from cStringIO import StringIO
18 except ImportError:
19 from StringIO import StringIO
21 # sibling imports
22 from buildbot import interfaces, util, sourcestamp
23 from buildbot.twcompat import implements, providedBy
25 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5)
26 Results = ["success", "warnings", "failure", "skipped", "exception"]
29 # build processes call the following methods:
31 # setDefaults
33 # currentlyBuilding
34 # currentlyIdle
35 # currentlyInterlocked
36 # currentlyOffline
37 # currentlyWaiting
39 # setCurrentActivity
40 # updateCurrentActivity
41 # addFileToCurrentActivity
42 # finishCurrentActivity
44 # startBuild
45 # finishBuild
47 STDOUT = interfaces.LOG_CHANNEL_STDOUT
48 STDERR = interfaces.LOG_CHANNEL_STDERR
49 HEADER = interfaces.LOG_CHANNEL_HEADER
50 ChunkTypes = ["stdout", "stderr", "header"]
52 class LogFileScanner(basic.NetstringReceiver):
53 def __init__(self, chunk_cb, channels=[]):
54 self.chunk_cb = chunk_cb
55 self.channels = channels
57 def stringReceived(self, line):
58 channel = int(line[0])
59 if not self.channels or (channel in self.channels):
60 self.chunk_cb((channel, line[1:]))
62 class LogFileProducer:
63 """What's the plan?
65 the LogFile has just one FD, used for both reading and writing.
66 Each time you add an entry, fd.seek to the end and then write.
68 Each reader (i.e. Producer) keeps track of their own offset. The reader
69 starts by seeking to the start of the logfile, and reading forwards.
70 Between each hunk of file they yield chunks, so they must remember their
71 offset before yielding and re-seek back to that offset before reading
72 more data. When their read() returns EOF, they're finished with the first
73 phase of the reading (everything that's already been written to disk).
75 After EOF, the remaining data is entirely in the current entries list.
76 These entries are all of the same channel, so we can do one "".join and
77 obtain a single chunk to be sent to the listener. But since that involves
78 a yield, and more data might arrive after we give up control, we have to
79 subscribe them before yielding. We can't subscribe them any earlier,
80 otherwise they'd get data out of order.
82 We're using a generator in the first place so that the listener can
83 throttle us, which means they're pulling. But the subscription means
84 we're pushing. Really we're a Producer. In the first phase we can be
85 either a PullProducer or a PushProducer. In the second phase we're only a
86 PushProducer.
88 So the client gives a LogFileConsumer to File.subscribeConsumer . This
89 Consumer must have registerProducer(), unregisterProducer(), and
90 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
91 except that writeChunk() takes chunks (tuples of (channel,text)) instead
92 of the normal write() which takes just text. The LogFileConsumer is
93 allowed to call stopProducing, pauseProducing, and resumeProducing on the
94 producer instance it is given. """
96 paused = False
97 subscribed = False
98 BUFFERSIZE = 2048
100 def __init__(self, logfile, consumer):
101 self.logfile = logfile
102 self.consumer = consumer
103 self.chunkGenerator = self.getChunks()
104 consumer.registerProducer(self, True)
106 def getChunks(self):
107 f = self.logfile.getFile()
108 offset = 0
109 chunks = []
110 p = LogFileScanner(chunks.append)
111 f.seek(offset)
112 data = f.read(self.BUFFERSIZE)
113 offset = f.tell()
114 while data:
115 p.dataReceived(data)
116 while chunks:
117 c = chunks.pop(0)
118 yield c
119 f.seek(offset)
120 data = f.read(self.BUFFERSIZE)
121 offset = f.tell()
122 del f
124 # now subscribe them to receive new entries
125 self.subscribed = True
126 self.logfile.watchers.append(self)
127 d = self.logfile.waitUntilFinished()
129 # then give them the not-yet-merged data
130 if self.logfile.runEntries:
131 channel = self.logfile.runEntries[0][0]
132 text = "".join([c[1] for c in self.logfile.runEntries])
133 yield (channel, text)
135 # now we've caught up to the present. Anything further will come from
136 # the logfile subscription. We add the callback *after* yielding the
137 # data from runEntries, because the logfile might have finished
138 # during the yield.
139 d.addCallback(self.logfileFinished)
141 def stopProducing(self):
142 # TODO: should we still call consumer.finish? probably not.
143 self.paused = True
144 self.consumer = None
145 self.done()
147 def done(self):
148 if self.chunkGenerator:
149 self.chunkGenerator = None # stop making chunks
150 if self.subscribed:
151 self.logfile.watchers.remove(self)
152 self.subscribed = False
154 def pauseProducing(self):
155 self.paused = True
157 def resumeProducing(self):
158 # Twisted-1.3.0 has a bug which causes hangs when resumeProducing
159 # calls transport.write (there is a recursive loop, fixed in 2.0 in
160 # t.i.abstract.FileDescriptor.doWrite by setting the producerPaused
161 # flag *before* calling resumeProducing). To work around this, we
162 # just put off the real resumeProducing for a moment. This probably
163 # has a performance hit, but I'm going to assume that the log files
164 # are not retrieved frequently enough for it to be an issue.
166 reactor.callLater(0, self._resumeProducing)
168 def _resumeProducing(self):
169 self.paused = False
170 if not self.chunkGenerator:
171 return
172 try:
173 while not self.paused:
174 chunk = self.chunkGenerator.next()
175 self.consumer.writeChunk(chunk)
176 # we exit this when the consumer says to stop, or we run out
177 # of chunks
178 except StopIteration:
179 # if the generator finished, it will have done releaseFile
180 self.chunkGenerator = None
181 # now everything goes through the subscription, and they don't get to
182 # pause anymore
184 def logChunk(self, build, step, logfile, channel, chunk):
185 if self.consumer:
186 self.consumer.writeChunk((channel, chunk))
188 def logfileFinished(self, logfile):
189 self.done()
190 if self.consumer:
191 self.consumer.unregisterProducer()
192 self.consumer.finish()
193 self.consumer = None
195 class LogFile:
196 """A LogFile keeps all of its contents on disk, in a non-pickle format to
197 which new entries can easily be appended. The file on disk has a name
198 like 12-log-compile-output, under the Builder's directory. The actual
199 filename is generated (before the LogFile is created) by
200 L{BuildStatus.generateLogfileName}.
202 Old LogFile pickles (which kept their contents in .entries) must be
203 upgraded. The L{BuilderStatus} is responsible for doing this, when it
204 loads the L{BuildStatus} into memory. The Build pickle is not modified,
205 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
206 logs."""
208 if implements:
209 implements(interfaces.IStatusLog, interfaces.ILogFile)
210 else:
211 __implements__ = (interfaces.IStatusLog, interfaces.ILogFile)
213 finished = False
214 length = 0
215 chunkSize = 10*1000
216 runLength = 0
217 runEntries = [] # provided so old pickled builds will getChunks() ok
218 entries = None
219 BUFFERSIZE = 2048
220 filename = None # relative to the Builder's basedir
221 openfile = None
223 def __init__(self, parent, name, logfilename):
225 @type parent: L{BuildStepStatus}
226 @param parent: the Step that this log is a part of
227 @type name: string
228 @param name: the name of this log, typically 'output'
229 @type logfilename: string
230 @param logfilename: the Builder-relative pathname for the saved entries
232 self.step = parent
233 self.name = name
234 self.filename = logfilename
235 fn = self.getFilename()
236 if os.path.exists(fn):
237 # the buildmaster was probably stopped abruptly, before the
238 # BuilderStatus could be saved, so BuilderStatus.nextBuildNumber
239 # is out of date, and we're overlapping with earlier builds now.
240 # Warn about it, but then overwrite the old pickle file
241 log.msg("Warning: Overwriting old serialized Build at %s" % fn)
242 self.openfile = open(fn, "w+")
243 self.runEntries = []
244 self.watchers = []
245 self.finishedWatchers = []
247 def getFilename(self):
248 return os.path.join(self.step.build.builder.basedir, self.filename)
250 def hasContents(self):
251 return os.path.exists(self.getFilename())
253 def getName(self):
254 return self.name
256 def getStep(self):
257 return self.step
259 def isFinished(self):
260 return self.finished
261 def waitUntilFinished(self):
262 if self.finished:
263 d = defer.succeed(self)
264 else:
265 d = defer.Deferred()
266 self.finishedWatchers.append(d)
267 return d
269 def getFile(self):
270 if self.openfile:
271 # this is the filehandle we're using to write to the log, so
272 # don't close it!
273 return self.openfile
274 # otherwise they get their own read-only handle
275 return open(self.getFilename(), "r")
277 def getText(self):
278 # this produces one ginormous string
279 return "".join(self.getChunks([STDOUT, STDERR], onlyText=True))
281 def getTextWithHeaders(self):
282 return "".join(self.getChunks(onlyText=True))
284 def getChunks(self, channels=[], onlyText=False):
285 # generate chunks for everything that was logged at the time we were
286 # first called, so remember how long the file was when we started.
287 # Don't read beyond that point. The current contents of
288 # self.runEntries will follow.
290 # this returns an iterator, which means arbitrary things could happen
291 # while we're yielding. This will faithfully deliver the log as it
292 # existed when it was started, and not return anything after that
293 # point. To use this in subscribe(catchup=True) without missing any
294 # data, you must insure that nothing will be added to the log during
295 # yield() calls.
297 f = self.getFile()
298 offset = 0
299 f.seek(0, 2)
300 remaining = f.tell()
302 leftover = None
303 if self.runEntries and (not channels or
304 (self.runEntries[0][0] in channels)):
305 leftover = (self.runEntries[0][0],
306 "".join([c[1] for c in self.runEntries]))
308 # freeze the state of the LogFile by passing a lot of parameters into
309 # a generator
310 return self._generateChunks(f, offset, remaining, leftover,
311 channels, onlyText)
313 def _generateChunks(self, f, offset, remaining, leftover,
314 channels, onlyText):
315 chunks = []
316 p = LogFileScanner(chunks.append, channels)
317 f.seek(offset)
318 data = f.read(min(remaining, self.BUFFERSIZE))
319 remaining -= len(data)
320 offset = f.tell()
321 while data:
322 p.dataReceived(data)
323 while chunks:
324 channel, text = chunks.pop(0)
325 if onlyText:
326 yield text
327 else:
328 yield (channel, text)
329 f.seek(offset)
330 data = f.read(min(remaining, self.BUFFERSIZE))
331 remaining -= len(data)
332 offset = f.tell()
333 del f
335 if leftover:
336 if onlyText:
337 yield leftover[1]
338 else:
339 yield leftover
341 def readlines(self, channel=STDOUT):
342 """Return an iterator that produces newline-terminated lines,
343 excluding header chunks."""
344 # TODO: make this memory-efficient, by turning it into a generator
345 # that retrieves chunks as necessary, like a pull-driven version of
346 # twisted.protocols.basic.LineReceiver
347 alltext = "".join(self.getChunks([channel], onlyText=True))
348 io = StringIO(alltext)
349 return io.readlines()
351 def subscribe(self, receiver, catchup):
352 if self.finished:
353 return
354 self.watchers.append(receiver)
355 if catchup:
356 for channel, text in self.getChunks():
357 # TODO: add logChunks(), to send over everything at once?
358 receiver.logChunk(self.step.build, self.step, self,
359 channel, text)
361 def unsubscribe(self, receiver):
362 if receiver in self.watchers:
363 self.watchers.remove(receiver)
365 def subscribeConsumer(self, consumer):
366 p = LogFileProducer(self, consumer)
367 p.resumeProducing()
369 # interface used by the build steps to add things to the log
371 def merge(self):
372 # merge all .runEntries (which are all of the same type) into a
373 # single chunk for .entries
374 if not self.runEntries:
375 return
376 channel = self.runEntries[0][0]
377 text = "".join([c[1] for c in self.runEntries])
378 assert channel < 10
379 f = self.openfile
380 f.seek(0, 2)
381 offset = 0
382 while offset < len(text):
383 size = min(len(text)-offset, self.chunkSize)
384 f.write("%d:%d" % (1 + size, channel))
385 f.write(text[offset:offset+size])
386 f.write(",")
387 offset += size
388 self.runEntries = []
389 self.runLength = 0
391 def addEntry(self, channel, text):
392 assert not self.finished
393 # we only add to .runEntries here. merge() is responsible for adding
394 # merged chunks to .entries
395 if self.runEntries and channel != self.runEntries[0][0]:
396 self.merge()
397 self.runEntries.append((channel, text))
398 self.runLength += len(text)
399 if self.runLength >= self.chunkSize:
400 self.merge()
402 for w in self.watchers:
403 w.logChunk(self.step.build, self.step, self, channel, text)
404 self.length += len(text)
406 def addStdout(self, text):
407 self.addEntry(STDOUT, text)
408 def addStderr(self, text):
409 self.addEntry(STDERR, text)
410 def addHeader(self, text):
411 self.addEntry(HEADER, text)
413 def finish(self):
414 self.merge()
415 if self.openfile:
416 # we don't do an explicit close, because there might be readers
417 # shareing the filehandle. As soon as they stop reading, the
418 # filehandle will be released and automatically closed. We will
419 # do a sync, however, to make sure the log gets saved in case of
420 # a crash.
421 os.fsync(self.openfile.fileno())
422 del self.openfile
423 self.finished = True
424 watchers = self.finishedWatchers
425 self.finishedWatchers = []
426 for w in watchers:
427 w.callback(self)
429 # persistence stuff
430 def __getstate__(self):
431 d = self.__dict__.copy()
432 del d['step'] # filled in upon unpickling
433 del d['watchers']
434 del d['finishedWatchers']
435 d['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
436 if d.has_key('finished'):
437 del d['finished']
438 if d.has_key('openfile'):
439 del d['openfile']
440 return d
442 def __setstate__(self, d):
443 self.__dict__ = d
444 self.watchers = [] # probably not necessary
445 self.finishedWatchers = [] # same
446 # self.step must be filled in by our parent
447 self.finished = True
449 def upgrade(self, logfilename):
450 """Save our .entries to a new-style offline log file (if necessary),
451 and modify our in-memory representation to use it. The original
452 pickled LogFile (inside the pickled Build) won't be modified."""
453 self.filename = logfilename
454 if not os.path.exists(self.getFilename()):
455 self.openfile = open(self.getFilename(), "w")
456 self.finished = False
457 for channel,text in self.entries:
458 self.addEntry(channel, text)
459 self.finish() # releases self.openfile, which will be closed
460 del self.entries
463 class HTMLLogFile:
464 if implements:
465 implements(interfaces.IStatusLog)
466 else:
467 __implements__ = interfaces.IStatusLog,
469 filename = None
471 def __init__(self, parent, name, logfilename, html):
472 self.step = parent
473 self.name = name
474 self.filename = logfilename
475 self.html = html
477 def getName(self):
478 return self.name # set in BuildStepStatus.addLog
479 def getStep(self):
480 return self.step
482 def isFinished(self):
483 return True
484 def waitUntilFinished(self):
485 return defer.succeed(self)
487 def hasContents(self):
488 return True
489 def getText(self):
490 return self.html # looks kinda like text
491 def getTextWithHeaders(self):
492 return self.html
493 def getChunks(self):
494 return [(STDERR, self.html)]
496 def subscribe(self, receiver, catchup):
497 pass
498 def unsubscribe(self, receiver):
499 pass
501 def finish(self):
502 pass
504 def __getstate__(self):
505 d = self.__dict__.copy()
506 del d['step']
507 return d
509 def upgrade(self, logfilename):
510 pass
513 class Event:
514 if implements:
515 implements(interfaces.IStatusEvent)
516 else:
517 __implements__ = interfaces.IStatusEvent,
519 started = None
520 finished = None
521 text = []
522 color = None
524 # IStatusEvent methods
525 def getTimes(self):
526 return (self.started, self.finished)
527 def getText(self):
528 return self.text
529 def getColor(self):
530 return self.color
531 def getLogs(self):
532 return []
534 def finish(self):
535 self.finished = util.now()
537 class TestResult:
538 if implements:
539 implements(interfaces.ITestResult)
540 else:
541 __implements__ = interfaces.ITestResult,
543 def __init__(self, name, results, text, logs):
544 assert isinstance(name, tuple)
545 self.name = name
546 self.results = results
547 self.text = text
548 self.logs = logs
550 def getName(self):
551 return self.name
553 def getResults(self):
554 return self.results
556 def getText(self):
557 return self.text
559 def getLogs(self):
560 return self.logs
563 class BuildSetStatus:
564 if implements:
565 implements(interfaces.IBuildSetStatus)
566 else:
567 __implements__ = interfaces.IBuildSetStatus,
569 def __init__(self, source, reason, builderNames, bsid=None):
570 self.source = source
571 self.reason = reason
572 self.builderNames = builderNames
573 self.id = bsid
574 self.successWatchers = []
575 self.finishedWatchers = []
576 self.stillHopeful = True
577 self.finished = False
579 def setBuildRequestStatuses(self, buildRequestStatuses):
580 self.buildRequests = buildRequestStatuses
581 def setResults(self, results):
582 # the build set succeeds only if all its component builds succeed
583 self.results = results
584 def giveUpHope(self):
585 self.stillHopeful = False
588 def notifySuccessWatchers(self):
589 for d in self.successWatchers:
590 d.callback(self)
591 self.successWatchers = []
593 def notifyFinishedWatchers(self):
594 self.finished = True
595 for d in self.finishedWatchers:
596 d.callback(self)
597 self.finishedWatchers = []
599 # methods for our clients
601 def getSourceStamp(self):
602 return self.source
603 def getReason(self):
604 return self.reason
605 def getResults(self):
606 return self.results
607 def getID(self):
608 return self.id
610 def getBuilderNames(self):
611 return self.builderNames
612 def getBuildRequests(self):
613 return self.buildRequests
614 def isFinished(self):
615 return self.finished
617 def waitUntilSuccess(self):
618 if self.finished or not self.stillHopeful:
619 # the deferreds have already fired
620 return defer.succeed(self)
621 d = defer.Deferred()
622 self.successWatchers.append(d)
623 return d
625 def waitUntilFinished(self):
626 if self.finished:
627 return defer.succeed(self)
628 d = defer.Deferred()
629 self.finishedWatchers.append(d)
630 return d
632 class BuildRequestStatus:
633 if implements:
634 implements(interfaces.IBuildRequestStatus)
635 else:
636 __implements__ = interfaces.IBuildRequestStatus,
638 def __init__(self, source, builderName):
639 self.source = source
640 self.builderName = builderName
641 self.builds = [] # list of BuildStatus objects
642 self.observers = []
644 def buildStarted(self, build):
645 self.builds.append(build)
646 for o in self.observers[:]:
647 o(build)
649 # methods called by our clients
650 def getSourceStamp(self):
651 return self.source
652 def getBuilderName(self):
653 return self.builderName
654 def getBuilds(self):
655 return self.builds
657 def subscribe(self, observer):
658 self.observers.append(observer)
659 for b in self.builds:
660 observer(b)
661 def unsubscribe(self, observer):
662 self.observers.remove(observer)
665 class BuildStepStatus(styles.Versioned):
667 I represent a collection of output status for a
668 L{buildbot.process.step.BuildStep}.
670 @type color: string
671 @cvar color: color that this step feels best represents its
672 current mood. yellow,green,red,orange are the
673 most likely choices, although purple indicates
674 an exception
675 @type progress: L{buildbot.status.progress.StepProgress}
676 @cvar progress: tracks ETA for the step
677 @type text: list of strings
678 @cvar text: list of short texts that describe the command and its status
679 @type text2: list of strings
680 @cvar text2: list of short texts added to the overall build description
681 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
682 @ivar logs: logs of steps
684 # note that these are created when the Build is set up, before each
685 # corresponding BuildStep has started.
686 if implements:
687 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
688 else:
689 __implements__ = interfaces.IBuildStepStatus, interfaces.IStatusEvent
690 persistenceVersion = 1
692 started = None
693 finished = None
694 progress = None
695 text = []
696 color = None
697 results = (None, [])
698 text2 = []
699 watchers = []
700 updates = {}
701 finishedWatchers = []
703 def __init__(self, parent):
704 assert interfaces.IBuildStatus(parent)
705 self.build = parent
706 self.logs = []
707 self.urls = {}
708 self.watchers = []
709 self.updates = {}
710 self.finishedWatchers = []
712 def getName(self):
713 """Returns a short string with the name of this step. This string
714 may have spaces in it."""
715 return self.name
717 def getBuild(self):
718 return self.build
720 def getTimes(self):
721 return (self.started, self.finished)
723 def getExpectations(self):
724 """Returns a list of tuples (name, current, target)."""
725 if not self.progress:
726 return []
727 ret = []
728 metrics = self.progress.progress.keys()
729 metrics.sort()
730 for m in metrics:
731 t = (m, self.progress.progress[m], self.progress.expectations[m])
732 ret.append(t)
733 return ret
735 def getLogs(self):
736 return self.logs
738 def getURLs(self):
739 return self.urls.copy()
741 def isFinished(self):
742 return (self.finished is not None)
744 def waitUntilFinished(self):
745 if self.finished:
746 d = defer.succeed(self)
747 else:
748 d = defer.Deferred()
749 self.finishedWatchers.append(d)
750 return d
752 # while the step is running, the following methods make sense.
753 # Afterwards they return None
755 def getETA(self):
756 if self.started is None:
757 return None # not started yet
758 if self.finished is not None:
759 return None # already finished
760 if not self.progress:
761 return None # no way to predict
762 return self.progress.remaining()
764 # Once you know the step has finished, the following methods are legal.
765 # Before this step has finished, they all return None.
767 def getText(self):
768 """Returns a list of strings which describe the step. These are
769 intended to be displayed in a narrow column. If more space is
770 available, the caller should join them together with spaces before
771 presenting them to the user."""
772 return self.text
774 def getColor(self):
775 """Returns a single string with the color that should be used to
776 display this step. 'green', 'orange', 'red', 'yellow' and 'purple'
777 are the most likely ones."""
778 return self.color
780 def getResults(self):
781 """Return a tuple describing the results of the step.
782 'result' is one of the constants in L{buildbot.status.builder}:
783 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
784 'strings' is an optional list of strings that the step wants to
785 append to the overall build's results. These strings are usually
786 more terse than the ones returned by getText(): in particular,
787 successful Steps do not usually contribute any text to the
788 overall build.
790 @rtype: tuple of int, list of strings
791 @returns: (result, strings)
793 return (self.results, self.text2)
795 # subscription interface
797 def subscribe(self, receiver, updateInterval=10):
798 # will get logStarted, logFinished, stepETAUpdate
799 assert receiver not in self.watchers
800 self.watchers.append(receiver)
801 self.sendETAUpdate(receiver, updateInterval)
803 def sendETAUpdate(self, receiver, updateInterval):
804 self.updates[receiver] = None
805 # they might unsubscribe during stepETAUpdate
806 receiver.stepETAUpdate(self.build, self,
807 self.getETA(), self.getExpectations())
808 if receiver in self.watchers:
809 self.updates[receiver] = reactor.callLater(updateInterval,
810 self.sendETAUpdate,
811 receiver,
812 updateInterval)
814 def unsubscribe(self, receiver):
815 if receiver in self.watchers:
816 self.watchers.remove(receiver)
817 if receiver in self.updates:
818 if self.updates[receiver] is not None:
819 self.updates[receiver].cancel()
820 del self.updates[receiver]
823 # methods to be invoked by the BuildStep
825 def setName(self, stepname):
826 self.name = stepname
828 def setProgress(self, stepprogress):
829 self.progress = stepprogress
831 def stepStarted(self):
832 self.started = util.now()
833 if self.build:
834 self.build.stepStarted(self)
836 def addLog(self, name):
837 assert self.started # addLog before stepStarted won't notify watchers
838 logfilename = self.build.generateLogfileName(self.name, name)
839 log = LogFile(self, name, logfilename)
840 self.logs.append(log)
841 for w in self.watchers:
842 receiver = w.logStarted(self.build, self, log)
843 if receiver:
844 log.subscribe(receiver, True)
845 d = log.waitUntilFinished()
846 d.addCallback(lambda log: log.unsubscribe(receiver))
847 d = log.waitUntilFinished()
848 d.addCallback(self.logFinished)
849 return log
851 def addHTMLLog(self, name, html):
852 assert self.started # addLog before stepStarted won't notify watchers
853 logfilename = self.build.generateLogfileName(self.name, name)
854 log = HTMLLogFile(self, name, logfilename, html)
855 self.logs.append(log)
856 for w in self.watchers:
857 receiver = w.logStarted(self.build, self, log)
858 # TODO: think about this: there isn't much point in letting
859 # them subscribe
860 #if receiver:
861 # log.subscribe(receiver, True)
862 w.logFinished(self.build, self, log)
864 def logFinished(self, log):
865 for w in self.watchers:
866 w.logFinished(self.build, self, log)
868 def addURL(self, name, url):
869 self.urls[name] = url
871 def setColor(self, color):
872 self.color = color
873 def setText(self, text):
874 self.text = text
875 def setText2(self, text):
876 self.text2 = text
878 def stepFinished(self, results):
879 self.finished = util.now()
880 self.results = results
881 for loog in self.logs:
882 if not loog.isFinished():
883 loog.finish()
885 for r in self.updates.keys():
886 if self.updates[r] is not None:
887 self.updates[r].cancel()
888 del self.updates[r]
890 watchers = self.finishedWatchers
891 self.finishedWatchers = []
892 for w in watchers:
893 w.callback(self)
895 # persistence
897 def __getstate__(self):
898 d = styles.Versioned.__getstate__(self)
899 del d['build'] # filled in when loading
900 if d.has_key('progress'):
901 del d['progress']
902 del d['watchers']
903 del d['finishedWatchers']
904 del d['updates']
905 return d
907 def __setstate__(self, d):
908 styles.Versioned.__setstate__(self, d)
909 # self.build must be filled in by our parent
910 for loog in self.logs:
911 loog.step = self
913 def upgradeToVersion1(self):
914 if not hasattr(self, "urls"):
915 self.urls = {}
918 class BuildStatus(styles.Versioned):
919 if implements:
920 implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
921 else:
922 __implements__ = interfaces.IBuildStatus, interfaces.IStatusEvent
923 persistenceVersion = 2
925 source = None
926 reason = None
927 changes = []
928 blamelist = []
929 progress = None
930 started = None
931 finished = None
932 currentStep = None
933 text = []
934 color = None
935 results = None
936 slavename = "???"
938 # these lists/dicts are defined here so that unserialized instances have
939 # (empty) values. They are set in __init__ to new objects to make sure
940 # each instance gets its own copy.
941 watchers = []
942 updates = {}
943 finishedWatchers = []
944 testResults = {}
946 def __init__(self, parent, number):
948 @type parent: L{BuilderStatus}
949 @type number: int
951 assert interfaces.IBuilderStatus(parent)
952 self.builder = parent
953 self.number = number
954 self.watchers = []
955 self.updates = {}
956 self.finishedWatchers = []
957 self.steps = []
958 self.testResults = {}
959 self.properties = {}
961 # IBuildStatus
963 def getBuilder(self):
965 @rtype: L{BuilderStatus}
967 return self.builder
969 def getProperty(self, propname):
970 return self.properties[propname]
972 def getNumber(self):
973 return self.number
975 def getPreviousBuild(self):
976 if self.number == 0:
977 return None
978 return self.builder.getBuild(self.number-1)
980 def getSourceStamp(self):
981 return (self.source.branch, self.source.revision, self.source.patch)
983 def getReason(self):
984 return self.reason
986 def getChanges(self):
987 return self.changes
989 def getResponsibleUsers(self):
990 return self.blamelist
992 def getInterestedUsers(self):
993 # TODO: the Builder should add others: sheriffs, domain-owners
994 return self.blamelist
996 def getSteps(self):
997 """Return a list of IBuildStepStatus objects. For invariant builds
998 (those which always use the same set of Steps), this should be the
999 complete list, however some of the steps may not have started yet
1000 (step.getTimes()[0] will be None). For variant builds, this may not
1001 be complete (asking again later may give you more of them)."""
1002 return self.steps
1004 def getTimes(self):
1005 return (self.started, self.finished)
1007 def isFinished(self):
1008 return (self.finished is not None)
1010 def waitUntilFinished(self):
1011 if self.finished:
1012 d = defer.succeed(self)
1013 else:
1014 d = defer.Deferred()
1015 self.finishedWatchers.append(d)
1016 return d
1018 # while the build is running, the following methods make sense.
1019 # Afterwards they return None
1021 def getETA(self):
1022 if self.finished is not None:
1023 return None
1024 if not self.progress:
1025 return None
1026 eta = self.progress.eta()
1027 if eta is None:
1028 return None
1029 return eta - util.now()
1031 def getCurrentStep(self):
1032 return self.currentStep
1034 # Once you know the build has finished, the following methods are legal.
1035 # Before ths build has finished, they all return None.
1037 def getText(self):
1038 text = []
1039 text.extend(self.text)
1040 for s in self.steps:
1041 text.extend(s.text2)
1042 return text
1044 def getColor(self):
1045 return self.color
1047 def getResults(self):
1048 return self.results
1050 def getSlavename(self):
1051 return self.slavename
1053 def getTestResults(self):
1054 return self.testResults
1056 def getLogs(self):
1057 # TODO: steps should contribute significant logs instead of this
1058 # hack, which returns every log from every step. The logs should get
1059 # names like "compile" and "test" instead of "compile.output"
1060 logs = []
1061 for s in self.steps:
1062 for log in s.getLogs():
1063 logs.append(log)
1064 return logs
1066 # subscription interface
1068 def subscribe(self, receiver, updateInterval=None):
1069 # will receive stepStarted and stepFinished messages
1070 # and maybe buildETAUpdate
1071 self.watchers.append(receiver)
1072 if updateInterval is not None:
1073 self.sendETAUpdate(receiver, updateInterval)
1075 def sendETAUpdate(self, receiver, updateInterval):
1076 self.updates[receiver] = None
1077 ETA = self.getETA()
1078 if ETA is not None:
1079 receiver.buildETAUpdate(self, self.getETA())
1080 # they might have unsubscribed during buildETAUpdate
1081 if receiver in self.watchers:
1082 self.updates[receiver] = reactor.callLater(updateInterval,
1083 self.sendETAUpdate,
1084 receiver,
1085 updateInterval)
1087 def unsubscribe(self, receiver):
1088 if receiver in self.watchers:
1089 self.watchers.remove(receiver)
1090 if receiver in self.updates:
1091 if self.updates[receiver] is not None:
1092 self.updates[receiver].cancel()
1093 del self.updates[receiver]
1095 # methods for the base.Build to invoke
1097 def addStepWithName(self, name):
1098 """The Build is setting up, and has added a new BuildStep to its
1099 list. Create a BuildStepStatus object to which it can send status
1100 updates."""
1102 s = BuildStepStatus(self)
1103 s.setName(name)
1104 self.steps.append(s)
1105 return s
1107 def setProperty(self, propname, value):
1108 self.properties[propname] = value
1110 def addTestResult(self, result):
1111 self.testResults[result.getName()] = result
1113 def setSourceStamp(self, sourceStamp):
1114 self.source = sourceStamp
1115 self.changes = self.source.changes
1117 def setReason(self, reason):
1118 self.reason = reason
1119 def setBlamelist(self, blamelist):
1120 self.blamelist = blamelist
1121 def setProgress(self, progress):
1122 self.progress = progress
1124 def buildStarted(self, build):
1125 """The Build has been set up and is about to be started. It can now
1126 be safely queried, so it is time to announce the new build."""
1128 self.started = util.now()
1129 # now that we're ready to report status, let the BuilderStatus tell
1130 # the world about us
1131 self.builder.buildStarted(self)
1133 def setSlavename(self, slavename):
1134 self.slavename = slavename
1136 def setText(self, text):
1137 assert isinstance(text, (list, tuple))
1138 self.text = text
1139 def setColor(self, color):
1140 self.color = color
1141 def setResults(self, results):
1142 self.results = results
1144 def buildFinished(self):
1145 self.currentStep = None
1146 self.finished = util.now()
1148 for r in self.updates.keys():
1149 if self.updates[r] is not None:
1150 self.updates[r].cancel()
1151 del self.updates[r]
1153 watchers = self.finishedWatchers
1154 self.finishedWatchers = []
1155 for w in watchers:
1156 w.callback(self)
1158 # methods called by our BuildStepStatus children
1160 def stepStarted(self, step):
1161 self.currentStep = step
1162 name = self.getBuilder().getName()
1163 for w in self.watchers:
1164 receiver = w.stepStarted(self, step)
1165 if receiver:
1166 if type(receiver) == type(()):
1167 step.subscribe(receiver[0], receiver[1])
1168 else:
1169 step.subscribe(receiver)
1170 d = step.waitUntilFinished()
1171 d.addCallback(lambda step: step.unsubscribe(receiver))
1173 step.waitUntilFinished().addCallback(self._stepFinished)
1175 def _stepFinished(self, step):
1176 results = step.getResults()
1177 for w in self.watchers:
1178 w.stepFinished(self, step, results)
1180 # methods called by our BuilderStatus parent
1182 def pruneLogs(self):
1183 # this build is somewhat old: remove the build logs to save space
1184 # TODO: delete logs visible through IBuildStatus.getLogs
1185 for s in self.steps:
1186 s.pruneLogs()
1188 def pruneSteps(self):
1189 # this build is very old: remove the build steps too
1190 self.steps = []
1192 # persistence stuff
1194 def generateLogfileName(self, stepname, logname):
1195 """Return a filename (relative to the Builder's base directory) where
1196 the logfile's contents can be stored uniquely.
1198 The base filename is made by combining our build number, the Step's
1199 name, and the log's name, then removing unsuitable characters. The
1200 filename is then made unique by appending _0, _1, etc, until it does
1201 not collide with any other logfile.
1203 These files are kept in the Builder's basedir (rather than a
1204 per-Build subdirectory) because that makes cleanup easier: cron and
1205 find will help get rid of the old logs, but the empty directories are
1206 more of a hassle to remove."""
1208 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
1209 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
1210 # now make it unique
1211 unique_counter = 0
1212 filename = starting_filename
1213 while filename in [l.filename
1214 for step in self.steps
1215 for l in step.getLogs()
1216 if l.filename]:
1217 filename = "%s_%d" % (starting_filename, unique_counter)
1218 unique_counter += 1
1219 return filename
1221 def __getstate__(self):
1222 d = styles.Versioned.__getstate__(self)
1223 # for now, a serialized Build is always "finished". We will never
1224 # save unfinished builds.
1225 if not self.finished:
1226 d['finished'] = True
1227 # TODO: push an "interrupted" step so it is clear that the build
1228 # was interrupted. The builder will have a 'shutdown' event, but
1229 # someone looking at just this build will be confused as to why
1230 # the last log is truncated.
1231 del d['builder'] # filled in by our parent when loading
1232 del d['watchers']
1233 del d['updates']
1234 del d['finishedWatchers']
1235 return d
1237 def __setstate__(self, d):
1238 styles.Versioned.__setstate__(self, d)
1239 # self.builder must be filled in by our parent when loading
1240 for step in self.steps:
1241 step.build = self
1242 self.watchers = []
1243 self.updates = {}
1244 self.finishedWatchers = []
1246 def upgradeToVersion1(self):
1247 if hasattr(self, "sourceStamp"):
1248 # the old .sourceStamp attribute wasn't actually very useful
1249 maxChangeNumber, patch = self.sourceStamp
1250 changes = getattr(self, 'changes', [])
1251 source = sourcestamp.SourceStamp(branch=None,
1252 revision=None,
1253 patch=patch,
1254 changes=changes)
1255 self.source = source
1256 self.changes = source.changes
1257 del self.sourceStamp
1259 def upgradeToVersion2(self):
1260 self.properties = {}
1262 def upgradeLogfiles(self):
1263 # upgrade any LogFiles that need it. This must occur after we've been
1264 # attached to our Builder, and after we know about all LogFiles of
1265 # all Steps (to get the filenames right).
1266 assert self.builder
1267 for s in self.steps:
1268 for l in s.getLogs():
1269 if l.filename:
1270 pass # new-style, log contents are on disk
1271 else:
1272 logfilename = self.generateLogfileName(s.name, l.name)
1273 # let the logfile update its .filename pointer,
1274 # transferring its contents onto disk if necessary
1275 l.upgrade(logfilename)
1277 def saveYourself(self):
1278 filename = os.path.join(self.builder.basedir, "%d" % self.number)
1279 if os.path.isdir(filename):
1280 # leftover from 0.5.0, which stored builds in directories
1281 shutil.rmtree(filename, ignore_errors=True)
1282 tmpfilename = filename + ".tmp"
1283 try:
1284 pickle.dump(self, open(tmpfilename, "wb"), -1)
1285 if sys.platform == 'win32':
1286 # windows cannot rename a file on top of an existing one, so
1287 # fall back to delete-first. There are ways this can fail and
1288 # lose the builder's history, so we avoid using it in the
1289 # general (non-windows) case
1290 if os.path.exists(filename):
1291 os.unlink(filename)
1292 os.rename(tmpfilename, filename)
1293 except:
1294 log.msg("unable to save build %s-#%d" % (self.builder.name,
1295 self.number))
1296 log.err()
1300 class BuilderStatus(styles.Versioned):
1301 """I handle status information for a single process.base.Builder object.
1302 That object sends status changes to me (frequently as Events), and I
1303 provide them on demand to the various status recipients, like the HTML
1304 waterfall display and the live status clients. It also sends build
1305 summaries to me, which I log and provide to status clients who aren't
1306 interested in seeing details of the individual build steps.
1308 I am responsible for maintaining the list of historic Events and Builds,
1309 pruning old ones, and loading them from / saving them to disk.
1311 I live in the buildbot.process.base.Builder object, in the .statusbag
1312 attribute.
1314 @type category: string
1315 @ivar category: user-defined category this builder belongs to; can be
1316 used to filter on in status clients
1319 if implements:
1320 implements(interfaces.IBuilderStatus)
1321 else:
1322 __implements__ = interfaces.IBuilderStatus,
1323 persistenceVersion = 1
1325 # these limit the amount of memory we consume, as well as the size of the
1326 # main Builder pickle. The Build and LogFile pickles on disk must be
1327 # handled separately.
1328 buildCacheSize = 30
1329 buildHorizon = 100 # forget builds beyond this
1330 stepHorizon = 50 # forget steps in builds beyond this
1332 category = None
1333 currentBigState = "offline" # or idle/waiting/interlocked/building
1334 basedir = None # filled in by our parent
1336 def __init__(self, buildername, category=None):
1337 self.name = buildername
1338 self.category = category
1340 self.slavenames = []
1341 self.events = []
1342 # these three hold Events, and are used to retrieve the current
1343 # state of the boxes.
1344 self.lastBuildStatus = None
1345 #self.currentBig = None
1346 #self.currentSmall = None
1347 self.currentBuilds = []
1348 self.pendingBuilds = []
1349 self.nextBuild = None
1350 self.watchers = []
1351 self.buildCache = [] # TODO: age builds out of the cache
1353 # persistence
1355 def __getstate__(self):
1356 # when saving, don't record transient stuff like what builds are
1357 # currently running, because they won't be there when we start back
1358 # up. Nor do we save self.watchers, nor anything that gets set by our
1359 # parent like .basedir and .status
1360 d = styles.Versioned.__getstate__(self)
1361 d['watchers'] = []
1362 del d['buildCache']
1363 for b in self.currentBuilds:
1364 b.saveYourself()
1365 # TODO: push a 'hey, build was interrupted' event
1366 del d['currentBuilds']
1367 del d['pendingBuilds']
1368 del d['currentBigState']
1369 del d['basedir']
1370 del d['status']
1371 del d['nextBuildNumber']
1372 return d
1374 def __setstate__(self, d):
1375 # when loading, re-initialize the transient stuff. Remember that
1376 # upgradeToVersion1 and such will be called after this finishes.
1377 styles.Versioned.__setstate__(self, d)
1378 self.buildCache = []
1379 self.currentBuilds = []
1380 self.pendingBuilds = []
1381 self.watchers = []
1382 self.slavenames = []
1383 # self.basedir must be filled in by our parent
1384 # self.status must be filled in by our parent
1386 def upgradeToVersion1(self):
1387 if hasattr(self, 'slavename'):
1388 self.slavenames = [self.slavename]
1389 del self.slavename
1390 if hasattr(self, 'nextBuildNumber'):
1391 del self.nextBuildNumber # determineNextBuildNumber chooses this
1393 def determineNextBuildNumber(self):
1394 """Scan our directory of saved BuildStatus instances to determine
1395 what our self.nextBuildNumber should be. Set it one larger than the
1396 highest-numbered build we discover. This is called by the top-level
1397 Status object shortly after we are created or loaded from disk.
1399 existing_builds = [int(f)
1400 for f in os.listdir(self.basedir)
1401 if re.match("^\d+$", f)]
1402 if existing_builds:
1403 self.nextBuildNumber = max(existing_builds) + 1
1404 else:
1405 self.nextBuildNumber = 0
1407 def saveYourself(self):
1408 for b in self.buildCache:
1409 if not b.isFinished:
1410 # interrupted build, need to save it anyway.
1411 # BuildStatus.saveYourself will mark it as interrupted.
1412 b.saveYourself()
1413 filename = os.path.join(self.basedir, "builder")
1414 tmpfilename = filename + ".tmp"
1415 try:
1416 pickle.dump(self, open(tmpfilename, "wb"), -1)
1417 if sys.platform == 'win32':
1418 # windows cannot rename a file on top of an existing one
1419 if os.path.exists(filename):
1420 os.unlink(filename)
1421 os.rename(tmpfilename, filename)
1422 except:
1423 log.msg("unable to save builder %s" % self.name)
1424 log.err()
1427 # build cache management
1429 def addBuildToCache(self, build):
1430 if build in self.buildCache:
1431 return
1432 self.buildCache.append(build)
1433 while len(self.buildCache) > self.buildCacheSize:
1434 self.buildCache.pop(0)
1436 def getBuildByNumber(self, number):
1437 for b in self.currentBuilds:
1438 if b.number == number:
1439 return b
1440 for build in self.buildCache:
1441 if build.number == number:
1442 return build
1443 filename = os.path.join(self.basedir, "%d" % number)
1444 try:
1445 build = pickle.load(open(filename, "rb"))
1446 styles.doUpgrade()
1447 build.builder = self
1448 # handle LogFiles from after 0.5.0 and before 0.6.5
1449 build.upgradeLogfiles()
1450 self.addBuildToCache(build)
1451 return build
1452 except IOError:
1453 raise IndexError("no such build %d" % number)
1454 except EOFError:
1455 raise IndexError("corrupted build pickle %d" % number)
1457 def prune(self):
1458 return # TODO: change this to walk through the filesystem
1459 # first, blow away all builds beyond our build horizon
1460 self.builds = self.builds[-self.buildHorizon:]
1461 # then prune steps in builds past the step horizon
1462 for b in self.builds[0:-self.stepHorizon]:
1463 b.pruneSteps()
1465 # IBuilderStatus methods
1466 def getName(self):
1467 return self.name
1469 def getState(self):
1470 return (self.currentBigState, self.currentBuilds)
1472 def getSlaves(self):
1473 return [self.status.getSlave(name) for name in self.slavenames]
1475 def getPendingBuilds(self):
1476 return self.pendingBuilds
1478 def getCurrentBuilds(self):
1479 return self.currentBuilds
1481 def getLastFinishedBuild(self):
1482 b = self.getBuild(-1)
1483 if not (b and b.isFinished()):
1484 b = self.getBuild(-2)
1485 return b
1487 def getBuild(self, number):
1488 if number < 0:
1489 number = self.nextBuildNumber + number
1490 if number < 0 or number >= self.nextBuildNumber:
1491 return None
1493 try:
1494 return self.getBuildByNumber(number)
1495 except IndexError:
1496 return None
1498 def getEvent(self, number):
1499 try:
1500 return self.events[number]
1501 except IndexError:
1502 return None
1504 def eventGenerator(self):
1505 """This function creates a generator which will provide all of this
1506 Builder's status events, starting with the most recent and
1507 progressing backwards in time. """
1509 # remember the oldest-to-earliest flow here. "next" means earlier.
1511 # TODO: interleave build steps and self.events by timestamp.
1512 # TODO: um, I think we're already doing that.
1514 eventIndex = -1
1515 e = self.getEvent(eventIndex)
1516 for Nb in range(1, self.nextBuildNumber+1):
1517 b = self.getBuild(-Nb)
1518 if not b:
1519 break
1520 steps = b.getSteps()
1521 for Ns in range(1, len(steps)+1):
1522 if steps[-Ns].started:
1523 step_start = steps[-Ns].getTimes()[0]
1524 while e is not None and e.getTimes()[0] > step_start:
1525 yield e
1526 eventIndex -= 1
1527 e = self.getEvent(eventIndex)
1528 yield steps[-Ns]
1529 yield b
1530 while e is not None:
1531 yield e
1532 eventIndex -= 1
1533 e = self.getEvent(eventIndex)
1535 def subscribe(self, receiver):
1536 # will get builderChangedState, buildStarted, and buildFinished
1537 self.watchers.append(receiver)
1538 self.publishState(receiver)
1540 def unsubscribe(self, receiver):
1541 self.watchers.remove(receiver)
1543 ## Builder interface (methods called by the Builder which feeds us)
1545 def setSlavenames(self, names):
1546 self.slavenames = names
1548 def addEvent(self, text=[], color=None):
1549 # this adds a duration event. When it is done, the user should call
1550 # e.finish(). They can also mangle it by modifying .text and .color
1551 e = Event()
1552 e.started = util.now()
1553 e.text = text
1554 e.color = color
1555 self.events.append(e)
1556 return e # they are free to mangle it further
1558 def addPointEvent(self, text=[], color=None):
1559 # this adds a point event, one which occurs as a single atomic
1560 # instant of time.
1561 e = Event()
1562 e.started = util.now()
1563 e.finished = 0
1564 e.text = text
1565 e.color = color
1566 self.events.append(e)
1567 return e # for consistency, but they really shouldn't touch it
1569 def setBigState(self, state):
1570 needToUpdate = state != self.currentBigState
1571 self.currentBigState = state
1572 if needToUpdate:
1573 self.publishState()
1575 def publishState(self, target=None):
1576 state = self.currentBigState
1578 if target is not None:
1579 # unicast
1580 target.builderChangedState(self.name, state)
1581 return
1582 for w in self.watchers:
1583 w.builderChangedState(self.name, state)
1585 def newBuild(self):
1586 """The Builder has decided to start a build, but the Build object is
1587 not yet ready to report status (it has not finished creating the
1588 Steps). Create a BuildStatus object that it can use."""
1589 number = self.nextBuildNumber
1590 self.nextBuildNumber += 1
1591 # TODO: self.saveYourself(), to make sure we don't forget about the
1592 # build number we've just allocated. This is not quite as important
1593 # as it was before we switch to determineNextBuildNumber, but I think
1594 # it may still be useful to have the new build save itself.
1595 s = BuildStatus(self, number)
1596 s.waitUntilFinished().addCallback(self._buildFinished)
1597 return s
1599 def addBuildRequest(self, brstatus):
1600 self.pendingBuilds.append(brstatus)
1601 def removeBuildRequest(self, brstatus):
1602 self.pendingBuilds.remove(brstatus)
1604 # buildStarted is called by our child BuildStatus instances
1605 def buildStarted(self, s):
1606 """Now the BuildStatus object is ready to go (it knows all of its
1607 Steps, its ETA, etc), so it is safe to notify our watchers."""
1609 assert s.builder is self # paranoia
1610 assert s.number == self.nextBuildNumber - 1
1611 assert s not in self.currentBuilds
1612 self.currentBuilds.append(s)
1613 self.addBuildToCache(s)
1615 # now that the BuildStatus is prepared to answer queries, we can
1616 # announce the new build to all our watchers
1618 for w in self.watchers: # TODO: maybe do this later? callLater(0)?
1619 receiver = w.buildStarted(self.getName(), s)
1620 if receiver:
1621 if type(receiver) == type(()):
1622 s.subscribe(receiver[0], receiver[1])
1623 else:
1624 s.subscribe(receiver)
1625 d = s.waitUntilFinished()
1626 d.addCallback(lambda s: s.unsubscribe(receiver))
1629 def _buildFinished(self, s):
1630 assert s in self.currentBuilds
1631 s.saveYourself()
1632 self.currentBuilds.remove(s)
1634 name = self.getName()
1635 results = s.getResults()
1636 for w in self.watchers:
1637 w.buildFinished(name, s, results)
1639 self.prune() # conserve disk
1642 # waterfall display (history)
1644 # I want some kind of build event that holds everything about the build:
1645 # why, what changes went into it, the results of the build, itemized
1646 # test results, etc. But, I do kind of need something to be inserted in
1647 # the event log first, because intermixing step events and the larger
1648 # build event is fraught with peril. Maybe an Event-like-thing that
1649 # doesn't have a file in it but does have links. Hmm, that's exactly
1650 # what it does now. The only difference would be that this event isn't
1651 # pushed to the clients.
1653 # publish to clients
1654 def sendLastBuildStatus(self, client):
1655 #client.newLastBuildStatus(self.lastBuildStatus)
1656 pass
1657 def sendCurrentActivityBigToEveryone(self):
1658 for s in self.subscribers:
1659 self.sendCurrentActivityBig(s)
1660 def sendCurrentActivityBig(self, client):
1661 state = self.currentBigState
1662 if state == "offline":
1663 client.currentlyOffline()
1664 elif state == "idle":
1665 client.currentlyIdle()
1666 elif state == "building":
1667 client.currentlyBuilding()
1668 else:
1669 log.msg("Hey, self.currentBigState is weird:", state)
1672 ## HTML display interface
1674 def getEventNumbered(self, num):
1675 # deal with dropped events, pruned events
1676 first = self.events[0].number
1677 if first + len(self.events)-1 != self.events[-1].number:
1678 log.msg(self,
1679 "lost an event somewhere: [0] is %d, [%d] is %d" % \
1680 (self.events[0].number,
1681 len(self.events) - 1,
1682 self.events[-1].number))
1683 for e in self.events:
1684 log.msg("e[%d]: " % e.number, e)
1685 return None
1686 offset = num - first
1687 log.msg(self, "offset", offset)
1688 try:
1689 return self.events[offset]
1690 except IndexError:
1691 return None
1693 ## Persistence of Status
1694 def loadYourOldEvents(self):
1695 if hasattr(self, "allEvents"):
1696 # first time, nothing to get from file. Note that this is only if
1697 # the Application gets .run() . If it gets .save()'ed, then the
1698 # .allEvents attribute goes away in the initial __getstate__ and
1699 # we try to load a non-existent file.
1700 return
1701 self.allEvents = self.loadFile("events", [])
1702 if self.allEvents:
1703 self.nextEventNumber = self.allEvents[-1].number + 1
1704 else:
1705 self.nextEventNumber = 0
1706 def saveYourOldEvents(self):
1707 self.saveFile("events", self.allEvents)
1709 ## clients
1711 def addClient(self, client):
1712 if client not in self.subscribers:
1713 self.subscribers.append(client)
1714 self.sendLastBuildStatus(client)
1715 self.sendCurrentActivityBig(client)
1716 client.newEvent(self.currentSmall)
1717 def removeClient(self, client):
1718 if client in self.subscribers:
1719 self.subscribers.remove(client)
1721 class SlaveStatus:
1722 if implements:
1723 implements(interfaces.ISlaveStatus)
1724 else:
1725 __implements__ = interfaces.ISlaveStatus,
1727 admin = None
1728 host = None
1729 connected = False
1731 def __init__(self, name):
1732 self.name = name
1734 def getName(self):
1735 return self.name
1736 def getAdmin(self):
1737 return self.admin
1738 def getHost(self):
1739 return self.host
1740 def isConnected(self):
1741 return self.connected
1743 def setAdmin(self, admin):
1744 self.admin = admin
1745 def setHost(self, host):
1746 self.host = host
1747 def setConnected(self, isConnected):
1748 self.connected = isConnected
1750 class Status:
1752 I represent the status of the buildmaster.
1754 if implements:
1755 implements(interfaces.IStatus)
1756 else:
1757 __implements__ = interfaces.IStatus,
1759 def __init__(self, botmaster, basedir):
1761 @type botmaster: L{buildbot.master.BotMaster}
1762 @param botmaster: the Status object uses C{.botmaster} to get at
1763 both the L{buildbot.master.BuildMaster} (for
1764 various buildbot-wide parameters) and the
1765 actual Builders (to get at their L{BuilderStatus}
1766 objects). It is not allowed to change or influence
1767 anything through this reference.
1768 @type basedir: string
1769 @param basedir: this provides a base directory in which saved status
1770 information (changes.pck, saved Build status
1771 pickles) can be stored
1773 self.botmaster = botmaster
1774 self.basedir = basedir
1775 self.watchers = []
1776 self.activeBuildSets = []
1777 assert os.path.isdir(basedir)
1780 # methods called by our clients
1782 def getProjectName(self):
1783 return self.botmaster.parent.projectName
1784 def getProjectURL(self):
1785 return self.botmaster.parent.projectURL
1786 def getBuildbotURL(self):
1787 return self.botmaster.parent.buildbotURL
1789 def getURLForThing(self, thing):
1790 prefix = self.getBuildbotURL()
1791 if not prefix:
1792 return None
1793 if providedBy(thing, interfaces.IStatus):
1794 return prefix
1795 if providedBy(thing, interfaces.ISchedulerStatus):
1796 pass
1797 if providedBy(thing, interfaces.IBuilderStatus):
1798 builder = thing
1799 return prefix + urllib.quote(builder.getName(), safe='')
1800 if providedBy(thing, interfaces.IBuildStatus):
1801 build = thing
1802 builder = build.getBuilder()
1803 return "%s%s/builds/%d" % (
1804 prefix,
1805 urllib.quote(builder.getName(), safe=''),
1806 build.getNumber())
1807 if providedBy(thing, interfaces.IBuildStepStatus):
1808 step = thing
1809 build = step.getBuild()
1810 builder = build.getBuilder()
1811 return "%s%s/builds/%d/%s" % (
1812 prefix,
1813 urllib.quote(builder.getName(), safe=''),
1814 build.getNumber(),
1815 "step-" + urllib.quote(step.getName(), safe=''))
1816 # IBuildSetStatus
1817 # IBuildRequestStatus
1818 # ISlaveStatus
1820 # IStatusEvent
1821 if providedBy(thing, interfaces.IStatusEvent):
1822 from buildbot.changes import changes
1823 # TODO: this is goofy, create IChange or something
1824 if isinstance(thing, changes.Change):
1825 change = thing
1826 return "%schanges/%d" % (prefix, change.number)
1828 if providedBy(thing, interfaces.IStatusLog):
1829 log = thing
1830 step = log.getStep()
1831 build = step.getBuild()
1832 builder = build.getBuilder()
1834 logs = step.getLogs()
1835 for i in range(len(logs)):
1836 if log is logs[i]:
1837 lognum = i
1838 break
1839 else:
1840 return None
1841 return "%s%s/builds/%d/%s/%d" % (
1842 prefix,
1843 urllib.quote(builder.getName(), safe=''),
1844 build.getNumber(),
1845 "step-" + urllib.quote(step.getName(), safe=''),
1846 lognum)
1849 def getSchedulers(self):
1850 return self.botmaster.parent.allSchedulers()
1852 def getBuilderNames(self, categories=None):
1853 if categories == None:
1854 return self.botmaster.builderNames[:] # don't let them break it
1856 l = []
1857 # respect addition order
1858 for name in self.botmaster.builderNames:
1859 builder = self.botmaster.builders[name]
1860 if builder.builder_status.category in categories:
1861 l.append(name)
1862 return l
1864 def getBuilder(self, name):
1866 @rtype: L{BuilderStatus}
1868 return self.botmaster.builders[name].builder_status
1870 def getSlave(self, slavename):
1871 return self.botmaster.slaves[slavename].slave_status
1873 def getBuildSets(self):
1874 return self.activeBuildSets[:]
1876 def subscribe(self, target):
1877 self.watchers.append(target)
1878 for name in self.botmaster.builderNames:
1879 self.announceNewBuilder(target, name, self.getBuilder(name))
1880 def unsubscribe(self, target):
1881 self.watchers.remove(target)
1884 # methods called by upstream objects
1886 def announceNewBuilder(self, target, name, builder_status):
1887 t = target.builderAdded(name, builder_status)
1888 if t:
1889 builder_status.subscribe(t)
1891 def builderAdded(self, name, basedir, category=None):
1893 @rtype: L{BuilderStatus}
1895 filename = os.path.join(self.basedir, basedir, "builder")
1896 log.msg("trying to load status pickle from %s" % filename)
1897 builder_status = None
1898 try:
1899 builder_status = pickle.load(open(filename, "rb"))
1900 styles.doUpgrade()
1901 except IOError:
1902 log.msg("no saved status pickle, creating a new one")
1903 except:
1904 log.msg("error while loading status pickle, creating a new one")
1905 log.msg("error follows:")
1906 log.err()
1907 if not builder_status:
1908 builder_status = BuilderStatus(name, category)
1909 builder_status.addPointEvent(["builder", "created"])
1910 log.msg("added builder %s in category %s" % (name, category))
1911 # an unpickled object might not have category set from before,
1912 # so set it here to make sure
1913 builder_status.category = category
1914 builder_status.basedir = os.path.join(self.basedir, basedir)
1915 builder_status.name = name # it might have been updated
1916 builder_status.status = self
1918 if not os.path.isdir(builder_status.basedir):
1919 os.mkdir(builder_status.basedir)
1920 builder_status.determineNextBuildNumber()
1922 builder_status.setBigState("offline")
1924 for t in self.watchers:
1925 self.announceNewBuilder(t, name, builder_status)
1927 return builder_status
1929 def builderRemoved(self, name):
1930 for t in self.watchers:
1931 t.builderRemoved(name)
1933 def prune(self):
1934 for b in self.botmaster.builders.values():
1935 b.builder_status.prune()
1937 def buildsetSubmitted(self, bss):
1938 self.activeBuildSets.append(bss)
1939 bss.waitUntilFinished().addCallback(self.activeBuildSets.remove)
1940 for t in self.watchers:
1941 t.buildsetSubmitted(bss)