1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 from __future__ import with_statement
17
18
19 import os, re, itertools
20 from cPickle import load, dump
21
22 from zope.interface import implements
23 from twisted.python import log, runtime
24 from twisted.persisted import styles
25 from buildbot import interfaces, util
26 from buildbot.util.lru import LRUCache
27 from buildbot.status.event import Event
28 from buildbot.status.build import BuildStatus
29 from buildbot.status.buildrequest import BuildRequestStatus
30
31
32 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED
33 from buildbot.status.results import EXCEPTION, RETRY, Results, worst_status
34 _hush_pyflakes = [ SUCCESS, WARNINGS, FAILURE, SKIPPED,
35 EXCEPTION, RETRY, Results, worst_status ]
36
38 """I handle status information for a single process.build.Builder object.
39 That object sends status changes to me (frequently as Events), and I
40 provide them on demand to the various status recipients, like the HTML
41 waterfall display and the live status clients. It also sends build
42 summaries to me, which I log and provide to status clients who aren't
43 interested in seeing details of the individual build steps.
44
45 I am responsible for maintaining the list of historic Events and Builds,
46 pruning old ones, and loading them from / saving them to disk.
47
48 I live in the buildbot.process.build.Builder object, in the
49 .builder_status attribute.
50
51 @type category: string
52 @ivar category: user-defined category this builder belongs to; can be
53 used to filter on in status clients
54 """
55
56 implements(interfaces.IBuilderStatus, interfaces.IEventSource)
57
58 persistenceVersion = 1
59 persistenceForgets = ( 'wasUpgraded', )
60
61 category = None
62 currentBigState = "offline"
63 basedir = None
64
65 - def __init__(self, buildername, category, master, description):
82
83
84
86
87
88
89
90 d = styles.Versioned.__getstate__(self)
91 d['watchers'] = []
92 del d['buildCache']
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
113
114
115
116
118 if hasattr(self, 'slavename'):
119 self.slavenames = [self.slavename]
120 del self.slavename
121 if hasattr(self, 'nextBuildNumber'):
122 del self.nextBuildNumber
123 self.wasUpgraded = True
124
126 """Scan our directory of saved BuildStatus instances to determine
127 what our self.nextBuildNumber should be. Set it one larger than the
128 highest-numbered build we discover. This is called by the top-level
129 Status object shortly after we are created or loaded from disk.
130 """
131 existing_builds = [int(f)
132 for f in os.listdir(self.basedir)
133 if re.match("^\d+$", f)]
134 if existing_builds:
135 self.nextBuildNumber = max(existing_builds) + 1
136 else:
137 self.nextBuildNumber = 0
138
140 for b in self.currentBuilds:
141 if not b.isFinished:
142
143
144 b.saveYourself()
145 filename = os.path.join(self.basedir, "builder")
146 tmpfilename = filename + ".tmp"
147 try:
148 with open(tmpfilename, "wb") as f:
149 dump(self, f, -1)
150 if runtime.platformType == 'win32':
151
152 if os.path.exists(filename):
153 os.unlink(filename)
154 os.rename(tmpfilename, filename)
155 except:
156 log.msg("unable to save builder %s" % self.name)
157 log.err()
158
159
160
163
166
169
197
199
200
201 if 'val' in kwargs:
202 return kwargs['val']
203
204
205 for b in self.currentBuilds:
206 if b.number == number:
207 return b
208
209
210 return self.loadBuildFromFile(number)
211
212 - def prune(self, events_only=False):
213
214 eventHorizon = self.master.config.eventHorizon
215 self.events = self.events[-eventHorizon:]
216
217 if events_only:
218 return
219
220
221 buildHorizon = self.master.config.buildHorizon
222 if buildHorizon is not None:
223 earliest_build = self.nextBuildNumber - buildHorizon
224 else:
225 earliest_build = 0
226
227 logHorizon = self.master.config.logHorizon
228 if logHorizon is not None:
229 earliest_log = self.nextBuildNumber - logHorizon
230 else:
231 earliest_log = 0
232
233 if earliest_log < earliest_build:
234 earliest_log = earliest_build
235
236 if earliest_build == 0:
237 return
238
239
240 build_re = re.compile(r"^([0-9]+)$")
241 build_log_re = re.compile(r"^([0-9]+)-.*$")
242
243 if not os.path.exists(self.basedir):
244 return
245
246 for filename in os.listdir(self.basedir):
247 num = None
248 mo = build_re.match(filename)
249 is_logfile = False
250 if mo:
251 num = int(mo.group(1))
252 else:
253 mo = build_log_re.match(filename)
254 if mo:
255 num = int(mo.group(1))
256 is_logfile = True
257
258 if num is None: continue
259 if num in self.buildCache.cache: continue
260
261 if (is_logfile and num < earliest_log) or num < earliest_build:
262 pathname = os.path.join(self.basedir, filename)
263 log.msg("pruning '%s'" % pathname)
264 try: os.unlink(pathname)
265 except OSError: pass
266
267
269
270
271 return self.name
272
276
279
282
285
294 d.addCallback(make_statuses)
295 return d
296
298 return self.currentBuilds
299
305
309
312
323
325 try:
326 return self.events[number]
327 except IndexError:
328 return None
329
333
334 - def generateFinishedBuilds(self, branches=[],
335 num_builds=None,
336 max_buildnum=None,
337 finished_before=None,
338 results=None,
339 max_search=200,
340 filter_fn=None):
341 got = 0
342 branches = set(branches)
343 for Nb in itertools.count(1):
344 if Nb > self.nextBuildNumber:
345 break
346 if Nb > max_search:
347 break
348 build = self.getBuild(-Nb)
349 if build is None:
350 continue
351 if max_buildnum is not None:
352 if build.getNumber() > max_buildnum:
353 continue
354 if not build.isFinished():
355 continue
356 if finished_before is not None:
357 start, end = build.getTimes()
358 if end >= finished_before:
359 continue
360
361
362 if branches and not branches & self._getBuildBranches(build):
363 continue
364 if results is not None:
365 if build.getResults() not in results:
366 continue
367 if filter_fn is not None:
368 if not filter_fn(build):
369 continue
370 got += 1
371 yield build
372 if num_builds is not None:
373 if got >= num_builds:
374 return
375
376 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
377 """This function creates a generator which will provide all of this
378 Builder's status events, starting with the most recent and
379 progressing backwards in time. """
380
381
382
383
384
385
386
387
388
389
390 eventIndex = -1
391 e = self.getEvent(eventIndex)
392 branches = set(branches)
393 for Nb in range(1, self.nextBuildNumber+1):
394 b = self.getBuild(-Nb)
395 if not b:
396
397
398
399 if Nb == 1:
400 continue
401 break
402 if b.getTimes()[0] < minTime:
403 break
404
405
406 if branches and not branches & self._getBuildBranches(b):
407 continue
408 if categories and not b.getBuilder().getCategory() in categories:
409 continue
410 if committers and not [True for c in b.getChanges() if c.who in committers]:
411 continue
412 steps = b.getSteps()
413 for Ns in range(1, len(steps)+1):
414 if steps[-Ns].started:
415 step_start = steps[-Ns].getTimes()[0]
416 while e is not None and e.getTimes()[0] > step_start:
417 yield e
418 eventIndex -= 1
419 e = self.getEvent(eventIndex)
420 yield steps[-Ns]
421 yield b
422 while e is not None:
423 yield e
424 eventIndex -= 1
425 e = self.getEvent(eventIndex)
426 if e and e.getTimes()[0] < minTime:
427 break
428
438
442
443
444
446 self.slavenames = names
447
457
468
474
488
490 """The Builder has decided to start a build, but the Build object is
491 not yet ready to report status (it has not finished creating the
492 Steps). Create a BuildStatus object that it can use."""
493 number = self.nextBuildNumber
494 self.nextBuildNumber += 1
495
496
497
498
499 s = BuildStatus(self, self.master, number)
500 s.waitUntilFinished().addCallback(self._buildFinished)
501 return s
502
503
505 """Now the BuildStatus object is ready to go (it knows all of its
506 Steps, its ETA, etc), so it is safe to notify our watchers."""
507
508 assert s.builder is self
509 assert s not in self.currentBuilds
510 self.currentBuilds.append(s)
511 self.buildCache.get(s.number, val=s)
512
513
514
515
516 for w in self.watchers:
517 try:
518 receiver = w.buildStarted(self.getName(), s)
519 if receiver:
520 if type(receiver) == type(()):
521 s.subscribe(receiver[0], receiver[1])
522 else:
523 s.subscribe(receiver)
524 d = s.waitUntilFinished()
525 d.addCallback(lambda s: s.unsubscribe(receiver))
526 except:
527 log.msg("Exception caught notifying %r of buildStarted event" % w)
528 log.err()
529
545
546
548 result = {}
549
550
551 result['basedir'] = os.path.basename(self.basedir)
552 result['category'] = self.category
553 result['slaves'] = self.slavenames
554 result['schedulers'] = [ s.name
555 for s in self.status.master.allSchedulers()
556 if self.name in s.builderNames ]
557
558
559
560
561
562
563 current_builds = [b.getNumber() for b in self.currentBuilds]
564 cached_builds = list(set(self.buildCache.keys() + current_builds))
565 cached_builds.sort()
566 result['cachedBuilds'] = cached_builds
567 result['currentBuilds'] = current_builds
568 result['state'] = self.getState()[0]
569
570
571 result['pendingBuilds'] = 0
572 return result
573
575 """Just like L{asDict}, but with a nonzero pendingBuilds."""
576 result = self.asDict()
577 d = self.getPendingBuildRequestStatuses()
578 def combine(statuses):
579 result['pendingBuilds'] = len(statuses)
580 return result
581 d.addCallback(combine)
582 return d
583
586
587
588