(refs #35) fix dependent scheduler re-checking: make calculation of upstream lazier
[buildbot.git] / buildbot / scheduler.py
blob313dee076228fee8e4c2b89b17b196382814ee3b
1 # -*- test-case-name: buildbot.test.test_dependencies -*-
3 import time, os.path
5 from zope.interface import implements
6 from twisted.internet import reactor
7 from twisted.application import service, internet, strports
8 from twisted.python import log, runtime
9 from twisted.protocols import basic
10 from twisted.cred import portal, checkers
11 from twisted.spread import pb
13 from buildbot import interfaces, buildset, util, pbutil
14 from buildbot.status import builder
15 from buildbot.sourcestamp import SourceStamp
16 from buildbot.changes.maildir import MaildirService
17 from buildbot.process.properties import Properties
20 class BaseScheduler(service.MultiService, util.ComparableMixin):
21 """
22 A Schduler creates BuildSets and submits them to the BuildMaster.
24 @ivar name: name of the scheduler
26 @ivar properties: additional properties specified in this
27 scheduler's configuration
28 @type properties: Properties object
29 """
30 implements(interfaces.IScheduler)
32 def __init__(self, name, properties={}):
33 """
34 @param name: name for this scheduler
36 @param properties: properties to be propagated from this scheduler
37 @type properties: dict
38 """
39 service.MultiService.__init__(self)
40 self.name = name
41 self.properties = Properties()
42 self.properties.update(properties, "Scheduler")
43 self.properties.setProperty("scheduler", name, "Scheduler")
45 def __repr__(self):
46 # TODO: why can't id() return a positive number? %d is ugly.
47 return "<Scheduler '%s' at %d>" % (self.name, id(self))
49 def submitBuildSet(self, bs):
50 self.parent.submitBuildSet(bs)
52 def addChange(self, change):
53 pass
55 class BaseUpstreamScheduler(BaseScheduler):
56 implements(interfaces.IUpstreamScheduler)
58 def __init__(self, name, properties={}):
59 BaseScheduler.__init__(self, name, properties)
60 self.successWatchers = []
62 def subscribeToSuccessfulBuilds(self, watcher):
63 self.successWatchers.append(watcher)
64 def unsubscribeToSuccessfulBuilds(self, watcher):
65 self.successWatchers.remove(watcher)
67 def submitBuildSet(self, bs):
68 d = bs.waitUntilFinished()
69 d.addCallback(self.buildSetFinished)
70 BaseScheduler.submitBuildSet(self, bs)
72 def buildSetFinished(self, bss):
73 if not self.running:
74 return
75 if bss.getResults() == builder.SUCCESS:
76 ss = bss.getSourceStamp()
77 for w in self.successWatchers:
78 w(ss)
81 class Scheduler(BaseUpstreamScheduler):
82 """The default Scheduler class will run a build after some period of time
83 called the C{treeStableTimer}, on a given set of Builders. It only pays
84 attention to a single branch. You you can provide a C{fileIsImportant}
85 function which will evaluate each Change to decide whether or not it
86 should trigger a new build.
87 """
89 fileIsImportant = None
90 compare_attrs = ('name', 'treeStableTimer', 'builderNames', 'branch',
91 'fileIsImportant', 'properties', 'categories')
93 def __init__(self, name, branch, treeStableTimer, builderNames,
94 fileIsImportant=None, properties={}, categories=None):
95 """
96 @param name: the name of this Scheduler
97 @param branch: The branch name that the Scheduler should pay
98 attention to. Any Change that is not on this branch
99 will be ignored. It can be set to None to only pay
100 attention to the default branch.
101 @param treeStableTimer: the duration, in seconds, for which the tree
102 must remain unchanged before a build will be
103 triggered. This is intended to avoid builds
104 of partially-committed fixes.
105 @param builderNames: a list of Builder names. When this Scheduler
106 decides to start a set of builds, they will be
107 run on the Builders named by this list.
109 @param fileIsImportant: A callable which takes one argument (a Change
110 instance) and returns True if the change is
111 worth building, and False if it is not.
112 Unimportant Changes are accumulated until the
113 build is triggered by an important change.
114 The default value of None means that all
115 Changes are important.
117 @param properties: properties to apply to all builds started from this
118 scheduler
119 @param categories: A list of categories of changes to accept
122 BaseUpstreamScheduler.__init__(self, name, properties)
123 self.treeStableTimer = treeStableTimer
124 errmsg = ("The builderNames= argument to Scheduler must be a list "
125 "of Builder description names (i.e. the 'name' key of the "
126 "Builder specification dictionary)")
127 assert isinstance(builderNames, (list, tuple)), errmsg
128 for b in builderNames:
129 assert isinstance(b, str), errmsg
130 self.builderNames = builderNames
131 self.branch = branch
132 if fileIsImportant:
133 assert callable(fileIsImportant)
134 self.fileIsImportant = fileIsImportant
136 self.importantChanges = []
137 self.unimportantChanges = []
138 self.nextBuildTime = None
139 self.timer = None
140 self.categories = categories
142 def listBuilderNames(self):
143 return self.builderNames
145 def getPendingBuildTimes(self):
146 if self.nextBuildTime is not None:
147 return [self.nextBuildTime]
148 return []
150 def addChange(self, change):
151 if change.branch != self.branch:
152 log.msg("%s ignoring off-branch %s" % (self, change))
153 return
154 if self.categories is not None and change.category not in self.categories:
155 log.msg("%s ignoring non-matching categories %s" % (self, change))
156 return
157 if not self.fileIsImportant:
158 self.addImportantChange(change)
159 elif self.fileIsImportant(change):
160 self.addImportantChange(change)
161 else:
162 self.addUnimportantChange(change)
164 def addImportantChange(self, change):
165 log.msg("%s: change is important, adding %s" % (self, change))
166 self.importantChanges.append(change)
167 self.nextBuildTime = max(self.nextBuildTime,
168 change.when + self.treeStableTimer)
169 self.setTimer(self.nextBuildTime)
171 def addUnimportantChange(self, change):
172 log.msg("%s: change is not important, adding %s" % (self, change))
173 self.unimportantChanges.append(change)
175 def setTimer(self, when):
176 log.msg("%s: setting timer to %s" %
177 (self, time.strftime("%H:%M:%S", time.localtime(when))))
178 now = util.now()
179 if when < now:
180 when = now
181 if self.timer:
182 self.timer.cancel()
183 self.timer = reactor.callLater(when - now, self.fireTimer)
185 def stopTimer(self):
186 if self.timer:
187 self.timer.cancel()
188 self.timer = None
190 def fireTimer(self):
191 # clear out our state
192 self.timer = None
193 self.nextBuildTime = None
194 changes = self.importantChanges + self.unimportantChanges
195 self.importantChanges = []
196 self.unimportantChanges = []
198 # create a BuildSet, submit it to the BuildMaster
199 bs = buildset.BuildSet(self.builderNames,
200 SourceStamp(changes=changes),
201 properties=self.properties)
202 self.submitBuildSet(bs)
204 def stopService(self):
205 self.stopTimer()
206 return service.MultiService.stopService(self)
209 class AnyBranchScheduler(BaseUpstreamScheduler):
210 """This Scheduler will handle changes on a variety of branches. It will
211 accumulate Changes for each branch separately. It works by creating a
212 separate Scheduler for each new branch it sees."""
214 schedulerFactory = Scheduler
215 fileIsImportant = None
217 compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames',
218 'fileIsImportant', 'properties')
220 def __init__(self, name, branches, treeStableTimer, builderNames,
221 fileIsImportant=None, properties={}):
223 @param name: the name of this Scheduler
224 @param branches: The branch names that the Scheduler should pay
225 attention to. Any Change that is not on one of these
226 branches will be ignored. It can be set to None to
227 accept changes from any branch. Don't use [] (an
228 empty list), because that means we don't pay
229 attention to *any* branches, so we'll never build
230 anything.
231 @param treeStableTimer: the duration, in seconds, for which the tree
232 must remain unchanged before a build will be
233 triggered. This is intended to avoid builds
234 of partially-committed fixes.
235 @param builderNames: a list of Builder names. When this Scheduler
236 decides to start a set of builds, they will be
237 run on the Builders named by this list.
239 @param fileIsImportant: A callable which takes one argument (a Change
240 instance) and returns True if the change is
241 worth building, and False if it is not.
242 Unimportant Changes are accumulated until the
243 build is triggered by an important change.
244 The default value of None means that all
245 Changes are important.
247 @param properties: properties to apply to all builds started from this
248 scheduler
251 BaseUpstreamScheduler.__init__(self, name, properties)
252 self.treeStableTimer = treeStableTimer
253 for b in builderNames:
254 assert isinstance(b, str)
255 self.builderNames = builderNames
256 self.branches = branches
257 if self.branches == []:
258 log.msg("AnyBranchScheduler %s: branches=[], so we will ignore "
259 "all branches, and never trigger any builds. Please set "
260 "branches=None to mean 'all branches'" % self)
261 # consider raising an exception here, to make this warning more
262 # prominent, but I can vaguely imagine situations where you might
263 # want to comment out branches temporarily and wouldn't
264 # appreciate it being treated as an error.
265 if fileIsImportant:
266 assert callable(fileIsImportant)
267 self.fileIsImportant = fileIsImportant
268 self.schedulers = {} # one per branch
270 def __repr__(self):
271 return "<AnyBranchScheduler '%s'>" % self.name
273 def listBuilderNames(self):
274 return self.builderNames
276 def getPendingBuildTimes(self):
277 bts = []
278 for s in self.schedulers.values():
279 if s.nextBuildTime is not None:
280 bts.append(s.nextBuildTime)
281 return bts
283 def buildSetFinished(self, bss):
284 # we don't care if a build has finished; one of the per-branch builders
285 # will take care of it, instead.
286 pass
288 def addChange(self, change):
289 branch = change.branch
290 if self.branches is not None and branch not in self.branches:
291 log.msg("%s ignoring off-branch %s" % (self, change))
292 return
293 s = self.schedulers.get(branch)
294 if not s:
295 if branch:
296 name = self.name + "." + branch
297 else:
298 name = self.name + ".<default>"
299 s = self.schedulerFactory(name, branch,
300 self.treeStableTimer,
301 self.builderNames,
302 self.fileIsImportant)
303 s.successWatchers = self.successWatchers
304 s.setServiceParent(self)
305 s.properties = self.properties
306 # TODO: does this result in schedulers that stack up forever?
307 # When I make the persistify-pass, think about this some more.
308 self.schedulers[branch] = s
309 s.addChange(change)
312 class Dependent(BaseUpstreamScheduler):
313 """This scheduler runs some set of 'downstream' builds when the
314 'upstream' scheduler has completed successfully."""
315 implements(interfaces.IDownstreamScheduler)
317 compare_attrs = ('name', 'upstream', 'builderNames', 'properties')
319 def __init__(self, name, upstream, builderNames, properties={}):
320 assert interfaces.IUpstreamScheduler.providedBy(upstream)
321 BaseUpstreamScheduler.__init__(self, name, properties)
322 self.upstream_name = upstream.name
323 self.upstream = None
324 self.builderNames = builderNames
326 def listBuilderNames(self):
327 return self.builderNames
329 def getPendingBuildTimes(self):
330 # report the upstream's value
331 return self.getUpstreamScheduler().getPendingBuildTimes()
333 def startService(self):
334 service.MultiService.startService(self)
335 self.upstream = self.findUpstreamScheduler()
336 self.upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt)
338 def stopService(self):
339 d = service.MultiService.stopService(self)
340 self.upstream.unsubscribeToSuccessfulBuilds(self.upstreamBuilt)
341 self.upstream = None
342 return d
344 def upstreamBuilt(self, ss):
345 bs = buildset.BuildSet(self.builderNames, ss,
346 properties=self.properties)
347 self.submitBuildSet(bs)
349 def findUpstreamScheduler(self):
350 # find our *active* upstream scheduler (which may not be self.upstream!) by name
351 upstream = None
352 for s in self.parent.allSchedulers():
353 if s.name == self.upstream_name and interfaces.IUpstreamScheduler.providedBy(s):
354 upstream = s
355 if not upstream:
356 log.msg("ERROR: Couldn't find upstream scheduler of name <%s>" %
357 self.upstream_name)
358 return upstream
360 def checkUpstreamScheduler(self):
361 # if we don't already have an upstream, then there's nothing to worry about
362 if not self.upstream:
363 return
365 upstream = self.findUpstreamScheduler()
367 # if it's already correct, we're good to go
368 if upstream is self.upstream:
369 return
371 # otherwise, associate with the new upstream. We also keep listening
372 # to the old upstream, in case it's in the middle of a build
373 upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt)
374 self.upstream = upstream
375 log.msg("Dependent <%s> connected to new Upstream <%s>" %
376 (self.name, up_name))
378 class Periodic(BaseUpstreamScheduler):
379 """Instead of watching for Changes, this Scheduler can just start a build
380 at fixed intervals. The C{periodicBuildTimer} parameter sets the number
381 of seconds to wait between such periodic builds. The first build will be
382 run immediately."""
384 # TODO: consider having this watch another (changed-based) scheduler and
385 # merely enforce a minimum time between builds.
387 compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch', 'properties')
389 def __init__(self, name, builderNames, periodicBuildTimer,
390 branch=None, properties={}):
391 BaseUpstreamScheduler.__init__(self, name, properties)
392 self.builderNames = builderNames
393 self.periodicBuildTimer = periodicBuildTimer
394 self.branch = branch
395 self.reason = ("The Periodic scheduler named '%s' triggered this build"
396 % name)
397 self.timer = internet.TimerService(self.periodicBuildTimer,
398 self.doPeriodicBuild)
399 self.timer.setServiceParent(self)
401 def listBuilderNames(self):
402 return self.builderNames
404 def getPendingBuildTimes(self):
405 # TODO: figure out when self.timer is going to fire next and report
406 # that
407 return []
409 def doPeriodicBuild(self):
410 bs = buildset.BuildSet(self.builderNames,
411 SourceStamp(branch=self.branch),
412 self.reason,
413 properties=self.properties)
414 self.submitBuildSet(bs)
418 class Nightly(BaseUpstreamScheduler):
419 """Imitate 'cron' scheduling. This can be used to schedule a nightly
420 build, or one which runs are certain times of the day, week, or month.
422 Pass some subset of minute, hour, dayOfMonth, month, and dayOfWeek; each
423 may be a single number or a list of valid values. The builds will be
424 triggered whenever the current time matches these values. Wildcards are
425 represented by a '*' string. All fields default to a wildcard except
426 'minute', so with no fields this defaults to a build every hour, on the
427 hour.
429 For example, the following master.cfg clause will cause a build to be
430 started every night at 3:00am::
432 s = Nightly('nightly', ['builder1', 'builder2'], hour=3, minute=0)
433 c['schedules'].append(s)
435 This scheduler will perform a build each monday morning at 6:23am and
436 again at 8:23am::
438 s = Nightly('BeforeWork', ['builder1'],
439 dayOfWeek=0, hour=[6,8], minute=23)
441 The following runs a build every two hours::
443 s = Nightly('every2hours', ['builder1'], hour=range(0, 24, 2))
445 And this one will run only on December 24th::
447 s = Nightly('SleighPreflightCheck', ['flying_circuits', 'radar'],
448 month=12, dayOfMonth=24, hour=12, minute=0)
450 For dayOfWeek and dayOfMonth, builds are triggered if the date matches
451 either of them. All time values are compared against the tuple returned
452 by time.localtime(), so month and dayOfMonth numbers start at 1, not
453 zero. dayOfWeek=0 is Monday, dayOfWeek=6 is Sunday.
455 onlyIfChanged functionality
456 s = Nightly('nightly', ['builder1', 'builder2'],
457 hour=3, minute=0, onlyIfChanged=True)
458 When the flag is True (False by default), the build is trigged if
459 the date matches and if the branch has changed
461 fileIsImportant parameter is implemented as defined in class Scheduler
464 compare_attrs = ('name', 'builderNames',
465 'minute', 'hour', 'dayOfMonth', 'month',
466 'dayOfWeek', 'branch', 'onlyIfChanged',
467 'fileIsImportant', 'properties')
469 def __init__(self, name, builderNames, minute=0, hour='*',
470 dayOfMonth='*', month='*', dayOfWeek='*',
471 branch=None, fileIsImportant=None, onlyIfChanged=False, properties={}):
472 # Setting minute=0 really makes this an 'Hourly' scheduler. This
473 # seemed like a better default than minute='*', which would result in
474 # a build every 60 seconds.
475 BaseUpstreamScheduler.__init__(self, name, properties)
476 self.builderNames = builderNames
477 self.minute = minute
478 self.hour = hour
479 self.dayOfMonth = dayOfMonth
480 self.month = month
481 self.dayOfWeek = dayOfWeek
482 self.branch = branch
483 self.onlyIfChanged = onlyIfChanged
484 self.delayedRun = None
485 self.nextRunTime = None
486 self.reason = ("The Nightly scheduler named '%s' triggered this build"
487 % name)
489 self.importantChanges = []
490 self.unimportantChanges = []
491 self.fileIsImportant = None
492 if fileIsImportant:
493 assert callable(fileIsImportant)
494 self.fileIsImportant = fileIsImportant
496 def addTime(self, timetuple, secs):
497 return time.localtime(time.mktime(timetuple)+secs)
498 def findFirstValueAtLeast(self, values, value, default=None):
499 for v in values:
500 if v >= value: return v
501 return default
503 def setTimer(self):
504 self.nextRunTime = self.calculateNextRunTime()
505 self.delayedRun = reactor.callLater(self.nextRunTime - time.time(),
506 self.doPeriodicBuild)
508 def startService(self):
509 BaseUpstreamScheduler.startService(self)
510 self.setTimer()
512 def stopService(self):
513 BaseUpstreamScheduler.stopService(self)
514 self.delayedRun.cancel()
516 def isRunTime(self, timetuple):
517 def check(ourvalue, value):
518 if ourvalue == '*': return True
519 if isinstance(ourvalue, int): return value == ourvalue
520 return (value in ourvalue)
522 if not check(self.minute, timetuple[4]):
523 #print 'bad minute', timetuple[4], self.minute
524 return False
526 if not check(self.hour, timetuple[3]):
527 #print 'bad hour', timetuple[3], self.hour
528 return False
530 if not check(self.month, timetuple[1]):
531 #print 'bad month', timetuple[1], self.month
532 return False
534 if self.dayOfMonth != '*' and self.dayOfWeek != '*':
535 # They specified both day(s) of month AND day(s) of week.
536 # This means that we only have to match one of the two. If
537 # neither one matches, this time is not the right time.
538 if not (check(self.dayOfMonth, timetuple[2]) or
539 check(self.dayOfWeek, timetuple[6])):
540 #print 'bad day'
541 return False
542 else:
543 if not check(self.dayOfMonth, timetuple[2]):
544 #print 'bad day of month'
545 return False
547 if not check(self.dayOfWeek, timetuple[6]):
548 #print 'bad day of week'
549 return False
551 return True
553 def calculateNextRunTime(self):
554 return self.calculateNextRunTimeFrom(time.time())
556 def calculateNextRunTimeFrom(self, now):
557 dateTime = time.localtime(now)
559 # Remove seconds by advancing to at least the next minue
560 dateTime = self.addTime(dateTime, 60-dateTime[5])
562 # Now we just keep adding minutes until we find something that matches
564 # It not an efficient algorithm, but it'll *work* for now
565 yearLimit = dateTime[0]+2
566 while not self.isRunTime(dateTime):
567 dateTime = self.addTime(dateTime, 60)
568 #print 'Trying', time.asctime(dateTime)
569 assert dateTime[0] < yearLimit, 'Something is wrong with this code'
570 return time.mktime(dateTime)
572 def listBuilderNames(self):
573 return self.builderNames
575 def getPendingBuildTimes(self):
576 # TODO: figure out when self.timer is going to fire next and report
577 # that
578 if self.nextRunTime is None: return []
579 return [self.nextRunTime]
581 def doPeriodicBuild(self):
582 # Schedule the next run
583 self.setTimer()
585 if self.onlyIfChanged:
586 if len(self.importantChanges) > 0:
587 changes = self.importantChanges + self.unimportantChanges
588 # And trigger a build
589 log.msg("Nightly Scheduler <%s>: triggering build" % self.name)
590 bs = buildset.BuildSet(self.builderNames,
591 SourceStamp(changes=changes),
592 self.reason,
593 properties=self.properties)
594 self.submitBuildSet(bs)
595 # Reset the change lists
596 self.importantChanges = []
597 self.unimportantChanges = []
598 else:
599 log.msg("Nightly Scheduler <%s>: skipping build - No important change" % self.name)
600 else:
601 # And trigger a build
602 bs = buildset.BuildSet(self.builderNames,
603 SourceStamp(branch=self.branch),
604 self.reason,
605 properties=self.properties)
606 self.submitBuildSet(bs)
608 def addChange(self, change):
609 if self.onlyIfChanged:
610 if change.branch != self.branch:
611 log.msg("Nightly Scheduler <%s>: ignoring change %s on off-branch %s" % (self.name, change.revision, change.branch))
612 return
613 if not self.fileIsImportant:
614 self.addImportantChange(change)
615 elif self.fileIsImportant(change):
616 self.addImportantChange(change)
617 else:
618 self.addUnimportantChange(change)
619 else:
620 log.msg("Nightly Scheduler <%s>: no add change" % self.name)
621 pass
623 def addImportantChange(self, change):
624 log.msg("Nightly Scheduler <%s>: change %s from %s is important, adding it" % (self.name, change.revision, change.who))
625 self.importantChanges.append(change)
627 def addUnimportantChange(self, change):
628 log.msg("Nightly Scheduler <%s>: change %s from %s is not important, adding it" % (self.name, change.revision, change.who))
629 self.unimportantChanges.append(change)
632 class TryBase(BaseScheduler):
633 def __init__(self, name, builderNames, properties={}):
634 BaseScheduler.__init__(self, name, properties)
635 self.builderNames = builderNames
637 def listBuilderNames(self):
638 return self.builderNames
640 def getPendingBuildTimes(self):
641 # we can't predict what the developers are going to do in the future
642 return []
644 def addChange(self, change):
645 # Try schedulers ignore Changes
646 pass
648 def processBuilderList(self, builderNames):
649 # self.builderNames is the configured list of builders
650 # available for try. If the user supplies a list of builders,
651 # it must be restricted to the configured list. If not, build
652 # on all of the configured builders.
653 if builderNames:
654 for b in builderNames:
655 if not b in self.builderNames:
656 log.msg("%s got with builder %s" % (self, b))
657 log.msg(" but that wasn't in our list: %s"
658 % (self.builderNames,))
659 return []
660 else:
661 builderNames = self.builderNames
662 return builderNames
664 class BadJobfile(Exception):
665 pass
667 class JobFileScanner(basic.NetstringReceiver):
668 def __init__(self):
669 self.strings = []
670 self.transport = self # so transport.loseConnection works
671 self.error = False
673 def stringReceived(self, s):
674 self.strings.append(s)
676 def loseConnection(self):
677 self.error = True
679 class Try_Jobdir(TryBase):
680 compare_attrs = ( 'name', 'builderNames', 'jobdir', 'properties' )
682 def __init__(self, name, builderNames, jobdir, properties={}):
683 TryBase.__init__(self, name, builderNames, properties)
684 self.jobdir = jobdir
685 self.watcher = MaildirService()
686 self.watcher.setServiceParent(self)
688 def setServiceParent(self, parent):
689 self.watcher.setBasedir(os.path.join(parent.basedir, self.jobdir))
690 TryBase.setServiceParent(self, parent)
692 def parseJob(self, f):
693 # jobfiles are serialized build requests. Each is a list of
694 # serialized netstrings, in the following order:
695 # "1", the version number of this format
696 # buildsetID, arbitrary string, used to find the buildSet later
697 # branch name, "" for default-branch
698 # base revision, "" for HEAD
699 # patchlevel, usually "1"
700 # patch
701 # builderNames...
702 p = JobFileScanner()
703 p.dataReceived(f.read())
704 if p.error:
705 raise BadJobfile("unable to parse netstrings")
706 s = p.strings
707 ver = s.pop(0)
708 if ver != "1":
709 raise BadJobfile("unknown version '%s'" % ver)
710 buildsetID, branch, baserev, patchlevel, diff = s[:5]
711 builderNames = s[5:]
712 if branch == "":
713 branch = None
714 if baserev == "":
715 baserev = None
716 patchlevel = int(patchlevel)
717 patch = (patchlevel, diff)
718 ss = SourceStamp(branch, baserev, patch)
719 return builderNames, ss, buildsetID
721 def messageReceived(self, filename):
722 md = os.path.join(self.parent.basedir, self.jobdir)
723 if runtime.platformType == "posix":
724 # open the file before moving it, because I'm afraid that once
725 # it's in cur/, someone might delete it at any moment
726 path = os.path.join(md, "new", filename)
727 f = open(path, "r")
728 os.rename(os.path.join(md, "new", filename),
729 os.path.join(md, "cur", filename))
730 else:
731 # do this backwards under windows, because you can't move a file
732 # that somebody is holding open. This was causing a Permission
733 # Denied error on bear's win32-twisted1.3 buildslave.
734 os.rename(os.path.join(md, "new", filename),
735 os.path.join(md, "cur", filename))
736 path = os.path.join(md, "cur", filename)
737 f = open(path, "r")
739 try:
740 builderNames, ss, bsid = self.parseJob(f)
741 except BadJobfile:
742 log.msg("%s reports a bad jobfile in %s" % (self, filename))
743 log.err()
744 return
745 # Validate/fixup the builder names.
746 builderNames = self.processBuilderList(builderNames)
747 if not builderNames:
748 return
749 reason = "'try' job"
750 bs = buildset.BuildSet(builderNames, ss, reason=reason,
751 bsid=bsid, properties=self.properties)
752 self.submitBuildSet(bs)
754 class Try_Userpass(TryBase):
755 compare_attrs = ( 'name', 'builderNames', 'port', 'userpass', 'properties' )
756 implements(portal.IRealm)
758 def __init__(self, name, builderNames, port, userpass, properties={}):
759 TryBase.__init__(self, name, builderNames, properties)
760 if type(port) is int:
761 port = "tcp:%d" % port
762 self.port = port
763 self.userpass = userpass
764 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
765 for user,passwd in self.userpass:
766 c.addUser(user, passwd)
768 p = portal.Portal(self)
769 p.registerChecker(c)
770 f = pb.PBServerFactory(p)
771 s = strports.service(port, f)
772 s.setServiceParent(self)
774 def getPort(self):
775 # utility method for tests: figure out which TCP port we just opened.
776 return self.services[0]._port.getHost().port
778 def requestAvatar(self, avatarID, mind, interface):
779 log.msg("%s got connection from user %s" % (self, avatarID))
780 assert interface == pb.IPerspective
781 p = Try_Userpass_Perspective(self, avatarID)
782 return (pb.IPerspective, p, lambda: None)
784 class Try_Userpass_Perspective(pbutil.NewCredPerspective):
785 def __init__(self, parent, username):
786 self.parent = parent
787 self.username = username
789 def perspective_try(self, branch, revision, patch, builderNames, properties={}):
790 log.msg("user %s requesting build on builders %s" % (self.username,
791 builderNames))
792 # Validate/fixup the builder names.
793 builderNames = self.parent.processBuilderList(builderNames)
794 if not builderNames:
795 return
796 ss = SourceStamp(branch, revision, patch)
797 reason = "'try' job from user %s" % self.username
799 # roll the specified props in with our inherited props
800 combined_props = Properties()
801 combined_props.updateFromProperties(self.parent.properties)
802 combined_props.update(properties, "try build")
804 bs = buildset.BuildSet(builderNames,
806 reason=reason,
807 properties=combined_props)
809 self.parent.submitBuildSet(bs)
811 # return a remotely-usable BuildSetStatus object
812 from buildbot.status.client import makeRemote
813 return makeRemote(bs.status)
815 class Triggerable(BaseUpstreamScheduler):
816 """This scheduler doesn't do anything until it is triggered by a Trigger
817 step in a factory. In general, that step will not complete until all of
818 the builds that I fire have finished.
821 compare_attrs = ('name', 'builderNames', 'properties')
823 def __init__(self, name, builderNames, properties={}):
824 BaseUpstreamScheduler.__init__(self, name, properties)
825 self.builderNames = builderNames
827 def listBuilderNames(self):
828 return self.builderNames
830 def getPendingBuildTimes(self):
831 return []
833 def trigger(self, ss, set_props=None):
834 """Trigger this scheduler. Returns a deferred that will fire when the
835 buildset is finished.
838 # properties for this buildset are composed of our own properties,
839 # potentially overridden by anything from the triggering build
840 props = Properties()
841 props.updateFromProperties(self.properties)
842 if set_props: props.updateFromProperties(set_props)
844 bs = buildset.BuildSet(self.builderNames, ss, properties=props)
845 d = bs.waitUntilFinished()
846 self.submitBuildSet(bs)
847 return d