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