1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from zope.interface import implements
18 from twisted.python import log, runtime
19 from twisted.persisted import styles
20 from twisted.internet import reactor, defer, threads
21 import twisted.internet.interfaces
22 from twisted.protocols import basic
23 from buildbot.process.properties import Properties
24 from buildbot.util import collections
25 from buildbot.util.eventual import eventually
26
27 import weakref
28 import os, shutil, re, urllib, itertools
29 import gc
30 import time
31 from cPickle import load, dump
32 from cStringIO import StringIO
33 from bz2 import BZ2File
34 from gzip import GzipFile
35
36
37 from buildbot import interfaces, util, sourcestamp
38
39 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6)
40 Results = ["success", "warnings", "failure", "skipped", "exception", "retry"]
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 STDOUT = interfaces.LOG_CHANNEL_STDOUT
69 STDERR = interfaces.LOG_CHANNEL_STDERR
70 HEADER = interfaces.LOG_CHANNEL_HEADER
71 ChunkTypes = ["stdout", "stderr", "header"]
74 "an address for NullTransport"
75 implements(twisted.internet.interfaces.IAddress)
76
78 "a do-nothing transport to make NetstringReceiver happy"
79 implements(twisted.internet.interfaces.ITransport)
80 - def write(self, data): raise NotImplementedError
87
89 - def __init__(self, chunk_cb, channels=[]):
90 self.chunk_cb = chunk_cb
91 self.channels = channels
92 self.makeConnection(NullTransport())
93
95 channel = int(line[0])
96 if not self.channels or (channel in self.channels):
97 self.chunk_cb((channel, line[1:]))
98
100 """What's the plan?
101
102 the LogFile has just one FD, used for both reading and writing.
103 Each time you add an entry, fd.seek to the end and then write.
104
105 Each reader (i.e. Producer) keeps track of their own offset. The reader
106 starts by seeking to the start of the logfile, and reading forwards.
107 Between each hunk of file they yield chunks, so they must remember their
108 offset before yielding and re-seek back to that offset before reading
109 more data. When their read() returns EOF, they're finished with the first
110 phase of the reading (everything that's already been written to disk).
111
112 After EOF, the remaining data is entirely in the current entries list.
113 These entries are all of the same channel, so we can do one "".join and
114 obtain a single chunk to be sent to the listener. But since that involves
115 a yield, and more data might arrive after we give up control, we have to
116 subscribe them before yielding. We can't subscribe them any earlier,
117 otherwise they'd get data out of order.
118
119 We're using a generator in the first place so that the listener can
120 throttle us, which means they're pulling. But the subscription means
121 we're pushing. Really we're a Producer. In the first phase we can be
122 either a PullProducer or a PushProducer. In the second phase we're only a
123 PushProducer.
124
125 So the client gives a LogFileConsumer to File.subscribeConsumer . This
126 Consumer must have registerProducer(), unregisterProducer(), and
127 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
128 except that writeChunk() takes chunks (tuples of (channel,text)) instead
129 of the normal write() which takes just text. The LogFileConsumer is
130 allowed to call stopProducing, pauseProducing, and resumeProducing on the
131 producer instance it is given. """
132
133 paused = False
134 subscribed = False
135 BUFFERSIZE = 2048
136
138 self.logfile = logfile
139 self.consumer = consumer
140 self.chunkGenerator = self.getChunks()
141 consumer.registerProducer(self, True)
142
144 f = self.logfile.getFile()
145 offset = 0
146 chunks = []
147 p = LogFileScanner(chunks.append)
148 f.seek(offset)
149 data = f.read(self.BUFFERSIZE)
150 offset = f.tell()
151 while data:
152 p.dataReceived(data)
153 while chunks:
154 c = chunks.pop(0)
155 yield c
156 f.seek(offset)
157 data = f.read(self.BUFFERSIZE)
158 offset = f.tell()
159 del f
160
161
162 self.subscribed = True
163 self.logfile.watchers.append(self)
164 d = self.logfile.waitUntilFinished()
165
166
167 if self.logfile.runEntries:
168 channel = self.logfile.runEntries[0][0]
169 text = "".join([c[1] for c in self.logfile.runEntries])
170 yield (channel, text)
171
172
173
174
175
176 d.addCallback(self.logfileFinished)
177
179
180 self.paused = True
181 self.consumer = None
182 self.done()
183
190
193
195
196
197
198
199
200
201
202
203 eventually(self._resumeProducing)
204
206 self.paused = False
207 if not self.chunkGenerator:
208 return
209 try:
210 while not self.paused:
211 chunk = self.chunkGenerator.next()
212 self.consumer.writeChunk(chunk)
213
214
215 except StopIteration:
216
217 self.chunkGenerator = None
218
219
220
221 - def logChunk(self, build, step, logfile, channel, chunk):
222 if self.consumer:
223 self.consumer.writeChunk((channel, chunk))
224
231
233 """Try to remove a file, and if failed, try again in timeout.
234 Increases the timeout by a factor of 4, and only keeps trying for
235 another retries-amount of times.
236
237 """
238 try:
239 os.unlink(filename)
240 except OSError:
241 if retries > 0:
242 reactor.callLater(timeout, _tryremove, filename, timeout * 4,
243 retries - 1)
244 else:
245 log.msg("giving up on removing %s after over %d seconds" %
246 (filename, timeout))
247
249 """A LogFile keeps all of its contents on disk, in a non-pickle format to
250 which new entries can easily be appended. The file on disk has a name
251 like 12-log-compile-output, under the Builder's directory. The actual
252 filename is generated (before the LogFile is created) by
253 L{BuildStatus.generateLogfileName}.
254
255 Old LogFile pickles (which kept their contents in .entries) must be
256 upgraded. The L{BuilderStatus} is responsible for doing this, when it
257 loads the L{BuildStatus} into memory. The Build pickle is not modified,
258 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
259 logs."""
260
261 implements(interfaces.IStatusLog, interfaces.ILogFile)
262
263 finished = False
264 length = 0
265 nonHeaderLength = 0
266 tailLength = 0
267 chunkSize = 10*1000
268 runLength = 0
269
270 logMaxSize = None
271
272 logMaxTailSize = None
273 maxLengthExceeded = False
274 runEntries = []
275 entries = None
276 BUFFERSIZE = 2048
277 filename = None
278 openfile = None
279 compressMethod = "bz2"
280
281 - def __init__(self, parent, name, logfilename):
282 """
283 @type parent: L{BuildStepStatus}
284 @param parent: the Step that this log is a part of
285 @type name: string
286 @param name: the name of this log, typically 'output'
287 @type logfilename: string
288 @param logfilename: the Builder-relative pathname for the saved entries
289 """
290 self.step = parent
291 self.name = name
292 self.filename = logfilename
293 fn = self.getFilename()
294 if os.path.exists(fn):
295
296
297
298
299 log.msg("Warning: Overwriting old serialized Build at %s" % fn)
300 dirname = os.path.dirname(fn)
301 if not os.path.exists(dirname):
302 os.makedirs(dirname)
303 self.openfile = open(fn, "w+")
304 self.runEntries = []
305 self.watchers = []
306 self.finishedWatchers = []
307 self.tailBuffer = []
308
311
312 - def hasContents(self):
313 return os.path.exists(self.getFilename() + '.bz2') or \
314 os.path.exists(self.getFilename() + '.gz') or \
315 os.path.exists(self.getFilename())
316
319
322
326 if self.finished:
327 d = defer.succeed(self)
328 else:
329 d = defer.Deferred()
330 self.finishedWatchers.append(d)
331 return d
332
334 if self.openfile:
335
336
337 return self.openfile
338
339
340 try:
341 return BZ2File(self.getFilename() + ".bz2", "r")
342 except IOError:
343 pass
344 try:
345 return GzipFile(self.getFilename() + ".gz", "r")
346 except IOError:
347 pass
348 return open(self.getFilename(), "r")
349
351
352 return "".join(self.getChunks([STDOUT, STDERR], onlyText=True))
353
355 return "".join(self.getChunks(onlyText=True))
356
357 - def getChunks(self, channels=[], onlyText=False):
358
359
360
361
362
363
364
365
366
367
368
369
370 f = self.getFile()
371 if not self.finished:
372 offset = 0
373 f.seek(0, 2)
374 remaining = f.tell()
375 else:
376 offset = 0
377 remaining = None
378
379 leftover = None
380 if self.runEntries and (not channels or
381 (self.runEntries[0][0] in channels)):
382 leftover = (self.runEntries[0][0],
383 "".join([c[1] for c in self.runEntries]))
384
385
386
387 return self._generateChunks(f, offset, remaining, leftover,
388 channels, onlyText)
389
390 - def _generateChunks(self, f, offset, remaining, leftover,
391 channels, onlyText):
392 chunks = []
393 p = LogFileScanner(chunks.append, channels)
394 f.seek(offset)
395 if remaining is not None:
396 data = f.read(min(remaining, self.BUFFERSIZE))
397 remaining -= len(data)
398 else:
399 data = f.read(self.BUFFERSIZE)
400
401 offset = f.tell()
402 while data:
403 p.dataReceived(data)
404 while chunks:
405 channel, text = chunks.pop(0)
406 if onlyText:
407 yield text
408 else:
409 yield (channel, text)
410 f.seek(offset)
411 if remaining is not None:
412 data = f.read(min(remaining, self.BUFFERSIZE))
413 remaining -= len(data)
414 else:
415 data = f.read(self.BUFFERSIZE)
416 offset = f.tell()
417 del f
418
419 if leftover:
420 if onlyText:
421 yield leftover[1]
422 else:
423 yield leftover
424
426 """Return an iterator that produces newline-terminated lines,
427 excluding header chunks."""
428
429
430
431 alltext = "".join(self.getChunks([channel], onlyText=True))
432 io = StringIO(alltext)
433 return io.readlines()
434
444
448
452
453
454
474
475 - def addEntry(self, channel, text):
476 assert not self.finished
477
478 if isinstance(text, unicode):
479 text = text.encode('utf-8')
480 if channel != HEADER:
481
482 if self.logMaxSize and self.nonHeaderLength > self.logMaxSize:
483
484 if not self.maxLengthExceeded:
485 msg = "\nOutput exceeded %i bytes, remaining output has been truncated\n" % self.logMaxSize
486 self.addEntry(HEADER, msg)
487 self.merge()
488 self.maxLengthExceeded = True
489
490 if self.logMaxTailSize:
491
492 self.tailBuffer.append((channel, text))
493 self.tailLength += len(text)
494 while self.tailLength > self.logMaxTailSize:
495
496 c,t = self.tailBuffer.pop(0)
497 n = len(t)
498 self.tailLength -= n
499 assert self.tailLength >= 0
500 return
501
502 self.nonHeaderLength += len(text)
503
504
505
506 if self.runEntries and channel != self.runEntries[0][0]:
507 self.merge()
508 self.runEntries.append((channel, text))
509 self.runLength += len(text)
510 if self.runLength >= self.chunkSize:
511 self.merge()
512
513 for w in self.watchers:
514 w.logChunk(self.step.build, self.step, self, channel, text)
515 self.length += len(text)
516
523
550
551
553
554 if self.compressMethod == "bz2":
555 compressed = self.getFilename() + ".bz2.tmp"
556 elif self.compressMethod == "gz":
557 compressed = self.getFilename() + ".gz.tmp"
558 d = threads.deferToThread(self._compressLog, compressed)
559 d.addCallback(self._renameCompressedLog, compressed)
560 d.addErrback(self._cleanupFailedCompress, compressed)
561 return d
562
564 infile = self.getFile()
565 if self.compressMethod == "bz2":
566 cf = BZ2File(compressed, 'w')
567 elif self.compressMethod == "gz":
568 cf = GzipFile(compressed, 'w')
569 bufsize = 1024*1024
570 while True:
571 buf = infile.read(bufsize)
572 cf.write(buf)
573 if len(buf) < bufsize:
574 break
575 cf.close()
591 log.msg("failed to compress %s" % self.getFilename())
592 if os.path.exists(compressed):
593 _tryremove(compressed, 1, 5)
594 failure.trap()
595
596
598 d = self.__dict__.copy()
599 del d['step']
600 del d['watchers']
601 del d['finishedWatchers']
602 d['entries'] = []
603 if d.has_key('finished'):
604 del d['finished']
605 if d.has_key('openfile'):
606 del d['openfile']
607 return d
608
615
617 """Save our .entries to a new-style offline log file (if necessary),
618 and modify our in-memory representation to use it. The original
619 pickled LogFile (inside the pickled Build) won't be modified."""
620 self.filename = logfilename
621 if not os.path.exists(self.getFilename()):
622 self.openfile = open(self.getFilename(), "w")
623 self.finished = False
624 for channel,text in self.entries:
625 self.addEntry(channel, text)
626 self.finish()
627 del self.entries
628
630 implements(interfaces.IStatusLog)
631
632 filename = None
633
634 - def __init__(self, parent, name, logfilename, html):
639
644
648 return defer.succeed(self)
649
650 - def hasContents(self):
658
663
666
668 d = self.__dict__.copy()
669 del d['step']
670 return d
671
674
693
715
718 implements(interfaces.IBuildSetStatus)
719
724
727
728
729
733
741 (external_idstring, reason, ssid, complete, results) = self._get_info()
742 return external_idstring
743
750
754
759
761 (external_idstring, reason, ssid, complete, results) = self._get_info()
762 return complete
763
765 return self.status._buildset_waitUntilSuccess(self.id)
767 return self.status._buildset_waitUntilFinished(self.id)
768
820
823 """
824 I represent a collection of output status for a
825 L{buildbot.process.step.BuildStep}.
826
827 Statistics contain any information gleaned from a step that is
828 not in the form of a logfile. As an example, steps that run
829 tests might gather statistics about the number of passed, failed,
830 or skipped tests.
831
832 @type progress: L{buildbot.status.progress.StepProgress}
833 @cvar progress: tracks ETA for the step
834 @type text: list of strings
835 @cvar text: list of short texts that describe the command and its status
836 @type text2: list of strings
837 @cvar text2: list of short texts added to the overall build description
838 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
839 @ivar logs: logs of steps
840 @type statistics: dict
841 @ivar statistics: results from running this step
842 """
843
844
845 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
846
847 persistenceVersion = 3
848 persistenceForgets = ( 'wasUpgraded', )
849
850 started = None
851 finished = None
852 progress = None
853 text = []
854 results = (None, [])
855 text2 = []
856 watchers = []
857 updates = {}
858 finishedWatchers = []
859 statistics = {}
860 step_number = None
861
862 - def __init__(self, parent, step_number):
875
877 """Returns a short string with the name of this step. This string
878 may have spaces in it."""
879 return self.name
880
883
886
888 """Returns a list of tuples (name, current, target)."""
889 if not self.progress:
890 return []
891 ret = []
892 metrics = self.progress.progress.keys()
893 metrics.sort()
894 for m in metrics:
895 t = (m, self.progress.progress[m], self.progress.expectations[m])
896 ret.append(t)
897 return ret
898
901
903 return self.urls.copy()
904
906 return (self.started is not None)
907
910
913
915 if self.finished:
916 d = defer.succeed(self)
917 else:
918 d = defer.Deferred()
919 self.finishedWatchers.append(d)
920 return d
921
922
923
924
933
934
935
936
938 """Returns a list of strings which describe the step. These are
939 intended to be displayed in a narrow column. If more space is
940 available, the caller should join them together with spaces before
941 presenting them to the user."""
942 return self.text
943
945 """Return a tuple describing the results of the step.
946 'result' is one of the constants in L{buildbot.status.builder}:
947 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
948 'strings' is an optional list of strings that the step wants to
949 append to the overall build's results. These strings are usually
950 more terse than the ones returned by getText(): in particular,
951 successful Steps do not usually contribute any text to the
952 overall build.
953
954 @rtype: tuple of int, list of strings
955 @returns: (result, strings)
956 """
957 return (self.results, self.text2)
958
960 """Return true if this step has a value for the given statistic.
961 """
962 return self.statistics.has_key(name)
963
965 """Return the given statistic, if present
966 """
967 return self.statistics.get(name, default)
968
969
970
971 - def subscribe(self, receiver, updateInterval=10):
976
987
995
996
997
998
1000 self.name = stepname
1001
1003 log.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color,))
1004
1007
1012
1030
1039
1043
1044 - def addURL(self, name, url):
1045 self.urls[name] = url
1046
1047 - def setText(self, text):
1048 self.text = text
1049 for w in self.watchers:
1050 w.stepTextChanged(self.build, self, text)
1051 - def setText2(self, text):
1052 self.text2 = text
1053 for w in self.watchers:
1054 w.stepText2Changed(self.build, self, text)
1055
1057 """Set the given statistic. Usually called by subclasses.
1058 """
1059 self.statistics[name] = value
1060
1062 self.skipped = skipped
1063
1092
1096
1098 return self.waitingForLocks
1099
1101 self.waitingForLocks = waiting
1102
1103
1104
1106 d = styles.Versioned.__getstate__(self)
1107 del d['build']
1108 if d.has_key('progress'):
1109 del d['progress']
1110 del d['watchers']
1111 del d['finishedWatchers']
1112 del d['updates']
1113 return d
1114
1125
1127 if not hasattr(self, "urls"):
1128 self.urls = {}
1129 self.wasUpgraded = True
1130
1132 if not hasattr(self, "statistics"):
1133 self.statistics = {}
1134 self.wasUpgraded = True
1135
1137 if not hasattr(self, "step_number"):
1138 self.step_number = 0
1139 self.wasUpgraded = True
1140
1142 result = {}
1143
1144 result['name'] = self.getName()
1145
1146
1147 result['text'] = self.getText()
1148 result['results'] = self.getResults()
1149 result['isStarted'] = self.isStarted()
1150 result['isFinished'] = self.isFinished()
1151 result['statistics'] = self.statistics
1152 result['times'] = self.getTimes()
1153 result['expectations'] = self.getExpectations()
1154 result['eta'] = self.getETA()
1155 result['urls'] = self.getURLs()
1156 result['step_number'] = self.step_number
1157
1158
1159
1160 return result
1161
1164 implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
1165
1166 persistenceVersion = 3
1167 persistenceForgets = ( 'wasUpgraded', )
1168
1169 source = None
1170 reason = None
1171 changes = []
1172 blamelist = []
1173 progress = None
1174 started = None
1175 finished = None
1176 currentStep = None
1177 text = []
1178 results = None
1179 slavename = "???"
1180
1181
1182
1183
1184 watchers = []
1185 updates = {}
1186 finishedWatchers = []
1187 testResults = {}
1188
1203
1206
1207
1208
1210 """
1211 @rtype: L{BuilderStatus}
1212 """
1213 return self.builder
1214
1217
1220
1223
1228
1233
1236
1239
1242
1246
1248 """Return a list of IBuildStepStatus objects. For invariant builds
1249 (those which always use the same set of Steps), this should be the
1250 complete list, however some of the steps may not have started yet
1251 (step.getTimes()[0] will be None). For variant builds, this may not
1252 be complete (asking again later may give you more of them)."""
1253 return self.steps
1254
1257
1258 _sentinel = []
1260 """Summarize the named statistic over all steps in which it
1261 exists, using combination_fn and initial_value to combine multiple
1262 results into a single result. This translates to a call to Python's
1263 X{reduce}::
1264 return reduce(summary_fn, step_stats_list, initial_value)
1265 """
1266 step_stats_list = [
1267 st.getStatistic(name)
1268 for st in self.steps
1269 if st.hasStatistic(name) ]
1270 if initial_value is self._sentinel:
1271 return reduce(summary_fn, step_stats_list)
1272 else:
1273 return reduce(summary_fn, step_stats_list, initial_value)
1274
1277
1279 if self.finished:
1280 d = defer.succeed(self)
1281 else:
1282 d = defer.Deferred()
1283 self.finishedWatchers.append(d)
1284 return d
1285
1286
1287
1288
1298
1301
1302
1303
1304
1305 - def getText(self):
1306 text = []
1307 text.extend(self.text)
1308 for s in self.steps:
1309 text.extend(s.text2)
1310 return text
1311
1314
1317
1320
1322 trs = self.testResults.keys()
1323 trs.sort()
1324 ret = [ self.testResults[t] for t in trs]
1325 return ret
1326
1336
1337
1338
1339 - def subscribe(self, receiver, updateInterval=None):
1340
1341
1342 self.watchers.append(receiver)
1343 if updateInterval is not None:
1344 self.sendETAUpdate(receiver, updateInterval)
1345
1357
1365
1366
1367
1369 """The Build is setting up, and has added a new BuildStep to its
1370 list. Create a BuildStepStatus object to which it can send status
1371 updates."""
1372
1373 s = BuildStepStatus(self, len(self.steps))
1374 s.setName(name)
1375 self.steps.append(s)
1376 return s
1377
1378 - def setProperty(self, propname, value, source, runtime=True):
1380
1383
1387
1394
1396 """The Build has been set up and is about to be started. It can now
1397 be safely queried, so it is time to announce the new build."""
1398
1399 self.started = util.now()
1400
1401
1402 self.builder.buildStarted(self)
1403
1406
1407 - def setText(self, text):
1408 assert isinstance(text, (list, tuple))
1409 self.text = text
1412
1426
1427
1428
1442
1447
1448
1449
1451
1452 self.steps = []
1453
1454
1455
1457 """Return a filename (relative to the Builder's base directory) where
1458 the logfile's contents can be stored uniquely.
1459
1460 The base filename is made by combining our build number, the Step's
1461 name, and the log's name, then removing unsuitable characters. The
1462 filename is then made unique by appending _0, _1, etc, until it does
1463 not collide with any other logfile.
1464
1465 These files are kept in the Builder's basedir (rather than a
1466 per-Build subdirectory) because that makes cleanup easier: cron and
1467 find will help get rid of the old logs, but the empty directories are
1468 more of a hassle to remove."""
1469
1470 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
1471 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
1472
1473 unique_counter = 0
1474 filename = starting_filename
1475 while filename in [l.filename
1476 for step in self.steps
1477 for l in step.getLogs()
1478 if l.filename]:
1479 filename = "%s_%d" % (starting_filename, unique_counter)
1480 unique_counter += 1
1481 return filename
1482
1484 d = styles.Versioned.__getstate__(self)
1485
1486
1487 if not self.finished:
1488 d['finished'] = True
1489
1490
1491
1492
1493 for k in 'builder', 'watchers', 'updates', 'finishedWatchers':
1494 if k in d: del d[k]
1495 return d
1496
1505
1519
1521 self.properties = {}
1522 self.wasUpgraded = True
1523
1530
1545
1551
1572
1599
1603 """I handle status information for a single process.base.Builder object.
1604 That object sends status changes to me (frequently as Events), and I
1605 provide them on demand to the various status recipients, like the HTML
1606 waterfall display and the live status clients. It also sends build
1607 summaries to me, which I log and provide to status clients who aren't
1608 interested in seeing details of the individual build steps.
1609
1610 I am responsible for maintaining the list of historic Events and Builds,
1611 pruning old ones, and loading them from / saving them to disk.
1612
1613 I live in the buildbot.process.base.Builder object, in the
1614 .builder_status attribute.
1615
1616 @type category: string
1617 @ivar category: user-defined category this builder belongs to; can be
1618 used to filter on in status clients
1619 """
1620
1621 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
1622
1623 persistenceVersion = 1
1624 persistenceForgets = ( 'wasUpgraded', )
1625
1626
1627
1628
1629 buildCacheSize = 15
1630 eventHorizon = 50
1631
1632
1633 logHorizon = 40
1634 buildHorizon = 100
1635
1636 category = None
1637 currentBigState = "offline"
1638 basedir = None
1639
1640 - def __init__(self, buildername, category=None):
1641 self.name = buildername
1642 self.category = category
1643
1644 self.slavenames = []
1645 self.events = []
1646
1647
1648 self.lastBuildStatus = None
1649
1650
1651 self.currentBuilds = []
1652 self.nextBuild = None
1653 self.watchers = []
1654 self.buildCache = weakref.WeakValueDictionary()
1655 self.buildCache_LRU = []
1656 self.logCompressionLimit = False
1657 self.logCompressionMethod = "bz2"
1658 self.logMaxSize = None
1659 self.logMaxTailSize = None
1660
1661
1662
1664
1665
1666
1667
1668 d = styles.Versioned.__getstate__(self)
1669 d['watchers'] = []
1670 del d['buildCache']
1671 del d['buildCache_LRU']
1672 for b in self.currentBuilds:
1673 b.saveYourself()
1674
1675 del d['currentBuilds']
1676 d.pop('pendingBuilds', None)
1677 del d['currentBigState']
1678 del d['basedir']
1679 del d['status']
1680 del d['nextBuildNumber']
1681 return d
1682
1684
1685
1686 styles.Versioned.__setstate__(self, d)
1687 self.buildCache = weakref.WeakValueDictionary()
1688 self.buildCache_LRU = []
1689 self.currentBuilds = []
1690 self.watchers = []
1691 self.slavenames = []
1692
1693
1694
1700
1702 if hasattr(self, 'slavename'):
1703 self.slavenames = [self.slavename]
1704 del self.slavename
1705 if hasattr(self, 'nextBuildNumber'):
1706 del self.nextBuildNumber
1707 self.wasUpgraded = True
1708
1710 """Scan our directory of saved BuildStatus instances to determine
1711 what our self.nextBuildNumber should be. Set it one larger than the
1712 highest-numbered build we discover. This is called by the top-level
1713 Status object shortly after we are created or loaded from disk.
1714 """
1715 existing_builds = [int(f)
1716 for f in os.listdir(self.basedir)
1717 if re.match("^\d+$", f)]
1718 if existing_builds:
1719 self.nextBuildNumber = max(existing_builds) + 1
1720 else:
1721 self.nextBuildNumber = 0
1722
1724 self.logCompressionLimit = lowerLimit
1725
1727 assert method in ("bz2", "gz")
1728 self.logCompressionMethod = method
1729
1732
1735
1737 for b in self.currentBuilds:
1738 if not b.isFinished:
1739
1740
1741 b.saveYourself()
1742 filename = os.path.join(self.basedir, "builder")
1743 tmpfilename = filename + ".tmp"
1744 try:
1745 dump(self, open(tmpfilename, "wb"), -1)
1746 if runtime.platformType == 'win32':
1747
1748 if os.path.exists(filename):
1749 os.unlink(filename)
1750 os.rename(tmpfilename, filename)
1751 except:
1752 log.msg("unable to save builder %s" % self.name)
1753 log.err()
1754
1755
1756
1757
1760
1767
1806
1807 - def prune(self, events_only=False):
1808
1809 self.events = self.events[-self.eventHorizon:]
1810
1811 if events_only:
1812 return
1813
1814 gc.collect()
1815
1816
1817 if self.buildHorizon is not None:
1818 earliest_build = self.nextBuildNumber - self.buildHorizon
1819 else:
1820 earliest_build = 0
1821
1822 if self.logHorizon is not None:
1823 earliest_log = self.nextBuildNumber - self.logHorizon
1824 else:
1825 earliest_log = 0
1826
1827 if earliest_log < earliest_build:
1828 earliest_log = earliest_build
1829
1830 if earliest_build == 0:
1831 return
1832
1833
1834 build_re = re.compile(r"^([0-9]+)$")
1835 build_log_re = re.compile(r"^([0-9]+)-.*$")
1836
1837 if not os.path.exists(self.basedir):
1838 return
1839
1840 for filename in os.listdir(self.basedir):
1841 num = None
1842 mo = build_re.match(filename)
1843 is_logfile = False
1844 if mo:
1845 num = int(mo.group(1))
1846 else:
1847 mo = build_log_re.match(filename)
1848 if mo:
1849 num = int(mo.group(1))
1850 is_logfile = True
1851
1852 if num is None: continue
1853 if num in self.buildCache: continue
1854
1855 if (is_logfile and num < earliest_log) or num < earliest_build:
1856 pathname = os.path.join(self.basedir, filename)
1857 log.msg("pruning '%s'" % pathname)
1858 try: os.unlink(pathname)
1859 except OSError: pass
1860
1861
1864
1867
1870
1875
1877 return self.currentBuilds
1878
1884
1887
1898
1900 try:
1901 return self.events[number]
1902 except IndexError:
1903 return None
1904
1905 - def generateFinishedBuilds(self, branches=[],
1906 num_builds=None,
1907 max_buildnum=None,
1908 finished_before=None,
1909 max_search=200):
1910 got = 0
1911 for Nb in itertools.count(1):
1912 if Nb > self.nextBuildNumber:
1913 break
1914 if Nb > max_search:
1915 break
1916 build = self.getBuild(-Nb)
1917 if build is None:
1918 continue
1919 if max_buildnum is not None:
1920 if build.getNumber() > max_buildnum:
1921 continue
1922 if not build.isFinished():
1923 continue
1924 if finished_before is not None:
1925 start, end = build.getTimes()
1926 if end >= finished_before:
1927 continue
1928 if branches:
1929 if build.getSourceStamp().branch not in branches:
1930 continue
1931 got += 1
1932 yield build
1933 if num_builds is not None:
1934 if got >= num_builds:
1935 return
1936
1937 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
1938 """This function creates a generator which will provide all of this
1939 Builder's status events, starting with the most recent and
1940 progressing backwards in time. """
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951 eventIndex = -1
1952 e = self.getEvent(eventIndex)
1953 for Nb in range(1, self.nextBuildNumber+1):
1954 b = self.getBuild(-Nb)
1955 if not b:
1956
1957
1958
1959 if Nb == 1:
1960 continue
1961 break
1962 if b.getTimes()[0] < minTime:
1963 break
1964 if branches and not b.getSourceStamp().branch in branches:
1965 continue
1966 if categories and not b.getBuilder().getCategory() in categories:
1967 continue
1968 if committers and not [True for c in b.getChanges() if c.who in committers]:
1969 continue
1970 steps = b.getSteps()
1971 for Ns in range(1, len(steps)+1):
1972 if steps[-Ns].started:
1973 step_start = steps[-Ns].getTimes()[0]
1974 while e is not None and e.getTimes()[0] > step_start:
1975 yield e
1976 eventIndex -= 1
1977 e = self.getEvent(eventIndex)
1978 yield steps[-Ns]
1979 yield b
1980 while e is not None:
1981 yield e
1982 eventIndex -= 1
1983 e = self.getEvent(eventIndex)
1984 if e and e.getTimes()[0] < minTime:
1985 break
1986
1988
1989
1990
1991
1992 self.watchers.append(receiver)
1993 self.publishState(receiver)
1994
1995 self.status._builder_subscribe(self.name, receiver)
1996
2000
2001
2002
2004 self.slavenames = names
2005
2015
2026
2032
2046
2048 """The Builder has decided to start a build, but the Build object is
2049 not yet ready to report status (it has not finished creating the
2050 Steps). Create a BuildStatus object that it can use."""
2051 number = self.nextBuildNumber
2052 self.nextBuildNumber += 1
2053
2054
2055
2056
2057 s = BuildStatus(self, number)
2058 s.waitUntilFinished().addCallback(self._buildFinished)
2059 return s
2060
2061
2063 """Now the BuildStatus object is ready to go (it knows all of its
2064 Steps, its ETA, etc), so it is safe to notify our watchers."""
2065
2066 assert s.builder is self
2067 assert s.number == self.nextBuildNumber - 1
2068 assert s not in self.currentBuilds
2069 self.currentBuilds.append(s)
2070 self.touchBuildCache(s)
2071
2072
2073
2074
2075 for w in self.watchers:
2076 try:
2077 receiver = w.buildStarted(self.getName(), s)
2078 if receiver:
2079 if type(receiver) == type(()):
2080 s.subscribe(receiver[0], receiver[1])
2081 else:
2082 s.subscribe(receiver)
2083 d = s.waitUntilFinished()
2084 d.addCallback(lambda s: s.unsubscribe(receiver))
2085 except:
2086 log.msg("Exception caught notifying %r of buildStarted event" % w)
2087 log.err()
2088
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2125 state = self.currentBigState
2126 if state == "offline":
2127 client.currentlyOffline()
2128 elif state == "idle":
2129 client.currentlyIdle()
2130 elif state == "building":
2131 client.currentlyBuilding()
2132 else:
2133 log.msg("Hey, self.currentBigState is weird:", state)
2134
2135
2136
2137
2139
2140 first = self.events[0].number
2141 if first + len(self.events)-1 != self.events[-1].number:
2142 log.msg(self,
2143 "lost an event somewhere: [0] is %d, [%d] is %d" % \
2144 (self.events[0].number,
2145 len(self.events) - 1,
2146 self.events[-1].number))
2147 for e in self.events:
2148 log.msg("e[%d]: " % e.number, e)
2149 return None
2150 offset = num - first
2151 log.msg(self, "offset", offset)
2152 try:
2153 return self.events[offset]
2154 except IndexError:
2155 return None
2156
2157
2159 if hasattr(self, "allEvents"):
2160
2161
2162
2163
2164 return
2165 self.allEvents = self.loadFile("events", [])
2166 if self.allEvents:
2167 self.nextEventNumber = self.allEvents[-1].number + 1
2168 else:
2169 self.nextEventNumber = 0
2171 self.saveFile("events", self.allEvents)
2172
2173
2174
2184
2186 result = {}
2187
2188
2189 result['basedir'] = os.path.basename(self.basedir)
2190 result['category'] = self.category
2191 result['slaves'] = self.slavenames
2192
2193
2194
2195
2196
2197
2198 current_builds = [b.getNumber() for b in self.currentBuilds]
2199 cached_builds = list(set(self.buildCache.keys() + current_builds))
2200 cached_builds.sort()
2201 result['cachedBuilds'] = cached_builds
2202 result['currentBuilds'] = current_builds
2203 result['state'] = self.getState()[0]
2204
2205 result['pendingBuilds'] = [
2206 b.getSourceStamp().asDict() for b in self.getPendingBuilds()
2207 ]
2208 return result
2209
2212 implements(interfaces.ISlaveStatus)
2213
2214 admin = None
2215 host = None
2216 access_uri = None
2217 version = None
2218 connected = False
2219 graceful_shutdown = False
2220
2222 self.name = name
2223 self._lastMessageReceived = 0
2224 self.runningBuilds = []
2225 self.graceful_callbacks = []
2226 self.connect_times = []
2227
2241 return self._lastMessageReceived
2243 return self.runningBuilds
2245 then = time.time() - 3600
2246 return len([ t for t in self.connect_times if t > then ])
2247
2259 self._lastMessageReceived = when
2260
2262
2263 now = time.time()
2264 self.connect_times = [ t for t in self.connect_times if t > now - 3600 ] + [ now ]
2265
2267 self.runningBuilds.append(build)
2270
2275 """Set the graceful shutdown flag, and notify all the watchers"""
2276 self.graceful_shutdown = graceful
2277 for cb in self.graceful_callbacks:
2278 eventually(cb, graceful)
2280 """Add watcher to the list of watchers to be notified when the
2281 graceful shutdown flag is changed."""
2282 if not watcher in self.graceful_callbacks:
2283 self.graceful_callbacks.append(watcher)
2285 """Remove watcher from the list of watchers to be notified when the
2286 graceful shutdown flag is changed."""
2287 if watcher in self.graceful_callbacks:
2288 self.graceful_callbacks.remove(watcher)
2289
2303
2305 """
2306 I represent the status of the buildmaster.
2307 """
2308 implements(interfaces.IStatus)
2309
2310 - def __init__(self, botmaster, basedir):
2311 """
2312 @type botmaster: L{buildbot.master.BotMaster}
2313 @param botmaster: the Status object uses C{.botmaster} to get at
2314 both the L{buildbot.master.BuildMaster} (for
2315 various buildbot-wide parameters) and the
2316 actual Builders (to get at their L{BuilderStatus}
2317 objects). It is not allowed to change or influence
2318 anything through this reference.
2319 @type basedir: string
2320 @param basedir: this provides a base directory in which saved status
2321 information (changes.pck, saved Build status
2322 pickles) can be stored
2323 """
2324 self.botmaster = botmaster
2325 self.db = None
2326 self.basedir = basedir
2327 self.watchers = []
2328 assert os.path.isdir(basedir)
2329
2330 self.logCompressionLimit = 4*1024
2331 self.logCompressionMethod = "bz2"
2332
2333 self.logMaxSize = None
2334 self.logMaxTailSize = None
2335
2336 self._builder_observers = collections.KeyedSets()
2337 self._buildreq_observers = collections.KeyedSets()
2338 self._buildset_success_waiters = collections.KeyedSets()
2339 self._buildset_finished_waiters = collections.KeyedSets()
2340
2341 @property
2344
2347
2350
2352 self.db = db
2353 self.db.subscribe_to("add-build", self._db_builds_changed)
2354 self.db.subscribe_to("add-buildset", self._db_buildset_added)
2355 self.db.subscribe_to("modify-buildset", self._db_buildsets_changed)
2356 self.db.subscribe_to("add-buildrequest", self._db_buildrequest_added)
2357 self.db.subscribe_to("cancel-buildrequest", self._db_buildrequest_cancelled)
2358
2359
2360
2367
2369 prefix = self.getBuildbotURL()
2370 if not prefix:
2371 return None
2372 if interfaces.IStatus.providedBy(thing):
2373 return prefix
2374 if interfaces.ISchedulerStatus.providedBy(thing):
2375 pass
2376 if interfaces.IBuilderStatus.providedBy(thing):
2377 builder = thing
2378 return prefix + "builders/%s" % (
2379 urllib.quote(builder.getName(), safe=''),
2380 )
2381 if interfaces.IBuildStatus.providedBy(thing):
2382 build = thing
2383 builder = build.getBuilder()
2384 return prefix + "builders/%s/builds/%d" % (
2385 urllib.quote(builder.getName(), safe=''),
2386 build.getNumber())
2387 if interfaces.IBuildStepStatus.providedBy(thing):
2388 step = thing
2389 build = step.getBuild()
2390 builder = build.getBuilder()
2391 return prefix + "builders/%s/builds/%d/steps/%s" % (
2392 urllib.quote(builder.getName(), safe=''),
2393 build.getNumber(),
2394 urllib.quote(step.getName(), safe=''))
2395
2396
2397
2398
2399
2400 if interfaces.IStatusEvent.providedBy(thing):
2401 from buildbot.changes import changes
2402
2403 if isinstance(thing, changes.Change):
2404 change = thing
2405 return "%schanges/%d" % (prefix, change.number)
2406
2407 if interfaces.IStatusLog.providedBy(thing):
2408 log = thing
2409 step = log.getStep()
2410 build = step.getBuild()
2411 builder = build.getBuilder()
2412
2413 logs = step.getLogs()
2414 for i in range(len(logs)):
2415 if log is logs[i]:
2416 break
2417 else:
2418 return None
2419 return prefix + "builders/%s/builds/%d/steps/%s/logs/%s" % (
2420 urllib.quote(builder.getName(), safe=''),
2421 build.getNumber(),
2422 urllib.quote(step.getName(), safe=''),
2423 urllib.quote(log.getName()))
2424
2427
2430
2433
2435 if categories == None:
2436 return self.botmaster.builderNames[:]
2437
2438 l = []
2439
2440 for name in self.botmaster.builderNames:
2441 builder = self.botmaster.builders[name]
2442 if builder.builder_status.category in categories:
2443 l.append(name)
2444 return l
2445
2447 """
2448 @rtype: L{BuilderStatus}
2449 """
2450 return self.botmaster.builders[name].builder_status
2451
2453 return self.botmaster.slaves.keys()
2454
2457
2461
2462 - def generateFinishedBuilds(self, builders=[], branches=[],
2463 num_builds=None, finished_before=None,
2464 max_search=200):
2465
2466 def want_builder(bn):
2467 if builders:
2468 return bn in builders
2469 return True
2470 builder_names = [bn
2471 for bn in self.getBuilderNames()
2472 if want_builder(bn)]
2473
2474
2475
2476
2477 sources = []
2478 for bn in builder_names:
2479 b = self.getBuilder(bn)
2480 g = b.generateFinishedBuilds(branches,
2481 finished_before=finished_before,
2482 max_search=max_search)
2483 sources.append(g)
2484
2485
2486 next_build = [None] * len(sources)
2487
2488 def refill():
2489 for i,g in enumerate(sources):
2490 if next_build[i]:
2491
2492 continue
2493 if not g:
2494
2495 continue
2496 try:
2497 next_build[i] = g.next()
2498 except StopIteration:
2499 next_build[i] = None
2500 sources[i] = None
2501
2502 got = 0
2503 while True:
2504 refill()
2505
2506 candidates = [(i, b, b.getTimes()[1])
2507 for i,b in enumerate(next_build)
2508 if b is not None]
2509 candidates.sort(lambda x,y: cmp(x[2], y[2]))
2510 if not candidates:
2511 return
2512
2513
2514 i, build, finshed_time = candidates[-1]
2515 next_build[i] = None
2516 got += 1
2517 yield build
2518 if num_builds is not None:
2519 if got >= num_builds:
2520 return
2521
2528
2529
2530
2531
2536
2538 """
2539 @rtype: L{BuilderStatus}
2540 """
2541 filename = os.path.join(self.basedir, basedir, "builder")
2542 log.msg("trying to load status pickle from %s" % filename)
2543 builder_status = None
2544 try:
2545 builder_status = load(open(filename, "rb"))
2546
2547
2548
2549
2550
2551
2552 versioneds = styles.versionedsToUpgrade
2553 styles.doUpgrade()
2554 if True in [ hasattr(o, 'wasUpgraded') for o in versioneds.values() ]:
2555 log.msg("re-writing upgraded builder pickle")
2556 builder_status.saveYourself()
2557
2558 except IOError:
2559 log.msg("no saved status pickle, creating a new one")
2560 except:
2561 log.msg("error while loading status pickle, creating a new one")
2562 log.msg("error follows:")
2563 log.err()
2564 if not builder_status:
2565 builder_status = BuilderStatus(name, category)
2566 builder_status.addPointEvent(["builder", "created"])
2567 log.msg("added builder %s in category %s" % (name, category))
2568
2569
2570 builder_status.category = category
2571 builder_status.basedir = os.path.join(self.basedir, basedir)
2572 builder_status.name = name
2573 builder_status.status = self
2574
2575 if not os.path.isdir(builder_status.basedir):
2576 os.makedirs(builder_status.basedir)
2577 builder_status.determineNextBuildNumber()
2578
2579 builder_status.setBigState("offline")
2580 builder_status.setLogCompressionLimit(self.logCompressionLimit)
2581 builder_status.setLogCompressionMethod(self.logCompressionMethod)
2582 builder_status.setLogMaxSize(self.logMaxSize)
2583 builder_status.setLogMaxTailSize(self.logMaxTailSize)
2584
2585 for t in self.watchers:
2586 self.announceNewBuilder(t, name, builder_status)
2587
2588 return builder_status
2589
2594
2599
2604
2609
2619
2621 for r in requests:
2622
2623
2624 pass
2625
2628
2630 brid,buildername,buildnum = self.db.get_build_info(bid)
2631 if brid in self._buildreq_observers:
2632 bs = self.getBuilder(buildername).getBuild(buildnum)
2633 if bs:
2634 for o in self._buildreq_observers[brid]:
2635 eventually(o, bs)
2636
2638 self._buildreq_observers.add(brid, observer)
2639
2641 self._buildreq_observers.discard(brid, observer)
2642
2648
2650 d = defer.Deferred()
2651 self._buildset_success_waiters.add(bsid, d)
2652
2653 self._db_buildsets_changed("modify-buildset", bsid)
2654 return d
2656 d = defer.Deferred()
2657 self._buildset_finished_waiters.add(bsid, d)
2658 self._db_buildsets_changed("modify-buildset", bsid)
2659 return d
2660
2662 for bsid in bsids:
2663 self._db_buildset_changed(bsid)
2664
2666
2667
2668 if (bsid not in self._buildset_success_waiters
2669 and bsid not in self._buildset_finished_waiters):
2670 return
2671 successful,finished = self.db.examine_buildset(bsid)
2672 bss = BuildSetStatus(bsid, self, self.db)
2673 if successful is not None:
2674 for d in self._buildset_success_waiters.pop(bsid):
2675 eventually(d.callback, bss)
2676 if finished:
2677 for d in self._buildset_finished_waiters.pop(bsid):
2678 eventually(d.callback, bss)
2679
2681
2682 self._builder_observers.add(buildername, watcher)
2683
2685 self._builder_observers.discard(buildername, watcher)
2686
2688 self._handle_buildrequest_event("added", brids)
2690 self._handle_buildrequest_event("cancelled", brids)
2704
2705
2706