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 from bz2 import BZ2File
16
17
18 from buildbot import interfaces, util, sourcestamp
19
20 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5)
21 Results = ["success", "warnings", "failure", "skipped", "exception"]
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 STDOUT = interfaces.LOG_CHANNEL_STDOUT
43 STDERR = interfaces.LOG_CHANNEL_STDERR
44 HEADER = interfaces.LOG_CHANNEL_HEADER
45 ChunkTypes = ["stdout", "stderr", "header"]
46
48 - def __init__(self, chunk_cb, channels=[]):
49 self.chunk_cb = chunk_cb
50 self.channels = channels
51
53 channel = int(line[0])
54 if not self.channels or (channel in self.channels):
55 self.chunk_cb((channel, line[1:]))
56
58 """What's the plan?
59
60 the LogFile has just one FD, used for both reading and writing.
61 Each time you add an entry, fd.seek to the end and then write.
62
63 Each reader (i.e. Producer) keeps track of their own offset. The reader
64 starts by seeking to the start of the logfile, and reading forwards.
65 Between each hunk of file they yield chunks, so they must remember their
66 offset before yielding and re-seek back to that offset before reading
67 more data. When their read() returns EOF, they're finished with the first
68 phase of the reading (everything that's already been written to disk).
69
70 After EOF, the remaining data is entirely in the current entries list.
71 These entries are all of the same channel, so we can do one "".join and
72 obtain a single chunk to be sent to the listener. But since that involves
73 a yield, and more data might arrive after we give up control, we have to
74 subscribe them before yielding. We can't subscribe them any earlier,
75 otherwise they'd get data out of order.
76
77 We're using a generator in the first place so that the listener can
78 throttle us, which means they're pulling. But the subscription means
79 we're pushing. Really we're a Producer. In the first phase we can be
80 either a PullProducer or a PushProducer. In the second phase we're only a
81 PushProducer.
82
83 So the client gives a LogFileConsumer to File.subscribeConsumer . This
84 Consumer must have registerProducer(), unregisterProducer(), and
85 writeChunk(), and is just like a regular twisted.interfaces.IConsumer,
86 except that writeChunk() takes chunks (tuples of (channel,text)) instead
87 of the normal write() which takes just text. The LogFileConsumer is
88 allowed to call stopProducing, pauseProducing, and resumeProducing on the
89 producer instance it is given. """
90
91 paused = False
92 subscribed = False
93 BUFFERSIZE = 2048
94
96 self.logfile = logfile
97 self.consumer = consumer
98 self.chunkGenerator = self.getChunks()
99 consumer.registerProducer(self, True)
100
102 f = self.logfile.getFile()
103 offset = 0
104 chunks = []
105 p = LogFileScanner(chunks.append)
106 f.seek(offset)
107 data = f.read(self.BUFFERSIZE)
108 offset = f.tell()
109 while data:
110 p.dataReceived(data)
111 while chunks:
112 c = chunks.pop(0)
113 yield c
114 f.seek(offset)
115 data = f.read(self.BUFFERSIZE)
116 offset = f.tell()
117 del f
118
119
120 self.subscribed = True
121 self.logfile.watchers.append(self)
122 d = self.logfile.waitUntilFinished()
123
124
125 if self.logfile.runEntries:
126 channel = self.logfile.runEntries[0][0]
127 text = "".join([c[1] for c in self.logfile.runEntries])
128 yield (channel, text)
129
130
131
132
133
134 d.addCallback(self.logfileFinished)
135
137
138 self.paused = True
139 self.consumer = None
140 self.done()
141
148
151
162
164 self.paused = False
165 if not self.chunkGenerator:
166 return
167 try:
168 while not self.paused:
169 chunk = self.chunkGenerator.next()
170 self.consumer.writeChunk(chunk)
171
172
173 except StopIteration:
174
175 self.chunkGenerator = None
176
177
178
179 - def logChunk(self, build, step, logfile, channel, chunk):
180 if self.consumer:
181 self.consumer.writeChunk((channel, chunk))
182
189
191 """Try to remove a file, and if failed, try again in timeout.
192 Increases the timeout by a factor of 4, and only keeps trying for
193 another retries-amount of times.
194
195 """
196 try:
197 os.unlink(filename)
198 except OSError:
199 if retries > 0:
200 reactor.callLater(timeout, _tryremove, filename, timeout * 4,
201 retries - 1)
202 else:
203 log.msg("giving up on removing %s after over %d seconds" %
204 (filename, timeout))
205
207 """A LogFile keeps all of its contents on disk, in a non-pickle format to
208 which new entries can easily be appended. The file on disk has a name
209 like 12-log-compile-output, under the Builder's directory. The actual
210 filename is generated (before the LogFile is created) by
211 L{BuildStatus.generateLogfileName}.
212
213 Old LogFile pickles (which kept their contents in .entries) must be
214 upgraded. The L{BuilderStatus} is responsible for doing this, when it
215 loads the L{BuildStatus} into memory. The Build pickle is not modified,
216 so users who go from 0.6.5 back to 0.6.4 don't have to lose their
217 logs."""
218
219 implements(interfaces.IStatusLog, interfaces.ILogFile)
220
221 finished = False
222 length = 0
223 chunkSize = 10*1000
224 runLength = 0
225 runEntries = []
226 entries = None
227 BUFFERSIZE = 2048
228 filename = None
229 openfile = None
230
231 - def __init__(self, parent, name, logfilename):
232 """
233 @type parent: L{BuildStepStatus}
234 @param parent: the Step that this log is a part of
235 @type name: string
236 @param name: the name of this log, typically 'output'
237 @type logfilename: string
238 @param logfilename: the Builder-relative pathname for the saved entries
239 """
240 self.step = parent
241 self.name = name
242 self.filename = logfilename
243 fn = self.getFilename()
244 if os.path.exists(fn):
245
246
247
248
249 log.msg("Warning: Overwriting old serialized Build at %s" % fn)
250 self.openfile = open(fn, "w+")
251 self.runEntries = []
252 self.watchers = []
253 self.finishedWatchers = []
254
257
258 - def hasContents(self):
259 return os.path.exists(self.getFilename() + '.bz2') or \
260 os.path.exists(self.getFilename())
261
264
267
277
279 if self.openfile:
280
281
282 return self.openfile
283
284
285 try:
286 return BZ2File(self.getFilename() + ".bz2", "r")
287 except IOError:
288 pass
289 return open(self.getFilename(), "r")
290
292
293 return "".join(self.getChunks([STDOUT, STDERR], onlyText=True))
294
296 return "".join(self.getChunks(onlyText=True))
297
298 - def getChunks(self, channels=[], onlyText=False):
299
300
301
302
303
304
305
306
307
308
309
310
311 f = self.getFile()
312 offset = 0
313 f.seek(0, 2)
314 remaining = f.tell()
315
316 leftover = None
317 if self.runEntries and (not channels or
318 (self.runEntries[0][0] in channels)):
319 leftover = (self.runEntries[0][0],
320 "".join([c[1] for c in self.runEntries]))
321
322
323
324 return self._generateChunks(f, offset, remaining, leftover,
325 channels, onlyText)
326
327 - def _generateChunks(self, f, offset, remaining, leftover,
328 channels, onlyText):
329 chunks = []
330 p = LogFileScanner(chunks.append, channels)
331 f.seek(offset)
332 data = f.read(min(remaining, self.BUFFERSIZE))
333 remaining -= len(data)
334 offset = f.tell()
335 while data:
336 p.dataReceived(data)
337 while chunks:
338 channel, text = chunks.pop(0)
339 if onlyText:
340 yield text
341 else:
342 yield (channel, text)
343 f.seek(offset)
344 data = f.read(min(remaining, self.BUFFERSIZE))
345 remaining -= len(data)
346 offset = f.tell()
347 del f
348
349 if leftover:
350 if onlyText:
351 yield leftover[1]
352 else:
353 yield leftover
354
356 """Return an iterator that produces newline-terminated lines,
357 excluding header chunks."""
358
359
360
361 alltext = "".join(self.getChunks([channel], onlyText=True))
362 io = StringIO(alltext)
363 return io.readlines()
364
374
378
382
383
384
386
387
388 if not self.runEntries:
389 return
390 channel = self.runEntries[0][0]
391 text = "".join([c[1] for c in self.runEntries])
392 assert channel < 10
393 f = self.openfile
394 f.seek(0, 2)
395 offset = 0
396 while offset < len(text):
397 size = min(len(text)-offset, self.chunkSize)
398 f.write("%d:%d" % (1 + size, channel))
399 f.write(text[offset:offset+size])
400 f.write(",")
401 offset += size
402 self.runEntries = []
403 self.runLength = 0
404
405 - def addEntry(self, channel, text):
406 assert not self.finished
407
408
409 if self.runEntries and channel != self.runEntries[0][0]:
410 self.merge()
411 self.runEntries.append((channel, text))
412 self.runLength += len(text)
413 if self.runLength >= self.chunkSize:
414 self.merge()
415
416 for w in self.watchers:
417 w.logChunk(self.step.build, self.step, self, channel, text)
418 self.length += len(text)
419
426
444
445
452
454 infile = self.getFile()
455 cf = BZ2File(compressed, 'w')
456 bufsize = 1024*1024
457 while True:
458 buf = infile.read(bufsize)
459 cf.write(buf)
460 if len(buf) < bufsize:
461 break
462 cf.close()
479
480
482 d = self.__dict__.copy()
483 del d['step']
484 del d['watchers']
485 del d['finishedWatchers']
486 d['entries'] = []
487 if d.has_key('finished'):
488 del d['finished']
489 if d.has_key('openfile'):
490 del d['openfile']
491 return d
492
499
501 """Save our .entries to a new-style offline log file (if necessary),
502 and modify our in-memory representation to use it. The original
503 pickled LogFile (inside the pickled Build) won't be modified."""
504 self.filename = logfilename
505 if not os.path.exists(self.getFilename()):
506 self.openfile = open(self.getFilename(), "w")
507 self.finished = False
508 for channel,text in self.entries:
509 self.addEntry(channel, text)
510 self.finish()
511 del self.entries
512
514 implements(interfaces.IStatusLog)
515
516 filename = None
517
518 - def __init__(self, parent, name, logfilename, html):
523
528
532 return defer.succeed(self)
533
534 - def hasContents(self):
542
547
550
552 d = self.__dict__.copy()
553 del d['step']
554 return d
555
558
559
577
599
600
602 implements(interfaces.IBuildSetStatus)
603
604 - def __init__(self, source, reason, builderNames, bsid=None):
613
615 self.buildRequests = buildRequestStatuses
620 self.stillHopeful = False
621
622
624 for d in self.successWatchers:
625 d.callback(self)
626 self.successWatchers = []
627
633
634
635
644
646 return self.builderNames
648 return self.buildRequests
651
653 if self.finished or not self.stillHopeful:
654
655 return defer.succeed(self)
656 d = defer.Deferred()
657 self.successWatchers.append(d)
658 return d
659
666
668 implements(interfaces.IBuildRequestStatus)
669
670 - def __init__(self, source, builderName):
671 self.source = source
672 self.builderName = builderName
673 self.builds = []
674 self.observers = []
675 self.submittedAt = None
676
678 self.builds.append(build)
679 for o in self.observers[:]:
680 o(build)
681
682
686 return self.builderName
689
691 self.observers.append(observer)
692 for b in self.builds:
693 observer(b)
695 self.observers.remove(observer)
696
701
702
704 """
705 I represent a collection of output status for a
706 L{buildbot.process.step.BuildStep}.
707
708 Statistics contain any information gleaned from a step that is
709 not in the form of a logfile. As an example, steps that run
710 tests might gather statistics about the number of passed, failed,
711 or skipped tests.
712
713 @type progress: L{buildbot.status.progress.StepProgress}
714 @cvar progress: tracks ETA for the step
715 @type text: list of strings
716 @cvar text: list of short texts that describe the command and its status
717 @type text2: list of strings
718 @cvar text2: list of short texts added to the overall build description
719 @type logs: dict of string -> L{buildbot.status.builder.LogFile}
720 @ivar logs: logs of steps
721 @type statistics: dict
722 @ivar statistics: results from running this step
723 """
724
725
726 implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
727 persistenceVersion = 2
728
729 started = None
730 finished = None
731 progress = None
732 text = []
733 results = (None, [])
734 text2 = []
735 watchers = []
736 updates = {}
737 finishedWatchers = []
738 statistics = {}
739
749
751 """Returns a short string with the name of this step. This string
752 may have spaces in it."""
753 return self.name
754
757
760
762 """Returns a list of tuples (name, current, target)."""
763 if not self.progress:
764 return []
765 ret = []
766 metrics = self.progress.progress.keys()
767 metrics.sort()
768 for m in metrics:
769 t = (m, self.progress.progress[m], self.progress.expectations[m])
770 ret.append(t)
771 return ret
772
775
777 return self.urls.copy()
778
780 return (self.started is not None)
781
784
792
793
794
795
804
805
806
807
809 """Returns a list of strings which describe the step. These are
810 intended to be displayed in a narrow column. If more space is
811 available, the caller should join them together with spaces before
812 presenting them to the user."""
813 return self.text
814
816 """Return a tuple describing the results of the step.
817 'result' is one of the constants in L{buildbot.status.builder}:
818 SUCCESS, WARNINGS, FAILURE, or SKIPPED.
819 'strings' is an optional list of strings that the step wants to
820 append to the overall build's results. These strings are usually
821 more terse than the ones returned by getText(): in particular,
822 successful Steps do not usually contribute any text to the
823 overall build.
824
825 @rtype: tuple of int, list of strings
826 @returns: (result, strings)
827 """
828 return (self.results, self.text2)
829
831 """Return true if this step has a value for the given statistic.
832 """
833 return self.statistics.has_key(name)
834
836 """Return the given statistic, if present
837 """
838 return self.statistics.get(name, default)
839
840
841
842 - def subscribe(self, receiver, updateInterval=10):
847
858
866
867
868
869
872
874 log.msg("BuildStepStatus.setColor is no longer supported -- ignoring color %s" % (color,))
875
878
883
898
911
915
917 self.urls[name] = url
918
919 - def setText(self, text):
920 self.text = text
921 for w in self.watchers:
922 w.stepTextChanged(self.build, self, text)
923 - def setText2(self, text):
924 self.text2 = text
925 for w in self.watchers:
926 w.stepText2Changed(self.build, self, text)
927
929 """Set the given statistic. Usually called by subclasses.
930 """
931 self.statistics[name] = value
932
959
963
964
965
967 d = styles.Versioned.__getstate__(self)
968 del d['build']
969 if d.has_key('progress'):
970 del d['progress']
971 del d['watchers']
972 del d['finishedWatchers']
973 del d['updates']
974 return d
975
983
985 if not hasattr(self, "urls"):
986 self.urls = {}
987
989 if not hasattr(self, "statistics"):
990 self.statistics = {}
991
992
994 implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
995 persistenceVersion = 3
996
997 source = None
998 reason = None
999 changes = []
1000 blamelist = []
1001 requests = []
1002 progress = None
1003 started = None
1004 finished = None
1005 currentStep = None
1006 text = []
1007 results = None
1008 slavename = "???"
1009
1010
1011
1012
1013 watchers = []
1014 updates = {}
1015 finishedWatchers = []
1016 testResults = {}
1017
1033
1036
1037
1038
1040 """
1041 @rtype: L{BuilderStatus}
1042 """
1043 return self.builder
1044
1047
1050
1053
1058
1063
1066
1069
1072
1075
1079
1081 """Return a list of IBuildStepStatus objects. For invariant builds
1082 (those which always use the same set of Steps), this should be the
1083 complete list, however some of the steps may not have started yet
1084 (step.getTimes()[0] will be None). For variant builds, this may not
1085 be complete (asking again later may give you more of them)."""
1086 return self.steps
1087
1090
1091 _sentinel = []
1093 """Summarize the named statistic over all steps in which it
1094 exists, using combination_fn and initial_value to combine multiple
1095 results into a single result. This translates to a call to Python's
1096 X{reduce}::
1097 return reduce(summary_fn, step_stats_list, initial_value)
1098 """
1099 step_stats_list = [
1100 st.getStatistic(name)
1101 for st in self.steps
1102 if st.hasStatistic(name) ]
1103 if initial_value is self._sentinel:
1104 return reduce(summary_fn, step_stats_list)
1105 else:
1106 return reduce(summary_fn, step_stats_list, initial_value)
1107
1110
1118
1119
1120
1121
1131
1134
1135
1136
1137
1138 - def getText(self):
1139 text = []
1140 text.extend(self.text)
1141 for s in self.steps:
1142 text.extend(s.text2)
1143 return text
1144
1147
1150
1153
1163
1164
1165
1166 - def subscribe(self, receiver, updateInterval=None):
1167
1168
1169 self.watchers.append(receiver)
1170 if updateInterval is not None:
1171 self.sendETAUpdate(receiver, updateInterval)
1172
1184
1192
1193
1194
1196 """The Build is setting up, and has added a new BuildStep to its
1197 list. Create a BuildStepStatus object to which it can send status
1198 updates."""
1199
1200 s = BuildStepStatus(self)
1201 s.setName(name)
1202 self.steps.append(s)
1203 return s
1204
1207
1210
1214
1217
1224
1226 """The Build has been set up and is about to be started. It can now
1227 be safely queried, so it is time to announce the new build."""
1228
1229 self.started = util.now()
1230
1231
1232 self.builder.buildStarted(self)
1233
1236
1237 - def setText(self, text):
1238 assert isinstance(text, (list, tuple))
1239 self.text = text
1242
1256
1257
1258
1273
1278
1279
1280
1282
1283 self.steps = []
1284
1285
1286
1288 """Return a filename (relative to the Builder's base directory) where
1289 the logfile's contents can be stored uniquely.
1290
1291 The base filename is made by combining our build number, the Step's
1292 name, and the log's name, then removing unsuitable characters. The
1293 filename is then made unique by appending _0, _1, etc, until it does
1294 not collide with any other logfile.
1295
1296 These files are kept in the Builder's basedir (rather than a
1297 per-Build subdirectory) because that makes cleanup easier: cron and
1298 find will help get rid of the old logs, but the empty directories are
1299 more of a hassle to remove."""
1300
1301 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
1302 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
1303
1304 unique_counter = 0
1305 filename = starting_filename
1306 while filename in [l.filename
1307 for step in self.steps
1308 for l in step.getLogs()
1309 if l.filename]:
1310 filename = "%s_%d" % (starting_filename, unique_counter)
1311 unique_counter += 1
1312 return filename
1313
1315 d = styles.Versioned.__getstate__(self)
1316
1317
1318 if not self.finished:
1319 d['finished'] = True
1320
1321
1322
1323
1324 for k in 'builder', 'watchers', 'updates', 'requests', 'finishedWatchers':
1325 if k in d: del d[k]
1326 return d
1327
1336
1349
1352
1358
1373
1379
1381 filename = os.path.join(self.builder.basedir, "%d" % self.number)
1382 if os.path.isdir(filename):
1383
1384 shutil.rmtree(filename, ignore_errors=True)
1385 tmpfilename = filename + ".tmp"
1386 try:
1387 dump(self, open(tmpfilename, "wb"), -1)
1388 if sys.platform == 'win32':
1389
1390
1391
1392
1393 if os.path.exists(filename):
1394 os.unlink(filename)
1395 os.rename(tmpfilename, filename)
1396 except:
1397 log.msg("unable to save build %s-#%d" % (self.builder.name,
1398 self.number))
1399 log.err()
1400
1401
1402
1404 """I handle status information for a single process.base.Builder object.
1405 That object sends status changes to me (frequently as Events), and I
1406 provide them on demand to the various status recipients, like the HTML
1407 waterfall display and the live status clients. It also sends build
1408 summaries to me, which I log and provide to status clients who aren't
1409 interested in seeing details of the individual build steps.
1410
1411 I am responsible for maintaining the list of historic Events and Builds,
1412 pruning old ones, and loading them from / saving them to disk.
1413
1414 I live in the buildbot.process.base.Builder object, in the
1415 .builder_status attribute.
1416
1417 @type category: string
1418 @ivar category: user-defined category this builder belongs to; can be
1419 used to filter on in status clients
1420 """
1421
1422 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
1423 persistenceVersion = 1
1424
1425
1426
1427
1428 buildCacheSize = 15
1429 eventHorizon = 50
1430
1431
1432 logHorizon = 40
1433 buildHorizon = 100
1434
1435 category = None
1436 currentBigState = "offline"
1437 basedir = None
1438
1439 - def __init__(self, buildername, category=None):
1440 self.name = buildername
1441 self.category = category
1442
1443 self.slavenames = []
1444 self.events = []
1445
1446
1447 self.lastBuildStatus = None
1448
1449
1450 self.currentBuilds = []
1451 self.pendingBuilds = []
1452 self.nextBuild = None
1453 self.watchers = []
1454 self.buildCache = weakref.WeakValueDictionary()
1455 self.buildCache_LRU = []
1456 self.logCompressionLimit = False
1457
1458
1459
1461
1462
1463
1464
1465 d = styles.Versioned.__getstate__(self)
1466 d['watchers'] = []
1467 del d['buildCache']
1468 del d['buildCache_LRU']
1469 for b in self.currentBuilds:
1470 b.saveYourself()
1471
1472 del d['currentBuilds']
1473 del d['pendingBuilds']
1474 del d['currentBigState']
1475 del d['basedir']
1476 del d['status']
1477 del d['nextBuildNumber']
1478 return d
1479
1481
1482
1483 styles.Versioned.__setstate__(self, d)
1484 self.buildCache = weakref.WeakValueDictionary()
1485 self.buildCache_LRU = []
1486 self.currentBuilds = []
1487 self.pendingBuilds = []
1488 self.watchers = []
1489 self.slavenames = []
1490
1491
1492
1504
1506 if hasattr(self, 'slavename'):
1507 self.slavenames = [self.slavename]
1508 del self.slavename
1509 if hasattr(self, 'nextBuildNumber'):
1510 del self.nextBuildNumber
1511
1513 """Scan our directory of saved BuildStatus instances to determine
1514 what our self.nextBuildNumber should be. Set it one larger than the
1515 highest-numbered build we discover. This is called by the top-level
1516 Status object shortly after we are created or loaded from disk.
1517 """
1518 existing_builds = [int(f)
1519 for f in os.listdir(self.basedir)
1520 if re.match("^\d+$", f)]
1521 if existing_builds:
1522 self.nextBuildNumber = max(existing_builds) + 1
1523 else:
1524 self.nextBuildNumber = 0
1525
1527 self.logCompressionLimit = lowerLimit
1528
1530 for b in self.currentBuilds:
1531 if not b.isFinished:
1532
1533
1534 b.saveYourself()
1535 filename = os.path.join(self.basedir, "builder")
1536 tmpfilename = filename + ".tmp"
1537 try:
1538 dump(self, open(tmpfilename, "wb"), -1)
1539 if sys.platform == 'win32':
1540
1541 if os.path.exists(filename):
1542 os.unlink(filename)
1543 os.rename(tmpfilename, filename)
1544 except:
1545 log.msg("unable to save builder %s" % self.name)
1546 log.err()
1547
1548
1549
1550
1553
1560
1588
1590 gc.collect()
1591
1592
1593 self.events = self.events[-self.eventHorizon:]
1594
1595
1596 if self.buildHorizon:
1597 earliest_build = self.nextBuildNumber - self.buildHorizon
1598 else:
1599 earliest_build = 0
1600
1601 if self.logHorizon:
1602 earliest_log = self.nextBuildNumber - self.logHorizon
1603 else:
1604 earliest_log = 0
1605
1606 if earliest_log < earliest_build:
1607 earliest_log = earliest_build
1608
1609 if earliest_build == 0:
1610 return
1611
1612
1613 build_re = re.compile(r"^([0-9]+)$")
1614 build_log_re = re.compile(r"^([0-9]+)-.*$")
1615 for filename in os.listdir(self.basedir):
1616 num = None
1617 mo = build_re.match(filename)
1618 is_logfile = False
1619 if mo:
1620 num = int(mo.group(1))
1621 else:
1622 mo = build_log_re.match(filename)
1623 if mo:
1624 num = int(mo.group(1))
1625 is_logfile = True
1626
1627 if num is None: continue
1628 if num in self.buildCache: continue
1629
1630 if (is_logfile and num < earliest_log) or num < earliest_build:
1631 pathname = os.path.join(self.basedir, filename)
1632 log.msg("pruning '%s'" % pathname)
1633 try: os.unlink(pathname)
1634 except OSError: pass
1635
1636
1639
1642
1645
1647 return self.pendingBuilds
1648
1650 return self.currentBuilds
1651
1657
1660
1671
1673 try:
1674 return self.events[number]
1675 except IndexError:
1676 return None
1677
1678 - def generateFinishedBuilds(self, branches=[],
1679 num_builds=None,
1680 max_buildnum=None,
1681 finished_before=None,
1682 max_search=200):
1683 got = 0
1684 for Nb in itertools.count(1):
1685 if Nb > self.nextBuildNumber:
1686 break
1687 if Nb > max_search:
1688 break
1689 build = self.getBuild(-Nb)
1690 if build is None:
1691 continue
1692 if max_buildnum is not None:
1693 if build.getNumber() > max_buildnum:
1694 continue
1695 if not build.isFinished():
1696 continue
1697 if finished_before is not None:
1698 start, end = build.getTimes()
1699 if end >= finished_before:
1700 continue
1701 if branches:
1702 if build.getSourceStamp().branch not in branches:
1703 continue
1704 got += 1
1705 yield build
1706 if num_builds is not None:
1707 if got >= num_builds:
1708 return
1709
1711 """This function creates a generator which will provide all of this
1712 Builder's status events, starting with the most recent and
1713 progressing backwards in time. """
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724 eventIndex = -1
1725 e = self.getEvent(eventIndex)
1726 for Nb in range(1, self.nextBuildNumber+1):
1727 b = self.getBuild(-Nb)
1728 if not b:
1729 break
1730 if branches and not b.getSourceStamp().branch in branches:
1731 continue
1732 if categories and not b.getBuilder().getCategory() in categories:
1733 continue
1734 steps = b.getSteps()
1735 for Ns in range(1, len(steps)+1):
1736 if steps[-Ns].started:
1737 step_start = steps[-Ns].getTimes()[0]
1738 while e is not None and e.getTimes()[0] > step_start:
1739 yield e
1740 eventIndex -= 1
1741 e = self.getEvent(eventIndex)
1742 yield steps[-Ns]
1743 yield b
1744 while e is not None:
1745 yield e
1746 eventIndex -= 1
1747 e = self.getEvent(eventIndex)
1748
1753
1756
1757
1758
1760 self.slavenames = names
1761
1770
1780
1786
1800
1802 """The Builder has decided to start a build, but the Build object is
1803 not yet ready to report status (it has not finished creating the
1804 Steps). Create a BuildStatus object that it can use."""
1805 number = self.nextBuildNumber
1806 self.nextBuildNumber += 1
1807
1808
1809
1810
1811 s = BuildStatus(self, number)
1812 s.waitUntilFinished().addCallback(self._buildFinished)
1813 return s
1814
1819
1825
1826
1828 """Now the BuildStatus object is ready to go (it knows all of its
1829 Steps, its ETA, etc), so it is safe to notify our watchers."""
1830
1831 assert s.builder is self
1832 assert s.number == self.nextBuildNumber - 1
1833 assert s not in self.currentBuilds
1834 self.currentBuilds.append(s)
1835 self.touchBuildCache(s)
1836
1837
1838
1839
1840 for w in self.watchers:
1841 try:
1842 receiver = w.buildStarted(self.getName(), s)
1843 if receiver:
1844 if type(receiver) == type(()):
1845 s.subscribe(receiver[0], receiver[1])
1846 else:
1847 s.subscribe(receiver)
1848 d = s.waitUntilFinished()
1849 d.addCallback(lambda s: s.unsubscribe(receiver))
1850 except:
1851 log.msg("Exception caught notifying %r of buildStarted event" % w)
1852 log.err()
1853
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1890 state = self.currentBigState
1891 if state == "offline":
1892 client.currentlyOffline()
1893 elif state == "idle":
1894 client.currentlyIdle()
1895 elif state == "building":
1896 client.currentlyBuilding()
1897 else:
1898 log.msg("Hey, self.currentBigState is weird:", state)
1899
1900
1901
1902
1904
1905 first = self.events[0].number
1906 if first + len(self.events)-1 != self.events[-1].number:
1907 log.msg(self,
1908 "lost an event somewhere: [0] is %d, [%d] is %d" % \
1909 (self.events[0].number,
1910 len(self.events) - 1,
1911 self.events[-1].number))
1912 for e in self.events:
1913 log.msg("e[%d]: " % e.number, e)
1914 return None
1915 offset = num - first
1916 log.msg(self, "offset", offset)
1917 try:
1918 return self.events[offset]
1919 except IndexError:
1920 return None
1921
1922
1924 if hasattr(self, "allEvents"):
1925
1926
1927
1928
1929 return
1930 self.allEvents = self.loadFile("events", [])
1931 if self.allEvents:
1932 self.nextEventNumber = self.allEvents[-1].number + 1
1933 else:
1934 self.nextEventNumber = 0
1936 self.saveFile("events", self.allEvents)
1937
1938
1939
1949
1951 implements(interfaces.ISlaveStatus)
1952
1953 admin = None
1954 host = None
1955 connected = False
1956 graceful_shutdown = False
1957
1959 self.name = name
1960 self._lastMessageReceived = 0
1961 self.runningBuilds = []
1962 self.graceful_callbacks = []
1963
1973 return self._lastMessageReceived
1975 return self.runningBuilds
1976
1984 self._lastMessageReceived = when
1985
1987 self.runningBuilds.append(build)
1990
1995 """Set the graceful shutdown flag, and notify all the watchers"""
1996 self.graceful_shutdown = graceful
1997 for cb in self.graceful_callbacks:
1998 reactor.callLater(0, cb, graceful)
2000 """Add watcher to the list of watchers to be notified when the
2001 graceful shutdown flag is changed."""
2002 if not watcher in self.graceful_callbacks:
2003 self.graceful_callbacks.append(watcher)
2005 """Remove watcher from the list of watchers to be notified when the
2006 graceful shutdown flag is changed."""
2007 if watcher in self.graceful_callbacks:
2008 self.graceful_callbacks.remove(watcher)
2009
2011 """
2012 I represent the status of the buildmaster.
2013 """
2014 implements(interfaces.IStatus)
2015
2016 - def __init__(self, botmaster, basedir):
2017 """
2018 @type botmaster: L{buildbot.master.BotMaster}
2019 @param botmaster: the Status object uses C{.botmaster} to get at
2020 both the L{buildbot.master.BuildMaster} (for
2021 various buildbot-wide parameters) and the
2022 actual Builders (to get at their L{BuilderStatus}
2023 objects). It is not allowed to change or influence
2024 anything through this reference.
2025 @type basedir: string
2026 @param basedir: this provides a base directory in which saved status
2027 information (changes.pck, saved Build status
2028 pickles) can be stored
2029 """
2030 self.botmaster = botmaster
2031 self.basedir = basedir
2032 self.watchers = []
2033 self.activeBuildSets = []
2034 assert os.path.isdir(basedir)
2035
2036 self.logCompressionLimit = 4*1024
2037
2038
2039
2040
2047
2049 prefix = self.getBuildbotURL()
2050 if not prefix:
2051 return None
2052 if interfaces.IStatus.providedBy(thing):
2053 return prefix
2054 if interfaces.ISchedulerStatus.providedBy(thing):
2055 pass
2056 if interfaces.IBuilderStatus.providedBy(thing):
2057 builder = thing
2058 return prefix + "builders/%s" % (
2059 urllib.quote(builder.getName(), safe=''),
2060 )
2061 if interfaces.IBuildStatus.providedBy(thing):
2062 build = thing
2063 builder = build.getBuilder()
2064 return prefix + "builders/%s/builds/%d" % (
2065 urllib.quote(builder.getName(), safe=''),
2066 build.getNumber())
2067 if interfaces.IBuildStepStatus.providedBy(thing):
2068 step = thing
2069 build = step.getBuild()
2070 builder = build.getBuilder()
2071 return prefix + "builders/%s/builds/%d/steps/%s" % (
2072 urllib.quote(builder.getName(), safe=''),
2073 build.getNumber(),
2074 urllib.quote(step.getName(), safe=''))
2075
2076
2077
2078
2079
2080 if interfaces.IStatusEvent.providedBy(thing):
2081 from buildbot.changes import changes
2082
2083 if isinstance(thing, changes.Change):
2084 change = thing
2085 return "%schanges/%d" % (prefix, change.number)
2086
2087 if interfaces.IStatusLog.providedBy(thing):
2088 log = thing
2089 step = log.getStep()
2090 build = step.getBuild()
2091 builder = build.getBuilder()
2092
2093 logs = step.getLogs()
2094 for i in range(len(logs)):
2095 if log is logs[i]:
2096 lognum = i
2097 break
2098 else:
2099 return None
2100 return prefix + "builders/%s/builds/%d/steps/%s/logs/%s" % (
2101 urllib.quote(builder.getName(), safe=''),
2102 build.getNumber(),
2103 urllib.quote(step.getName(), safe=''),
2104 urllib.quote(log.getName()))
2105
2108
2111
2114
2116 if categories == None:
2117 return self.botmaster.builderNames[:]
2118
2119 l = []
2120
2121 for name in self.botmaster.builderNames:
2122 builder = self.botmaster.builders[name]
2123 if builder.builder_status.category in categories:
2124 l.append(name)
2125 return l
2126
2128 """
2129 @rtype: L{BuilderStatus}
2130 """
2131 return self.botmaster.builders[name].builder_status
2132
2134 return self.botmaster.slaves.keys()
2135
2138
2140 return self.activeBuildSets[:]
2141
2142 - def generateFinishedBuilds(self, builders=[], branches=[],
2143 num_builds=None, finished_before=None,
2144 max_search=200):
2145
2146 def want_builder(bn):
2147 if builders:
2148 return bn in builders
2149 return True
2150 builder_names = [bn
2151 for bn in self.getBuilderNames()
2152 if want_builder(bn)]
2153
2154
2155
2156
2157 sources = []
2158 for bn in builder_names:
2159 b = self.getBuilder(bn)
2160 g = b.generateFinishedBuilds(branches,
2161 finished_before=finished_before,
2162 max_search=max_search)
2163 sources.append(g)
2164
2165
2166 next_build = [None] * len(sources)
2167
2168 def refill():
2169 for i,g in enumerate(sources):
2170 if next_build[i]:
2171
2172 continue
2173 if not g:
2174
2175 continue
2176 try:
2177 next_build[i] = g.next()
2178 except StopIteration:
2179 next_build[i] = None
2180 sources[i] = None
2181
2182 got = 0
2183 while True:
2184 refill()
2185
2186 candidates = [(i, b, b.getTimes()[1])
2187 for i,b in enumerate(next_build)
2188 if b is not None]
2189 candidates.sort(lambda x,y: cmp(x[2], y[2]))
2190 if not candidates:
2191 return
2192
2193
2194 i, build, finshed_time = candidates[-1]
2195 next_build[i] = None
2196 got += 1
2197 yield build
2198 if num_builds is not None:
2199 if got >= num_builds:
2200 return
2201
2208
2209
2210
2211
2216
2218 """
2219 @rtype: L{BuilderStatus}
2220 """
2221 filename = os.path.join(self.basedir, basedir, "builder")
2222 log.msg("trying to load status pickle from %s" % filename)
2223 builder_status = None
2224 try:
2225 builder_status = load(open(filename, "rb"))
2226 styles.doUpgrade()
2227 except IOError:
2228 log.msg("no saved status pickle, creating a new one")
2229 except:
2230 log.msg("error while loading status pickle, creating a new one")
2231 log.msg("error follows:")
2232 log.err()
2233 if not builder_status:
2234 builder_status = BuilderStatus(name, category)
2235 builder_status.addPointEvent(["builder", "created"])
2236 log.msg("added builder %s in category %s" % (name, category))
2237
2238
2239 builder_status.category = category
2240 builder_status.basedir = os.path.join(self.basedir, basedir)
2241 builder_status.name = name
2242 builder_status.status = self
2243
2244 if not os.path.isdir(builder_status.basedir):
2245 os.makedirs(builder_status.basedir)
2246 builder_status.determineNextBuildNumber()
2247
2248 builder_status.setBigState("offline")
2249 builder_status.setLogCompressionLimit(self.logCompressionLimit)
2250
2251 for t in self.watchers:
2252 self.announceNewBuilder(t, name, builder_status)
2253
2254 return builder_status
2255
2259
2265