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
17 from cStringIO
import StringIO
19 from StringIO
import StringIO
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:
35 # currentlyInterlocked
40 # updateCurrentActivity
41 # addFileToCurrentActivity
42 # finishCurrentActivity
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
:
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
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. """
100 def __init__(self
, logfile
, consumer
):
101 self
.logfile
= logfile
102 self
.consumer
= consumer
103 self
.chunkGenerator
= self
.getChunks()
104 consumer
.registerProducer(self
, True)
107 f
= self
.logfile
.getFile()
110 p
= LogFileScanner(chunks
.append
)
112 data
= f
.read(self
.BUFFERSIZE
)
120 data
= f
.read(self
.BUFFERSIZE
)
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
139 d
.addCallback(self
.logfileFinished
)
141 def stopProducing(self
):
142 # TODO: should we still call consumer.finish? probably not.
148 if self
.chunkGenerator
:
149 self
.chunkGenerator
= None # stop making chunks
151 self
.logfile
.watchers
.remove(self
)
152 self
.subscribed
= False
154 def pauseProducing(self
):
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
):
170 if not self
.chunkGenerator
:
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
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
184 def logChunk(self
, build
, step
, logfile
, channel
, chunk
):
186 self
.consumer
.writeChunk((channel
, chunk
))
188 def logfileFinished(self
, logfile
):
191 self
.consumer
.unregisterProducer()
192 self
.consumer
.finish()
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
209 implements(interfaces
.IStatusLog
, interfaces
.ILogFile
)
211 __implements__
= (interfaces
.IStatusLog
, interfaces
.ILogFile
)
217 runEntries
= [] # provided so old pickled builds will getChunks() ok
220 filename
= None # relative to the Builder's basedir
223 def __init__(self
, parent
, name
, logfilename
):
225 @type parent: L{BuildStepStatus}
226 @param parent: the Step that this log is a part of
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
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+")
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())
259 def isFinished(self
):
261 def waitUntilFinished(self
):
263 d
= defer
.succeed(self
)
266 self
.finishedWatchers
.append(d
)
271 # this is the filehandle we're using to write to the log, so
274 # otherwise they get their own read-only handle
275 return open(self
.getFilename(), "r")
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
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
310 return self
._generateChunks
(f
, offset
, remaining
, leftover
,
313 def _generateChunks(self
, f
, offset
, remaining
, leftover
,
316 p
= LogFileScanner(chunks
.append
, channels
)
318 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
319 remaining
-= len(data
)
324 channel
, text
= chunks
.pop(0)
328 yield (channel
, text
)
330 data
= f
.read(min(remaining
, self
.BUFFERSIZE
))
331 remaining
-= len(data
)
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
):
354 self
.watchers
.append(receiver
)
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
,
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
)
369 # interface used by the build steps to add things to the log
372 # merge all .runEntries (which are all of the same type) into a
373 # single chunk for .entries
374 if not self
.runEntries
:
376 channel
= self
.runEntries
[0][0]
377 text
= "".join([c
[1] for c
in self
.runEntries
])
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
])
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]:
397 self
.runEntries
.append((channel
, text
))
398 self
.runLength
+= len(text
)
399 if self
.runLength
>= self
.chunkSize
:
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
)
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
421 os
.fsync(self
.openfile
.fileno())
424 watchers
= self
.finishedWatchers
425 self
.finishedWatchers
= []
430 def __getstate__(self
):
431 d
= self
.__dict
__.copy()
432 del d
['step'] # filled in upon unpickling
434 del d
['finishedWatchers']
435 d
['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
436 if d
.has_key('finished'):
438 if d
.has_key('openfile'):
442 def __setstate__(self
, d
):
444 self
.watchers
= [] # probably not necessary
445 self
.finishedWatchers
= [] # same
446 # self.step must be filled in by our parent
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
465 implements(interfaces
.IStatusLog
)
467 __implements__
= interfaces
.IStatusLog
,
471 def __init__(self
, parent
, name
, logfilename
, html
):
474 self
.filename
= logfilename
478 return self
.name
# set in BuildStepStatus.addLog
482 def isFinished(self
):
484 def waitUntilFinished(self
):
485 return defer
.succeed(self
)
487 def hasContents(self
):
490 return self
.html
# looks kinda like text
491 def getTextWithHeaders(self
):
494 return [(STDERR
, self
.html
)]
496 def subscribe(self
, receiver
, catchup
):
498 def unsubscribe(self
, receiver
):
504 def __getstate__(self
):
505 d
= self
.__dict
__.copy()
509 def upgrade(self
, logfilename
):
515 implements(interfaces
.IStatusEvent
)
517 __implements__
= interfaces
.IStatusEvent
,
524 # IStatusEvent methods
526 return (self
.started
, self
.finished
)
535 self
.finished
= util
.now()
539 implements(interfaces
.ITestResult
)
541 __implements__
= interfaces
.ITestResult
,
543 def __init__(self
, name
, results
, text
, logs
):
544 assert isinstance(name
, tuple)
546 self
.results
= results
553 def getResults(self
):
563 class BuildSetStatus
:
565 implements(interfaces
.IBuildSetStatus
)
567 __implements__
= interfaces
.IBuildSetStatus
,
569 def __init__(self
, source
, reason
, builderNames
, bsid
=None):
572 self
.builderNames
= builderNames
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
:
591 self
.successWatchers
= []
593 def notifyFinishedWatchers(self
):
595 for d
in self
.finishedWatchers
:
597 self
.finishedWatchers
= []
599 # methods for our clients
601 def getSourceStamp(self
):
605 def getResults(self
):
610 def getBuilderNames(self
):
611 return self
.builderNames
612 def getBuildRequests(self
):
613 return self
.buildRequests
614 def isFinished(self
):
617 def waitUntilSuccess(self
):
618 if self
.finished
or not self
.stillHopeful
:
619 # the deferreds have already fired
620 return defer
.succeed(self
)
622 self
.successWatchers
.append(d
)
625 def waitUntilFinished(self
):
627 return defer
.succeed(self
)
629 self
.finishedWatchers
.append(d
)
632 class BuildRequestStatus
:
634 implements(interfaces
.IBuildRequestStatus
)
636 __implements__
= interfaces
.IBuildRequestStatus
,
638 def __init__(self
, source
, builderName
):
640 self
.builderName
= builderName
641 self
.builds
= [] # list of BuildStatus objects
644 def buildStarted(self
, build
):
645 self
.builds
.append(build
)
646 for o
in self
.observers
[:]:
649 # methods called by our clients
650 def getSourceStamp(self
):
652 def getBuilderName(self
):
653 return self
.builderName
657 def subscribe(self
, observer
):
658 self
.observers
.append(observer
)
659 for b
in self
.builds
:
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}.
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
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.
687 implements(interfaces
.IBuildStepStatus
, interfaces
.IStatusEvent
)
689 __implements__
= interfaces
.IBuildStepStatus
, interfaces
.IStatusEvent
690 persistenceVersion
= 1
701 finishedWatchers
= []
703 def __init__(self
, parent
):
704 assert interfaces
.IBuildStatus(parent
)
710 self
.finishedWatchers
= []
713 """Returns a short string with the name of this step. This string
714 may have spaces in it."""
721 return (self
.started
, self
.finished
)
723 def getExpectations(self
):
724 """Returns a list of tuples (name, current, target)."""
725 if not self
.progress
:
728 metrics
= self
.progress
.progress
.keys()
731 t
= (m
, self
.progress
.progress
[m
], self
.progress
.expectations
[m
])
739 return self
.urls
.copy()
741 def isFinished(self
):
742 return (self
.finished
is not None)
744 def waitUntilFinished(self
):
746 d
= defer
.succeed(self
)
749 self
.finishedWatchers
.append(d
)
752 # while the step is running, the following methods make sense.
753 # Afterwards they return None
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.
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."""
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."""
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
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
,
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
):
828 def setProgress(self
, stepprogress
):
829 self
.progress
= stepprogress
831 def stepStarted(self
):
832 self
.started
= util
.now()
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
)
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
)
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
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
):
873 def setText(self
, text
):
875 def setText2(self
, 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():
885 for r
in self
.updates
.keys():
886 if self
.updates
[r
] is not None:
887 self
.updates
[r
].cancel()
890 watchers
= self
.finishedWatchers
891 self
.finishedWatchers
= []
897 def __getstate__(self
):
898 d
= styles
.Versioned
.__getstate
__(self
)
899 del d
['build'] # filled in when loading
900 if d
.has_key('progress'):
903 del d
['finishedWatchers']
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
:
913 def upgradeToVersion1(self
):
914 if not hasattr(self
, "urls"):
918 class BuildStatus(styles
.Versioned
):
920 implements(interfaces
.IBuildStatus
, interfaces
.IStatusEvent
)
922 __implements__
= interfaces
.IBuildStatus
, interfaces
.IStatusEvent
923 persistenceVersion
= 2
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.
943 finishedWatchers
= []
946 def __init__(self
, parent
, number
):
948 @type parent: L{BuilderStatus}
951 assert interfaces
.IBuilderStatus(parent
)
952 self
.builder
= parent
956 self
.finishedWatchers
= []
958 self
.testResults
= {}
963 def getBuilder(self
):
965 @rtype: L{BuilderStatus}
969 def getProperty(self
, propname
):
970 return self
.properties
[propname
]
975 def getPreviousBuild(self
):
978 return self
.builder
.getBuild(self
.number
-1)
980 def getSourceStamp(self
):
981 return (self
.source
.branch
, self
.source
.revision
, self
.source
.patch
)
986 def getChanges(self
):
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
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)."""
1005 return (self
.started
, self
.finished
)
1007 def isFinished(self
):
1008 return (self
.finished
is not None)
1010 def waitUntilFinished(self
):
1012 d
= defer
.succeed(self
)
1014 d
= defer
.Deferred()
1015 self
.finishedWatchers
.append(d
)
1018 # while the build is running, the following methods make sense.
1019 # Afterwards they return None
1022 if self
.finished
is not None:
1024 if not self
.progress
:
1026 eta
= self
.progress
.eta()
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.
1039 text
.extend(self
.text
)
1040 for s
in self
.steps
:
1041 text
.extend(s
.text2
)
1047 def getResults(self
):
1050 def getSlavename(self
):
1051 return self
.slavename
1053 def getTestResults(self
):
1054 return self
.testResults
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"
1061 for s
in self
.steps
:
1062 for log
in s
.getLogs():
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
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
,
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
1102 s
= BuildStepStatus(self
)
1104 self
.steps
.append(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))
1139 def setColor(self
, 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()
1153 watchers
= self
.finishedWatchers
1154 self
.finishedWatchers
= []
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
)
1166 if type(receiver
) == type(()):
1167 step
.subscribe(receiver
[0], receiver
[1])
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
:
1188 def pruneSteps(self
):
1189 # this build is very old: remove the build steps too
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
1212 filename
= starting_filename
1213 while filename
in [l
.filename
1214 for step
in self
.steps
1215 for l
in step
.getLogs()
1217 filename
= "%s_%d" % (starting_filename
, unique_counter
)
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
1234 del d
['finishedWatchers']
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
:
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,
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).
1267 for s
in self
.steps
:
1268 for l
in s
.getLogs():
1270 pass # new-style, log contents are on disk
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"
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
):
1292 os
.rename(tmpfilename
, filename
)
1294 log
.msg("unable to save build %s-#%d" % (self
.builder
.name
,
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
1314 @type category: string
1315 @ivar category: user-defined category this builder belongs to; can be
1316 used to filter on in status clients
1320 implements(interfaces
.IBuilderStatus
)
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.
1329 buildHorizon
= 100 # forget builds beyond this
1330 stepHorizon
= 50 # forget steps in builds beyond this
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
= []
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
1351 self
.buildCache
= [] # TODO: age builds out of the cache
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
)
1363 for b
in self
.currentBuilds
:
1365 # TODO: push a 'hey, build was interrupted' event
1366 del d
['currentBuilds']
1367 del d
['pendingBuilds']
1368 del d
['currentBigState']
1371 del d
['nextBuildNumber']
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
= []
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
]
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
)]
1403 self
.nextBuildNumber
= max(existing_builds
) + 1
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.
1413 filename
= os
.path
.join(self
.basedir
, "builder")
1414 tmpfilename
= filename
+ ".tmp"
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
):
1421 os
.rename(tmpfilename
, filename
)
1423 log
.msg("unable to save builder %s" % self
.name
)
1427 # build cache management
1429 def addBuildToCache(self
, build
):
1430 if build
in self
.buildCache
:
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
:
1440 for build
in self
.buildCache
:
1441 if build
.number
== number
:
1443 filename
= os
.path
.join(self
.basedir
, "%d" % number
)
1445 build
= pickle
.load(open(filename
, "rb"))
1447 build
.builder
= self
1448 # handle LogFiles from after 0.5.0 and before 0.6.5
1449 build
.upgradeLogfiles()
1450 self
.addBuildToCache(build
)
1453 raise IndexError("no such build %d" % number
)
1455 raise IndexError("corrupted build pickle %d" % number
)
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
]:
1465 # IBuilderStatus methods
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)
1487 def getBuild(self
, number
):
1489 number
= self
.nextBuildNumber
+ number
1490 if number
< 0 or number
>= self
.nextBuildNumber
:
1494 return self
.getBuildByNumber(number
)
1498 def getEvent(self
, number
):
1500 return self
.events
[number
]
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.
1515 e
= self
.getEvent(eventIndex
)
1516 for Nb
in range(1, self
.nextBuildNumber
+1):
1517 b
= self
.getBuild(-Nb
)
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
:
1527 e
= self
.getEvent(eventIndex
)
1530 while e
is not None:
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
1552 e
.started
= util
.now()
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
1562 e
.started
= util
.now()
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
1575 def publishState(self
, target
=None):
1576 state
= self
.currentBigState
1578 if target
is not None:
1580 target
.builderChangedState(self
.name
, state
)
1582 for w
in self
.watchers
:
1583 w
.builderChangedState(self
.name
, state
)
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
)
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
)
1621 if type(receiver
) == type(()):
1622 s
.subscribe(receiver
[0], receiver
[1])
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
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)
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()
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
:
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
)
1686 offset
= num
- first
1687 log
.msg(self
, "offset", offset
)
1689 return self
.events
[offset
]
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.
1701 self
.allEvents
= self
.loadFile("events", [])
1703 self
.nextEventNumber
= self
.allEvents
[-1].number
+ 1
1705 self
.nextEventNumber
= 0
1706 def saveYourOldEvents(self
):
1707 self
.saveFile("events", self
.allEvents
)
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
)
1723 implements(interfaces
.ISlaveStatus
)
1725 __implements__
= interfaces
.ISlaveStatus
,
1731 def __init__(self
, name
):
1740 def isConnected(self
):
1741 return self
.connected
1743 def setAdmin(self
, admin
):
1745 def setHost(self
, host
):
1747 def setConnected(self
, isConnected
):
1748 self
.connected
= isConnected
1752 I represent the status of the buildmaster.
1755 implements(interfaces
.IStatus
)
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
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()
1793 if providedBy(thing
, interfaces
.IStatus
):
1795 if providedBy(thing
, interfaces
.ISchedulerStatus
):
1797 if providedBy(thing
, interfaces
.IBuilderStatus
):
1799 return prefix
+ urllib
.quote(builder
.getName(), safe
='')
1800 if providedBy(thing
, interfaces
.IBuildStatus
):
1802 builder
= build
.getBuilder()
1803 return "%s%s/builds/%d" % (
1805 urllib
.quote(builder
.getName(), safe
=''),
1807 if providedBy(thing
, interfaces
.IBuildStepStatus
):
1809 build
= step
.getBuild()
1810 builder
= build
.getBuilder()
1811 return "%s%s/builds/%d/%s" % (
1813 urllib
.quote(builder
.getName(), safe
=''),
1815 "step-" + urllib
.quote(step
.getName(), safe
=''))
1817 # IBuildRequestStatus
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
):
1826 return "%schanges/%d" % (prefix
, change
.number
)
1828 if providedBy(thing
, interfaces
.IStatusLog
):
1830 step
= log
.getStep()
1831 build
= step
.getBuild()
1832 builder
= build
.getBuilder()
1834 logs
= step
.getLogs()
1835 for i
in range(len(logs
)):
1841 return "%s%s/builds/%d/%s/%d" % (
1843 urllib
.quote(builder
.getName(), safe
=''),
1845 "step-" + urllib
.quote(step
.getName(), safe
=''),
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
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
:
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
)
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
1899 builder_status
= pickle
.load(open(filename
, "rb"))
1902 log
.msg("no saved status pickle, creating a new one")
1904 log
.msg("error while loading status pickle, creating a new one")
1905 log
.msg("error follows:")
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
)
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
)