1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import weakref
18 import os, re, itertools
19 from cPickle import load, dump
20
21 from zope.interface import implements
22 from twisted.python import log, runtime
23 from twisted.persisted import styles
24 from buildbot.process import metrics
25 from buildbot import interfaces, util
26 from buildbot.status.event import Event
27 from buildbot.status.build import BuildStatus
28 from buildbot.status.buildrequest import BuildRequestStatus
29
30
31 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED
32 from buildbot.status.results import EXCEPTION, RETRY, Results, worst_status
33 _hush_pyflakes = [ SUCCESS, WARNINGS, FAILURE, SKIPPED,
34 EXCEPTION, RETRY, Results, worst_status ]
35
37 """I handle status information for a single process.build.Builder object.
38 That object sends status changes to me (frequently as Events), and I
39 provide them on demand to the various status recipients, like the HTML
40 waterfall display and the live status clients. It also sends build
41 summaries to me, which I log and provide to status clients who aren't
42 interested in seeing details of the individual build steps.
43
44 I am responsible for maintaining the list of historic Events and Builds,
45 pruning old ones, and loading them from / saving them to disk.
46
47 I live in the buildbot.process.build.Builder object, in the
48 .builder_status attribute.
49
50 @type category: string
51 @ivar category: user-defined category this builder belongs to; can be
52 used to filter on in status clients
53 """
54
55 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
56
57 persistenceVersion = 1
58 persistenceForgets = ( 'wasUpgraded', )
59
60 category = None
61 currentBigState = "offline"
62 basedir = None
63
64 - def __init__(self, buildername, category, master):
65 self.name = buildername
66 self.category = category
67 self.master = master
68
69 self.slavenames = []
70 self.events = []
71
72
73 self.lastBuildStatus = None
74
75
76 self.currentBuilds = []
77 self.nextBuild = None
78 self.watchers = []
79 self.buildCache = weakref.WeakValueDictionary()
80 self.buildCache_LRU = []
81
82
83
85
86
87
88
89 d = styles.Versioned.__getstate__(self)
90 d['watchers'] = []
91 del d['buildCache']
92 del d['buildCache_LRU']
93 for b in self.currentBuilds:
94 b.saveYourself()
95
96 del d['currentBuilds']
97 d.pop('pendingBuilds', None)
98 del d['currentBigState']
99 del d['basedir']
100 del d['status']
101 del d['nextBuildNumber']
102 del d['master']
103 return d
104
106
107
108 styles.Versioned.__setstate__(self, d)
109 self.buildCache = weakref.WeakValueDictionary()
110 self.buildCache_LRU = []
111 self.currentBuilds = []
112 self.watchers = []
113 self.slavenames = []
114
115
116
117
119 if hasattr(self, 'slavename'):
120 self.slavenames = [self.slavename]
121 del self.slavename
122 if hasattr(self, 'nextBuildNumber'):
123 del self.nextBuildNumber
124 self.wasUpgraded = True
125
127 """Scan our directory of saved BuildStatus instances to determine
128 what our self.nextBuildNumber should be. Set it one larger than the
129 highest-numbered build we discover. This is called by the top-level
130 Status object shortly after we are created or loaded from disk.
131 """
132 existing_builds = [int(f)
133 for f in os.listdir(self.basedir)
134 if re.match("^\d+$", f)]
135 if existing_builds:
136 self.nextBuildNumber = max(existing_builds) + 1
137 else:
138 self.nextBuildNumber = 0
139
158
159
160
161
164
172
214
215 - def prune(self, events_only=False):
216
217 eventHorizon = self.master.config.eventHorizon
218 self.events = self.events[-eventHorizon:]
219
220 if events_only:
221 return
222
223
224 buildHorizon = self.master.config.buildHorizon
225 if buildHorizon is not None:
226 earliest_build = self.nextBuildNumber - self.buildHorizon
227 else:
228 earliest_build = 0
229
230 logHorizon = self.master.config.logHorizon
231 if logHorizon is not None:
232 earliest_log = self.nextBuildNumber - logHorizon
233 else:
234 earliest_log = 0
235
236 if earliest_log < earliest_build:
237 earliest_log = earliest_build
238
239 if earliest_build == 0:
240 return
241
242
243 build_re = re.compile(r"^([0-9]+)$")
244 build_log_re = re.compile(r"^([0-9]+)-.*$")
245
246 if not os.path.exists(self.basedir):
247 return
248
249 for filename in os.listdir(self.basedir):
250 num = None
251 mo = build_re.match(filename)
252 is_logfile = False
253 if mo:
254 num = int(mo.group(1))
255 else:
256 mo = build_log_re.match(filename)
257 if mo:
258 num = int(mo.group(1))
259 is_logfile = True
260
261 if num is None: continue
262 if num in self.buildCache: continue
263
264 if (is_logfile and num < earliest_log) or num < earliest_build:
265 pathname = os.path.join(self.basedir, filename)
266 log.msg("pruning '%s'" % pathname)
267 try: os.unlink(pathname)
268 except OSError: pass
269
270
273
276
279
288 d.addCallback(make_statuses)
289 return d
290
292 return self.currentBuilds
293
299
302
313
315 try:
316 return self.events[number]
317 except IndexError:
318 return None
319
320 - def generateFinishedBuilds(self, branches=[],
321 num_builds=None,
322 max_buildnum=None,
323 finished_before=None,
324 max_search=200):
325 got = 0
326 for Nb in itertools.count(1):
327 if Nb > self.nextBuildNumber:
328 break
329 if Nb > max_search:
330 break
331 build = self.getBuild(-Nb)
332 if build is None:
333 continue
334 if max_buildnum is not None:
335 if build.getNumber() > max_buildnum:
336 continue
337 if not build.isFinished():
338 continue
339 if finished_before is not None:
340 start, end = build.getTimes()
341 if end >= finished_before:
342 continue
343 if branches:
344 if build.getSourceStamp().branch not in branches:
345 continue
346 got += 1
347 yield build
348 if num_builds is not None:
349 if got >= num_builds:
350 return
351
352 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
353 """This function creates a generator which will provide all of this
354 Builder's status events, starting with the most recent and
355 progressing backwards in time. """
356
357
358
359
360
361
362
363
364
365
366 eventIndex = -1
367 e = self.getEvent(eventIndex)
368 for Nb in range(1, self.nextBuildNumber+1):
369 b = self.getBuild(-Nb)
370 if not b:
371
372
373
374 if Nb == 1:
375 continue
376 break
377 if b.getTimes()[0] < minTime:
378 break
379 if branches and not b.getSourceStamp().branch in branches:
380 continue
381 if categories and not b.getBuilder().getCategory() in categories:
382 continue
383 if committers and not [True for c in b.getChanges() if c.who in committers]:
384 continue
385 steps = b.getSteps()
386 for Ns in range(1, len(steps)+1):
387 if steps[-Ns].started:
388 step_start = steps[-Ns].getTimes()[0]
389 while e is not None and e.getTimes()[0] > step_start:
390 yield e
391 eventIndex -= 1
392 e = self.getEvent(eventIndex)
393 yield steps[-Ns]
394 yield b
395 while e is not None:
396 yield e
397 eventIndex -= 1
398 e = self.getEvent(eventIndex)
399 if e and e.getTimes()[0] < minTime:
400 break
401
411
415
416
417
419 self.slavenames = names
420
430
441
447
461
463 """The Builder has decided to start a build, but the Build object is
464 not yet ready to report status (it has not finished creating the
465 Steps). Create a BuildStatus object that it can use."""
466 number = self.nextBuildNumber
467 self.nextBuildNumber += 1
468
469
470
471
472 s = BuildStatus(self, self.master, number)
473 s.waitUntilFinished().addCallback(self._buildFinished)
474 return s
475
476
478 """Now the BuildStatus object is ready to go (it knows all of its
479 Steps, its ETA, etc), so it is safe to notify our watchers."""
480
481 assert s.builder is self
482 assert s not in self.currentBuilds
483 self.currentBuilds.append(s)
484 self.touchBuildCache(s)
485
486
487
488
489 for w in self.watchers:
490 try:
491 receiver = w.buildStarted(self.getName(), s)
492 if receiver:
493 if type(receiver) == type(()):
494 s.subscribe(receiver[0], receiver[1])
495 else:
496 s.subscribe(receiver)
497 d = s.waitUntilFinished()
498 d.addCallback(lambda s: s.unsubscribe(receiver))
499 except:
500 log.msg("Exception caught notifying %r of buildStarted event" % w)
501 log.err()
502
518
519
521 result = {}
522
523
524 result['basedir'] = os.path.basename(self.basedir)
525 result['category'] = self.category
526 result['slaves'] = self.slavenames
527 result['schedulers'] = [ s.name
528 for s in self.status.master.allSchedulers()
529 if self.name in s.builderNames ]
530
531
532
533
534
535
536 current_builds = [b.getNumber() for b in self.currentBuilds]
537 cached_builds = list(set(self.buildCache.keys() + current_builds))
538 cached_builds.sort()
539 result['cachedBuilds'] = cached_builds
540 result['currentBuilds'] = current_builds
541 result['state'] = self.getState()[0]
542
543
544 result['pendingBuilds'] = 0
545 return result
546
548 """Just like L{asDict}, but with a nonzero pendingBuilds."""
549 result = self.asDict()
550 d = self.getPendingBuildRequestStatuses()
551 def combine(statuses):
552 result['pendingBuilds'] = len(statuses)
553 return result
554 d.addCallback(combine)
555 return d
556
559
560
561