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