| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: buildbot.test.test_status -*- | |
| 2 | |
| 3 from zope.interface import implements | |
| 4 from twisted.python import log | |
| 5 from twisted.persisted import styles | |
| 6 from twisted.internet import reactor, defer, threads | |
| 7 from twisted.protocols import basic | |
| 8 from buildbot.process.properties import Properties | |
| 9 | |
| 10 import weakref | |
| 11 import os, shutil, sys, re, urllib, itertools | |
| 12 import gc | |
| 13 from cPickle import load, dump | |
| 14 from cStringIO import StringIO | |
| 15 | |
| 16 try: # bz2 is not available on py23 | |
| 17 from bz2 import BZ2File | |
| 18 except ImportError: | |
| 19 BZ2File = None | |
| 20 | |
| 21 try: | |
| 22 from gzip import GzipFile | |
| 23 except ImportError: | |
| 24 GzipFile = None | |
| 25 | |
| 26 # sibling imports | |
| 27 from buildbot import interfaces, util, sourcestamp | |
| 28 | |
| 29 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5) | |
| 30 Results = ["success", "warnings", "failure", "skipped", "exception"] | |
| 31 | |
| 32 | |
| 33 # build processes call the following methods: | |
| 34 # | |
| 35 # setDefaults | |
| 36 # | |
| 37 # currentlyBuilding | |
| 38 # currentlyIdle | |
| 39 # currentlyInterlocked | |
| 40 # currentlyOffline | |
| 41 # currentlyWaiting | |
| 42 # | |
| 43 # setCurrentActivity | |
| 44 # updateCurrentActivity | |
| 45 # addFileToCurrentActivity | |
| 46 # finishCurrentActivity | |
| 47 # | |
| 48 # startBuild | |
| 49 # finishBuild | |
| 50 | |
| 51 STDOUT = interfaces.LOG_CHANNEL_STDOUT | |
| 52 STDERR = interfaces.LOG_CHANNEL_STDERR | |
| 53 HEADER = interfaces.LOG_CHANNEL_HEADER | |
| 54 ChunkTypes = ["stdout", "stderr", "header"] | |
| 55 | |
| 56 class LogFileScanner(basic.NetstringReceiver): | |
| 57 def __init__(self, chunk_cb, channels=[]): | |
| 58 self.chunk_cb = chunk_cb | |
| 59 self.channels = channels | |
| 60 | |
| 61 def stringReceived(self, line): | |
| 62 channel = int(line[0]) | |
| 63 if not self.channels or (channel in self.channels): | |
| 64 self.chunk_cb((channel, line[1:])) | |
| 65 | |
| 66 class LogFileProducer: | |
| 67 """What's the plan? | |
| 68 | |
| 69 the LogFile has just one FD, used for both reading and writing. | |
| 70 Each time you add an entry, fd.seek to the end and then write. | |
| 71 | |
| 72 Each reader (i.e. Producer) keeps track of their own offset. The reader | |
| 73 starts by seeking to the start of the logfile, and reading forwards. | |
| 74 Between each hunk of file they yield chunks, so they must remember their | |
| 75 offset before yielding and re-seek back to that offset before reading | |
| 76 more data. When their read() returns EOF, they're finished with the first | |
| 77 phase of the reading (everything that's already been written to disk). | |
| 78 | |
| 79 After EOF, the remaining data is entirely in the current entries list. | |
| 80 These entries are all of the same channel, so we can do one "".join and | |
| 81 obtain a single chunk to be sent to the listener. But since that involves | |
| 82 a yield, and more data might arrive after we give up control, we have to | |
| 83 subscribe them before yielding. We can't subscribe them any earlier, | |
| 84 otherwise they'd get data out of order. | |
| 85 | |
| 86 We're using a generator in the first place so that the listener can | |
| 87 throttle us, which means they're pulling. But the subscription means | |
| 88 we're pushing. Really we're a Producer. In the first phase we can be | |
| 89 either a PullProducer or a PushProducer. In the second phase we're only a | |
| 90 PushProducer. | |
| 91 | |
| 92 So the client gives a LogFileConsumer to File.subscribeConsumer . This | |
| 93 Consumer must have registerProducer(), unregisterProducer(), and | |
| 94 writeChunk(), and is just like a regular twisted.interfaces.IConsumer, | |
| 95 except that writeChunk() takes chunks (tuples of (channel,text)) instead | |
| 96 of the normal write() which takes just text. The LogFileConsumer is | |
| 97 allowed to call stopProducing, pauseProducing, and resumeProducing on the | |
| 98 producer instance it is given. """ | |
| 99 | |
| 100 paused = False | |
| 101 subscribed = False | |
| 102 BUFFERSIZE = 2048 | |
| 103 | |
| 104 def __init__(self, logfile, consumer): | |
| 105 self.logfile = logfile | |
| 106 self.consumer = consumer | |
| 107 self.chunkGenerator = self.getChunks() | |
| 108 consumer.registerProducer(self, True) | |
| 109 | |
| 110 def getChunks(self): | |
| 111 f = self.logfile.getFile() | |
| 112 offset = 0 | |
| 113 chunks = [] | |
| 114 p = LogFileScanner(chunks.append) | |
| 115 f.seek(offset) | |
| 116 data = f.read(self.BUFFERSIZE) | |
| 117 offset = f.tell() | |
| 118 while data: | |
| 119 p.dataReceived(data) | |
| 120 while chunks: | |
| 121 c = chunks.pop(0) | |
| 122 yield c | |
| 123 f.seek(offset) | |
| 124 data = f.read(self.BUFFERSIZE) | |
| 125 offset = f.tell() | |
| 126 del f | |
| 127 | |
| 128 # now subscribe them to receive new entries | |
| 129 self.subscribed = True | |
| 130 self.logfile.watchers.append(self) | |
| 131 d = self.logfile.waitUntilFinished() | |
| 132 | |
| 133 # then give them the not-yet-merged data | |
| 134 if self.logfile.runEntries: | |
| 135 channel = self.logfile.runEntries[0][0] | |
| 136 text = "".join([c[1] for c in self.logfile.runEntries]) | |
| 137 yield (channel, text) | |
| 138 | |
| 139 # now we've caught up to the present. Anything further will come from | |
| 140 # the logfile subscription. We add the callback *after* yielding the | |
| 141 # data from runEntries, because the logfile might have finished | |
| 142 # during the yield. | |
| 143 d.addCallback(self.logfileFinished) | |
| 144 | |
| 145 def stopProducing(self): | |
| 146 # TODO: should we still call consumer.finish? probably not. | |
| 147 self.paused = True | |
| 148 self.consumer = None | |
| 149 self.done() | |
| 150 | |
| 151 def done(self): | |
| 152 if self.chunkGenerator: | |
| 153 self.chunkGenerator = None # stop making chunks | |
| 154 if self.subscribed: | |
| 155 self.logfile.watchers.remove(self) | |
| 156 self.subscribed = False | |
| 157 | |
| 158 def pauseProducing(self): | |
| 159 self.paused = True | |
| 160 | |
| 161 def resumeProducing(self): | |
| 162 # Twisted-1.3.0 has a bug which causes hangs when resumeProducing | |
| 163 # calls transport.write (there is a recursive loop, fixed in 2.0 in | |
| 164 # t.i.abstract.FileDescriptor.doWrite by setting the producerPaused | |
| 165 # flag *before* calling resumeProducing). To work around this, we | |
| 166 # just put off the real resumeProducing for a moment. This probably | |
| 167 # has a performance hit, but I'm going to assume that the log files | |
| 168 # are not retrieved frequently enough for it to be an issue. | |
| 169 | |
| 170 reactor.callLater(0, self._resumeProducing) | |
| 171 | |
| 172 def _resumeProducing(self): | |
| 173 self.paused = False | |
| 174 if not self.chunkGenerator: | |
| 175 return | |
| 176 try: | |
| 177 while not self.paused: | |
| 178 chunk = self.chunkGenerator.next() | |
| 179 self.consumer.writeChunk(chunk) | |
| 180 # we exit this when the consumer says to stop, or we run out | |
| 181 # of chunks | |
| 182 except StopIteration: | |
| 183 # if the generator finished, it will have done releaseFile | |
| 184 self.chunkGenerator = None | |
| 185 # now everything goes through the subscription, and they don't get to | |
| 186 # pause anymore | |
| 187 | |
| 188 def logChunk(self, build, step, logfile, channel, chunk): | |
| 189 if self.consumer: | |
| 190 self.consumer.writeChunk((channel, chunk)) | |
| 191 | |
| 192 def logfileFinished(self, logfile): | |
| 193 self.done() | |
| 194 if self.consumer: | |
| 195 self.consumer.unregisterProducer() | |
| 196 self.consumer.finish() | |
| 197 self.consumer = None | |
| 198 | |
| 199 def _tryremove(filename, timeout, retries): | |
| 200 """Try to remove a file, and if failed, try again in timeout. | |
| 201 Increases the timeout by a factor of 4, and only keeps trying for | |
| 202 another retries-amount of times. | |
| 203 | |
| 204 """ | |
| 205 try: | |
| 206 os.unlink(filename) | |
| 207 except OSError: | |
| 208 if retries > 0: | |
| 209 reactor.callLater(timeout, _tryremove, filename, timeout * 4, | |
| 210 retries - 1) | |
| 211 else: | |
| 212 log.msg("giving up on removing %s after over %d seconds" % | |
| 213 (filename, timeout)) | |
| 214 | |
| 215 class LogFile: | |
| 216 """A LogFile keeps all of its contents on disk, in a non-pickle format to | |
| 217 which new entries can easily be appended. The file on disk has a name | |
| 218 like 12-log-compile-output, under the Builder's directory. The actual | |
| 219 filename is generated (before the LogFile is created) by | |
| 220 L{BuildStatus.generateLogfileName}. | |
| 221 | |
| 222 Old LogFile pickles (which kept their contents in .entries) must be | |
| 223 upgraded. The L{BuilderStatus} is responsible for doing this, when it | |
| 224 loads the L{BuildStatus} into memory. The Build pickle is not modified, | |
| 225 so users who go from 0.6.5 back to 0.6.4 don't have to lose their | |
| 226 logs.""" | |
| 227 | |
| 228 implements(interfaces.IStatusLog, interfaces.ILogFile) | |
| 229 | |
| 230 finished = False | |
| 231 length = 0 | |
| 232 nonHeaderLength = 0 | |
| 233 tailLength = 0 | |
| 234 chunkSize = 10*1000 | |
| 235 runLength = 0 | |
| 236 # No max size by default | |
| 237 logMaxSize = None | |
| 238 # Don't keep a tail buffer by default | |
| 239 logMaxTailSize = None | |
| 240 maxLengthExceeded = False | |
| 241 runEntries = [] # provided so old pickled builds will getChunks() ok | |
| 242 entries = None | |
| 243 BUFFERSIZE = 2048 | |
| 244 filename = None # relative to the Builder's basedir | |
| 245 openfile = None | |
| 246 compressMethod = "bz2" | |
| 247 | |
| 248 def __init__(self, parent, name, logfilename): | |
| 249 """ | |
| 250 @type parent: L{BuildStepStatus} | |
| 251 @param parent: the Step that this log is a part of | |
| 252 @type name: string | |
| 253 @param name: the name of this log, typically 'output' | |
| 254 @type logfilename: string | |
| 255 @param logfilename: the Builder-relative pathname for the saved entries | |
| 256 """ | |
| 257 self.step = parent | |
| 258 self.name = name | |
| 259 self.filename = logfilename | |
| 260 fn = self.getFilename() | |
| 261 if os.path.exists(fn): | |
| 262 # the buildmaster was probably stopped abruptly, before the | |
| 263 # BuilderStatus could be saved, so BuilderStatus.nextBuildNumber | |
| 264 # is out of date, and we're overlapping with earlier builds now. | |
| 265 # Warn about it, but then overwrite the old pickle file | |
| 266 log.msg("Warning: Overwriting old serialized Build at %s" % fn) | |
| 267 dirname = os.path.dirname(fn) | |
| 268 if not os.path.exists(dirname): | |
| 269 os.makedirs(dirname) | |
| 270 self.openfile = open(fn, "w+") | |
| 271 self.runEntries = [] | |
| 272 self.watchers = [] | |
| 273 self.finishedWatchers = [] | |
| 274 self.tailBuffer = [] | |
| 275 | |
| 276 def getFilename(self): | |
| 277 return os.path.join(self.step.build.builder.basedir, self.filename) | |
| 278 | |
| 279 def hasContents(self): | |
| 280 return os.path.exists(self.getFilename() + '.bz2') or \ | |
| 281 os.path.exists(self.getFilename() + '.gz') or \ | |
| 282 os.path.exists(self.getFilename()) | |
| 283 | |
| 284 def getName(self): | |
| 285 return self.name | |
| 286 | |
| 287 def getStep(self): | |
| 288 return self.step | |
| 289 | |
| 290 def isFinished(self): | |
| 291 return self.finished | |
| 292 def waitUntilFinished(self): | |
| 293 if self.finished: | |
| 294 d = defer.succeed(self) | |
| 295 else: | |
| 296 d = defer.Deferred() | |
| 297 self.finishedWatchers.append(d) | |
| 298 return d | |
| 299 | |
| 300 def getFile(self): | |
| 301 if self.openfile: | |
| 302 # this is the filehandle we're using to write to the log, so | |
| 303 # don't close it! | |
| 304 return self.openfile | |
| 305 # otherwise they get their own read-only handle | |
| 306 # try a compressed log first | |
| 307 if BZ2File is not None: | |
| 308 try: | |
| 309 return BZ2File(self.getFilename() + ".bz2", "r") | |
| 310 except IOError: | |
| 311 pass | |
| 312 if GzipFile is not None: | |
| 313 try: | |
| 314 return GzipFile(self.getFilename() + ".gz", "r") | |
| 315 except IOError: | |
| 316 pass | |
| 317 return open(self.getFilename(), "r") | |
| 318 | |
| 319 def getText(self): | |
| 320 # this produces one ginormous string | |
| 321 return "".join(self.getChunks([STDOUT, STDERR], onlyText=True)) | |
| 322 | |
| 323 def getTextWithHeaders(self): | |
| 324 return "".join(self.getChunks(onlyText=True)) | |
| 325 | |
| 326 def getChunks(self, channels=[], onlyText=False): | |
| 327 # generate chunks for everything that was logged at the time we were | |
| 328 # first called, so remember how long the file was when we started. | |
| 329 # Don't read beyond that point. The current contents of | |
| 330 # self.runEntries will follow. | |
| 331 | |
| 332 # this returns an iterator, which means arbitrary things could happen | |
| 333 # while we're yielding. This will faithfully deliver the log as it | |
| 334 # existed when it was started, and not return anything after that | |
| 335 # point. To use this in subscribe(catchup=True) without missing any | |
| 336 # data, you must insure that nothing will be added to the log during | |
| 337 # yield() calls. | |
| 338 | |
| 339 f = self.getFile() | |
| 340 if not self.finished: | |
| 341 offset = 0 | |
| 342 f.seek(0, 2) | |
| 343 remaining = f.tell() | |
| 344 else: | |
| 345 offset = 0 | |
| 346 remaining = None | |
| 347 | |
| 348 leftover = None | |
| 349 if self.runEntries and (not channels or | |
| 350 (self.runEntries[0][0] in channels)): | |
| 351 leftover = (self.runEntries[0][0], | |
| 352 "".join([c[1] for c in self.runEntries])) | |
| 353 | |
| 354 # freeze the state of the LogFile by passing a lot of parameters into | |
| 355 # a generator | |
| 356 return self._generateChunks(f, offset, remaining, leftover, | |
| 357 channels, onlyText) | |
| 358 | |
| 359 def _generateChunks(self, f, offset, remaining, leftover, | |
| 360 channels, onlyText): | |
| 361 chunks = [] | |
| 362 p = LogFileScanner(chunks.append, channels) | |
| 363 f.seek(offset) | |
| 364 if remaining is not None: | |
| 365 data = f.read(min(remaining, self.BUFFERSIZE)) | |
| 366 remaining -= len(data) | |
| 367 else: | |
| 368 data = f.read(self.BUFFERSIZE) | |
| 369 | |
| 370 offset = f.tell() | |
| 371 while data: | |
| 372 p.dataReceived(data) | |
| 373 while chunks: | |
| 374 channel, text = chunks.pop(0) | |
| 375 if onlyText: | |
| 376 yield text | |
| 377 else: | |
| 378 yield (channel, text) | |
| 379 f.seek(offset) | |
| 380 if remaining is not None: | |
| 381 data = f.read(min(remaining, self.BUFFERSIZE)) | |
| 382 remaining -= len(data) | |
| 383 else: | |
| 384 data = f.read(self.BUFFERSIZE) | |
| 385 offset = f.tell() | |
| 386 del f | |
| 387 | |
| 388 if leftover: | |
| 389 if onlyText: | |
| 390 yield leftover[1] | |
| 391 else: | |
| 392 yield leftover | |
| 393 | |
| 394 def readlines(self, channel=STDOUT): | |
| 395 """Return an iterator that produces newline-terminated lines, | |
| 396 excluding header chunks.""" | |
| 397 # TODO: make this memory-efficient, by turning it into a generator | |
| 398 # that retrieves chunks as necessary, like a pull-driven version of | |
| 399 # twisted.protocols.basic.LineReceiver | |
| 400 alltext = "".join(self.getChunks([channel], onlyText=True)) | |
| 401 io = StringIO(alltext) | |
| 402 return io.readlines() | |
| 403 | |
| 404 def subscribe(self, receiver, catchup): | |
| 405 if self.finished: | |
| 406 return | |
| 407 self.watchers.append(receiver) | |
| 408 if catchup: | |
| 409 for channel, text in self.getChunks(): | |
| 410 # TODO: add logChunks(), to send over everything at once? | |
| 411 receiver.logChunk(self.step.build, self.step, self, | |
| 412 channel, text) | |
| 413 | |
| 414 def unsubscribe(self, receiver): | |
| 415 if receiver in self.watchers: | |
| 416 self.watchers.remove(receiver) | |
| 417 | |
| 418 def subscribeConsumer(self, consumer): | |
| 419 p = LogFileProducer(self, consumer) | |
| 420 p.resumeProducing() | |
| 421 | |
| 422 # interface used by the build steps to add things to the log | |
| 423 | |
| 424 def merge(self): | |
| 425 # merge all .runEntries (which are all of the same type) into a | |
| 426 # single chunk for .entries | |
| 427 if not self.runEntries: | |
| 428 return | |
| 429 channel = self.runEntries[0][0] | |
| 430 text = "".join([c[1] for c in self.runEntries]) | |
| 431 assert channel < 10 | |
| 432 f = self.openfile | |
| 433 f.seek(0, 2) | |
| 434 offset = 0 | |
| 435 while offset < len(text): | |
| 436 size = min(len(text)-offset, self.chunkSize) | |
| 437 f.write("%d:%d" % (1 + size, channel)) | |
| 438 f.write(text[offset:offset+size]) | |
| 439 f.write(",") | |
| 440 offset += size | |
| 441 self.runEntries = [] | |
| 442 self.runLength = 0 | |
| 443 | |
| 444 def addEntry(self, channel, text): | |
| 445 assert not self.finished | |
| 446 | |
| 447 if isinstance(text, unicode): | |
| 448 text = text.encode('utf-8') | |
| 449 if channel != HEADER: | |
| 450 # Truncate the log if it's more than logMaxSize bytes | |
| 451 if self.logMaxSize and self.nonHeaderLength > self.logMaxSize: | |
| 452 # Add a message about what's going on | |
| 453 if not self.maxLengthExceeded: | |
| 454 msg = "\nOutput exceeded %i bytes, remaining output has been
truncated\n" % self.logMaxSize | |
| 455 self.addEntry(HEADER, msg) | |
| 456 self.merge() | |
| 457 self.maxLengthExceeded = True | |
| 458 | |
| 459 if self.logMaxTailSize: | |
| 460 # Update the tail buffer | |
| 461 self.tailBuffer.append((channel, text)) | |
| 462 self.tailLength += len(text) | |
| 463 while self.tailLength > self.logMaxTailSize: | |
| 464 # Drop some stuff off the beginning of the buffer | |
| 465 c,t = self.tailBuffer.pop(0) | |
| 466 n = len(t) | |
| 467 self.tailLength -= n | |
| 468 assert self.tailLength >= 0 | |
| 469 return | |
| 470 | |
| 471 self.nonHeaderLength += len(text) | |
| 472 | |
| 473 # we only add to .runEntries here. merge() is responsible for adding | |
| 474 # merged chunks to .entries | |
| 475 if self.runEntries and channel != self.runEntries[0][0]: | |
| 476 self.merge() | |
| 477 self.runEntries.append((channel, text)) | |
| 478 self.runLength += len(text) | |
| 479 if self.runLength >= self.chunkSize: | |
| 480 self.merge() | |
| 481 | |
| 482 for w in self.watchers: | |
| 483 w.logChunk(self.step.build, self.step, self, channel, text) | |
| 484 self.length += len(text) | |
| 485 | |
| 486 def addStdout(self, text): | |
| 487 self.addEntry(STDOUT, text) | |
| 488 def addStderr(self, text): | |
| 489 self.addEntry(STDERR, text) | |
| 490 def addHeader(self, text): | |
| 491 self.addEntry(HEADER, text) | |
| 492 | |
| 493 def finish(self): | |
| 494 if self.tailBuffer: | |
| 495 msg = "\nFinal %i bytes follow below:\n" % self.tailLength | |
| 496 tmp = self.runEntries | |
| 497 self.runEntries = [(HEADER, msg)] | |
| 498 self.merge() | |
| 499 self.runEntries = self.tailBuffer | |
| 500 self.merge() | |
| 501 self.runEntries = tmp | |
| 502 self.merge() | |
| 503 self.tailBuffer = [] | |
| 504 else: | |
| 505 self.merge() | |
| 506 | |
| 507 if self.openfile: | |
| 508 # we don't do an explicit close, because there might be readers | |
| 509 # shareing the filehandle. As soon as they stop reading, the | |
| 510 # filehandle will be released and automatically closed. | |
| 511 self.openfile.flush() | |
| 512 del self.openfile | |
| 513 self.finished = True | |
| 514 watchers = self.finishedWatchers | |
| 515 self.finishedWatchers = [] | |
| 516 for w in watchers: | |
| 517 w.callback(self) | |
| 518 self.watchers = [] | |
| 519 | |
| 520 | |
| 521 def compressLog(self): | |
| 522 # bail out if there's no compression support | |
| 523 if self.compressMethod == "bz2": | |
| 524 if BZ2File is None: | |
| 525 return | |
| 526 compressed = self.getFilename() + ".bz2.tmp" | |
| 527 elif self.compressMethod == "gz": | |
| 528 if GzipFile is None: | |
| 529 return | |
| 530 compressed = self.getFilename() + ".gz.tmp" | |
| 531 d = threads.deferToThread(self._compressLog, compressed) | |
| 532 d.addCallback(self._renameCompressedLog, compressed) | |
| 533 d.addErrback(self._cleanupFailedCompress, compressed) | |
| 534 return d | |
| 535 | |
| 536 def _compressLog(self, compressed): | |
| 537 infile = self.getFile() | |
| 538 if self.compressMethod == "bz2": | |
| 539 cf = BZ2File(compressed, 'w') | |
| 540 elif self.compressMethod == "gz": | |
| 541 cf = GzipFile(compressed, 'w') | |
| 542 bufsize = 1024*1024 | |
| 543 while True: | |
| 544 buf = infile.read(bufsize) | |
| 545 cf.write(buf) | |
| 546 if len(buf) < bufsize: | |
| 547 break | |
| 548 cf.close() | |
| 549 def _renameCompressedLog(self, rv, compressed): | |
| 550 if self.compressMethod == "bz2": | |
| 551 filename = self.getFilename() + '.bz2' | |
| 552 else: | |
| 553 filename = self.getFilename() + '.gz' | |
| 554 if sys.platform == 'win32': | |
| 555 # windows cannot rename a file on top of an existing one, so | |
| 556 # fall back to delete-first. There are ways this can fail and | |
| 557 # lose the builder's history, so we avoid using it in the | |
| 558 # general (non-windows) case | |
| 559 if os.path.exists(filename): | |
| 560 os.unlink(filename) | |
| 561 os.rename(compressed, filename) | |
| 562 _tryremove(self.getFilename(), 1, 5) | |
| 563 def _cleanupFailedCompress(self, failure, compressed): | |
| 564 log.msg("failed to compress %s" % self.getFilename()) | |
| 565 if os.path.exists(compressed): | |
| 566 _tryremove(compressed, 1, 5) | |
| 567 failure.trap() # reraise the failure | |
| 568 | |
| 569 # persistence stuff | |
| 570 def __getstate__(self): | |
| 571 d = self.__dict__.copy() | |
| 572 del d['step'] # filled in upon unpickling | |
| 573 del d['watchers'] | |
| 574 del d['finishedWatchers'] | |
| 575 d['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really? | |
| 576 if d.has_key('finished'): | |
| 577 del d['finished'] | |
| 578 if d.has_key('openfile'): | |
| 579 del d['openfile'] | |
| 580 return d | |
| 581 | |
| 582 def __setstate__(self, d): | |
| 583 self.__dict__ = d | |
| 584 self.watchers = [] # probably not necessary | |
| 585 self.finishedWatchers = [] # same | |
| 586 # self.step must be filled in by our parent | |
| 587 self.finished = True | |
| 588 | |
| 589 def upgrade(self, logfilename): | |
| 590 """Save our .entries to a new-style offline log file (if necessary), | |
| 591 and modify our in-memory representation to use it. The original | |
| 592 pickled LogFile (inside the pickled Build) won't be modified.""" | |
| 593 self.filename = logfilename | |
| 594 if not os.path.exists(self.getFilename()): | |
| 595 self.openfile = open(self.getFilename(), "w") | |
| 596 self.finished = False | |
| 597 for channel,text in self.entries: | |
| 598 self.addEntry(channel, text) | |
| 599 self.finish() # releases self.openfile, which will be closed | |
| 600 del self.entries | |
| 601 | |
| 602 class HTMLLogFile: | |
| 603 implements(interfaces.IStatusLog) | |
| 604 | |
| 605 filename = None | |
| 606 | |
| 607 def __init__(self, parent, name, logfilename, html): | |
| 608 self.step = parent | |
| 609 self.name = name | |
| 610 self.filename = logfilename | |
| 611 self.html = html | |
| 612 | |
| 613 def getName(self): | |
| 614 return self.name # set in BuildStepStatus.addLog | |
| 615 def getStep(self): | |
| 616 return self.step | |
| 617 | |
| 618 def isFinished(self): | |
| 619 return True | |
| 620 def waitUntilFinished(self): | |
| 621 return defer.succeed(self) | |
| 622 | |
| 623 def hasContents(self): | |
| 624 return True | |
| 625 def getText(self): | |
| 626 return self.html # looks kinda like text | |
| 627 def getTextWithHeaders(self): | |
| 628 return self.html | |
| 629 def getChunks(self): | |
| 630 return [(STDERR, self.html)] | |
| 631 | |
| 632 def subscribe(self, receiver, catchup): | |
| 633 pass | |
| 634 def unsubscribe(self, receiver): | |
| 635 pass | |
| 636 | |
| 637 def finish(self): | |
| 638 pass | |
| 639 | |
| 640 def __getstate__(self): | |
| 641 d = self.__dict__.copy() | |
| 642 del d['step'] | |
| 643 return d | |
| 644 | |
| 645 def upgrade(self, logfilename): | |
| 646 pass | |
| 647 | |
| 648 | |
| 649 class Event: | |
| 650 implements(interfaces.IStatusEvent) | |
| 651 | |
| 652 started = None | |
| 653 finished = None | |
| 654 text = [] | |
| 655 | |
| 656 # IStatusEvent methods | |
| 657 def getTimes(self): | |
| 658 return (self.started, self.finished) | |
| 659 def getText(self): | |
| 660 return self.text | |
| 661 def getLogs(self): | |
| 662 return [] | |
| 663 | |
| 664 def finish(self): | |
| 665 self.finished = util.now() | |
| 666 | |
| 667 class TestResult: | |
| 668 implements(interfaces.ITestResult) | |
| 669 | |
| 670 def __init__(self, name, results, text, logs): | |
| 671 assert isinstance(name, tuple) | |
| 672 self.name = name | |
| 673 self.results = results | |
| 674 self.text = text | |
| 675 self.logs = logs | |
| 676 | |
| 677 def getName(self): | |
| 678 return self.name | |
| 679 | |
| 680 def getResults(self): | |
| 681 return self.results | |
| 682 | |
| 683 def getText(self): | |
| 684 return self.text | |
| 685 | |
| 686 def getLogs(self): | |
| 687 return self.logs | |
| 688 | |
| 689 | |
| 690 class BuildSetStatus: | |
| 691 implements(interfaces.IBuildSetStatus) | |
| 692 | |
| 693 def __init__(self, source, reason, builderNames, bsid=None): | |
| 694 self.source = source | |
| 695 self.reason = reason | |
| 696 self.builderNames = builderNames | |
| 697 self.id = bsid | |
| 698 self.successWatchers = [] | |
| 699 self.finishedWatchers = [] | |
| 700 self.stillHopeful = True | |
| 701 self.finished = False | |
| 702 self.results = None | |
| 703 | |
| 704 def setBuildRequestStatuses(self, buildRequestStatuses): | |
| 705 self.buildRequests = buildRequestStatuses | |
| 706 def setResults(self, results): | |
| 707 # the build set succeeds only if all its component builds succeed | |
| 708 self.results = results | |
| 709 def giveUpHope(self): | |
| 710 self.stillHopeful = False | |
| 711 | |
| 712 | |
| 713 def notifySuccessWatchers(self): | |
| 714 for d in self.successWatchers: | |
| 715 d.callback(self) | |
| 716 self.successWatchers = [] | |
| 717 | |
| 718 def notifyFinishedWatchers(self): | |
| 719 self.finished = True | |
| 720 for d in self.finishedWatchers: | |
| 721 d.callback(self) | |
| 722 self.finishedWatchers = [] | |
| 723 | |
| 724 # methods for our clients | |
| 725 | |
| 726 def getSourceStamp(self): | |
| 727 return self.source | |
| 728 def getReason(self): | |
| 729 return self.reason | |
| 730 def getResults(self): | |
| 731 return self.results | |
| 732 def getID(self): | |
| 733 return self.id | |
| 734 | |
| 735 def getBuilderNames(self): | |
| 736 return self.builderNames | |
| 737 def getBuildRequests(self): | |
| 738 return self.buildRequests | |
| 739 def isFinished(self): | |
| 740 return self.finished | |
| 741 | |
| 742 def waitUntilSuccess(self): | |
| 743 if self.finished or not self.stillHopeful: | |
| 744 # the deferreds have already fired | |
| 745 return defer.succeed(self) | |
| 746 d = defer.Deferred() | |
| 747 self.successWatchers.append(d) | |
| 748 return d | |
| 749 | |
| 750 def waitUntilFinished(self): | |
| 751 if self.finished: | |
| 752 return defer.succeed(self) | |
| 753 d = defer.Deferred() | |
| 754 self.finishedWatchers.append(d) | |
| 755 return d | |
| 756 | |
| 757 def asDict(self): | |
| 758 result = {} | |
| 759 # Constant | |
| 760 result['source'] = self.getSourceStamp().asDict() | |
| 761 result['reason'] = self.getReason() | |
| 762 result['results'] = self.getResults() | |
| 763 result['builderNames'] = self.getBuilderNames() | |
| 764 result['isFinished'] = self.isFinished() | |
| 765 | |
| 766 # Transient | |
| 767 result['buildRequests'] = [ | |
| 768 build.asDict() for build in self.getBuildRequests()] | |
| 769 return result | |
| 770 | |
| 771 | |
| 772 class BuildRequestStatus: | |
| 773 implements(interfaces.IBuildRequestStatus) | |
| 774 | |
| 775 def __init__(self, source, builderName): | |
| 776 self.source = source | |
| 777 self.builderName = builderName | |
| 778 self.builds = [] # list of BuildStatus objects | |
| 779 self.observers = [] | |
| 780 self.submittedAt = None | |
| 781 | |
| 782 def buildStarted(self, build): | |
| 783 self.builds.append(build) | |
| 784 for o in self.observers[:]: | |
| 785 o(build) | |
| 786 | |
| 787 # methods called by our clients | |
| 788 def getSourceStamp(self): | |
| 789 return self.source | |
| 790 def getBuilderName(self): | |
| 791 return self.builderName | |
| 792 def getBuilds(self): | |
| 793 return self.builds | |
| 794 | |
| 795 def subscribe(self, observer): | |
| 796 self.observers.append(observer) | |
| 797 for b in self.builds: | |
| 798 observer(b) | |
| 799 def unsubscribe(self, observer): | |
| 800 self.observers.remove(observer) | |
| 801 | |
| 802 def getSubmitTime(self): | |
| 803 return self.submittedAt | |
| 804 def setSubmitTime(self, t): | |
| 805 self.submittedAt = t | |
| 806 | |
| 807 def asDict(self): | |
| 808 result = {} | |
| 809 # Constant | |
| 810 result['source'] = self.source.asDict() | |
| 811 result['builderName'] = self.getBuilderName() | |
| 812 result['submittedAt'] = self.getSubmitTime() | |
| 813 | |
| 814 # Transient | |
| 815 result['builds'] = [build.getNumber() for build in self.getBuilds()] | |
| 816 return result | |
| 817 | |
| 818 | |
| 819 class BuildStepStatus(styles.Versioned): | |
| 820 """ | |
| 821 I represent a collection of output status for a | |
| 822 L{buildbot.process.step.BuildStep}. | |
| 823 | |
| 824 Statistics contain any information gleaned from a step that is | |
| 825 not in the form of a logfile. As an example, steps that run | |
| 826 tests might gather statistics about the number of passed, failed, | |
| 827 or skipped tests. | |
| 828 | |
| 829 @type progress: L{buildbot.status.progress.StepProgress} | |
| 830 @cvar progress: tracks ETA for the step | |
| 831 @type text: list of strings | |
| 832 @cvar text: list of short texts that describe the command and its status | |
| 833 @type text2: list of strings | |
| 834 @cvar text2: list of short texts added to the overall build description | |
| 835 @type logs: dict of string -> L{buildbot.status.builder.LogFile} | |
| 836 @ivar logs: logs of steps | |
| 837 @type statistics: dict | |
| 838 @ivar statistics: results from running this step | |
| 839 """ | |
| 840 # note that these are created when the Build is set up, before each | |
| 841 # corresponding BuildStep has started. | |
| 842 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent) | |
| 843 persistenceVersion = 3 | |
| 844 | |
| 845 started = None | |
| 846 finished = None | |
| 847 progress = None | |
| 848 text = [] | |
| 849 results = (None, []) | |
| 850 text2 = [] | |
| 851 watchers = [] | |
| 852 updates = {} | |
| 853 finishedWatchers = [] | |
| 854 statistics = {} | |
| 855 step_number = None | |
| 856 | |
| 857 def __init__(self, parent, step_number): | |
| 858 assert interfaces.IBuildStatus(parent) | |
| 859 self.build = parent | |
| 860 self.step_number = step_number | |
| 861 self.logs = [] | |
| 862 self.urls = {} | |
| 863 self.watchers = [] | |
| 864 self.updates = {} | |
| 865 self.finishedWatchers = [] | |
| 866 self.statistics = {} | |
| 867 | |
| 868 def getName(self): | |
| 869 """Returns a short string with the name of this step. This string | |
| 870 may have spaces in it.""" | |
| 871 return self.name | |
| 872 | |
| 873 def getBuild(self): | |
| 874 return self.build | |
| 875 | |
| 876 def getTimes(self): | |
| 877 return (self.started, self.finished) | |
| 878 | |
| 879 def getExpectations(self): | |
| 880 """Returns a list of tuples (name, current, target).""" | |
| 881 if not self.progress: | |
| 882 return [] | |
| 883 ret = [] | |
| 884 metrics = self.progress.progress.keys() | |
| 885 metrics.sort() | |
| 886 for m in metrics: | |
| 887 t = (m, self.progress.progress[m], self.progress.expectations[m]) | |
| 888 ret.append(t) | |
| 889 return ret | |
| 890 | |
| 891 def getLogs(self): | |
| 892 return self.logs | |
| 893 | |
| 894 def getURLs(self): | |
| 895 return self.urls.copy() | |
| 896 | |
| 897 def isStarted(self): | |
| 898 return (self.started is not None) | |
| 899 | |
| 900 def isFinished(self): | |
| 901 return (self.finished is not None) | |
| 902 | |
| 903 def waitUntilFinished(self): | |
| 904 if self.finished: | |
| 905 d = defer.succeed(self) | |
| 906 else: | |
| 907 d = defer.Deferred() | |
| 908 self.finishedWatchers.append(d) | |
| 909 return d | |
| 910 | |
| 911 # while the step is running, the following methods make sense. | |
| 912 # Afterwards they return None | |
| 913 | |
| 914 def getETA(self): | |
| 915 if self.started is None: | |
| 916 return None # not started yet | |
| 917 if self.finished is not None: | |
| 918 return None # already finished | |
| 919 if not self.progress: | |
| 920 return None # no way to predict | |
| 921 return self.progress.remaining() | |
| 922 | |
| 923 # Once you know the step has finished, the following methods are legal. | |
| 924 # Before this step has finished, they all return None. | |
| 925 | |
| 926 def getText(self): | |
| 927 """Returns a list of strings which describe the step. These are | |
| 928 intended to be displayed in a narrow column. If more space is | |
| 929 available, the caller should join them together with spaces before | |
| 930 presenting them to the user.""" | |
| 931 return self.text | |
| 932 | |
| 933 def getResults(self): | |
| 934 """Return a tuple describing the results of the step. | |
| 935 'result' is one of the constants in L{buildbot.status.builder}: | |
| 936 SUCCESS, WARNINGS, FAILURE, or SKIPPED. | |
| 937 'strings' is an optional list of strings that the step wants to | |
| 938 append to the overall build's results. These strings are usually | |
| 939 more terse than the ones returned by getText(): in particular, | |
| 940 successful Steps do not usually contribute any text to the | |
| 941 overall build. | |
| 942 | |
| 943 @rtype: tuple of int, list of strings | |
| 944 @returns: (result, strings) | |
| 945 """ | |
| 946 return (self.results, self.text2) | |
| 947 | |
| 948 def hasStatistic(self, name): | |
| 949 """Return true if this step has a value for the given statistic. | |
| 950 """ | |
| 951 return self.statistics.has_key(name) | |
| 952 | |
| 953 def getStatistic(self, name, default=None): | |
| 954 """Return the given statistic, if present | |
| 955 """ | |
| 956 return self.statistics.get(name, default) | |
| 957 | |
| 958 # subscription interface | |
| 959 | |
| 960 def subscribe(self, receiver, updateInterval=10): | |
| 961 # will get logStarted, logFinished, stepETAUpdate | |
| 962 assert receiver not in self.watchers | |
| 963 self.watchers.append(receiver) | |
| 964 self.sendETAUpdate(receiver, updateInterval) | |
| 965 | |
| 966 def sendETAUpdate(self, receiver, updateInterval): | |
| 967 self.updates[receiver] = None | |
| 968 # they might unsubscribe during stepETAUpdate | |
| 969 receiver.stepETAUpdate(self.build, self, | |
| 970 self.getETA(), self.getExpectations()) | |
| 971 if receiver in self.watchers: | |
| 972 self.updates[receiver] = reactor.callLater(updateInterval, | |
| 973 self.sendETAUpdate, | |
| 974 receiver, | |
| 975 updateInterval) | |
| 976 | |
| 977 def unsubscribe(self, receiver): | |
| 978 if receiver in self.watchers: | |
| 979 self.watchers.remove(receiver) | |
| 980 if receiver in self.updates: | |
| 981 if self.updates[receiver] is not None: | |
| 982 self.updates[receiver].cancel() | |
| 983 del self.updates[receiver] | |
| 984 | |
| 985 | |
| 986 # methods to be invoked by the BuildStep | |
| 987 | |
| 988 def setName(self, stepname): | |
| 989 self.name = stepname | |
| 990 | |
| 991 def setColor(self, color): | |
| 992 log.msg("BuildStepStatus.setColor is no longer supported -- ignoring col
or %s" % (color,)) | |
| 993 | |
| 994 def setProgress(self, stepprogress): | |
| 995 self.progress = stepprogress | |
| 996 | |
| 997 def stepStarted(self): | |
| 998 self.started = util.now() | |
| 999 if self.build: | |
| 1000 self.build.stepStarted(self) | |
| 1001 | |
| 1002 def addLog(self, name): | |
| 1003 assert self.started # addLog before stepStarted won't notify watchers | |
| 1004 logfilename = self.build.generateLogfileName(self.name, name) | |
| 1005 log = LogFile(self, name, logfilename) | |
| 1006 log.logMaxSize = self.build.builder.logMaxSize | |
| 1007 log.logMaxTailSize = self.build.builder.logMaxTailSize | |
| 1008 log.compressMethod = self.build.builder.logCompressionMethod | |
| 1009 self.logs.append(log) | |
| 1010 for w in self.watchers: | |
| 1011 receiver = w.logStarted(self.build, self, log) | |
| 1012 if receiver: | |
| 1013 log.subscribe(receiver, True) | |
| 1014 d = log.waitUntilFinished() | |
| 1015 d.addCallback(lambda log: log.unsubscribe(receiver)) | |
| 1016 d = log.waitUntilFinished() | |
| 1017 d.addCallback(self.logFinished) | |
| 1018 return log | |
| 1019 | |
| 1020 def addHTMLLog(self, name, html): | |
| 1021 assert self.started # addLog before stepStarted won't notify watchers | |
| 1022 logfilename = self.build.generateLogfileName(self.name, name) | |
| 1023 log = HTMLLogFile(self, name, logfilename, html) | |
| 1024 self.logs.append(log) | |
| 1025 for w in self.watchers: | |
| 1026 receiver = w.logStarted(self.build, self, log) | |
| 1027 # TODO: think about this: there isn't much point in letting | |
| 1028 # them subscribe | |
| 1029 #if receiver: | |
| 1030 # log.subscribe(receiver, True) | |
| 1031 w.logFinished(self.build, self, log) | |
| 1032 | |
| 1033 def logFinished(self, log): | |
| 1034 for w in self.watchers: | |
| 1035 w.logFinished(self.build, self, log) | |
| 1036 | |
| 1037 def addURL(self, name, url): | |
| 1038 self.urls[name] = url | |
| 1039 | |
| 1040 def setText(self, text): | |
| 1041 self.text = text | |
| 1042 for w in self.watchers: | |
| 1043 w.stepTextChanged(self.build, self, text) | |
| 1044 def setText2(self, text): | |
| 1045 self.text2 = text | |
| 1046 for w in self.watchers: | |
| 1047 w.stepText2Changed(self.build, self, text) | |
| 1048 | |
| 1049 def setStatistic(self, name, value): | |
| 1050 """Set the given statistic. Usually called by subclasses. | |
| 1051 """ | |
| 1052 self.statistics[name] = value | |
| 1053 | |
| 1054 def stepFinished(self, results): | |
| 1055 self.finished = util.now() | |
| 1056 self.results = results | |
| 1057 cld = [] # deferreds for log compression | |
| 1058 logCompressionLimit = self.build.builder.logCompressionLimit | |
| 1059 for loog in self.logs: | |
| 1060 if not loog.isFinished(): | |
| 1061 loog.finish() | |
| 1062 # if log compression is on, and it's a real LogFile, | |
| 1063 # HTMLLogFiles aren't files | |
| 1064 if logCompressionLimit is not False and \ | |
| 1065 isinstance(loog, LogFile): | |
| 1066 if os.path.getsize(loog.getFilename()) > logCompressionLimit: | |
| 1067 loog_deferred = loog.compressLog() | |
| 1068 if loog_deferred: | |
| 1069 cld.append(loog_deferred) | |
| 1070 | |
| 1071 for r in self.updates.keys(): | |
| 1072 if self.updates[r] is not None: | |
| 1073 self.updates[r].cancel() | |
| 1074 del self.updates[r] | |
| 1075 | |
| 1076 watchers = self.finishedWatchers | |
| 1077 self.finishedWatchers = [] | |
| 1078 for w in watchers: | |
| 1079 w.callback(self) | |
| 1080 if cld: | |
| 1081 return defer.DeferredList(cld) | |
| 1082 | |
| 1083 def checkLogfiles(self): | |
| 1084 # filter out logs that have been deleted | |
| 1085 self.logs = [ l for l in self.logs if l.hasContents() ] | |
| 1086 | |
| 1087 # persistence | |
| 1088 | |
| 1089 def __getstate__(self): | |
| 1090 d = styles.Versioned.__getstate__(self) | |
| 1091 del d['build'] # filled in when loading | |
| 1092 if d.has_key('progress'): | |
| 1093 del d['progress'] | |
| 1094 del d['watchers'] | |
| 1095 del d['finishedWatchers'] | |
| 1096 del d['updates'] | |
| 1097 return d | |
| 1098 | |
| 1099 def __setstate__(self, d): | |
| 1100 styles.Versioned.__setstate__(self, d) | |
| 1101 # self.build must be filled in by our parent | |
| 1102 | |
| 1103 # point the logs to this object | |
| 1104 for loog in self.logs: | |
| 1105 loog.step = self | |
| 1106 | |
| 1107 def upgradeToVersion1(self): | |
| 1108 if not hasattr(self, "urls"): | |
| 1109 self.urls = {} | |
| 1110 | |
| 1111 def upgradeToVersion2(self): | |
| 1112 if not hasattr(self, "statistics"): | |
| 1113 self.statistics = {} | |
| 1114 | |
| 1115 def upgradeToVersion3(self): | |
| 1116 if not hasattr(self, "step_number"): | |
| 1117 self.step_number = 0 | |
| 1118 | |
| 1119 def asDict(self): | |
| 1120 result = {} | |
| 1121 # Constant | |
| 1122 result['name'] = self.getName() | |
| 1123 | |
| 1124 # Transient | |
| 1125 result['text'] = self.getText() | |
| 1126 result['results'] = self.getResults() | |
| 1127 result['isStarted'] = self.isStarted() | |
| 1128 result['isFinished'] = self.isFinished() | |
| 1129 result['statistics'] = self.statistics | |
| 1130 result['times'] = self.getTimes() | |
| 1131 result['expectations'] = self.getExpectations() | |
| 1132 result['eta'] = self.getETA() | |
| 1133 result['urls'] = self.getURLs() | |
| 1134 result['step_number'] = self.step_number | |
| 1135 result['logs'] = [[l.getName(), | |
| 1136 self.build.builder.status.getURLForThing(l)] | |
| 1137 for l in self.getLogs()] | |
| 1138 return result | |
| 1139 | |
| 1140 | |
| 1141 class BuildStatus(styles.Versioned): | |
| 1142 implements(interfaces.IBuildStatus, interfaces.IStatusEvent) | |
| 1143 persistenceVersion = 3 | |
| 1144 | |
| 1145 source = None | |
| 1146 reason = None | |
| 1147 changes = [] | |
| 1148 blamelist = [] | |
| 1149 requests = [] | |
| 1150 progress = None | |
| 1151 started = None | |
| 1152 finished = None | |
| 1153 currentStep = None | |
| 1154 text = [] | |
| 1155 results = None | |
| 1156 slavename = "???" | |
| 1157 | |
| 1158 # these lists/dicts are defined here so that unserialized instances have | |
| 1159 # (empty) values. They are set in __init__ to new objects to make sure | |
| 1160 # each instance gets its own copy. | |
| 1161 watchers = [] | |
| 1162 updates = {} | |
| 1163 finishedWatchers = [] | |
| 1164 testResults = {} | |
| 1165 | |
| 1166 def __init__(self, parent, number): | |
| 1167 """ | |
| 1168 @type parent: L{BuilderStatus} | |
| 1169 @type number: int | |
| 1170 """ | |
| 1171 assert interfaces.IBuilderStatus(parent) | |
| 1172 self.builder = parent | |
| 1173 self.number = number | |
| 1174 self.watchers = [] | |
| 1175 self.updates = {} | |
| 1176 self.finishedWatchers = [] | |
| 1177 self.steps = [] | |
| 1178 self.testResults = {} | |
| 1179 self.properties = Properties() | |
| 1180 self.requests = [] | |
| 1181 | |
| 1182 def __repr__(self): | |
| 1183 return "<%s #%s>" % (self.__class__.__name__, self.number) | |
| 1184 | |
| 1185 # IBuildStatus | |
| 1186 | |
| 1187 def getBuilder(self): | |
| 1188 """ | |
| 1189 @rtype: L{BuilderStatus} | |
| 1190 """ | |
| 1191 return self.builder | |
| 1192 | |
| 1193 def getProperty(self, propname): | |
| 1194 return self.properties[propname] | |
| 1195 | |
| 1196 def getProperties(self): | |
| 1197 return self.properties | |
| 1198 | |
| 1199 def getNumber(self): | |
| 1200 return self.number | |
| 1201 | |
| 1202 def getPreviousBuild(self): | |
| 1203 if self.number == 0: | |
| 1204 return None | |
| 1205 return self.builder.getBuild(self.number-1) | |
| 1206 | |
| 1207 def getSourceStamp(self, absolute=False): | |
| 1208 if not absolute or not self.properties.has_key('got_revision'): | |
| 1209 return self.source | |
| 1210 return self.source.getAbsoluteSourceStamp(self.properties['got_revision'
]) | |
| 1211 | |
| 1212 def getReason(self): | |
| 1213 return self.reason | |
| 1214 | |
| 1215 def getChanges(self): | |
| 1216 return self.changes | |
| 1217 | |
| 1218 def getRequests(self): | |
| 1219 return self.requests | |
| 1220 | |
| 1221 def getResponsibleUsers(self): | |
| 1222 return self.blamelist | |
| 1223 | |
| 1224 def getInterestedUsers(self): | |
| 1225 # TODO: the Builder should add others: sheriffs, domain-owners | |
| 1226 return self.blamelist + self.properties.getProperty('owners', []) | |
| 1227 | |
| 1228 def getSteps(self): | |
| 1229 """Return a list of IBuildStepStatus objects. For invariant builds | |
| 1230 (those which always use the same set of Steps), this should be the | |
| 1231 complete list, however some of the steps may not have started yet | |
| 1232 (step.getTimes()[0] will be None). For variant builds, this may not | |
| 1233 be complete (asking again later may give you more of them).""" | |
| 1234 return self.steps | |
| 1235 | |
| 1236 def getTimes(self): | |
| 1237 return (self.started, self.finished) | |
| 1238 | |
| 1239 _sentinel = [] # used as a sentinel to indicate unspecified initial_value | |
| 1240 def getSummaryStatistic(self, name, summary_fn, initial_value=_sentinel): | |
| 1241 """Summarize the named statistic over all steps in which it | |
| 1242 exists, using combination_fn and initial_value to combine multiple | |
| 1243 results into a single result. This translates to a call to Python's | |
| 1244 X{reduce}:: | |
| 1245 return reduce(summary_fn, step_stats_list, initial_value) | |
| 1246 """ | |
| 1247 step_stats_list = [ | |
| 1248 st.getStatistic(name) | |
| 1249 for st in self.steps | |
| 1250 if st.hasStatistic(name) ] | |
| 1251 if initial_value is self._sentinel: | |
| 1252 return reduce(summary_fn, step_stats_list) | |
| 1253 else: | |
| 1254 return reduce(summary_fn, step_stats_list, initial_value) | |
| 1255 | |
| 1256 def isFinished(self): | |
| 1257 return (self.finished is not None) | |
| 1258 | |
| 1259 def waitUntilFinished(self): | |
| 1260 if self.finished: | |
| 1261 d = defer.succeed(self) | |
| 1262 else: | |
| 1263 d = defer.Deferred() | |
| 1264 self.finishedWatchers.append(d) | |
| 1265 return d | |
| 1266 | |
| 1267 # while the build is running, the following methods make sense. | |
| 1268 # Afterwards they return None | |
| 1269 | |
| 1270 def getETA(self): | |
| 1271 if self.finished is not None: | |
| 1272 return None | |
| 1273 if not self.progress: | |
| 1274 return None | |
| 1275 eta = self.progress.eta() | |
| 1276 if eta is None: | |
| 1277 return None | |
| 1278 return eta - util.now() | |
| 1279 | |
| 1280 def getCurrentStep(self): | |
| 1281 return self.currentStep | |
| 1282 | |
| 1283 # Once you know the build has finished, the following methods are legal. | |
| 1284 # Before ths build has finished, they all return None. | |
| 1285 | |
| 1286 def getText(self): | |
| 1287 text = [] | |
| 1288 text.extend(self.text) | |
| 1289 for s in self.steps: | |
| 1290 text.extend(s.text2) | |
| 1291 return text | |
| 1292 | |
| 1293 def getResults(self): | |
| 1294 return self.results | |
| 1295 | |
| 1296 def getSlavename(self): | |
| 1297 return self.slavename | |
| 1298 | |
| 1299 def getTestResults(self): | |
| 1300 return self.testResults | |
| 1301 | |
| 1302 def getLogs(self): | |
| 1303 # TODO: steps should contribute significant logs instead of this | |
| 1304 # hack, which returns every log from every step. The logs should get | |
| 1305 # names like "compile" and "test" instead of "compile.output" | |
| 1306 logs = [] | |
| 1307 for s in self.steps: | |
| 1308 for log in s.getLogs(): | |
| 1309 logs.append(log) | |
| 1310 return logs | |
| 1311 | |
| 1312 # subscription interface | |
| 1313 | |
| 1314 def subscribe(self, receiver, updateInterval=None): | |
| 1315 # will receive stepStarted and stepFinished messages | |
| 1316 # and maybe buildETAUpdate | |
| 1317 self.watchers.append(receiver) | |
| 1318 if updateInterval is not None: | |
| 1319 self.sendETAUpdate(receiver, updateInterval) | |
| 1320 | |
| 1321 def sendETAUpdate(self, receiver, updateInterval): | |
| 1322 self.updates[receiver] = None | |
| 1323 ETA = self.getETA() | |
| 1324 if ETA is not None: | |
| 1325 receiver.buildETAUpdate(self, self.getETA()) | |
| 1326 # they might have unsubscribed during buildETAUpdate | |
| 1327 if receiver in self.watchers: | |
| 1328 self.updates[receiver] = reactor.callLater(updateInterval, | |
| 1329 self.sendETAUpdate, | |
| 1330 receiver, | |
| 1331 updateInterval) | |
| 1332 | |
| 1333 def unsubscribe(self, receiver): | |
| 1334 if receiver in self.watchers: | |
| 1335 self.watchers.remove(receiver) | |
| 1336 if receiver in self.updates: | |
| 1337 if self.updates[receiver] is not None: | |
| 1338 self.updates[receiver].cancel() | |
| 1339 del self.updates[receiver] | |
| 1340 | |
| 1341 # methods for the base.Build to invoke | |
| 1342 | |
| 1343 def addStepWithName(self, name): | |
| 1344 """The Build is setting up, and has added a new BuildStep to its | |
| 1345 list. Create a BuildStepStatus object to which it can send status | |
| 1346 updates.""" | |
| 1347 | |
| 1348 s = BuildStepStatus(self, len(self.steps)) | |
| 1349 s.setName(name) | |
| 1350 self.steps.append(s) | |
| 1351 return s | |
| 1352 | |
| 1353 def setProperty(self, propname, value, source): | |
| 1354 self.properties.setProperty(propname, value, source) | |
| 1355 | |
| 1356 def addTestResult(self, result): | |
| 1357 self.testResults[result.getName()] = result | |
| 1358 | |
| 1359 def setSourceStamp(self, sourceStamp): | |
| 1360 self.source = sourceStamp | |
| 1361 self.changes = self.source.changes | |
| 1362 | |
| 1363 def setRequests(self, requests): | |
| 1364 self.requests = requests | |
| 1365 | |
| 1366 def setReason(self, reason): | |
| 1367 self.reason = reason | |
| 1368 def setBlamelist(self, blamelist): | |
| 1369 self.blamelist = blamelist | |
| 1370 def setProgress(self, progress): | |
| 1371 self.progress = progress | |
| 1372 | |
| 1373 def buildStarted(self, build): | |
| 1374 """The Build has been set up and is about to be started. It can now | |
| 1375 be safely queried, so it is time to announce the new build.""" | |
| 1376 | |
| 1377 self.started = util.now() | |
| 1378 # now that we're ready to report status, let the BuilderStatus tell | |
| 1379 # the world about us | |
| 1380 self.builder.buildStarted(self) | |
| 1381 | |
| 1382 def setSlavename(self, slavename): | |
| 1383 self.slavename = slavename | |
| 1384 | |
| 1385 def setText(self, text): | |
| 1386 assert isinstance(text, (list, tuple)) | |
| 1387 self.text = text | |
| 1388 def setResults(self, results): | |
| 1389 self.results = results | |
| 1390 | |
| 1391 def buildFinished(self): | |
| 1392 self.currentStep = None | |
| 1393 self.finished = util.now() | |
| 1394 | |
| 1395 for r in self.updates.keys(): | |
| 1396 if self.updates[r] is not None: | |
| 1397 self.updates[r].cancel() | |
| 1398 del self.updates[r] | |
| 1399 | |
| 1400 watchers = self.finishedWatchers | |
| 1401 self.finishedWatchers = [] | |
| 1402 for w in watchers: | |
| 1403 w.callback(self) | |
| 1404 | |
| 1405 # methods called by our BuildStepStatus children | |
| 1406 | |
| 1407 def stepStarted(self, step): | |
| 1408 self.currentStep = step | |
| 1409 name = self.getBuilder().getName() | |
| 1410 for w in self.watchers: | |
| 1411 receiver = w.stepStarted(self, step) | |
| 1412 if receiver: | |
| 1413 if type(receiver) == type(()): | |
| 1414 step.subscribe(receiver[0], receiver[1]) | |
| 1415 else: | |
| 1416 step.subscribe(receiver) | |
| 1417 d = step.waitUntilFinished() | |
| 1418 d.addCallback(lambda step: step.unsubscribe(receiver)) | |
| 1419 | |
| 1420 step.waitUntilFinished().addCallback(self._stepFinished) | |
| 1421 | |
| 1422 def _stepFinished(self, step): | |
| 1423 results = step.getResults() | |
| 1424 for w in self.watchers: | |
| 1425 w.stepFinished(self, step, results) | |
| 1426 | |
| 1427 # methods called by our BuilderStatus parent | |
| 1428 | |
| 1429 def pruneSteps(self): | |
| 1430 # this build is very old: remove the build steps too | |
| 1431 self.steps = [] | |
| 1432 | |
| 1433 # persistence stuff | |
| 1434 | |
| 1435 def generateLogfileName(self, stepname, logname): | |
| 1436 """Return a filename (relative to the Builder's base directory) where | |
| 1437 the logfile's contents can be stored uniquely. | |
| 1438 | |
| 1439 The base filename is made by combining our build number, the Step's | |
| 1440 name, and the log's name, then removing unsuitable characters. The | |
| 1441 filename is then made unique by appending _0, _1, etc, until it does | |
| 1442 not collide with any other logfile. | |
| 1443 | |
| 1444 These files are kept in the Builder's basedir (rather than a | |
| 1445 per-Build subdirectory) because that makes cleanup easier: cron and | |
| 1446 find will help get rid of the old logs, but the empty directories are | |
| 1447 more of a hassle to remove.""" | |
| 1448 | |
| 1449 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname) | |
| 1450 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename) | |
| 1451 # now make it unique | |
| 1452 unique_counter = 0 | |
| 1453 filename = starting_filename | |
| 1454 while filename in [l.filename | |
| 1455 for step in self.steps | |
| 1456 for l in step.getLogs() | |
| 1457 if l.filename]: | |
| 1458 filename = "%s_%d" % (starting_filename, unique_counter) | |
| 1459 unique_counter += 1 | |
| 1460 return filename | |
| 1461 | |
| 1462 def __getstate__(self): | |
| 1463 d = styles.Versioned.__getstate__(self) | |
| 1464 # for now, a serialized Build is always "finished". We will never | |
| 1465 # save unfinished builds. | |
| 1466 if not self.finished: | |
| 1467 d['finished'] = True | |
| 1468 # TODO: push an "interrupted" step so it is clear that the build | |
| 1469 # was interrupted. The builder will have a 'shutdown' event, but | |
| 1470 # someone looking at just this build will be confused as to why | |
| 1471 # the last log is truncated. | |
| 1472 for k in 'builder', 'watchers', 'updates', 'requests', 'finishedWatchers
': | |
| 1473 if k in d: del d[k] | |
| 1474 return d | |
| 1475 | |
| 1476 def __setstate__(self, d): | |
| 1477 styles.Versioned.__setstate__(self, d) | |
| 1478 # self.builder must be filled in by our parent when loading | |
| 1479 for step in self.steps: | |
| 1480 step.build = self | |
| 1481 self.watchers = [] | |
| 1482 self.updates = {} | |
| 1483 self.finishedWatchers = [] | |
| 1484 | |
| 1485 def upgradeToVersion1(self): | |
| 1486 if hasattr(self, "sourceStamp"): | |
| 1487 # the old .sourceStamp attribute wasn't actually very useful | |
| 1488 maxChangeNumber, patch = self.sourceStamp | |
| 1489 changes = getattr(self, 'changes', []) | |
| 1490 source = sourcestamp.SourceStamp(branch=None, | |
| 1491 revision=None, | |
| 1492 patch=patch, | |
| 1493 changes=changes) | |
| 1494 self.source = source | |
| 1495 self.changes = source.changes | |
| 1496 del self.sourceStamp | |
| 1497 | |
| 1498 def upgradeToVersion2(self): | |
| 1499 self.properties = {} | |
| 1500 | |
| 1501 def upgradeToVersion3(self): | |
| 1502 # in version 3, self.properties became a Properties object | |
| 1503 propdict = self.properties | |
| 1504 self.properties = Properties() | |
| 1505 self.properties.update(propdict, "Upgrade from previous version") | |
| 1506 | |
| 1507 def upgradeLogfiles(self): | |
| 1508 # upgrade any LogFiles that need it. This must occur after we've been | |
| 1509 # attached to our Builder, and after we know about all LogFiles of | |
| 1510 # all Steps (to get the filenames right). | |
| 1511 assert self.builder | |
| 1512 for s in self.steps: | |
| 1513 for l in s.getLogs(): | |
| 1514 if l.filename: | |
| 1515 pass # new-style, log contents are on disk | |
| 1516 else: | |
| 1517 logfilename = self.generateLogfileName(s.name, l.name) | |
| 1518 # let the logfile update its .filename pointer, | |
| 1519 # transferring its contents onto disk if necessary | |
| 1520 l.upgrade(logfilename) | |
| 1521 | |
| 1522 def checkLogfiles(self): | |
| 1523 # check that all logfiles exist, and remove references to any that | |
| 1524 # have been deleted (e.g., by purge()) | |
| 1525 for s in self.steps: | |
| 1526 s.checkLogfiles() | |
| 1527 | |
| 1528 def saveYourself(self): | |
| 1529 filename = os.path.join(self.builder.basedir, "%d" % self.number) | |
| 1530 if os.path.isdir(filename): | |
| 1531 # leftover from 0.5.0, which stored builds in directories | |
| 1532 shutil.rmtree(filename, ignore_errors=True) | |
| 1533 tmpfilename = filename + ".tmp" | |
| 1534 try: | |
| 1535 dump(self, open(tmpfilename, "wb"), -1) | |
| 1536 if sys.platform == 'win32': | |
| 1537 # windows cannot rename a file on top of an existing one, so | |
| 1538 # fall back to delete-first. There are ways this can fail and | |
| 1539 # lose the builder's history, so we avoid using it in the | |
| 1540 # general (non-windows) case | |
| 1541 if os.path.exists(filename): | |
| 1542 os.unlink(filename) | |
| 1543 os.rename(tmpfilename, filename) | |
| 1544 except: | |
| 1545 log.msg("unable to save build %s-#%d" % (self.builder.name, | |
| 1546 self.number)) | |
| 1547 log.err() | |
| 1548 | |
| 1549 def asDict(self): | |
| 1550 result = {} | |
| 1551 # Constant | |
| 1552 result['builderName'] = self.builder.name | |
| 1553 result['number'] = self.getNumber() | |
| 1554 result['sourceStamp'] = self.getSourceStamp().asDict() | |
| 1555 result['reason'] = self.getReason() | |
| 1556 result['requests'] = [r.asDict() for r in self.getRequests()] | |
| 1557 result['blame'] = self.getResponsibleUsers() | |
| 1558 result['changes'] = [c.asText() for c in self.getChanges()] | |
| 1559 | |
| 1560 # Transient | |
| 1561 result['properties'] = self.getProperties().asList() | |
| 1562 result['times'] = self.getTimes() | |
| 1563 result['text'] = self.getText() | |
| 1564 result['results'] = self.getResults() | |
| 1565 result['slave'] = self.getSlavename() | |
| 1566 # TODO(maruel): Add. | |
| 1567 #result['test_results'] = self.getTestResults() | |
| 1568 result['logs'] = [[l.getName(), | |
| 1569 self.builder.status.getURLForThing(l)] for l in self.getLogs()] | |
| 1570 result['eta'] = self.getETA() | |
| 1571 result['steps'] = [bss.asDict() for bss in self.steps] | |
| 1572 if self.getCurrentStep(): | |
| 1573 result['currentStep'] = self.getCurrentStep().asDict() | |
| 1574 else: | |
| 1575 result['currentStep'] = None | |
| 1576 return result | |
| 1577 | |
| 1578 | |
| 1579 | |
| 1580 class BuilderStatus(styles.Versioned): | |
| 1581 """I handle status information for a single process.base.Builder object. | |
| 1582 That object sends status changes to me (frequently as Events), and I | |
| 1583 provide them on demand to the various status recipients, like the HTML | |
| 1584 waterfall display and the live status clients. It also sends build | |
| 1585 summaries to me, which I log and provide to status clients who aren't | |
| 1586 interested in seeing details of the individual build steps. | |
| 1587 | |
| 1588 I am responsible for maintaining the list of historic Events and Builds, | |
| 1589 pruning old ones, and loading them from / saving them to disk. | |
| 1590 | |
| 1591 I live in the buildbot.process.base.Builder object, in the | |
| 1592 .builder_status attribute. | |
| 1593 | |
| 1594 @type category: string | |
| 1595 @ivar category: user-defined category this builder belongs to; can be | |
| 1596 used to filter on in status clients | |
| 1597 """ | |
| 1598 | |
| 1599 implements(interfaces.IBuilderStatus, interfaces.IEventSource) | |
| 1600 persistenceVersion = 1 | |
| 1601 | |
| 1602 # these limit the amount of memory we consume, as well as the size of the | |
| 1603 # main Builder pickle. The Build and LogFile pickles on disk must be | |
| 1604 # handled separately. | |
| 1605 buildCacheSize = 15 | |
| 1606 eventHorizon = 50 # forget events beyond this | |
| 1607 | |
| 1608 # these limit on-disk storage | |
| 1609 logHorizon = 40 # forget logs in steps in builds beyond this | |
| 1610 buildHorizon = 100 # forget builds beyond this | |
| 1611 | |
| 1612 category = None | |
| 1613 currentBigState = "offline" # or idle/waiting/interlocked/building | |
| 1614 basedir = None # filled in by our parent | |
| 1615 | |
| 1616 def __init__(self, buildername, category=None): | |
| 1617 self.name = buildername | |
| 1618 self.category = category | |
| 1619 | |
| 1620 self.slavenames = [] | |
| 1621 self.events = [] | |
| 1622 # these three hold Events, and are used to retrieve the current | |
| 1623 # state of the boxes. | |
| 1624 self.lastBuildStatus = None | |
| 1625 #self.currentBig = None | |
| 1626 #self.currentSmall = None | |
| 1627 self.currentBuilds = [] | |
| 1628 self.pendingBuilds = [] | |
| 1629 self.nextBuild = None | |
| 1630 self.watchers = [] | |
| 1631 self.buildCache = weakref.WeakValueDictionary() | |
| 1632 self.buildCache_LRU = [] | |
| 1633 self.logCompressionLimit = False # default to no compression for tests | |
| 1634 self.logCompressionMethod = "bz2" | |
| 1635 self.logMaxSize = None # No default limit | |
| 1636 self.logMaxTailSize = None # No tail buffering | |
| 1637 | |
| 1638 # persistence | |
| 1639 | |
| 1640 def __getstate__(self): | |
| 1641 # when saving, don't record transient stuff like what builds are | |
| 1642 # currently running, because they won't be there when we start back | |
| 1643 # up. Nor do we save self.watchers, nor anything that gets set by our | |
| 1644 # parent like .basedir and .status | |
| 1645 d = styles.Versioned.__getstate__(self) | |
| 1646 d['watchers'] = [] | |
| 1647 del d['buildCache'] | |
| 1648 del d['buildCache_LRU'] | |
| 1649 for b in self.currentBuilds: | |
| 1650 b.saveYourself() | |
| 1651 # TODO: push a 'hey, build was interrupted' event | |
| 1652 del d['currentBuilds'] | |
| 1653 del d['pendingBuilds'] | |
| 1654 del d['currentBigState'] | |
| 1655 del d['basedir'] | |
| 1656 del d['status'] | |
| 1657 del d['nextBuildNumber'] | |
| 1658 return d | |
| 1659 | |
| 1660 def __setstate__(self, d): | |
| 1661 # when loading, re-initialize the transient stuff. Remember that | |
| 1662 # upgradeToVersion1 and such will be called after this finishes. | |
| 1663 styles.Versioned.__setstate__(self, d) | |
| 1664 self.buildCache = weakref.WeakValueDictionary() | |
| 1665 self.buildCache_LRU = [] | |
| 1666 self.currentBuilds = [] | |
| 1667 self.pendingBuilds = [] | |
| 1668 self.watchers = [] | |
| 1669 self.slavenames = [] | |
| 1670 # self.basedir must be filled in by our parent | |
| 1671 # self.status must be filled in by our parent | |
| 1672 | |
| 1673 def reconfigFromBuildmaster(self, buildmaster): | |
| 1674 # Note that we do not hang onto the buildmaster, since this object | |
| 1675 # gets pickled and unpickled. | |
| 1676 if buildmaster.buildCacheSize: | |
| 1677 self.buildCacheSize = buildmaster.buildCacheSize | |
| 1678 if buildmaster.eventHorizon: | |
| 1679 self.eventHorizon = buildmaster.eventHorizon | |
| 1680 if buildmaster.logHorizon: | |
| 1681 self.logHorizon = buildmaster.logHorizon | |
| 1682 if buildmaster.buildHorizon: | |
| 1683 self.buildHorizon = buildmaster.buildHorizon | |
| 1684 | |
| 1685 def upgradeToVersion1(self): | |
| 1686 if hasattr(self, 'slavename'): | |
| 1687 self.slavenames = [self.slavename] | |
| 1688 del self.slavename | |
| 1689 if hasattr(self, 'nextBuildNumber'): | |
| 1690 del self.nextBuildNumber # determineNextBuildNumber chooses this | |
| 1691 | |
| 1692 def determineNextBuildNumber(self): | |
| 1693 """Scan our directory of saved BuildStatus instances to determine | |
| 1694 what our self.nextBuildNumber should be. Set it one larger than the | |
| 1695 highest-numbered build we discover. This is called by the top-level | |
| 1696 Status object shortly after we are created or loaded from disk. | |
| 1697 """ | |
| 1698 existing_builds = [int(f) | |
| 1699 for f in os.listdir(self.basedir) | |
| 1700 if re.match("^\d+$", f)] | |
| 1701 if existing_builds: | |
| 1702 self.nextBuildNumber = max(existing_builds) + 1 | |
| 1703 else: | |
| 1704 self.nextBuildNumber = 0 | |
| 1705 | |
| 1706 def setLogCompressionLimit(self, lowerLimit): | |
| 1707 self.logCompressionLimit = lowerLimit | |
| 1708 | |
| 1709 def setLogCompressionMethod(self, method): | |
| 1710 assert method in ("bz2", "gz") | |
| 1711 self.logCompressionMethod = method | |
| 1712 | |
| 1713 def setLogMaxSize(self, upperLimit): | |
| 1714 self.logMaxSize = upperLimit | |
| 1715 | |
| 1716 def setLogMaxTailSize(self, tailSize): | |
| 1717 self.logMaxTailSize = tailSize | |
| 1718 | |
| 1719 def saveYourself(self): | |
| 1720 for b in self.currentBuilds: | |
| 1721 if not b.isFinished: | |
| 1722 # interrupted build, need to save it anyway. | |
| 1723 # BuildStatus.saveYourself will mark it as interrupted. | |
| 1724 b.saveYourself() | |
| 1725 filename = os.path.join(self.basedir, "builder") | |
| 1726 tmpfilename = filename + ".tmp" | |
| 1727 try: | |
| 1728 dump(self, open(tmpfilename, "wb"), -1) | |
| 1729 if sys.platform == 'win32': | |
| 1730 # windows cannot rename a file on top of an existing one | |
| 1731 if os.path.exists(filename): | |
| 1732 os.unlink(filename) | |
| 1733 os.rename(tmpfilename, filename) | |
| 1734 except: | |
| 1735 log.msg("unable to save builder %s" % self.name) | |
| 1736 log.err() | |
| 1737 | |
| 1738 | |
| 1739 # build cache management | |
| 1740 | |
| 1741 def makeBuildFilename(self, number): | |
| 1742 return os.path.join(self.basedir, "%d" % number) | |
| 1743 | |
| 1744 def touchBuildCache(self, build): | |
| 1745 self.buildCache[build.number] = build | |
| 1746 if build in self.buildCache_LRU: | |
| 1747 self.buildCache_LRU.remove(build) | |
| 1748 self.buildCache_LRU = self.buildCache_LRU[-(self.buildCacheSize-1):] + [
build ] | |
| 1749 return build | |
| 1750 | |
| 1751 def getBuildByNumber(self, number): | |
| 1752 # first look in currentBuilds | |
| 1753 for b in self.currentBuilds: | |
| 1754 if b.number == number: | |
| 1755 return self.touchBuildCache(b) | |
| 1756 | |
| 1757 # then in the buildCache | |
| 1758 if number in self.buildCache: | |
| 1759 return self.touchBuildCache(self.buildCache[number]) | |
| 1760 | |
| 1761 # then fall back to loading it from disk | |
| 1762 filename = self.makeBuildFilename(number) | |
| 1763 try: | |
| 1764 log.msg("Loading builder %s's build %d from on-disk pickle" | |
| 1765 % (self.name, number)) | |
| 1766 build = load(open(filename, "rb")) | |
| 1767 styles.doUpgrade() | |
| 1768 build.builder = self | |
| 1769 # handle LogFiles from after 0.5.0 and before 0.6.5 | |
| 1770 build.upgradeLogfiles() | |
| 1771 # check that logfiles exist | |
| 1772 build.checkLogfiles() | |
| 1773 return self.touchBuildCache(build) | |
| 1774 except IOError: | |
| 1775 raise IndexError("no such build %d" % number) | |
| 1776 except EOFError: | |
| 1777 raise IndexError("corrupted build pickle %d" % number) | |
| 1778 | |
| 1779 def prune(self): | |
| 1780 gc.collect() | |
| 1781 | |
| 1782 # begin by pruning our own events | |
| 1783 self.events = self.events[-self.eventHorizon:] | |
| 1784 | |
| 1785 # get the horizons straight | |
| 1786 if self.buildHorizon: | |
| 1787 earliest_build = self.nextBuildNumber - self.buildHorizon | |
| 1788 else: | |
| 1789 earliest_build = 0 | |
| 1790 | |
| 1791 if self.logHorizon: | |
| 1792 earliest_log = self.nextBuildNumber - self.logHorizon | |
| 1793 else: | |
| 1794 earliest_log = 0 | |
| 1795 | |
| 1796 if earliest_log < earliest_build: | |
| 1797 earliest_log = earliest_build | |
| 1798 | |
| 1799 if earliest_build == 0: | |
| 1800 return | |
| 1801 | |
| 1802 # skim the directory and delete anything that shouldn't be there anymore | |
| 1803 build_re = re.compile(r"^([0-9]+)$") | |
| 1804 build_log_re = re.compile(r"^([0-9]+)-.*$") | |
| 1805 # if the directory doesn't exist, bail out here | |
| 1806 if not os.path.exists(self.basedir): | |
| 1807 return | |
| 1808 | |
| 1809 for filename in os.listdir(self.basedir): | |
| 1810 num = None | |
| 1811 mo = build_re.match(filename) | |
| 1812 is_logfile = False | |
| 1813 if mo: | |
| 1814 num = int(mo.group(1)) | |
| 1815 else: | |
| 1816 mo = build_log_re.match(filename) | |
| 1817 if mo: | |
| 1818 num = int(mo.group(1)) | |
| 1819 is_logfile = True | |
| 1820 | |
| 1821 if num is None: continue | |
| 1822 if num in self.buildCache: continue | |
| 1823 | |
| 1824 if (is_logfile and num < earliest_log) or num < earliest_build: | |
| 1825 pathname = os.path.join(self.basedir, filename) | |
| 1826 log.msg("pruning '%s'" % pathname) | |
| 1827 try: os.unlink(pathname) | |
| 1828 except OSError: pass | |
| 1829 | |
| 1830 # IBuilderStatus methods | |
| 1831 def getName(self): | |
| 1832 return self.name | |
| 1833 | |
| 1834 def getState(self): | |
| 1835 return (self.currentBigState, self.currentBuilds) | |
| 1836 | |
| 1837 def getSlaves(self): | |
| 1838 return [self.status.getSlave(name) for name in self.slavenames] | |
| 1839 | |
| 1840 def getPendingBuilds(self): | |
| 1841 return self.pendingBuilds | |
| 1842 | |
| 1843 def getCurrentBuilds(self): | |
| 1844 return self.currentBuilds | |
| 1845 | |
| 1846 def getLastFinishedBuild(self): | |
| 1847 b = self.getBuild(-1) | |
| 1848 if not (b and b.isFinished()): | |
| 1849 b = self.getBuild(-2) | |
| 1850 return b | |
| 1851 | |
| 1852 def getCategory(self): | |
| 1853 return self.category | |
| 1854 | |
| 1855 def getBuild(self, number): | |
| 1856 if number < 0: | |
| 1857 number = self.nextBuildNumber + number | |
| 1858 if number < 0 or number >= self.nextBuildNumber: | |
| 1859 return None | |
| 1860 | |
| 1861 try: | |
| 1862 return self.getBuildByNumber(number) | |
| 1863 except IndexError: | |
| 1864 return None | |
| 1865 | |
| 1866 def getEvent(self, number): | |
| 1867 try: | |
| 1868 return self.events[number] | |
| 1869 except IndexError: | |
| 1870 return None | |
| 1871 | |
| 1872 def generateFinishedBuilds(self, branches=[], | |
| 1873 num_builds=None, | |
| 1874 max_buildnum=None, | |
| 1875 finished_before=None, | |
| 1876 max_search=200): | |
| 1877 got = 0 | |
| 1878 for Nb in itertools.count(1): | |
| 1879 if Nb > self.nextBuildNumber: | |
| 1880 break | |
| 1881 if Nb > max_search: | |
| 1882 break | |
| 1883 build = self.getBuild(-Nb) | |
| 1884 if build is None: | |
| 1885 continue | |
| 1886 if max_buildnum is not None: | |
| 1887 if build.getNumber() > max_buildnum: | |
| 1888 continue | |
| 1889 if not build.isFinished(): | |
| 1890 continue | |
| 1891 if finished_before is not None: | |
| 1892 start, end = build.getTimes() | |
| 1893 if end >= finished_before: | |
| 1894 continue | |
| 1895 if branches: | |
| 1896 if build.getSourceStamp().branch not in branches: | |
| 1897 continue | |
| 1898 got += 1 | |
| 1899 yield build | |
| 1900 if num_builds is not None: | |
| 1901 if got >= num_builds: | |
| 1902 return | |
| 1903 | |
| 1904 def eventGenerator(self, branches=[], categories=[], committers=[], minTime=
0): | |
| 1905 """This function creates a generator which will provide all of this | |
| 1906 Builder's status events, starting with the most recent and | |
| 1907 progressing backwards in time. """ | |
| 1908 | |
| 1909 # remember the oldest-to-earliest flow here. "next" means earlier. | |
| 1910 | |
| 1911 # TODO: interleave build steps and self.events by timestamp. | |
| 1912 # TODO: um, I think we're already doing that. | |
| 1913 | |
| 1914 # TODO: there's probably something clever we could do here to | |
| 1915 # interleave two event streams (one from self.getBuild and the other | |
| 1916 # from self.getEvent), which would be simpler than this control flow | |
| 1917 | |
| 1918 eventIndex = -1 | |
| 1919 e = self.getEvent(eventIndex) | |
| 1920 for Nb in range(1, self.nextBuildNumber+1): | |
| 1921 b = self.getBuild(-Nb) | |
| 1922 if not b: | |
| 1923 # HACK: If this is the first build we are looking at, it is | |
| 1924 # possible it's in progress but locked before it has written a | |
| 1925 # pickle; in this case keep looking. | |
| 1926 if Nb == 1: | |
| 1927 continue | |
| 1928 break | |
| 1929 if b.getTimes()[0] < minTime: | |
| 1930 break | |
| 1931 if branches and not b.getSourceStamp().branch in branches: | |
| 1932 continue | |
| 1933 if categories and not b.getBuilder().getCategory() in categories: | |
| 1934 continue | |
| 1935 if committers and not [True for c in b.getChanges() if c.who in comm
itters]: | |
| 1936 continue | |
| 1937 steps = b.getSteps() | |
| 1938 for Ns in range(1, len(steps)+1): | |
| 1939 if steps[-Ns].started: | |
| 1940 step_start = steps[-Ns].getTimes()[0] | |
| 1941 while e is not None and e.getTimes()[0] > step_start: | |
| 1942 yield e | |
| 1943 eventIndex -= 1 | |
| 1944 e = self.getEvent(eventIndex) | |
| 1945 yield steps[-Ns] | |
| 1946 yield b | |
| 1947 while e is not None: | |
| 1948 yield e | |
| 1949 eventIndex -= 1 | |
| 1950 e = self.getEvent(eventIndex) | |
| 1951 if e and e.getTimes()[0] < minTime: | |
| 1952 break | |
| 1953 | |
| 1954 def subscribe(self, receiver): | |
| 1955 # will get builderChangedState, buildStarted, and buildFinished | |
| 1956 self.watchers.append(receiver) | |
| 1957 self.publishState(receiver) | |
| 1958 | |
| 1959 def unsubscribe(self, receiver): | |
| 1960 self.watchers.remove(receiver) | |
| 1961 | |
| 1962 ## Builder interface (methods called by the Builder which feeds us) | |
| 1963 | |
| 1964 def setSlavenames(self, names): | |
| 1965 self.slavenames = names | |
| 1966 | |
| 1967 def addEvent(self, text=[]): | |
| 1968 # this adds a duration event. When it is done, the user should call | |
| 1969 # e.finish(). They can also mangle it by modifying .text | |
| 1970 e = Event() | |
| 1971 e.started = util.now() | |
| 1972 e.text = text | |
| 1973 self.events.append(e) | |
| 1974 return e # they are free to mangle it further | |
| 1975 | |
| 1976 def addPointEvent(self, text=[]): | |
| 1977 # this adds a point event, one which occurs as a single atomic | |
| 1978 # instant of time. | |
| 1979 e = Event() | |
| 1980 e.started = util.now() | |
| 1981 e.finished = 0 | |
| 1982 e.text = text | |
| 1983 self.events.append(e) | |
| 1984 return e # for consistency, but they really shouldn't touch it | |
| 1985 | |
| 1986 def setBigState(self, state): | |
| 1987 needToUpdate = state != self.currentBigState | |
| 1988 self.currentBigState = state | |
| 1989 if needToUpdate: | |
| 1990 self.publishState() | |
| 1991 | |
| 1992 def publishState(self, target=None): | |
| 1993 state = self.currentBigState | |
| 1994 | |
| 1995 if target is not None: | |
| 1996 # unicast | |
| 1997 target.builderChangedState(self.name, state) | |
| 1998 return | |
| 1999 for w in self.watchers: | |
| 2000 try: | |
| 2001 w.builderChangedState(self.name, state) | |
| 2002 except: | |
| 2003 log.msg("Exception caught publishing state to %r" % w) | |
| 2004 log.err() | |
| 2005 | |
| 2006 def newBuild(self): | |
| 2007 """The Builder has decided to start a build, but the Build object is | |
| 2008 not yet ready to report status (it has not finished creating the | |
| 2009 Steps). Create a BuildStatus object that it can use.""" | |
| 2010 number = self.nextBuildNumber | |
| 2011 self.nextBuildNumber += 1 | |
| 2012 # TODO: self.saveYourself(), to make sure we don't forget about the | |
| 2013 # build number we've just allocated. This is not quite as important | |
| 2014 # as it was before we switch to determineNextBuildNumber, but I think | |
| 2015 # it may still be useful to have the new build save itself. | |
| 2016 s = BuildStatus(self, number) | |
| 2017 s.waitUntilFinished().addCallback(self._buildFinished) | |
| 2018 return s | |
| 2019 | |
| 2020 def addBuildRequest(self, brstatus): | |
| 2021 self.pendingBuilds.append(brstatus) | |
| 2022 for w in self.watchers: | |
| 2023 w.requestSubmitted(brstatus) | |
| 2024 | |
| 2025 def removeBuildRequest(self, brstatus, cancelled=False): | |
| 2026 self.pendingBuilds.remove(brstatus) | |
| 2027 if cancelled: | |
| 2028 for w in self.watchers: | |
| 2029 w.requestCancelled(self, brstatus) | |
| 2030 | |
| 2031 # buildStarted is called by our child BuildStatus instances | |
| 2032 def buildStarted(self, s): | |
| 2033 """Now the BuildStatus object is ready to go (it knows all of its | |
| 2034 Steps, its ETA, etc), so it is safe to notify our watchers.""" | |
| 2035 | |
| 2036 assert s.builder is self # paranoia | |
| 2037 assert s.number == self.nextBuildNumber - 1 | |
| 2038 assert s not in self.currentBuilds | |
| 2039 self.currentBuilds.append(s) | |
| 2040 self.touchBuildCache(s) | |
| 2041 | |
| 2042 # now that the BuildStatus is prepared to answer queries, we can | |
| 2043 # announce the new build to all our watchers | |
| 2044 | |
| 2045 for w in self.watchers: # TODO: maybe do this later? callLater(0)? | |
| 2046 try: | |
| 2047 receiver = w.buildStarted(self.getName(), s) | |
| 2048 if receiver: | |
| 2049 if type(receiver) == type(()): | |
| 2050 s.subscribe(receiver[0], receiver[1]) | |
| 2051 else: | |
| 2052 s.subscribe(receiver) | |
| 2053 d = s.waitUntilFinished() | |
| 2054 d.addCallback(lambda s: s.unsubscribe(receiver)) | |
| 2055 except: | |
| 2056 log.msg("Exception caught notifying %r of buildStarted event" %
w) | |
| 2057 log.err() | |
| 2058 | |
| 2059 def _buildFinished(self, s): | |
| 2060 assert s in self.currentBuilds | |
| 2061 s.saveYourself() | |
| 2062 self.currentBuilds.remove(s) | |
| 2063 | |
| 2064 name = self.getName() | |
| 2065 results = s.getResults() | |
| 2066 for w in self.watchers: | |
| 2067 try: | |
| 2068 w.buildFinished(name, s, results) | |
| 2069 except: | |
| 2070 log.msg("Exception caught notifying %r of buildFinished event" %
w) | |
| 2071 log.err() | |
| 2072 | |
| 2073 self.prune() # conserve disk | |
| 2074 | |
| 2075 | |
| 2076 # waterfall display (history) | |
| 2077 | |
| 2078 # I want some kind of build event that holds everything about the build: | |
| 2079 # why, what changes went into it, the results of the build, itemized | |
| 2080 # test results, etc. But, I do kind of need something to be inserted in | |
| 2081 # the event log first, because intermixing step events and the larger | |
| 2082 # build event is fraught with peril. Maybe an Event-like-thing that | |
| 2083 # doesn't have a file in it but does have links. Hmm, that's exactly | |
| 2084 # what it does now. The only difference would be that this event isn't | |
| 2085 # pushed to the clients. | |
| 2086 | |
| 2087 # publish to clients | |
| 2088 def sendLastBuildStatus(self, client): | |
| 2089 #client.newLastBuildStatus(self.lastBuildStatus) | |
| 2090 pass | |
| 2091 def sendCurrentActivityBigToEveryone(self): | |
| 2092 for s in self.subscribers: | |
| 2093 self.sendCurrentActivityBig(s) | |
| 2094 def sendCurrentActivityBig(self, client): | |
| 2095 state = self.currentBigState | |
| 2096 if state == "offline": | |
| 2097 client.currentlyOffline() | |
| 2098 elif state == "idle": | |
| 2099 client.currentlyIdle() | |
| 2100 elif state == "building": | |
| 2101 client.currentlyBuilding() | |
| 2102 else: | |
| 2103 log.msg("Hey, self.currentBigState is weird:", state) | |
| 2104 | |
| 2105 | |
| 2106 ## HTML display interface | |
| 2107 | |
| 2108 def getEventNumbered(self, num): | |
| 2109 # deal with dropped events, pruned events | |
| 2110 first = self.events[0].number | |
| 2111 if first + len(self.events)-1 != self.events[-1].number: | |
| 2112 log.msg(self, | |
| 2113 "lost an event somewhere: [0] is %d, [%d] is %d" % \ | |
| 2114 (self.events[0].number, | |
| 2115 len(self.events) - 1, | |
| 2116 self.events[-1].number)) | |
| 2117 for e in self.events: | |
| 2118 log.msg("e[%d]: " % e.number, e) | |
| 2119 return None | |
| 2120 offset = num - first | |
| 2121 log.msg(self, "offset", offset) | |
| 2122 try: | |
| 2123 return self.events[offset] | |
| 2124 except IndexError: | |
| 2125 return None | |
| 2126 | |
| 2127 ## Persistence of Status | |
| 2128 def loadYourOldEvents(self): | |
| 2129 if hasattr(self, "allEvents"): | |
| 2130 # first time, nothing to get from file. Note that this is only if | |
| 2131 # the Application gets .run() . If it gets .save()'ed, then the | |
| 2132 # .allEvents attribute goes away in the initial __getstate__ and | |
| 2133 # we try to load a non-existent file. | |
| 2134 return | |
| 2135 self.allEvents = self.loadFile("events", []) | |
| 2136 if self.allEvents: | |
| 2137 self.nextEventNumber = self.allEvents[-1].number + 1 | |
| 2138 else: | |
| 2139 self.nextEventNumber = 0 | |
| 2140 def saveYourOldEvents(self): | |
| 2141 self.saveFile("events", self.allEvents) | |
| 2142 | |
| 2143 ## clients | |
| 2144 | |
| 2145 def addClient(self, client): | |
| 2146 if client not in self.subscribers: | |
| 2147 self.subscribers.append(client) | |
| 2148 self.sendLastBuildStatus(client) | |
| 2149 self.sendCurrentActivityBig(client) | |
| 2150 client.newEvent(self.currentSmall) | |
| 2151 def removeClient(self, client): | |
| 2152 if client in self.subscribers: | |
| 2153 self.subscribers.remove(client) | |
| 2154 | |
| 2155 def asDict(self): | |
| 2156 result = {} | |
| 2157 # Constant | |
| 2158 # TODO(maruel): Fix me. We don't want to leak the full path. | |
| 2159 result['basedir'] = os.path.basename(self.basedir) | |
| 2160 result['category'] = self.category | |
| 2161 result['slaves'] = self.slavenames | |
| 2162 #result['url'] = self.parent.getURLForThing(self) | |
| 2163 # TODO(maruel): Add cache settings? Do we care? | |
| 2164 | |
| 2165 # Transient | |
| 2166 # Collect build numbers. | |
| 2167 # Important: Only grab the *cached* builds numbers to reduce I/O. | |
| 2168 current_builds = [b.getNumber() for b in self.currentBuilds] | |
| 2169 cached_builds = list(set(self.buildCache.keys() + current_builds)) | |
| 2170 cached_builds.sort() | |
| 2171 result['cachedBuilds'] = cached_builds | |
| 2172 result['currentBuilds'] = current_builds | |
| 2173 result['state'] = self.getState()[0] | |
| 2174 result['pendingBuilds'] = len(self.getPendingBuilds()) | |
| 2175 return result | |
| 2176 | |
| 2177 | |
| 2178 class SlaveStatus: | |
| 2179 implements(interfaces.ISlaveStatus) | |
| 2180 | |
| 2181 admin = None | |
| 2182 host = None | |
| 2183 access_uri = None | |
| 2184 version = None | |
| 2185 connected = False | |
| 2186 graceful_shutdown = False | |
| 2187 | |
| 2188 def __init__(self, name): | |
| 2189 self.name = name | |
| 2190 self._lastMessageReceived = 0 | |
| 2191 self.runningBuilds = [] | |
| 2192 self.graceful_callbacks = [] | |
| 2193 | |
| 2194 def getName(self): | |
| 2195 return self.name | |
| 2196 def getAdmin(self): | |
| 2197 return self.admin | |
| 2198 def getHost(self): | |
| 2199 return self.host | |
| 2200 def getAccessURI(self): | |
| 2201 return self.access_uri | |
| 2202 def getVersion(self): | |
| 2203 return self.version | |
| 2204 def isConnected(self): | |
| 2205 return self.connected | |
| 2206 def lastMessageReceived(self): | |
| 2207 return self._lastMessageReceived | |
| 2208 def getRunningBuilds(self): | |
| 2209 return self.runningBuilds | |
| 2210 | |
| 2211 def setAdmin(self, admin): | |
| 2212 self.admin = admin | |
| 2213 def setHost(self, host): | |
| 2214 self.host = host | |
| 2215 def setAccessURI(self, access_uri): | |
| 2216 self.access_uri = access_uri | |
| 2217 def setVersion(self, version): | |
| 2218 self.version = version | |
| 2219 def setConnected(self, isConnected): | |
| 2220 self.connected = isConnected | |
| 2221 def setLastMessageReceived(self, when): | |
| 2222 self._lastMessageReceived = when | |
| 2223 | |
| 2224 def buildStarted(self, build): | |
| 2225 self.runningBuilds.append(build) | |
| 2226 def buildFinished(self, build): | |
| 2227 self.runningBuilds.remove(build) | |
| 2228 | |
| 2229 def getGraceful(self): | |
| 2230 """Return the graceful shutdown flag""" | |
| 2231 return self.graceful_shutdown | |
| 2232 def setGraceful(self, graceful): | |
| 2233 """Set the graceful shutdown flag, and notify all the watchers""" | |
| 2234 self.graceful_shutdown = graceful | |
| 2235 for cb in self.graceful_callbacks: | |
| 2236 reactor.callLater(0, cb, graceful) | |
| 2237 def addGracefulWatcher(self, watcher): | |
| 2238 """Add watcher to the list of watchers to be notified when the | |
| 2239 graceful shutdown flag is changed.""" | |
| 2240 if not watcher in self.graceful_callbacks: | |
| 2241 self.graceful_callbacks.append(watcher) | |
| 2242 def removeGracefulWatcher(self, watcher): | |
| 2243 """Remove watcher from the list of watchers to be notified when the | |
| 2244 graceful shutdown flag is changed.""" | |
| 2245 if watcher in self.graceful_callbacks: | |
| 2246 self.graceful_callbacks.remove(watcher) | |
| 2247 | |
| 2248 def asDict(self): | |
| 2249 result = {} | |
| 2250 # Constant | |
| 2251 result['name'] = self.getName() | |
| 2252 result['access_uri'] = self.getAccessURI() | |
| 2253 | |
| 2254 # Transient (since it changes when the slave reconnects) | |
| 2255 result['host'] = self.getHost() | |
| 2256 result['admin'] = self.getAdmin() | |
| 2257 result['version'] = self.getVersion() | |
| 2258 result['connected'] = self.isConnected() | |
| 2259 result['runningBuilds'] = [b.asDict() for b in self.getRunningBuilds()] | |
| 2260 return result | |
| 2261 | |
| 2262 | |
| 2263 class Status: | |
| 2264 """ | |
| 2265 I represent the status of the buildmaster. | |
| 2266 """ | |
| 2267 implements(interfaces.IStatus) | |
| 2268 | |
| 2269 def __init__(self, botmaster, basedir): | |
| 2270 """ | |
| 2271 @type botmaster: L{buildbot.master.BotMaster} | |
| 2272 @param botmaster: the Status object uses C{.botmaster} to get at | |
| 2273 both the L{buildbot.master.BuildMaster} (for | |
| 2274 various buildbot-wide parameters) and the | |
| 2275 actual Builders (to get at their L{BuilderStatus} | |
| 2276 objects). It is not allowed to change or influence | |
| 2277 anything through this reference. | |
| 2278 @type basedir: string | |
| 2279 @param basedir: this provides a base directory in which saved status | |
| 2280 information (changes.pck, saved Build status | |
| 2281 pickles) can be stored | |
| 2282 """ | |
| 2283 self.botmaster = botmaster | |
| 2284 self.basedir = basedir | |
| 2285 self.watchers = [] | |
| 2286 self.activeBuildSets = [] | |
| 2287 assert os.path.isdir(basedir) | |
| 2288 # compress logs bigger than 4k, a good default on linux | |
| 2289 self.logCompressionLimit = 4*1024 | |
| 2290 self.logCompressionMethod = "bz2" | |
| 2291 # No default limit to the log size | |
| 2292 self.logMaxSize = None | |
| 2293 self.logMaxTailSize = None | |
| 2294 | |
| 2295 | |
| 2296 # methods called by our clients | |
| 2297 | |
| 2298 def getProjectName(self): | |
| 2299 return self.botmaster.parent.projectName | |
| 2300 def getProjectURL(self): | |
| 2301 return self.botmaster.parent.projectURL | |
| 2302 def getBuildbotURL(self): | |
| 2303 return self.botmaster.parent.buildbotURL | |
| 2304 | |
| 2305 def getURLForThing(self, thing): | |
| 2306 prefix = self.getBuildbotURL() | |
| 2307 if not prefix: | |
| 2308 return None | |
| 2309 if interfaces.IStatus.providedBy(thing): | |
| 2310 return prefix | |
| 2311 if interfaces.ISchedulerStatus.providedBy(thing): | |
| 2312 pass | |
| 2313 if interfaces.IBuilderStatus.providedBy(thing): | |
| 2314 builder = thing | |
| 2315 return prefix + "builders/%s" % ( | |
| 2316 urllib.quote(builder.getName(), safe=''), | |
| 2317 ) | |
| 2318 if interfaces.IBuildStatus.providedBy(thing): | |
| 2319 build = thing | |
| 2320 builder = build.getBuilder() | |
| 2321 return prefix + "builders/%s/builds/%d" % ( | |
| 2322 urllib.quote(builder.getName(), safe=''), | |
| 2323 build.getNumber()) | |
| 2324 if interfaces.IBuildStepStatus.providedBy(thing): | |
| 2325 step = thing | |
| 2326 build = step.getBuild() | |
| 2327 builder = build.getBuilder() | |
| 2328 return prefix + "builders/%s/builds/%d/steps/%s" % ( | |
| 2329 urllib.quote(builder.getName(), safe=''), | |
| 2330 build.getNumber(), | |
| 2331 urllib.quote(step.getName(), safe='')) | |
| 2332 # IBuildSetStatus | |
| 2333 # IBuildRequestStatus | |
| 2334 # ISlaveStatus | |
| 2335 | |
| 2336 # IStatusEvent | |
| 2337 if interfaces.IStatusEvent.providedBy(thing): | |
| 2338 from buildbot.changes import changes | |
| 2339 # TODO: this is goofy, create IChange or something | |
| 2340 if isinstance(thing, changes.Change): | |
| 2341 change = thing | |
| 2342 return "%schanges/%d" % (prefix, change.number) | |
| 2343 | |
| 2344 if interfaces.IStatusLog.providedBy(thing): | |
| 2345 log = thing | |
| 2346 step = log.getStep() | |
| 2347 build = step.getBuild() | |
| 2348 builder = build.getBuilder() | |
| 2349 | |
| 2350 logs = step.getLogs() | |
| 2351 for i in range(len(logs)): | |
| 2352 if log is logs[i]: | |
| 2353 lognum = i | |
| 2354 break | |
| 2355 else: | |
| 2356 return None | |
| 2357 return prefix + "builders/%s/builds/%d/steps/%s/logs/%s" % ( | |
| 2358 urllib.quote(builder.getName(), safe=''), | |
| 2359 build.getNumber(), | |
| 2360 urllib.quote(step.getName(), safe=''), | |
| 2361 urllib.quote(log.getName())) | |
| 2362 | |
| 2363 def getChangeSources(self): | |
| 2364 return list(self.botmaster.parent.change_svc) | |
| 2365 | |
| 2366 def getChange(self, number): | |
| 2367 return self.botmaster.parent.change_svc.getChangeNumbered(number) | |
| 2368 | |
| 2369 def getSchedulers(self): | |
| 2370 return self.botmaster.parent.allSchedulers() | |
| 2371 | |
| 2372 def getBuilderNames(self, categories=None): | |
| 2373 if categories == None: | |
| 2374 return self.botmaster.builderNames[:] # don't let them break it | |
| 2375 | |
| 2376 l = [] | |
| 2377 # respect addition order | |
| 2378 for name in self.botmaster.builderNames: | |
| 2379 builder = self.botmaster.builders[name] | |
| 2380 if builder.builder_status.category in categories: | |
| 2381 l.append(name) | |
| 2382 return l | |
| 2383 | |
| 2384 def getBuilder(self, name): | |
| 2385 """ | |
| 2386 @rtype: L{BuilderStatus} | |
| 2387 """ | |
| 2388 return self.botmaster.builders[name].builder_status | |
| 2389 | |
| 2390 def getSlaveNames(self): | |
| 2391 return self.botmaster.slaves.keys() | |
| 2392 | |
| 2393 def getSlave(self, slavename): | |
| 2394 return self.botmaster.slaves[slavename].slave_status | |
| 2395 | |
| 2396 def getBuildSets(self): | |
| 2397 return self.activeBuildSets[:] | |
| 2398 | |
| 2399 def generateFinishedBuilds(self, builders=[], branches=[], | |
| 2400 num_builds=None, finished_before=None, | |
| 2401 max_search=200): | |
| 2402 | |
| 2403 def want_builder(bn): | |
| 2404 if builders: | |
| 2405 return bn in builders | |
| 2406 return True | |
| 2407 builder_names = [bn | |
| 2408 for bn in self.getBuilderNames() | |
| 2409 if want_builder(bn)] | |
| 2410 | |
| 2411 # 'sources' is a list of generators, one for each Builder we're | |
| 2412 # using. When the generator is exhausted, it is replaced in this list | |
| 2413 # with None. | |
| 2414 sources = [] | |
| 2415 for bn in builder_names: | |
| 2416 b = self.getBuilder(bn) | |
| 2417 g = b.generateFinishedBuilds(branches, | |
| 2418 finished_before=finished_before, | |
| 2419 max_search=max_search) | |
| 2420 sources.append(g) | |
| 2421 | |
| 2422 # next_build the next build from each source | |
| 2423 next_build = [None] * len(sources) | |
| 2424 | |
| 2425 def refill(): | |
| 2426 for i,g in enumerate(sources): | |
| 2427 if next_build[i]: | |
| 2428 # already filled | |
| 2429 continue | |
| 2430 if not g: | |
| 2431 # already exhausted | |
| 2432 continue | |
| 2433 try: | |
| 2434 next_build[i] = g.next() | |
| 2435 except StopIteration: | |
| 2436 next_build[i] = None | |
| 2437 sources[i] = None | |
| 2438 | |
| 2439 got = 0 | |
| 2440 while True: | |
| 2441 refill() | |
| 2442 # find the latest build among all the candidates | |
| 2443 candidates = [(i, b, b.getTimes()[1]) | |
| 2444 for i,b in enumerate(next_build) | |
| 2445 if b is not None] | |
| 2446 candidates.sort(lambda x,y: cmp(x[2], y[2])) | |
| 2447 if not candidates: | |
| 2448 return | |
| 2449 | |
| 2450 # and remove it from the list | |
| 2451 i, build, finshed_time = candidates[-1] | |
| 2452 next_build[i] = None | |
| 2453 got += 1 | |
| 2454 yield build | |
| 2455 if num_builds is not None: | |
| 2456 if got >= num_builds: | |
| 2457 return | |
| 2458 | |
| 2459 def subscribe(self, target): | |
| 2460 self.watchers.append(target) | |
| 2461 for name in self.botmaster.builderNames: | |
| 2462 self.announceNewBuilder(target, name, self.getBuilder(name)) | |
| 2463 def unsubscribe(self, target): | |
| 2464 self.watchers.remove(target) | |
| 2465 | |
| 2466 | |
| 2467 # methods called by upstream objects | |
| 2468 | |
| 2469 def announceNewBuilder(self, target, name, builder_status): | |
| 2470 t = target.builderAdded(name, builder_status) | |
| 2471 if t: | |
| 2472 builder_status.subscribe(t) | |
| 2473 | |
| 2474 def builderAdded(self, name, basedir, category=None): | |
| 2475 """ | |
| 2476 @rtype: L{BuilderStatus} | |
| 2477 """ | |
| 2478 filename = os.path.join(self.basedir, basedir, "builder") | |
| 2479 log.msg("trying to load status pickle from %s" % filename) | |
| 2480 builder_status = None | |
| 2481 try: | |
| 2482 builder_status = load(open(filename, "rb")) | |
| 2483 styles.doUpgrade() | |
| 2484 except IOError: | |
| 2485 log.msg("no saved status pickle, creating a new one") | |
| 2486 except: | |
| 2487 log.msg("error while loading status pickle, creating a new one") | |
| 2488 log.msg("error follows:") | |
| 2489 log.err() | |
| 2490 if not builder_status: | |
| 2491 builder_status = BuilderStatus(name, category) | |
| 2492 builder_status.addPointEvent(["builder", "created"]) | |
| 2493 log.msg("added builder %s in category %s" % (name, category)) | |
| 2494 # an unpickled object might not have category set from before, | |
| 2495 # so set it here to make sure | |
| 2496 builder_status.category = category | |
| 2497 builder_status.basedir = os.path.join(self.basedir, basedir) | |
| 2498 builder_status.name = name # it might have been updated | |
| 2499 builder_status.status = self | |
| 2500 | |
| 2501 if not os.path.isdir(builder_status.basedir): | |
| 2502 os.makedirs(builder_status.basedir) | |
| 2503 builder_status.determineNextBuildNumber() | |
| 2504 | |
| 2505 builder_status.setBigState("offline") | |
| 2506 builder_status.setLogCompressionLimit(self.logCompressionLimit) | |
| 2507 builder_status.setLogCompressionMethod(self.logCompressionMethod) | |
| 2508 builder_status.setLogMaxSize(self.logMaxSize) | |
| 2509 builder_status.setLogMaxTailSize(self.logMaxTailSize) | |
| 2510 | |
| 2511 for t in self.watchers: | |
| 2512 self.announceNewBuilder(t, name, builder_status) | |
| 2513 | |
| 2514 return builder_status | |
| 2515 | |
| 2516 def builderRemoved(self, name): | |
| 2517 for t in self.watchers: | |
| 2518 t.builderRemoved(name) | |
| 2519 | |
| 2520 def slaveConnected(self, name): | |
| 2521 for t in self.watchers: | |
| 2522 t.slaveConnected(name) | |
| 2523 | |
| 2524 def slaveDisconnected(self, name): | |
| 2525 for t in self.watchers: | |
| 2526 t.slaveDisconnected(name) | |
| 2527 | |
| 2528 def buildsetSubmitted(self, bss): | |
| 2529 self.activeBuildSets.append(bss) | |
| 2530 bss.waitUntilFinished().addCallback(self.activeBuildSets.remove) | |
| 2531 for t in self.watchers: | |
| 2532 t.buildsetSubmitted(bss) | |
| 2533 | |
| 2534 def changeAdded(self, change): | |
| 2535 for t in self.watchers: | |
| 2536 t.changeAdded(change) | |
| 2537 | |
| 2538 def asDict(self): | |
| 2539 result = {} | |
| 2540 # Constant | |
| 2541 result['projectName'] = self.getProjectName() | |
| 2542 result['projectURL'] = self.getProjectURL() | |
| 2543 result['buildbotURL'] = self.getBuildbotURL() | |
| 2544 # TODO: self.getSchedulers() | |
| 2545 # self.getChangeSources() | |
| 2546 return result | |
| 2547 | |
| 2548 # vim: set ts=4 sts=4 sw=4 et: | |
| OLD | NEW |