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