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):
66 self.name = buildername
67 self.category = category
68 self.master = master
69
70 self.slavenames = []
71 self.events = []
72
73
74 self.lastBuildStatus = None
75
76
77 self.currentBuilds = []
78 self.nextBuild = None
79 self.watchers = []
80 self.buildCache = LRUCache(self.cacheMiss)
81
82
83
85
86
87
88
89 d = styles.Versioned.__getstate__(self)
90 d['watchers'] = []
91 del d['buildCache']
92 for b in self.currentBuilds:
93 b.saveYourself()
94
95 del d['currentBuilds']
96 d.pop('pendingBuilds', None)
97 del d['currentBigState']
98 del d['basedir']
99 del d['status']
100 del d['nextBuildNumber']
101 del d['master']
102 return d
103
112
113
114
115
117 if hasattr(self, 'slavename'):
118 self.slavenames = [self.slavename]
119 del self.slavename
120 if hasattr(self, 'nextBuildNumber'):
121 del self.nextBuildNumber
122 self.wasUpgraded = True
123
125 """Scan our directory of saved BuildStatus instances to determine
126 what our self.nextBuildNumber should be. Set it one larger than the
127 highest-numbered build we discover. This is called by the top-level
128 Status object shortly after we are created or loaded from disk.
129 """
130 existing_builds = [int(f)
131 for f in os.listdir(self.basedir)
132 if re.match("^\d+$", f)]
133 if existing_builds:
134 self.nextBuildNumber = max(existing_builds) + 1
135 else:
136 self.nextBuildNumber = 0
137
139 for b in self.currentBuilds:
140 if not b.isFinished:
141
142
143 b.saveYourself()
144 filename = os.path.join(self.basedir, "builder")
145 tmpfilename = filename + ".tmp"
146 try:
147 with open(tmpfilename, "wb") as f:
148 dump(self, f, -1)
149 if runtime.platformType == 'win32':
150
151 if os.path.exists(filename):
152 os.unlink(filename)
153 os.rename(tmpfilename, filename)
154 except:
155 log.msg("unable to save builder %s" % self.name)
156 log.err()
157
158
159
162
165
168
196
198
199
200 if 'val' in kwargs:
201 return kwargs['val']
202
203
204 for b in self.currentBuilds:
205 if b.number == number:
206 return b
207
208
209 return self.loadBuildFromFile(number)
210
211 - def prune(self, events_only=False):
212
213 eventHorizon = self.master.config.eventHorizon
214 self.events = self.events[-eventHorizon:]
215
216 if events_only:
217 return
218
219
220 buildHorizon = self.master.config.buildHorizon
221 if buildHorizon is not None:
222 earliest_build = self.nextBuildNumber - buildHorizon
223 else:
224 earliest_build = 0
225
226 logHorizon = self.master.config.logHorizon
227 if logHorizon is not None:
228 earliest_log = self.nextBuildNumber - logHorizon
229 else:
230 earliest_log = 0
231
232 if earliest_log < earliest_build:
233 earliest_log = earliest_build
234
235 if earliest_build == 0:
236 return
237
238
239 build_re = re.compile(r"^([0-9]+)$")
240 build_log_re = re.compile(r"^([0-9]+)-.*$")
241
242 if not os.path.exists(self.basedir):
243 return
244
245 for filename in os.listdir(self.basedir):
246 num = None
247 mo = build_re.match(filename)
248 is_logfile = False
249 if mo:
250 num = int(mo.group(1))
251 else:
252 mo = build_log_re.match(filename)
253 if mo:
254 num = int(mo.group(1))
255 is_logfile = True
256
257 if num is None: continue
258 if num in self.buildCache.cache: continue
259
260 if (is_logfile and num < earliest_log) or num < earliest_build:
261 pathname = os.path.join(self.basedir, filename)
262 log.msg("pruning '%s'" % pathname)
263 try: os.unlink(pathname)
264 except OSError: pass
265
266
268
269
270 return self.name
271
274
277
286 d.addCallback(make_statuses)
287 return d
288
290 return self.currentBuilds
291
297
301
304
315
317 try:
318 return self.events[number]
319 except IndexError:
320 return None
321
325
326 - def generateFinishedBuilds(self, branches=[],
327 num_builds=None,
328 max_buildnum=None,
329 finished_before=None,
330 results=None,
331 max_search=200):
332 got = 0
333 branches = set(branches)
334 for Nb in itertools.count(1):
335 if Nb > self.nextBuildNumber:
336 break
337 if Nb > max_search:
338 break
339 build = self.getBuild(-Nb)
340 if build is None:
341 continue
342 if max_buildnum is not None:
343 if build.getNumber() > max_buildnum:
344 continue
345 if not build.isFinished():
346 continue
347 if finished_before is not None:
348 start, end = build.getTimes()
349 if end >= finished_before:
350 continue
351
352
353 if branches and not branches & self._getBuildBranches(build):
354 continue
355 if results is not None:
356 if build.getResults() not in results:
357 continue
358 got += 1
359 yield build
360 if num_builds is not None:
361 if got >= num_builds:
362 return
363
364 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
365 """This function creates a generator which will provide all of this
366 Builder's status events, starting with the most recent and
367 progressing backwards in time. """
368
369
370
371
372
373
374
375
376
377
378 eventIndex = -1
379 e = self.getEvent(eventIndex)
380 branches = set(branches)
381 for Nb in range(1, self.nextBuildNumber+1):
382 b = self.getBuild(-Nb)
383 if not b:
384
385
386
387 if Nb == 1:
388 continue
389 break
390 if b.getTimes()[0] < minTime:
391 break
392
393
394 if branches and not branches & self._getBuildBranches(b):
395 continue
396 if categories and not b.getBuilder().getCategory() in categories:
397 continue
398 if committers and not [True for c in b.getChanges() if c.who in committers]:
399 continue
400 steps = b.getSteps()
401 for Ns in range(1, len(steps)+1):
402 if steps[-Ns].started:
403 step_start = steps[-Ns].getTimes()[0]
404 while e is not None and e.getTimes()[0] > step_start:
405 yield e
406 eventIndex -= 1
407 e = self.getEvent(eventIndex)
408 yield steps[-Ns]
409 yield b
410 while e is not None:
411 yield e
412 eventIndex -= 1
413 e = self.getEvent(eventIndex)
414 if e and e.getTimes()[0] < minTime:
415 break
416
426
430
431
432
434 self.slavenames = names
435
445
456
462
476
478 """The Builder has decided to start a build, but the Build object is
479 not yet ready to report status (it has not finished creating the
480 Steps). Create a BuildStatus object that it can use."""
481 number = self.nextBuildNumber
482 self.nextBuildNumber += 1
483
484
485
486
487 s = BuildStatus(self, self.master, number)
488 s.waitUntilFinished().addCallback(self._buildFinished)
489 return s
490
491
493 """Now the BuildStatus object is ready to go (it knows all of its
494 Steps, its ETA, etc), so it is safe to notify our watchers."""
495
496 assert s.builder is self
497 assert s not in self.currentBuilds
498 self.currentBuilds.append(s)
499 self.buildCache.get(s.number, val=s)
500
501
502
503
504 for w in self.watchers:
505 try:
506 receiver = w.buildStarted(self.getName(), s)
507 if receiver:
508 if type(receiver) == type(()):
509 s.subscribe(receiver[0], receiver[1])
510 else:
511 s.subscribe(receiver)
512 d = s.waitUntilFinished()
513 d.addCallback(lambda s: s.unsubscribe(receiver))
514 except:
515 log.msg("Exception caught notifying %r of buildStarted event" % w)
516 log.err()
517
533
534
536 result = {}
537
538
539 result['basedir'] = os.path.basename(self.basedir)
540 result['category'] = self.category
541 result['slaves'] = self.slavenames
542 result['schedulers'] = [ s.name
543 for s in self.status.master.allSchedulers()
544 if self.name in s.builderNames ]
545
546
547
548
549
550
551 current_builds = [b.getNumber() for b in self.currentBuilds]
552 cached_builds = list(set(self.buildCache.keys() + current_builds))
553 cached_builds.sort()
554 result['cachedBuilds'] = cached_builds
555 result['currentBuilds'] = current_builds
556 result['state'] = self.getState()[0]
557
558
559 result['pendingBuilds'] = 0
560 return result
561
563 """Just like L{asDict}, but with a nonzero pendingBuilds."""
564 result = self.asDict()
565 d = self.getPendingBuildRequestStatuses()
566 def combine(statuses):
567 result['pendingBuilds'] = len(statuses)
568 return result
569 d.addCallback(combine)
570 return d
571
574
575
576