Package buildbot :: Package status :: Module builder
[frames] | no frames]

Source Code for Module buildbot.status.builder

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 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  # user modules expect these symbols to be present here 
 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   
37 -class BuilderStatus(styles.Versioned):
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" # or idle/waiting/interlocked/building 63 basedir = None # filled in by our parent 64
65 - def __init__(self, buildername, category, master, description):
66 self.name = buildername 67 self.category = category 68 self.description = description 69 self.master = master 70 71 self.slavenames = [] 72 self.events = [] 73 # these three hold Events, and are used to retrieve the current 74 # state of the boxes. 75 self.lastBuildStatus = None 76 #self.currentBig = None 77 #self.currentSmall = None 78 self.currentBuilds = [] 79 self.nextBuild = None 80 self.watchers = [] 81 self.buildCache = LRUCache(self.cacheMiss)
82 83 # persistence 84
85 - def __getstate__(self):
86 # when saving, don't record transient stuff like what builds are 87 # currently running, because they won't be there when we start back 88 # up. Nor do we save self.watchers, nor anything that gets set by our 89 # parent like .basedir and .status 90 d = styles.Versioned.__getstate__(self) 91 d['watchers'] = [] 92 del d['buildCache'] 93 for b in self.currentBuilds: 94 b.saveYourself() 95 # TODO: push a 'hey, build was interrupted' event 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
105 - def __setstate__(self, d):
106 # when loading, re-initialize the transient stuff. Remember that 107 # upgradeToVersion1 and such will be called after this finishes. 108 styles.Versioned.__setstate__(self, d) 109 self.buildCache = LRUCache(self.cacheMiss) 110 self.currentBuilds = [] 111 self.watchers = [] 112 self.slavenames = []
113 # self.basedir must be filled in by our parent 114 # self.status must be filled in by our parent 115 # self.master must be filled in by our parent 116
117 - def upgradeToVersion1(self):
118 if hasattr(self, 'slavename'): 119 self.slavenames = [self.slavename] 120 del self.slavename 121 if hasattr(self, 'nextBuildNumber'): 122 del self.nextBuildNumber # determineNextBuildNumber chooses this 123 self.wasUpgraded = True
124
125 - def determineNextBuildNumber(self):
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
139 - def saveYourself(self):
140 for b in self.currentBuilds: 141 if not b.isFinished: 142 # interrupted build, need to save it anyway. 143 # BuildStatus.saveYourself will mark it as interrupted. 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 # windows cannot rename a file on top of an existing one 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 # build cache management 160
161 - def setCacheSize(self, size):
162 self.buildCache.set_max_size(size)
163
164 - def makeBuildFilename(self, number):
165 return os.path.join(self.basedir, "%d" % number)
166
167 - def getBuildByNumber(self, number):
168 return self.buildCache.get(number)
169
170 - def loadBuildFromFile(self, number):
171 filename = self.makeBuildFilename(number) 172 try: 173 log.msg("Loading builder %s's build %d from on-disk pickle" 174 % (self.name, number)) 175 with open(filename, "rb") as f: 176 build = load(f) 177 build.setProcessObjects(self, self.master) 178 179 # (bug #1068) if we need to upgrade, we probably need to rewrite 180 # this pickle, too. We determine this by looking at the list of 181 # Versioned objects that have been unpickled, and (after doUpgrade) 182 # checking to see if any of them set wasUpgraded. The Versioneds' 183 # upgradeToVersionNN methods all set this. 184 versioneds = styles.versionedsToUpgrade 185 styles.doUpgrade() 186 if True in [ hasattr(o, 'wasUpgraded') for o in versioneds.values() ]: 187 log.msg("re-writing upgraded build pickle") 188 build.saveYourself() 189 190 # check that logfiles exist 191 build.checkLogfiles() 192 return build 193 except IOError: 194 raise IndexError("no such build %d" % number) 195 except EOFError: 196 raise IndexError("corrupted build pickle %d" % number)
197
198 - def cacheMiss(self, number, **kwargs):
199 # If kwargs['val'] exists, this is a new value being added to 200 # the cache. Just return it. 201 if 'val' in kwargs: 202 return kwargs['val'] 203 204 # first look in currentBuilds 205 for b in self.currentBuilds: 206 if b.number == number: 207 return b 208 209 # then fall back to loading it from disk 210 return self.loadBuildFromFile(number)
211
212 - def prune(self, events_only=False):
213 # begin by pruning our own events 214 eventHorizon = self.master.config.eventHorizon 215 self.events = self.events[-eventHorizon:] 216 217 if events_only: 218 return 219 220 # get the horizons straight 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 # skim the directory and delete anything that shouldn't be there anymore 240 build_re = re.compile(r"^([0-9]+)$") 241 build_log_re = re.compile(r"^([0-9]+)-.*$") 242 # if the directory doesn't exist, bail out here 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 # IBuilderStatus methods
268 - def getName(self):
269 # if builderstatus page does show not up without any reason then 270 # str(self.name) may be a workaround 271 return self.name
272
273 - def setDescription(self, description):
274 # used during reconfig 275 self.description = description
276
277 - def getDescription(self):
278 return self.description
279
280 - def getState(self):
281 return (self.currentBigState, self.currentBuilds)
282
283 - def getSlaves(self):
284 return [self.status.getSlave(name) for name in self.slavenames]
285
287 db = self.status.master.db 288 d = db.buildrequests.getBuildRequests(claimed=False, 289 buildername=self.name) 290 def make_statuses(brdicts): 291 return [BuildRequestStatus(self.name, brdict['brid'], 292 self.status) 293 for brdict in brdicts]
294 d.addCallback(make_statuses) 295 return d
296
297 - def getCurrentBuilds(self):
298 return self.currentBuilds
299
300 - def getLastFinishedBuild(self):
301 b = self.getBuild(-1) 302 if not (b and b.isFinished()): 303 b = self.getBuild(-2) 304 return b
305
306 - def setCategory(self, category):
307 # used during reconfig 308 self.category = category
309
310 - def getCategory(self):
311 return self.category
312
313 - def getBuild(self, number):
314 if number < 0: 315 number = self.nextBuildNumber + number 316 if number < 0 or number >= self.nextBuildNumber: 317 return None 318 319 try: 320 return self.getBuildByNumber(number) 321 except IndexError: 322 return None
323
324 - def getEvent(self, number):
325 try: 326 return self.events[number] 327 except IndexError: 328 return None
329
330 - def _getBuildBranches(self, build):
331 return set([ ss.branch 332 for ss in build.getSourceStamps() ])
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 # if we were asked to filter on branches, and none of the 361 # sourcestamps match, skip this build 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 # remember the oldest-to-earliest flow here. "next" means earlier. 382 383 # TODO: interleave build steps and self.events by timestamp. 384 # TODO: um, I think we're already doing that. 385 386 # TODO: there's probably something clever we could do here to 387 # interleave two event streams (one from self.getBuild and the other 388 # from self.getEvent), which would be simpler than this control flow 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 # HACK: If this is the first build we are looking at, it is 397 # possible it's in progress but locked before it has written a 398 # pickle; in this case keep looking. 399 if Nb == 1: 400 continue 401 break 402 if b.getTimes()[0] < minTime: 403 break 404 # if we were asked to filter on branches, and none of the 405 # sourcestamps match, skip this build 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
429 - def subscribe(self, receiver):
430 # will get builderChangedState, buildStarted, buildFinished, 431 # requestSubmitted, requestCancelled. Note that a request which is 432 # resubmitted (due to a slave disconnect) will cause requestSubmitted 433 # to be invoked multiple times. 434 self.watchers.append(receiver) 435 self.publishState(receiver) 436 # our parent Status provides requestSubmitted and requestCancelled 437 self.status._builder_subscribe(self.name, receiver)
438
439 - def unsubscribe(self, receiver):
440 self.watchers.remove(receiver) 441 self.status._builder_unsubscribe(self.name, receiver)
442 443 ## Builder interface (methods called by the Builder which feeds us) 444
445 - def setSlavenames(self, names):
446 self.slavenames = names
447
448 - def addEvent(self, text=[]):
449 # this adds a duration event. When it is done, the user should call 450 # e.finish(). They can also mangle it by modifying .text 451 e = Event() 452 e.started = util.now() 453 e.text = text 454 self.events.append(e) 455 self.prune(events_only=True) 456 return e # they are free to mangle it further
457
458 - def addPointEvent(self, text=[]):
459 # this adds a point event, one which occurs as a single atomic 460 # instant of time. 461 e = Event() 462 e.started = util.now() 463 e.finished = 0 464 e.text = text 465 self.events.append(e) 466 self.prune(events_only=True) 467 return e # for consistency, but they really shouldn't touch it
468
469 - def setBigState(self, state):
470 needToUpdate = state != self.currentBigState 471 self.currentBigState = state 472 if needToUpdate: 473 self.publishState()
474
475 - def publishState(self, target=None):
476 state = self.currentBigState 477 478 if target is not None: 479 # unicast 480 target.builderChangedState(self.name, state) 481 return 482 for w in self.watchers: 483 try: 484 w.builderChangedState(self.name, state) 485 except: 486 log.msg("Exception caught publishing state to %r" % w) 487 log.err()
488
489 - def newBuild(self):
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 # TODO: self.saveYourself(), to make sure we don't forget about the 496 # build number we've just allocated. This is not quite as important 497 # as it was before we switch to determineNextBuildNumber, but I think 498 # it may still be useful to have the new build save itself. 499 s = BuildStatus(self, self.master, number) 500 s.waitUntilFinished().addCallback(self._buildFinished) 501 return s
502 503 # buildStarted is called by our child BuildStatus instances
504 - def buildStarted(self, s):
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 # paranoia 509 assert s not in self.currentBuilds 510 self.currentBuilds.append(s) 511 self.buildCache.get(s.number, val=s) 512 513 # now that the BuildStatus is prepared to answer queries, we can 514 # announce the new build to all our watchers 515 516 for w in self.watchers: # TODO: maybe do this later? callLater(0)? 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
530 - def _buildFinished(self, s):
531 assert s in self.currentBuilds 532 s.saveYourself() 533 self.currentBuilds.remove(s) 534 535 name = self.getName() 536 results = s.getResults() 537 for w in self.watchers: 538 try: 539 w.buildFinished(name, s, results) 540 except: 541 log.msg("Exception caught notifying %r of buildFinished event" % w) 542 log.err() 543 544 self.prune() # conserve disk
545 546
547 - def asDict(self):
548 result = {} 549 # Constant 550 # TODO(maruel): Fix me. We don't want to leak the full path. 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 #result['url'] = self.parent.getURLForThing(self) 558 # TODO(maruel): Add cache settings? Do we care? 559 560 # Transient 561 # Collect build numbers. 562 # Important: Only grab the *cached* builds numbers to reduce I/O. 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 # lies, but we don't have synchronous access to this info; use 570 # asDict_async instead 571 result['pendingBuilds'] = 0 572 return result
573
574 - def asDict_async(self):
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
584 - def getMetrics(self):
585 return self.botmaster.parent.metrics
586 587 # vim: set ts=4 sts=4 sw=4 et: 588