1
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:
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
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
57 - def __init__(self, chunk_cb, channels=[]):
58 self.chunk_cb = chunk_cb
59 self.channels = channels
60
62 channel = int(line[0])
63 if not self.channels or (channel in self.channels):
64 self.chunk_cb((channel, line[1:]))
65
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
105 self.logfile = logfile
106 self.consumer = consumer
107 self.chunkGenerator = self.getChunks()
108 consumer.registerProducer(self, True)
109
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
129 self.subscribed = True
130 self.logfile.watchers.append(self)
131 d = self.logfile.waitUntilFinished()
132
133
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
140
141
142
143 d.addCallback(self.logfileFinished)
144
146
147 self.paused = True
148 self.consumer = None
149 self.done()
150
157
160
162
163
164
165
166
167
168
169
170 reactor.callLater(0, self._resumeProducing)
171
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
181
182 except StopIteration:
183
184 self.chunkGenerator = None
185
186
187
188 - def logChunk(self, build, step, logfile, channel, chunk):
189 if self.consumer:
190 self.consumer.writeChunk((channel, chunk))
191
198
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
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
237 logMaxSize = None
238
239 logMaxTailSize = None
240 maxLengthExceeded = False
241 runEntries = []
242 entries = None
243 BUFFERSIZE = 2048
244 filename = None
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
263
264
265
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
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
286
289
299
301 if self.openfile:
302
303
304 return self.openfile
305
306
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
320
321 return "".join(self.getChunks([STDOUT, STDERR], onlyText=True))
322
324 return "".join(self.getChunks(onlyText=True))
325
326 - def getChunks(self, channels=[], onlyText=False):
327
328
329
330
331
332
333
334
335
336
337
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
355
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
395 """Return an iterator that produces newline-terminated lines,
396 excluding header chunks."""
397
398
399
400 alltext = "".join(self.getChunks([channel], onlyText=True))
401 io = StringIO(alltext)
402 return io.readlines()
403
413
417
421
422
423
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
451 if self.logMaxSize and self.nonHeaderLength > self.logMaxSize:
452
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
461 self.tailBuffer.append((channel, text))
462 self.tailLength += len(text)
463 while self.tailLength > self.logMaxTailSize:
464
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
474
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
492
519
520
522
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
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()
564 log.msg("failed to compress %s" % self.getFilename())
565 if os.path.exists(compressed):
566 _tryremove(compressed, 1, 5)
567 failure.trap()
568
569
571 d = self.__dict__.copy()
572 del d['step']
573 del d['watchers']
574 del d['finishedWatchers']
575 d['entries'] = []
576 if d.has_key('finished'):
577 del d['finished']
578 if d.has_key('openfile'):
579 del d['openfile']
580 return d
581
588
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()
600 del self.entries
601
603 implements(interfaces.IStatusLog)
604
605 filename = None
606
607 - def __init__(self, parent, name, logfilename, html):
612
617
621 return defer.succeed(self)
622
623 - def hasContents(self):
631
636
639
641 d = self.__dict__.copy()
642 del d['step']
643 return d
644
647
648
666
688
689
691 implements(interfaces.IBuildSetStatus)
692
693 - def __init__(self, source, reason, builderNames, bsid=None):
702
704 self.buildRequests = buildRequestStatuses
709 self.stillHopeful = False
710
711
713 for d in self.successWatchers:
714 d.callback(self)
715 self.successWatchers = []
716
722
723
724
733
735 return self.builderNames
737 return self.buildRequests
740
742 if self.finished or not self.stillHopeful:
743
744 return defer.succeed(self)
745 d = defer.Deferred()
746 self.successWatchers.append(d)
747 return d
748
755
757 implements(interfaces.IBuildRequestStatus)
758
759 - def __init__(self, source, builderName):
760 self.source = source
761 self.builderName = builderName
762 self.builds = []
763 self.observers = []
764 self.submittedAt = None
765
767 self.builds.append(build)
768 for o in self.observers[:]:
769 o(build)
770
771
775 return self.builderName
778
780 self.observers.append(observer)
781 for b in self.builds:
782 observer(b)
784 self.observers.remove(observer)
785
790
791
793 """
794 I represent a collection of output status for a
795 L{buildbot.process.step.BuildStep}.
796
797 Statistics contain any information gleaned from a step that is
798 not in the form of a logfile. As an example, steps that run
799 tests might gather statistics about the number of passed, failed,
800 or skipped tests.
801
802 @type progress: L{buildbot.status.progress.StepProgress}
803 @cvar progress: tracks ETA for the step
804 @type text: list of strings
805 @cvar text: list of short texts that describe the command and its status
806 @type text2: list of strings
807 @cvar text2: list of short texts added to the overall build description
808 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
809 @ivar logs: logs of steps
810 @type statistics: dict
811 @ivar statistics: results from running this step
812 """
813
814
815 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
816 persistenceVersion = 2
817
818 started = None
819 finished = None
820 progress = None
821 text = []
822 results = (None, [])
823 text2 = []
824 watchers = []
825 updates = {}
826 finishedWatchers = []
827 statistics = {}
828
838
840 """Returns a short string with the name of this step. This string
841 may have spaces in it."""
842 return self.name
843
846
849
851 """Returns a list of tuples (name, current, target)."""
852 if not self.progress:
853 return []
854 ret = []
855 metrics = self.progress.progress.keys()
856 metrics.sort()
857 for m in metrics:
858 t = (m, self.progress.progress[m], self.progress.expectations[m])
859 ret.append(t)
860 return ret
861
864
866 return self.urls.copy()
867
869 return (self.started is not None)
870
873
881
882
883
884
893
894
895
896
898 """Returns a list of strings which describe the step. These are
899 intended to be displayed in a narrow column. If more space is
900 available, the caller should join them together with spaces before
901 presenting them to the user."""
902 return self.text
903
905 """Return a tuple describing the results of the step.
906 'result' is one of the constants in L{buildbot.status.builder}:
907 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
908 'strings' is an optional list of strings that the step wants to
909 append to the overall build's results. These strings are usually
910 more terse than the ones returned by getText(): in particular,
911 successful Steps do not usually contribute any text to the
912 overall build.
913
914 @rtype: tuple of int, list of strings
915 @returns: (result, strings)
916 """
917 return (self.results, self.text2)
918
920 """Return true if this step has a value for the given statistic.
921 """
922 return self.statistics.has_key(name)
923
925 """Return the given statistic, if present
926 """
927 return self.statistics.get(name, default)
928
929
930
931 - def subscribe(self, receiver, updateInterval=10):
936
947
955
956
957
958
961
963 log.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color,))
964
967
972
990
1003
1007
1008 - def addURL(self, name, url):
1009 self.urls[name] = url
1010
1011 - def setText(self, text):
1012 self.text = text
1013 for w in self.watchers:
1014 w.stepTextChanged(self.build, self, text)
1015 - def setText2(self, text):
1016 self.text2 = text
1017 for w in self.watchers:
1018 w.stepText2Changed(self.build, self, text)
1019
1021 """Set the given statistic. Usually called by subclasses.
1022 """
1023 self.statistics[name] = value
1024
1053
1057
1058
1059
1061 d = styles.Versioned.__getstate__(self)
1062 del d['build']
1063 if d.has_key('progress'):
1064 del d['progress']
1065 del d['watchers']
1066 del d['finishedWatchers']
1067 del d['updates']
1068 return d
1069
1071 styles.Versioned.__setstate__(self, d)
1072
1073
1074
1075 for loog in self.logs:
1076 loog.step = self
1077
1079 if not hasattr(self, "urls"):
1080 self.urls = {}
1081
1083 if not hasattr(self, "statistics"):
1084 self.statistics = {}
1085
1086
1088 implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
1089 persistenceVersion = 3
1090
1091 source = None
1092 reason = None
1093 changes = []
1094 blamelist = []
1095 requests = []
1096 progress = None
1097 started = None
1098 finished = None
1099 currentStep = None
1100 text = []
1101 results = None
1102 slavename = "???"
1103
1104
1105
1106
1107 watchers = []
1108 updates = {}
1109 finishedWatchers = []
1110 testResults = {}
1111
1127
1130
1131
1132
1134 """
1135 @rtype: L{BuilderStatus}
1136 """
1137 return self.builder
1138
1141
1144
1147
1152
1157
1160
1163
1166
1169
1173
1175 """Return a list of IBuildStepStatus objects. For invariant builds
1176 (those which always use the same set of Steps), this should be the
1177 complete list, however some of the steps may not have started yet
1178 (step.getTimes()[0] will be None). For variant builds, this may not
1179 be complete (asking again later may give you more of them)."""
1180 return self.steps
1181
1184
1185 _sentinel = []
1187 """Summarize the named statistic over all steps in which it
1188 exists, using combination_fn and initial_value to combine multiple
1189 results into a single result. This translates to a call to Python's
1190 X{reduce}::
1191 return reduce(summary_fn, step_stats_list, initial_value)
1192 """
1193 step_stats_list = [
1194 st.getStatistic(name)
1195 for st in self.steps
1196 if st.hasStatistic(name) ]
1197 if initial_value is self._sentinel:
1198 return reduce(summary_fn, step_stats_list)
1199 else:
1200 return reduce(summary_fn, step_stats_list, initial_value)
1201
1204
1212
1213
1214
1215
1225
1228
1229
1230
1231
1232 - def getText(self):
1233 text = []
1234 text.extend(self.text)
1235 for s in self.steps:
1236 text.extend(s.text2)
1237 return text
1238
1241
1244
1247
1257
1258
1259
1260 - def subscribe(self, receiver, updateInterval=None):
1261
1262
1263 self.watchers.append(receiver)
1264 if updateInterval is not None:
1265 self.sendETAUpdate(receiver, updateInterval)
1266
1278
1286
1287
1288
1290 """The Build is setting up, and has added a new BuildStep to its
1291 list. Create a BuildStepStatus object to which it can send status
1292 updates."""
1293
1294 s = BuildStepStatus(self)
1295 s.setName(name)
1296 self.steps.append(s)
1297 return s
1298
1301
1304
1308
1311
1318
1320 """The Build has been set up and is about to be started. It can now
1321 be safely queried, so it is time to announce the new build."""
1322
1323 self.started = util.now()
1324
1325
1326 self.builder.buildStarted(self)
1327
1330
1331 - def setText(self, text):
1332 assert isinstance(text, (list, tuple))
1333 self.text = text
1336
1350
1351
1352
1367
1372
1373
1374
1376
1377 self.steps = []
1378
1379
1380
1382 """Return a filename (relative to the Builder's base directory) where
1383 the logfile's contents can be stored uniquely.
1384
1385 The base filename is made by combining our build number, the Step's
1386 name, and the log's name, then removing unsuitable characters. The
1387 filename is then made unique by appending _0, _1, etc, until it does
1388 not collide with any other logfile.
1389
1390 These files are kept in the Builder's basedir (rather than a
1391 per-Build subdirectory) because that makes cleanup easier: cron and
1392 find will help get rid of the old logs, but the empty directories are
1393 more of a hassle to remove."""
1394
1395 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
1396 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
1397
1398 unique_counter = 0
1399 filename = starting_filename
1400 while filename in [l.filename
1401 for step in self.steps
1402 for l in step.getLogs()
1403 if l.filename]:
1404 filename = "%s_%d" % (starting_filename, unique_counter)
1405 unique_counter += 1
1406 return filename
1407
1409 d = styles.Versioned.__getstate__(self)
1410
1411
1412 if not self.finished:
1413 d['finished'] = True
1414
1415
1416
1417
1418 for k in 'builder', 'watchers', 'updates', 'requests', 'finishedWatchers':
1419 if k in d: del d[k]
1420 return d
1421
1430
1443
1446
1452
1467
1473
1475 filename = os.path.join(self.builder.basedir, "%d" % self.number)
1476 if os.path.isdir(filename):
1477
1478 shutil.rmtree(filename, ignore_errors=True)
1479 tmpfilename = filename + ".tmp"
1480 try:
1481 dump(self, open(tmpfilename, "wb"), -1)
1482 if sys.platform == 'win32':
1483
1484
1485
1486
1487 if os.path.exists(filename):
1488 os.unlink(filename)
1489 os.rename(tmpfilename, filename)
1490 except:
1491 log.msg("unable to save build %s-#%d" % (self.builder.name,
1492 self.number))
1493 log.err()
1494
1495
1496
1498 """I handle status information for a single process.base.Builder object.
1499 That object sends status changes to me (frequently as Events), and I
1500 provide them on demand to the various status recipients, like the HTML
1501 waterfall display and the live status clients. It also sends build
1502 summaries to me, which I log and provide to status clients who aren't
1503 interested in seeing details of the individual build steps.
1504
1505 I am responsible for maintaining the list of historic Events and Builds,
1506 pruning old ones, and loading them from / saving them to disk.
1507
1508 I live in the buildbot.process.base.Builder object, in the
1509 .builder_status attribute.
1510
1511 @type category: string
1512 @ivar category: user-defined category this builder belongs to; can be
1513 used to filter on in status clients
1514 """
1515
1516 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
1517 persistenceVersion = 1
1518
1519
1520
1521
1522 buildCacheSize = 15
1523 eventHorizon = 50
1524
1525
1526 logHorizon = 40
1527 buildHorizon = 100
1528
1529 category = None
1530 currentBigState = "offline"
1531 basedir = None
1532
1533 - def __init__(self, buildername, category=None):
1534 self.name = buildername
1535 self.category = category
1536
1537 self.slavenames = []
1538 self.events = []
1539
1540
1541 self.lastBuildStatus = None
1542
1543
1544 self.currentBuilds = []
1545 self.pendingBuilds = []
1546 self.nextBuild = None
1547 self.watchers = []
1548 self.buildCache = weakref.WeakValueDictionary()
1549 self.buildCache_LRU = []
1550 self.logCompressionLimit = False
1551 self.logCompressionMethod = "bz2"
1552 self.logMaxSize = None
1553 self.logMaxTailSize = None
1554
1555
1556
1558
1559
1560
1561
1562 d = styles.Versioned.__getstate__(self)
1563 d['watchers'] = []
1564 del d['buildCache']
1565 del d['buildCache_LRU']
1566 for b in self.currentBuilds:
1567 b.saveYourself()
1568
1569 del d['currentBuilds']
1570 del d['pendingBuilds']
1571 del d['currentBigState']
1572 del d['basedir']
1573 del d['status']
1574 del d['nextBuildNumber']
1575 return d
1576
1578
1579
1580 styles.Versioned.__setstate__(self, d)
1581 self.buildCache = weakref.WeakValueDictionary()
1582 self.buildCache_LRU = []
1583 self.currentBuilds = []
1584 self.pendingBuilds = []
1585 self.watchers = []
1586 self.slavenames = []
1587
1588
1589
1601
1603 if hasattr(self, 'slavename'):
1604 self.slavenames = [self.slavename]
1605 del self.slavename
1606 if hasattr(self, 'nextBuildNumber'):
1607 del self.nextBuildNumber
1608
1610 """Scan our directory of saved BuildStatus instances to determine
1611 what our self.nextBuildNumber should be. Set it one larger than the
1612 highest-numbered build we discover. This is called by the top-level
1613 Status object shortly after we are created or loaded from disk.
1614 """
1615 existing_builds = [int(f)
1616 for f in os.listdir(self.basedir)
1617 if re.match("^\d+$", f)]
1618 if existing_builds:
1619 self.nextBuildNumber = max(existing_builds) + 1
1620 else:
1621 self.nextBuildNumber = 0
1622
1624 self.logCompressionLimit = lowerLimit
1625
1627 assert method in ("bz2", "gz")
1628 self.logCompressionMethod = method
1629
1632
1635
1637 for b in self.currentBuilds:
1638 if not b.isFinished:
1639
1640
1641 b.saveYourself()
1642 filename = os.path.join(self.basedir, "builder")
1643 tmpfilename = filename + ".tmp"
1644 try:
1645 dump(self, open(tmpfilename, "wb"), -1)
1646 if sys.platform == 'win32':
1647
1648 if os.path.exists(filename):
1649 os.unlink(filename)
1650 os.rename(tmpfilename, filename)
1651 except:
1652 log.msg("unable to save builder %s" % self.name)
1653 log.err()
1654
1655
1656
1657
1660
1667
1695
1697 gc.collect()
1698
1699
1700 self.events = self.events[-self.eventHorizon:]
1701
1702
1703 if self.buildHorizon:
1704 earliest_build = self.nextBuildNumber - self.buildHorizon
1705 else:
1706 earliest_build = 0
1707
1708 if self.logHorizon:
1709 earliest_log = self.nextBuildNumber - self.logHorizon
1710 else:
1711 earliest_log = 0
1712
1713 if earliest_log < earliest_build:
1714 earliest_log = earliest_build
1715
1716 if earliest_build == 0:
1717 return
1718
1719
1720 build_re = re.compile(r"^([0-9]+)$")
1721 build_log_re = re.compile(r"^([0-9]+)-.*$")
1722
1723 if not os.path.exists(self.basedir):
1724 return
1725
1726 for filename in os.listdir(self.basedir):
1727 num = None
1728 mo = build_re.match(filename)
1729 is_logfile = False
1730 if mo:
1731 num = int(mo.group(1))
1732 else:
1733 mo = build_log_re.match(filename)
1734 if mo:
1735 num = int(mo.group(1))
1736 is_logfile = True
1737
1738 if num is None: continue
1739 if num in self.buildCache: continue
1740
1741 if (is_logfile and num < earliest_log) or num < earliest_build:
1742 pathname = os.path.join(self.basedir, filename)
1743 log.msg("pruning '%s'" % pathname)
1744 try: os.unlink(pathname)
1745 except OSError: pass
1746
1747
1750
1753
1756
1758 return self.pendingBuilds
1759
1761 return self.currentBuilds
1762
1768
1771
1782
1784 try:
1785 return self.events[number]
1786 except IndexError:
1787 return None
1788
1789 - def generateFinishedBuilds(self, branches=[],
1790 num_builds=None,
1791 max_buildnum=None,
1792 finished_before=None,
1793 max_search=200):
1794 got = 0
1795 for Nb in itertools.count(1):
1796 if Nb > self.nextBuildNumber:
1797 break
1798 if Nb > max_search:
1799 break
1800 build = self.getBuild(-Nb)
1801 if build is None:
1802 continue
1803 if max_buildnum is not None:
1804 if build.getNumber() > max_buildnum:
1805 continue
1806 if not build.isFinished():
1807 continue
1808 if finished_before is not None:
1809 start, end = build.getTimes()
1810 if end >= finished_before:
1811 continue
1812 if branches:
1813 if build.getSourceStamp().branch not in branches:
1814 continue
1815 got += 1
1816 yield build
1817 if num_builds is not None:
1818 if got >= num_builds:
1819 return
1820
1822 """This function creates a generator which will provide all of this
1823 Builder's status events, starting with the most recent and
1824 progressing backwards in time. """
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835 eventIndex = -1
1836 e = self.getEvent(eventIndex)
1837 for Nb in range(1, self.nextBuildNumber+1):
1838 b = self.getBuild(-Nb)
1839 if not b:
1840
1841
1842
1843 if Nb == 1:
1844 continue
1845 break
1846 if branches and not b.getSourceStamp().branch in branches:
1847 continue
1848 if categories and not b.getBuilder().getCategory() in categories:
1849 continue
1850 steps = b.getSteps()
1851 for Ns in range(1, len(steps)+1):
1852 if steps[-Ns].started:
1853 step_start = steps[-Ns].getTimes()[0]
1854 while e is not None and e.getTimes()[0] > step_start:
1855 yield e
1856 eventIndex -= 1
1857 e = self.getEvent(eventIndex)
1858 yield steps[-Ns]
1859 yield b
1860 while e is not None:
1861 yield e
1862 eventIndex -= 1
1863 e = self.getEvent(eventIndex)
1864
1869
1872
1873
1874
1876 self.slavenames = names
1877
1886
1896
1902
1916
1918 """The Builder has decided to start a build, but the Build object is
1919 not yet ready to report status (it has not finished creating the
1920 Steps). Create a BuildStatus object that it can use."""
1921 number = self.nextBuildNumber
1922 self.nextBuildNumber += 1
1923
1924
1925
1926
1927 s = BuildStatus(self, number)
1928 s.waitUntilFinished().addCallback(self._buildFinished)
1929 return s
1930
1935
1941
1942
1944 """Now the BuildStatus object is ready to go (it knows all of its
1945 Steps, its ETA, etc), so it is safe to notify our watchers."""
1946
1947 assert s.builder is self
1948 assert s.number == self.nextBuildNumber - 1
1949 assert s not in self.currentBuilds
1950 self.currentBuilds.append(s)
1951 self.touchBuildCache(s)
1952
1953
1954
1955
1956 for w in self.watchers:
1957 try:
1958 receiver = w.buildStarted(self.getName(), s)
1959 if receiver:
1960 if type(receiver) == type(()):
1961 s.subscribe(receiver[0], receiver[1])
1962 else:
1963 s.subscribe(receiver)
1964 d = s.waitUntilFinished()
1965 d.addCallback(lambda s: s.unsubscribe(receiver))
1966 except:
1967 log.msg("Exception caught notifying %r of buildStarted event" % w)
1968 log.err()
1969
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
2006 state = self.currentBigState
2007 if state == "offline":
2008 client.currentlyOffline()
2009 elif state == "idle":
2010 client.currentlyIdle()
2011 elif state == "building":
2012 client.currentlyBuilding()
2013 else:
2014 log.msg("Hey, self.currentBigState is weird:", state)
2015
2016
2017
2018
2020
2021 first = self.events[0].number
2022 if first + len(self.events)-1 != self.events[-1].number:
2023 log.msg(self,
2024 "lost an event somewhere: [0] is %d, [%d] is %d" % \
2025 (self.events[0].number,
2026 len(self.events) - 1,
2027 self.events[-1].number))
2028 for e in self.events:
2029 log.msg("e[%d]: " % e.number, e)
2030 return None
2031 offset = num - first
2032 log.msg(self, "offset", offset)
2033 try:
2034 return self.events[offset]
2035 except IndexError:
2036 return None
2037
2038
2040 if hasattr(self, "allEvents"):
2041
2042
2043
2044
2045 return
2046 self.allEvents = self.loadFile("events", [])
2047 if self.allEvents:
2048 self.nextEventNumber = self.allEvents[-1].number + 1
2049 else:
2050 self.nextEventNumber = 0
2052 self.saveFile("events", self.allEvents)
2053
2054
2055
2065
2067 implements(interfaces.ISlaveStatus)
2068
2069 admin = None
2070 host = None
2071 access_uri = None
2072 version = None
2073 connected = False
2074 graceful_shutdown = False
2075
2077 self.name = name
2078 self._lastMessageReceived = 0
2079 self.runningBuilds = []
2080 self.graceful_callbacks = []
2081
2095 return self._lastMessageReceived
2097 return self.runningBuilds
2098
2110 self._lastMessageReceived = when
2111
2113 self.runningBuilds.append(build)
2116
2121 """Set the graceful shutdown flag, and notify all the watchers"""
2122 self.graceful_shutdown = graceful
2123 for cb in self.graceful_callbacks:
2124 reactor.callLater(0, cb, graceful)
2126 """Add watcher to the list of watchers to be notified when the
2127 graceful shutdown flag is changed."""
2128 if not watcher in self.graceful_callbacks:
2129 self.graceful_callbacks.append(watcher)
2131 """Remove watcher from the list of watchers to be notified when the
2132 graceful shutdown flag is changed."""
2133 if watcher in self.graceful_callbacks:
2134 self.graceful_callbacks.remove(watcher)
2135
2137 """
2138 I represent the status of the buildmaster.
2139 """
2140 implements(interfaces.IStatus)
2141
2142 - def __init__(self, botmaster, basedir):
2143 """
2144 @type botmaster: L{buildbot.master.BotMaster}
2145 @param botmaster: the Status object uses C{.botmaster} to get at
2146 both the L{buildbot.master.BuildMaster} (for
2147 various buildbot-wide parameters) and the
2148 actual Builders (to get at their L{BuilderStatus}
2149 objects). It is not allowed to change or influence
2150 anything through this reference.
2151 @type basedir: string
2152 @param basedir: this provides a base directory in which saved status
2153 information (changes.pck, saved Build status
2154 pickles) can be stored
2155 """
2156 self.botmaster = botmaster
2157 self.basedir = basedir
2158 self.watchers = []
2159 self.activeBuildSets = []
2160 assert os.path.isdir(basedir)
2161
2162 self.logCompressionLimit = 4*1024
2163 self.logCompressionMethod = "bz2"
2164
2165 self.logMaxSize = None
2166 self.logMaxTailSize = None
2167
2168
2169
2170
2177
2179 prefix = self.getBuildbotURL()
2180 if not prefix:
2181 return None
2182 if interfaces.IStatus.providedBy(thing):
2183 return prefix
2184 if interfaces.ISchedulerStatus.providedBy(thing):
2185 pass
2186 if interfaces.IBuilderStatus.providedBy(thing):
2187 builder = thing
2188 return prefix + "builders/%s" % (
2189 urllib.quote(builder.getName(), safe=''),
2190 )
2191 if interfaces.IBuildStatus.providedBy(thing):
2192 build = thing
2193 builder = build.getBuilder()
2194 return prefix + "builders/%s/builds/%d" % (
2195 urllib.quote(builder.getName(), safe=''),
2196 build.getNumber())
2197 if interfaces.IBuildStepStatus.providedBy(thing):
2198 step = thing
2199 build = step.getBuild()
2200 builder = build.getBuilder()
2201 return prefix + "builders/%s/builds/%d/steps/%s" % (
2202 urllib.quote(builder.getName(), safe=''),
2203 build.getNumber(),
2204 urllib.quote(step.getName(), safe=''))
2205
2206
2207
2208
2209
2210 if interfaces.IStatusEvent.providedBy(thing):
2211 from buildbot.changes import changes
2212
2213 if isinstance(thing, changes.Change):
2214 change = thing
2215 return "%schanges/%d" % (prefix, change.number)
2216
2217 if interfaces.IStatusLog.providedBy(thing):
2218 log = thing
2219 step = log.getStep()
2220 build = step.getBuild()
2221 builder = build.getBuilder()
2222
2223 logs = step.getLogs()
2224 for i in range(len(logs)):
2225 if log is logs[i]:
2226 lognum = i
2227 break
2228 else:
2229 return None
2230 return prefix + "builders/%s/builds/%d/steps/%s/logs/%s" % (
2231 urllib.quote(builder.getName(), safe=''),
2232 build.getNumber(),
2233 urllib.quote(step.getName(), safe=''),
2234 urllib.quote(log.getName()))
2235
2238
2241
2244
2246 if categories == None:
2247 return self.botmaster.builderNames[:]
2248
2249 l = []
2250
2251 for name in self.botmaster.builderNames:
2252 builder = self.botmaster.builders[name]
2253 if builder.builder_status.category in categories:
2254 l.append(name)
2255 return l
2256
2258 """
2259 @rtype: L{BuilderStatus}
2260 """
2261 return self.botmaster.builders[name].builder_status
2262
2264 return self.botmaster.slaves.keys()
2265
2268
2270 return self.activeBuildSets[:]
2271
2272 - def generateFinishedBuilds(self, builders=[], branches=[],
2273 num_builds=None, finished_before=None,
2274 max_search=200):
2275
2276 def want_builder(bn):
2277 if builders:
2278 return bn in builders
2279 return True
2280 builder_names = [bn
2281 for bn in self.getBuilderNames()
2282 if want_builder(bn)]
2283
2284
2285
2286
2287 sources = []
2288 for bn in builder_names:
2289 b = self.getBuilder(bn)
2290 g = b.generateFinishedBuilds(branches,
2291 finished_before=finished_before,
2292 max_search=max_search)
2293 sources.append(g)
2294
2295
2296 next_build = [None] * len(sources)
2297
2298 def refill():
2299 for i,g in enumerate(sources):
2300 if next_build[i]:
2301
2302 continue
2303 if not g:
2304
2305 continue
2306 try:
2307 next_build[i] = g.next()
2308 except StopIteration:
2309 next_build[i] = None
2310 sources[i] = None
2311
2312 got = 0
2313 while True:
2314 refill()
2315
2316 candidates = [(i, b, b.getTimes()[1])
2317 for i,b in enumerate(next_build)
2318 if b is not None]
2319 candidates.sort(lambda x,y: cmp(x[2], y[2]))
2320 if not candidates:
2321 return
2322
2323
2324 i, build, finshed_time = candidates[-1]
2325 next_build[i] = None
2326 got += 1
2327 yield build
2328 if num_builds is not None:
2329 if got >= num_builds:
2330 return
2331
2338
2339
2340
2341
2346
2348 """
2349 @rtype: L{BuilderStatus}
2350 """
2351 filename = os.path.join(self.basedir, basedir, "builder")
2352 log.msg("trying to load status pickle from %s" % filename)
2353 builder_status = None
2354 try:
2355 builder_status = load(open(filename, "rb"))
2356 styles.doUpgrade()
2357 except IOError:
2358 log.msg("no saved status pickle, creating a new one")
2359 except:
2360 log.msg("error while loading status pickle, creating a new one")
2361 log.msg("error follows:")
2362 log.err()
2363 if not builder_status:
2364 builder_status = BuilderStatus(name, category)
2365 builder_status.addPointEvent(["builder", "created"])
2366 log.msg("added builder %s in category %s" % (name, category))
2367
2368
2369 builder_status.category = category
2370 builder_status.basedir = os.path.join(self.basedir, basedir)
2371 builder_status.name = name
2372 builder_status.status = self
2373
2374 if not os.path.isdir(builder_status.basedir):
2375 os.makedirs(builder_status.basedir)
2376 builder_status.determineNextBuildNumber()
2377
2378 builder_status.setBigState("offline")
2379 builder_status.setLogCompressionLimit(self.logCompressionLimit)
2380 builder_status.setLogCompressionMethod(self.logCompressionMethod)
2381 builder_status.setLogMaxSize(self.logMaxSize)
2382 builder_status.setLogMaxTailSize(self.logMaxTailSize)
2383
2384 for t in self.watchers:
2385 self.announceNewBuilder(t, name, builder_status)
2386
2387 return builder_status
2388
2392
2398