1
2
3 from zope.interface import implements
4 from twisted.python import log, runtime
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 from buildbot.util import collections
10 from buildbot.util.eventual import eventually
11
12 import weakref
13 import os, shutil, re, urllib, itertools
14 import gc
15 import time
16 from cPickle import load, dump
17 from cStringIO import StringIO
18 from bz2 import BZ2File
19 from gzip import GzipFile
20
21
22 from buildbot import interfaces, util, sourcestamp
23
24 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6)
25 Results = ["success", "warnings", "failure", "skipped", "exception", "retry"]
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 STDOUT = interfaces.LOG_CHANNEL_STDOUT
54 STDERR = interfaces.LOG_CHANNEL_STDERR
55 HEADER = interfaces.LOG_CHANNEL_HEADER
56 ChunkTypes = ["stdout", "stderr", "header"]
59 - def __init__(self, chunk_cb, channels=[]):
60 self.chunk_cb = chunk_cb
61 self.channels = channels
62
64 channel = int(line[0])
65 if not self.channels or (channel in self.channels):
66 self.chunk_cb((channel, line[1:]))
67
69 """What's the plan?
70
71 the LogFile has just one FD, used for both reading and writing.
72 Each time you add an entry, fd.seek to the end and then write.
73
74 Each reader (i.e. Producer) keeps track of their own offset. The reader
75 starts by seeking to the start of the logfile, and reading forwards.
76 Between each hunk of file they yield chunks, so they must remember their
77 offset before yielding and re-seek back to that offset before reading
78 more data. When their read() returns EOF, they're finished with the first
79 phase of the reading (everything that's already been written to disk).
80
81 After EOF, the remaining data is entirely in the current entries list.
82 These entries are all of the same channel, so we can do one "".join and
83 obtain a single chunk to be sent to the listener. But since that involves
84 a yield, and more data might arrive after we give up control, we have to
85 subscribe them before yielding. We can't subscribe them any earlier,
86 otherwise they'd get data out of order.
87
88 We're using a generator in the first place so that the listener can
89 throttle us, which means they're pulling. But the subscription means
90 we're pushing. Really we're a Producer. In the first phase we can be
91 either a PullProducer or a PushProducer. In the second phase we're only a
92 PushProducer.
93
94 So the client gives a LogFileConsumer to File.subscribeConsumer . This
95 Consumer must have registerProducer(), unregisterProducer(), and
96 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
97 except that writeChunk() takes chunks (tuples of (channel,text)) instead
98 of the normal write() which takes just text. The LogFileConsumer is
99 allowed to call stopProducing, pauseProducing, and resumeProducing on the
100 producer instance it is given. """
101
102 paused = False
103 subscribed = False
104 BUFFERSIZE = 2048
105
107 self.logfile = logfile
108 self.consumer = consumer
109 self.chunkGenerator = self.getChunks()
110 consumer.registerProducer(self, True)
111
113 f = self.logfile.getFile()
114 offset = 0
115 chunks = []
116 p = LogFileScanner(chunks.append)
117 f.seek(offset)
118 data = f.read(self.BUFFERSIZE)
119 offset = f.tell()
120 while data:
121 p.dataReceived(data)
122 while chunks:
123 c = chunks.pop(0)
124 yield c
125 f.seek(offset)
126 data = f.read(self.BUFFERSIZE)
127 offset = f.tell()
128 del f
129
130
131 self.subscribed = True
132 self.logfile.watchers.append(self)
133 d = self.logfile.waitUntilFinished()
134
135
136 if self.logfile.runEntries:
137 channel = self.logfile.runEntries[0][0]
138 text = "".join([c[1] for c in self.logfile.runEntries])
139 yield (channel, text)
140
141
142
143
144
145 d.addCallback(self.logfileFinished)
146
148
149 self.paused = True
150 self.consumer = None
151 self.done()
152
159
162
164
165
166
167
168
169
170
171
172 eventually(self._resumeProducing)
173
175 self.paused = False
176 if not self.chunkGenerator:
177 return
178 try:
179 while not self.paused:
180 chunk = self.chunkGenerator.next()
181 self.consumer.writeChunk(chunk)
182
183
184 except StopIteration:
185
186 self.chunkGenerator = None
187
188
189
190 - def logChunk(self, build, step, logfile, channel, chunk):
191 if self.consumer:
192 self.consumer.writeChunk((channel, chunk))
193
200
202 """Try to remove a file, and if failed, try again in timeout.
203 Increases the timeout by a factor of 4, and only keeps trying for
204 another retries-amount of times.
205
206 """
207 try:
208 os.unlink(filename)
209 except OSError:
210 if retries > 0:
211 reactor.callLater(timeout, _tryremove, filename, timeout * 4,
212 retries - 1)
213 else:
214 log.msg("giving up on removing %s after over %d seconds" %
215 (filename, timeout))
216
218 """A LogFile keeps all of its contents on disk, in a non-pickle format to
219 which new entries can easily be appended. The file on disk has a name
220 like 12-log-compile-output, under the Builder's directory. The actual
221 filename is generated (before the LogFile is created) by
222 L{BuildStatus.generateLogfileName}.
223
224 Old LogFile pickles (which kept their contents in .entries) must be
225 upgraded. The L{BuilderStatus} is responsible for doing this, when it
226 loads the L{BuildStatus} into memory. The Build pickle is not modified,
227 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
228 logs."""
229
230 implements(interfaces.IStatusLog, interfaces.ILogFile)
231
232 finished = False
233 length = 0
234 nonHeaderLength = 0
235 tailLength = 0
236 chunkSize = 10*1000
237 runLength = 0
238
239 logMaxSize = None
240
241 logMaxTailSize = None
242 maxLengthExceeded = False
243 runEntries = []
244 entries = None
245 BUFFERSIZE = 2048
246 filename = None
247 openfile = None
248 compressMethod = "bz2"
249
250 - def __init__(self, parent, name, logfilename):
251 """
252 @type parent: L{BuildStepStatus}
253 @param parent: the Step that this log is a part of
254 @type name: string
255 @param name: the name of this log, typically 'output'
256 @type logfilename: string
257 @param logfilename: the Builder-relative pathname for the saved entries
258 """
259 self.step = parent
260 self.name = name
261 self.filename = logfilename
262 fn = self.getFilename()
263 if os.path.exists(fn):
264
265
266
267
268 log.msg("Warning: Overwriting old serialized Build at %s" % fn)
269 dirname = os.path.dirname(fn)
270 if not os.path.exists(dirname):
271 os.makedirs(dirname)
272 self.openfile = open(fn, "w+")
273 self.runEntries = []
274 self.watchers = []
275 self.finishedWatchers = []
276 self.tailBuffer = []
277
280
281 - def hasContents(self):
282 return os.path.exists(self.getFilename() + '.bz2') or \
283 os.path.exists(self.getFilename() + '.gz') or \
284 os.path.exists(self.getFilename())
285
288
291
295 if self.finished:
296 d = defer.succeed(self)
297 else:
298 d = defer.Deferred()
299 self.finishedWatchers.append(d)
300 return d
301
303 if self.openfile:
304
305
306 return self.openfile
307
308
309 try:
310 return BZ2File(self.getFilename() + ".bz2", "r")
311 except IOError:
312 pass
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
425
426
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
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 compressed = self.getFilename() + ".bz2.tmp"
525 elif self.compressMethod == "gz":
526 compressed = self.getFilename() + ".gz.tmp"
527 d = threads.deferToThread(self._compressLog, compressed)
528 d.addCallback(self._renameCompressedLog, compressed)
529 d.addErrback(self._cleanupFailedCompress, compressed)
530 return d
531
533 infile = self.getFile()
534 if self.compressMethod == "bz2":
535 cf = BZ2File(compressed, 'w')
536 elif self.compressMethod == "gz":
537 cf = GzipFile(compressed, 'w')
538 bufsize = 1024*1024
539 while True:
540 buf = infile.read(bufsize)
541 cf.write(buf)
542 if len(buf) < bufsize:
543 break
544 cf.close()
560 log.msg("failed to compress %s" % self.getFilename())
561 if os.path.exists(compressed):
562 _tryremove(compressed, 1, 5)
563 failure.trap()
564
565
567 d = self.__dict__.copy()
568 del d['step']
569 del d['watchers']
570 del d['finishedWatchers']
571 d['entries'] = []
572 if d.has_key('finished'):
573 del d['finished']
574 if d.has_key('openfile'):
575 del d['openfile']
576 return d
577
584
586 """Save our .entries to a new-style offline log file (if necessary),
587 and modify our in-memory representation to use it. The original
588 pickled LogFile (inside the pickled Build) won't be modified."""
589 self.filename = logfilename
590 if not os.path.exists(self.getFilename()):
591 self.openfile = open(self.getFilename(), "w")
592 self.finished = False
593 for channel,text in self.entries:
594 self.addEntry(channel, text)
595 self.finish()
596 del self.entries
597
599 implements(interfaces.IStatusLog)
600
601 filename = None
602
603 - def __init__(self, parent, name, logfilename, html):
608
613
617 return defer.succeed(self)
618
619 - def hasContents(self):
627
632
635
637 d = self.__dict__.copy()
638 del d['step']
639 return d
640
643
662
684
687 implements(interfaces.IBuildSetStatus)
688
693
696
697
698
702
710 (external_idstring, reason, ssid, complete, results) = self._get_info()
711 return external_idstring
712
719
723
728
730 (external_idstring, reason, ssid, complete, results) = self._get_info()
731 return complete
732
734 return self.status._buildset_waitUntilSuccess(self.id)
736 return self.status._buildset_waitUntilFinished(self.id)
737
789
792 """
793 I represent a collection of output status for a
794 L{buildbot.process.step.BuildStep}.
795
796 Statistics contain any information gleaned from a step that is
797 not in the form of a logfile. As an example, steps that run
798 tests might gather statistics about the number of passed, failed,
799 or skipped tests.
800
801 @type progress: L{buildbot.status.progress.StepProgress}
802 @cvar progress: tracks ETA for the step
803 @type text: list of strings
804 @cvar text: list of short texts that describe the command and its status
805 @type text2: list of strings
806 @cvar text2: list of short texts added to the overall build description
807 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
808 @ivar logs: logs of steps
809 @type statistics: dict
810 @ivar statistics: results from running this step
811 """
812
813
814 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
815 persistenceVersion = 3
816
817 started = None
818 finished = None
819 progress = None
820 text = []
821 results = (None, [])
822 text2 = []
823 watchers = []
824 updates = {}
825 finishedWatchers = []
826 statistics = {}
827 step_number = None
828
829 - def __init__(self, parent, step_number):
842
844 """Returns a short string with the name of this step. This string
845 may have spaces in it."""
846 return self.name
847
850
853
855 """Returns a list of tuples (name, current, target)."""
856 if not self.progress:
857 return []
858 ret = []
859 metrics = self.progress.progress.keys()
860 metrics.sort()
861 for m in metrics:
862 t = (m, self.progress.progress[m], self.progress.expectations[m])
863 ret.append(t)
864 return ret
865
868
870 return self.urls.copy()
871
873 return (self.started is not None)
874
877
880
882 if self.finished:
883 d = defer.succeed(self)
884 else:
885 d = defer.Deferred()
886 self.finishedWatchers.append(d)
887 return d
888
889
890
891
900
901
902
903
905 """Returns a list of strings which describe the step. These are
906 intended to be displayed in a narrow column. If more space is
907 available, the caller should join them together with spaces before
908 presenting them to the user."""
909 return self.text
910
912 """Return a tuple describing the results of the step.
913 'result' is one of the constants in L{buildbot.status.builder}:
914 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
915 'strings' is an optional list of strings that the step wants to
916 append to the overall build's results. These strings are usually
917 more terse than the ones returned by getText(): in particular,
918 successful Steps do not usually contribute any text to the
919 overall build.
920
921 @rtype: tuple of int, list of strings
922 @returns: (result, strings)
923 """
924 return (self.results, self.text2)
925
927 """Return true if this step has a value for the given statistic.
928 """
929 return self.statistics.has_key(name)
930
932 """Return the given statistic, if present
933 """
934 return self.statistics.get(name, default)
935
936
937
938 - def subscribe(self, receiver, updateInterval=10):
943
954
962
963
964
965
968
970 log.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color,))
971
974
979
997
1010
1014
1015 - def addURL(self, name, url):
1016 self.urls[name] = url
1017
1018 - def setText(self, text):
1019 self.text = text
1020 for w in self.watchers:
1021 w.stepTextChanged(self.build, self, text)
1022 - def setText2(self, text):
1023 self.text2 = text
1024 for w in self.watchers:
1025 w.stepText2Changed(self.build, self, text)
1026
1028 """Set the given statistic. Usually called by subclasses.
1029 """
1030 self.statistics[name] = value
1031
1033 self.skipped = skipped
1034
1063
1067
1069 return self.waitingForLocks
1070
1072 self.waitingForLocks = waiting
1073
1074
1075
1077 d = styles.Versioned.__getstate__(self)
1078 del d['build']
1079 if d.has_key('progress'):
1080 del d['progress']
1081 del d['watchers']
1082 del d['finishedWatchers']
1083 del d['updates']
1084 return d
1085
1087 styles.Versioned.__setstate__(self, d)
1088
1089
1090
1091 for loog in self.logs:
1092 loog.step = self
1093
1095 if not hasattr(self, "urls"):
1096 self.urls = {}
1097
1099 if not hasattr(self, "statistics"):
1100 self.statistics = {}
1101
1103 if not hasattr(self, "step_number"):
1104 self.step_number = 0
1105
1107 result = {}
1108
1109 result['name'] = self.getName()
1110
1111
1112 result['text'] = self.getText()
1113 result['results'] = self.getResults()
1114 result['isStarted'] = self.isStarted()
1115 result['isFinished'] = self.isFinished()
1116 result['statistics'] = self.statistics
1117 result['times'] = self.getTimes()
1118 result['expectations'] = self.getExpectations()
1119 result['eta'] = self.getETA()
1120 result['urls'] = self.getURLs()
1121 result['step_number'] = self.step_number
1122
1123
1124
1125 return result
1126
1129 implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
1130 persistenceVersion = 3
1131
1132 source = None
1133 reason = None
1134 changes = []
1135 blamelist = []
1136 progress = None
1137 started = None
1138 finished = None
1139 currentStep = None
1140 text = []
1141 results = None
1142 slavename = "???"
1143
1144
1145
1146
1147 watchers = []
1148 updates = {}
1149 finishedWatchers = []
1150 testResults = {}
1151
1166
1169
1170
1171
1173 """
1174 @rtype: L{BuilderStatus}
1175 """
1176 return self.builder
1177
1180
1183
1186
1191
1196
1199
1202
1205
1209
1211 """Return a list of IBuildStepStatus objects. For invariant builds
1212 (those which always use the same set of Steps), this should be the
1213 complete list, however some of the steps may not have started yet
1214 (step.getTimes()[0] will be None). For variant builds, this may not
1215 be complete (asking again later may give you more of them)."""
1216 return self.steps
1217
1220
1221 _sentinel = []
1223 """Summarize the named statistic over all steps in which it
1224 exists, using combination_fn and initial_value to combine multiple
1225 results into a single result. This translates to a call to Python's
1226 X{reduce}::
1227 return reduce(summary_fn, step_stats_list, initial_value)
1228 """
1229 step_stats_list = [
1230 st.getStatistic(name)
1231 for st in self.steps
1232 if st.hasStatistic(name) ]
1233 if initial_value is self._sentinel:
1234 return reduce(summary_fn, step_stats_list)
1235 else:
1236 return reduce(summary_fn, step_stats_list, initial_value)
1237
1240
1242 if self.finished:
1243 d = defer.succeed(self)
1244 else:
1245 d = defer.Deferred()
1246 self.finishedWatchers.append(d)
1247 return d
1248
1249
1250
1251
1261
1264
1265
1266
1267
1268 - def getText(self):
1269 text = []
1270 text.extend(self.text)
1271 for s in self.steps:
1272 text.extend(s.text2)
1273 return text
1274
1277
1280
1283
1285 trs = self.testResults.keys()
1286 trs.sort()
1287 ret = [ self.testResults[t] for t in trs]
1288 return ret
1289
1299
1300
1301
1302 - def subscribe(self, receiver, updateInterval=None):
1303
1304
1305 self.watchers.append(receiver)
1306 if updateInterval is not None:
1307 self.sendETAUpdate(receiver, updateInterval)
1308
1320
1328
1329
1330
1332 """The Build is setting up, and has added a new BuildStep to its
1333 list. Create a BuildStepStatus object to which it can send status
1334 updates."""
1335
1336 s = BuildStepStatus(self, len(self.steps))
1337 s.setName(name)
1338 self.steps.append(s)
1339 return s
1340
1341 - def setProperty(self, propname, value, source, runtime=True):
1343
1346
1350
1357
1359 """The Build has been set up and is about to be started. It can now
1360 be safely queried, so it is time to announce the new build."""
1361
1362 self.started = util.now()
1363
1364
1365 self.builder.buildStarted(self)
1366
1369
1370 - def setText(self, text):
1371 assert isinstance(text, (list, tuple))
1372 self.text = text
1375
1389
1390
1391
1405
1410
1411
1412
1414
1415 self.steps = []
1416
1417
1418
1420 """Return a filename (relative to the Builder's base directory) where
1421 the logfile's contents can be stored uniquely.
1422
1423 The base filename is made by combining our build number, the Step's
1424 name, and the log's name, then removing unsuitable characters. The
1425 filename is then made unique by appending _0, _1, etc, until it does
1426 not collide with any other logfile.
1427
1428 These files are kept in the Builder's basedir (rather than a
1429 per-Build subdirectory) because that makes cleanup easier: cron and
1430 find will help get rid of the old logs, but the empty directories are
1431 more of a hassle to remove."""
1432
1433 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
1434 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
1435
1436 unique_counter = 0
1437 filename = starting_filename
1438 while filename in [l.filename
1439 for step in self.steps
1440 for l in step.getLogs()
1441 if l.filename]:
1442 filename = "%s_%d" % (starting_filename, unique_counter)
1443 unique_counter += 1
1444 return filename
1445
1447 d = styles.Versioned.__getstate__(self)
1448
1449
1450 if not self.finished:
1451 d['finished'] = True
1452
1453
1454
1455
1456 for k in 'builder', 'watchers', 'updates', 'finishedWatchers':
1457 if k in d: del d[k]
1458 return d
1459
1468
1481
1484
1490
1505
1511
1532
1559
1563 """I handle status information for a single process.base.Builder object.
1564 That object sends status changes to me (frequently as Events), and I
1565 provide them on demand to the various status recipients, like the HTML
1566 waterfall display and the live status clients. It also sends build
1567 summaries to me, which I log and provide to status clients who aren't
1568 interested in seeing details of the individual build steps.
1569
1570 I am responsible for maintaining the list of historic Events and Builds,
1571 pruning old ones, and loading them from / saving them to disk.
1572
1573 I live in the buildbot.process.base.Builder object, in the
1574 .builder_status attribute.
1575
1576 @type category: string
1577 @ivar category: user-defined category this builder belongs to; can be
1578 used to filter on in status clients
1579 """
1580
1581 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
1582 persistenceVersion = 1
1583
1584
1585
1586
1587 buildCacheSize = 15
1588 eventHorizon = 50
1589
1590
1591 logHorizon = 40
1592 buildHorizon = 100
1593
1594 category = None
1595 currentBigState = "offline"
1596 basedir = None
1597
1598 - def __init__(self, buildername, category=None):
1599 self.name = buildername
1600 self.category = category
1601
1602 self.slavenames = []
1603 self.events = []
1604
1605
1606 self.lastBuildStatus = None
1607
1608
1609 self.currentBuilds = []
1610 self.nextBuild = None
1611 self.watchers = []
1612 self.buildCache = weakref.WeakValueDictionary()
1613 self.buildCache_LRU = []
1614 self.logCompressionLimit = False
1615 self.logCompressionMethod = "bz2"
1616 self.logMaxSize = None
1617 self.logMaxTailSize = None
1618
1619
1620
1622
1623
1624
1625
1626 d = styles.Versioned.__getstate__(self)
1627 d['watchers'] = []
1628 del d['buildCache']
1629 del d['buildCache_LRU']
1630 for b in self.currentBuilds:
1631 b.saveYourself()
1632
1633 del d['currentBuilds']
1634 d.pop('pendingBuilds', None)
1635 del d['currentBigState']
1636 del d['basedir']
1637 del d['status']
1638 del d['nextBuildNumber']
1639 return d
1640
1642
1643
1644 styles.Versioned.__setstate__(self, d)
1645 self.buildCache = weakref.WeakValueDictionary()
1646 self.buildCache_LRU = []
1647 self.currentBuilds = []
1648 self.watchers = []
1649 self.slavenames = []
1650
1651
1652
1658
1660 if hasattr(self, 'slavename'):
1661 self.slavenames = [self.slavename]
1662 del self.slavename
1663 if hasattr(self, 'nextBuildNumber'):
1664 del self.nextBuildNumber
1665
1667 """Scan our directory of saved BuildStatus instances to determine
1668 what our self.nextBuildNumber should be. Set it one larger than the
1669 highest-numbered build we discover. This is called by the top-level
1670 Status object shortly after we are created or loaded from disk.
1671 """
1672 existing_builds = [int(f)
1673 for f in os.listdir(self.basedir)
1674 if re.match("^\d+$", f)]
1675 if existing_builds:
1676 self.nextBuildNumber = max(existing_builds) + 1
1677 else:
1678 self.nextBuildNumber = 0
1679
1681 self.logCompressionLimit = lowerLimit
1682
1684 assert method in ("bz2", "gz")
1685 self.logCompressionMethod = method
1686
1689
1692
1694 for b in self.currentBuilds:
1695 if not b.isFinished:
1696
1697
1698 b.saveYourself()
1699 filename = os.path.join(self.basedir, "builder")
1700 tmpfilename = filename + ".tmp"
1701 try:
1702 dump(self, open(tmpfilename, "wb"), -1)
1703 if runtime.platformType == 'win32':
1704
1705 if os.path.exists(filename):
1706 os.unlink(filename)
1707 os.rename(tmpfilename, filename)
1708 except:
1709 log.msg("unable to save builder %s" % self.name)
1710 log.err()
1711
1712
1713
1714
1717
1724
1752
1753 - def prune(self, events_only=False):
1754
1755 self.events = self.events[-self.eventHorizon:]
1756
1757 if events_only:
1758 return
1759
1760 gc.collect()
1761
1762
1763 if self.buildHorizon:
1764 earliest_build = self.nextBuildNumber - self.buildHorizon
1765 else:
1766 earliest_build = 0
1767
1768 if self.logHorizon:
1769 earliest_log = self.nextBuildNumber - self.logHorizon
1770 else:
1771 earliest_log = 0
1772
1773 if earliest_log < earliest_build:
1774 earliest_log = earliest_build
1775
1776 if earliest_build == 0:
1777 return
1778
1779
1780 build_re = re.compile(r"^([0-9]+)$")
1781 build_log_re = re.compile(r"^([0-9]+)-.*$")
1782
1783 if not os.path.exists(self.basedir):
1784 return
1785
1786 for filename in os.listdir(self.basedir):
1787 num = None
1788 mo = build_re.match(filename)
1789 is_logfile = False
1790 if mo:
1791 num = int(mo.group(1))
1792 else:
1793 mo = build_log_re.match(filename)
1794 if mo:
1795 num = int(mo.group(1))
1796 is_logfile = True
1797
1798 if num is None: continue
1799 if num in self.buildCache: continue
1800
1801 if (is_logfile and num < earliest_log) or num < earliest_build:
1802 pathname = os.path.join(self.basedir, filename)
1803 log.msg("pruning '%s'" % pathname)
1804 try: os.unlink(pathname)
1805 except OSError: pass
1806
1807
1810
1813
1816
1821
1823 return self.currentBuilds
1824
1830
1833
1844
1846 try:
1847 return self.events[number]
1848 except IndexError:
1849 return None
1850
1851 - def generateFinishedBuilds(self, branches=[],
1852 num_builds=None,
1853 max_buildnum=None,
1854 finished_before=None,
1855 max_search=200):
1856 got = 0
1857 for Nb in itertools.count(1):
1858 if Nb > self.nextBuildNumber:
1859 break
1860 if Nb > max_search:
1861 break
1862 build = self.getBuild(-Nb)
1863 if build is None:
1864 continue
1865 if max_buildnum is not None:
1866 if build.getNumber() > max_buildnum:
1867 continue
1868 if not build.isFinished():
1869 continue
1870 if finished_before is not None:
1871 start, end = build.getTimes()
1872 if end >= finished_before:
1873 continue
1874 if branches:
1875 if build.getSourceStamp().branch not in branches:
1876 continue
1877 got += 1
1878 yield build
1879 if num_builds is not None:
1880 if got >= num_builds:
1881 return
1882
1883 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
1884 """This function creates a generator which will provide all of this
1885 Builder's status events, starting with the most recent and
1886 progressing backwards in time. """
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897 eventIndex = -1
1898 e = self.getEvent(eventIndex)
1899 for Nb in range(1, self.nextBuildNumber+1):
1900 b = self.getBuild(-Nb)
1901 if not b:
1902
1903
1904
1905 if Nb == 1:
1906 continue
1907 break
1908 if b.getTimes()[0] < minTime:
1909 break
1910 if branches and not b.getSourceStamp().branch in branches:
1911 continue
1912 if categories and not b.getBuilder().getCategory() in categories:
1913 continue
1914 if committers and not [True for c in b.getChanges() if c.who in committers]:
1915 continue
1916 steps = b.getSteps()
1917 for Ns in range(1, len(steps)+1):
1918 if steps[-Ns].started:
1919 step_start = steps[-Ns].getTimes()[0]
1920 while e is not None and e.getTimes()[0] > step_start:
1921 yield e
1922 eventIndex -= 1
1923 e = self.getEvent(eventIndex)
1924 yield steps[-Ns]
1925 yield b
1926 while e is not None:
1927 yield e
1928 eventIndex -= 1
1929 e = self.getEvent(eventIndex)
1930 if e and e.getTimes()[0] < minTime:
1931 break
1932
1934
1935
1936
1937
1938 self.watchers.append(receiver)
1939 self.publishState(receiver)
1940
1941 self.status._builder_subscribe(self.name, receiver)
1942
1946
1947
1948
1950 self.slavenames = names
1951
1961
1972
1978
1992
1994 """The Builder has decided to start a build, but the Build object is
1995 not yet ready to report status (it has not finished creating the
1996 Steps). Create a BuildStatus object that it can use."""
1997 number = self.nextBuildNumber
1998 self.nextBuildNumber += 1
1999
2000
2001
2002
2003 s = BuildStatus(self, number)
2004 s.waitUntilFinished().addCallback(self._buildFinished)
2005 return s
2006
2007
2009 """Now the BuildStatus object is ready to go (it knows all of its
2010 Steps, its ETA, etc), so it is safe to notify our watchers."""
2011
2012 assert s.builder is self
2013 assert s.number == self.nextBuildNumber - 1
2014 assert s not in self.currentBuilds
2015 self.currentBuilds.append(s)
2016 self.touchBuildCache(s)
2017
2018
2019
2020
2021 for w in self.watchers:
2022 try:
2023 receiver = w.buildStarted(self.getName(), s)
2024 if receiver:
2025 if type(receiver) == type(()):
2026 s.subscribe(receiver[0], receiver[1])
2027 else:
2028 s.subscribe(receiver)
2029 d = s.waitUntilFinished()
2030 d.addCallback(lambda s: s.unsubscribe(receiver))
2031 except:
2032 log.msg("Exception caught notifying %r of buildStarted event" % w)
2033 log.err()
2034
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2071 state = self.currentBigState
2072 if state == "offline":
2073 client.currentlyOffline()
2074 elif state == "idle":
2075 client.currentlyIdle()
2076 elif state == "building":
2077 client.currentlyBuilding()
2078 else:
2079 log.msg("Hey, self.currentBigState is weird:", state)
2080
2081
2082
2083
2085
2086 first = self.events[0].number
2087 if first + len(self.events)-1 != self.events[-1].number:
2088 log.msg(self,
2089 "lost an event somewhere: [0] is %d, [%d] is %d" % \
2090 (self.events[0].number,
2091 len(self.events) - 1,
2092 self.events[-1].number))
2093 for e in self.events:
2094 log.msg("e[%d]: " % e.number, e)
2095 return None
2096 offset = num - first
2097 log.msg(self, "offset", offset)
2098 try:
2099 return self.events[offset]
2100 except IndexError:
2101 return None
2102
2103
2105 if hasattr(self, "allEvents"):
2106
2107
2108
2109
2110 return
2111 self.allEvents = self.loadFile("events", [])
2112 if self.allEvents:
2113 self.nextEventNumber = self.allEvents[-1].number + 1
2114 else:
2115 self.nextEventNumber = 0
2117 self.saveFile("events", self.allEvents)
2118
2119
2120
2130
2132 result = {}
2133
2134
2135 result['basedir'] = os.path.basename(self.basedir)
2136 result['category'] = self.category
2137 result['slaves'] = self.slavenames
2138
2139
2140
2141
2142
2143
2144 current_builds = [b.getNumber() for b in self.currentBuilds]
2145 cached_builds = list(set(self.buildCache.keys() + current_builds))
2146 cached_builds.sort()
2147 result['cachedBuilds'] = cached_builds
2148 result['currentBuilds'] = current_builds
2149 result['state'] = self.getState()[0]
2150
2151 result['pendingBuilds'] = [
2152 b.getSourceStamp().asDict() for b in self.getPendingBuilds()
2153 ]
2154 return result
2155
2158 implements(interfaces.ISlaveStatus)
2159
2160 admin = None
2161 host = None
2162 access_uri = None
2163 version = None
2164 connected = False
2165 graceful_shutdown = False
2166
2168 self.name = name
2169 self._lastMessageReceived = 0
2170 self.runningBuilds = []
2171 self.graceful_callbacks = []
2172 self.connect_times = []
2173
2187 return self._lastMessageReceived
2189 return self.runningBuilds
2191 then = time.time() - 3600
2192 return len([ t for t in self.connect_times if t > then ])
2193
2205 self._lastMessageReceived = when
2206
2208
2209 now = time.time()
2210 self.connect_times = [ t for t in self.connect_times if t > now - 3600 ] + [ now ]
2211
2213 self.runningBuilds.append(build)
2216
2221 """Set the graceful shutdown flag, and notify all the watchers"""
2222 self.graceful_shutdown = graceful
2223 for cb in self.graceful_callbacks:
2224 eventually(cb, graceful)
2226 """Add watcher to the list of watchers to be notified when the
2227 graceful shutdown flag is changed."""
2228 if not watcher in self.graceful_callbacks:
2229 self.graceful_callbacks.append(watcher)
2231 """Remove watcher from the list of watchers to be notified when the
2232 graceful shutdown flag is changed."""
2233 if watcher in self.graceful_callbacks:
2234 self.graceful_callbacks.remove(watcher)
2235
2249
2251 """
2252 I represent the status of the buildmaster.
2253 """
2254 implements(interfaces.IStatus)
2255
2256 - def __init__(self, botmaster, basedir):
2257 """
2258 @type botmaster: L{buildbot.master.BotMaster}
2259 @param botmaster: the Status object uses C{.botmaster} to get at
2260 both the L{buildbot.master.BuildMaster} (for
2261 various buildbot-wide parameters) and the
2262 actual Builders (to get at their L{BuilderStatus}
2263 objects). It is not allowed to change or influence
2264 anything through this reference.
2265 @type basedir: string
2266 @param basedir: this provides a base directory in which saved status
2267 information (changes.pck, saved Build status
2268 pickles) can be stored
2269 """
2270 self.botmaster = botmaster
2271 self.db = None
2272 self.basedir = basedir
2273 self.watchers = []
2274 assert os.path.isdir(basedir)
2275
2276 self.logCompressionLimit = 4*1024
2277 self.logCompressionMethod = "bz2"
2278
2279 self.logMaxSize = None
2280 self.logMaxTailSize = None
2281
2282 self._builder_observers = collections.KeyedSets()
2283 self._buildreq_observers = collections.KeyedSets()
2284 self._buildset_success_waiters = collections.KeyedSets()
2285 self._buildset_finished_waiters = collections.KeyedSets()
2286
2287 @property
2290
2293
2296
2298 self.db = db
2299 self.db.subscribe_to("add-build", self._db_builds_changed)
2300 self.db.subscribe_to("add-buildset", self._db_buildset_added)
2301 self.db.subscribe_to("modify-buildset", self._db_buildsets_changed)
2302 self.db.subscribe_to("add-buildrequest", self._db_buildrequest_added)
2303 self.db.subscribe_to("cancel-buildrequest", self._db_buildrequest_cancelled)
2304
2305
2306
2313
2315 prefix = self.getBuildbotURL()
2316 if not prefix:
2317 return None
2318 if interfaces.IStatus.providedBy(thing):
2319 return prefix
2320 if interfaces.ISchedulerStatus.providedBy(thing):
2321 pass
2322 if interfaces.IBuilderStatus.providedBy(thing):
2323 builder = thing
2324 return prefix + "builders/%s" % (
2325 urllib.quote(builder.getName(), safe=''),
2326 )
2327 if interfaces.IBuildStatus.providedBy(thing):
2328 build = thing
2329 builder = build.getBuilder()
2330 return prefix + "builders/%s/builds/%d" % (
2331 urllib.quote(builder.getName(), safe=''),
2332 build.getNumber())
2333 if interfaces.IBuildStepStatus.providedBy(thing):
2334 step = thing
2335 build = step.getBuild()
2336 builder = build.getBuilder()
2337 return prefix + "builders/%s/builds/%d/steps/%s" % (
2338 urllib.quote(builder.getName(), safe=''),
2339 build.getNumber(),
2340 urllib.quote(step.getName(), safe=''))
2341
2342
2343
2344
2345
2346 if interfaces.IStatusEvent.providedBy(thing):
2347 from buildbot.changes import changes
2348
2349 if isinstance(thing, changes.Change):
2350 change = thing
2351 return "%schanges/%d" % (prefix, change.number)
2352
2353 if interfaces.IStatusLog.providedBy(thing):
2354 log = thing
2355 step = log.getStep()
2356 build = step.getBuild()
2357 builder = build.getBuilder()
2358
2359 logs = step.getLogs()
2360 for i in range(len(logs)):
2361 if log is logs[i]:
2362 break
2363 else:
2364 return None
2365 return prefix + "builders/%s/builds/%d/steps/%s/logs/%s" % (
2366 urllib.quote(builder.getName(), safe=''),
2367 build.getNumber(),
2368 urllib.quote(step.getName(), safe=''),
2369 urllib.quote(log.getName()))
2370
2373
2376
2379
2381 if categories == None:
2382 return self.botmaster.builderNames[:]
2383
2384 l = []
2385
2386 for name in self.botmaster.builderNames:
2387 builder = self.botmaster.builders[name]
2388 if builder.builder_status.category in categories:
2389 l.append(name)
2390 return l
2391
2393 """
2394 @rtype: L{BuilderStatus}
2395 """
2396 return self.botmaster.builders[name].builder_status
2397
2399 return self.botmaster.slaves.keys()
2400
2403
2407
2408 - def generateFinishedBuilds(self, builders=[], branches=[],
2409 num_builds=None, finished_before=None,
2410 max_search=200):
2411
2412 def want_builder(bn):
2413 if builders:
2414 return bn in builders
2415 return True
2416 builder_names = [bn
2417 for bn in self.getBuilderNames()
2418 if want_builder(bn)]
2419
2420
2421
2422
2423 sources = []
2424 for bn in builder_names:
2425 b = self.getBuilder(bn)
2426 g = b.generateFinishedBuilds(branches,
2427 finished_before=finished_before,
2428 max_search=max_search)
2429 sources.append(g)
2430
2431
2432 next_build = [None] * len(sources)
2433
2434 def refill():
2435 for i,g in enumerate(sources):
2436 if next_build[i]:
2437
2438 continue
2439 if not g:
2440
2441 continue
2442 try:
2443 next_build[i] = g.next()
2444 except StopIteration:
2445 next_build[i] = None
2446 sources[i] = None
2447
2448 got = 0
2449 while True:
2450 refill()
2451
2452 candidates = [(i, b, b.getTimes()[1])
2453 for i,b in enumerate(next_build)
2454 if b is not None]
2455 candidates.sort(lambda x,y: cmp(x[2], y[2]))
2456 if not candidates:
2457 return
2458
2459
2460 i, build, finshed_time = candidates[-1]
2461 next_build[i] = None
2462 got += 1
2463 yield build
2464 if num_builds is not None:
2465 if got >= num_builds:
2466 return
2467
2474
2475
2476
2477
2482
2484 """
2485 @rtype: L{BuilderStatus}
2486 """
2487 filename = os.path.join(self.basedir, basedir, "builder")
2488 log.msg("trying to load status pickle from %s" % filename)
2489 builder_status = None
2490 try:
2491 builder_status = load(open(filename, "rb"))
2492 styles.doUpgrade()
2493 except IOError:
2494 log.msg("no saved status pickle, creating a new one")
2495 except:
2496 log.msg("error while loading status pickle, creating a new one")
2497 log.msg("error follows:")
2498 log.err()
2499 if not builder_status:
2500 builder_status = BuilderStatus(name, category)
2501 builder_status.addPointEvent(["builder", "created"])
2502 log.msg("added builder %s in category %s" % (name, category))
2503
2504
2505 builder_status.category = category
2506 builder_status.basedir = os.path.join(self.basedir, basedir)
2507 builder_status.name = name
2508 builder_status.status = self
2509
2510 if not os.path.isdir(builder_status.basedir):
2511 os.makedirs(builder_status.basedir)
2512 builder_status.determineNextBuildNumber()
2513
2514 builder_status.setBigState("offline")
2515 builder_status.setLogCompressionLimit(self.logCompressionLimit)
2516 builder_status.setLogCompressionMethod(self.logCompressionMethod)
2517 builder_status.setLogMaxSize(self.logMaxSize)
2518 builder_status.setLogMaxTailSize(self.logMaxTailSize)
2519
2520 for t in self.watchers:
2521 self.announceNewBuilder(t, name, builder_status)
2522
2523 return builder_status
2524
2529
2534
2539
2544
2554
2556 for r in requests:
2557
2558
2559 pass
2560
2563
2565 brid,buildername,buildnum = self.db.get_build_info(bid)
2566 if brid in self._buildreq_observers:
2567 bs = self.getBuilder(buildername).getBuild(buildnum)
2568 if bs:
2569 for o in self._buildreq_observers[brid]:
2570 eventually(o, bs)
2571
2573 self._buildreq_observers.add(brid, observer)
2574
2576 self._buildreq_observers.discard(brid, observer)
2577
2583
2585 d = defer.Deferred()
2586 self._buildset_success_waiters.add(bsid, d)
2587
2588 self._db_buildsets_changed("modify-buildset", bsid)
2589 return d
2591 d = defer.Deferred()
2592 self._buildset_finished_waiters.add(bsid, d)
2593 self._db_buildsets_changed("modify-buildset", bsid)
2594 return d
2595
2597 for bsid in bsids:
2598 self._db_buildset_changed(bsid)
2599
2601
2602
2603 if (bsid not in self._buildset_success_waiters
2604 and bsid not in self._buildset_finished_waiters):
2605 return
2606 successful,finished = self.db.examine_buildset(bsid)
2607 bss = BuildSetStatus(bsid, self, self.db)
2608 if successful is not None:
2609 for d in self._buildset_success_waiters.pop(bsid):
2610 eventually(d.callback, bss)
2611 if finished:
2612 for d in self._buildset_finished_waiters.pop(bsid):
2613 eventually(d.callback, bss)
2614
2616
2617 self._builder_observers.add(buildername, watcher)
2618
2620 self._builder_observers.discard(buildername, watcher)
2621
2623 self._handle_buildrequest_event("added", brids)
2625 self._handle_buildrequest_event("cancelled", brids)
2639
2640
2641