Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(282)

Side by Side Diff: third_party/buildbot_7_12/buildbot/process/buildstep.py

Issue 12207158: Bye bye buildbot 0.7.12. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 # -*- test-case-name: buildbot.test.test_steps -*-
2
3 from zope.interface import implements
4 from twisted.internet import reactor, defer, error
5 from twisted.protocols import basic
6 from twisted.spread import pb
7 from twisted.python import log
8 from twisted.python.failure import Failure
9 from twisted.web.util import formatFailure
10
11 from buildbot import interfaces, locks
12 from buildbot.status import progress
13 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, \
14 EXCEPTION
15
16 """
17 BuildStep and RemoteCommand classes for master-side representation of the
18 build process
19 """
20
21 class RemoteCommand(pb.Referenceable):
22 """
23 I represent a single command to be run on the slave. I handle the details
24 of reliably gathering status updates from the slave (acknowledging each),
25 and (eventually, in a future release) recovering from interrupted builds.
26 This is the master-side object that is known to the slave-side
27 L{buildbot.slave.bot.SlaveBuilder}, to which status updates are sent.
28
29 My command should be started by calling .run(), which returns a
30 Deferred that will fire when the command has finished, or will
31 errback if an exception is raised.
32
33 Typically __init__ or run() will set up self.remote_command to be a
34 string which corresponds to one of the SlaveCommands registered in
35 the buildslave, and self.args to a dictionary of arguments that will
36 be passed to the SlaveCommand instance.
37
38 start, remoteUpdate, and remoteComplete are available to be overridden
39
40 @type commandCounter: list of one int
41 @cvar commandCounter: provides a unique value for each
42 RemoteCommand executed across all slaves
43 @type active: boolean
44 @ivar active: whether the command is currently running
45 """
46 commandCounter = [0] # we use a list as a poor man's singleton
47 active = False
48
49 def __init__(self, remote_command, args):
50 """
51 @type remote_command: string
52 @param remote_command: remote command to start. This will be
53 passed to
54 L{buildbot.slave.bot.SlaveBuilder.remote_startCom mand}
55 and needs to have been registered
56 slave-side by
57 L{buildbot.slave.registry.registerSlaveCommand}
58 @type args: dict
59 @param args: arguments to send to the remote command
60 """
61
62 self.remote_command = remote_command
63 self.args = args
64
65 def run(self, step, remote):
66 self.active = True
67 self.step = step
68 self.remote = remote
69 c = self.commandCounter[0]
70 self.commandCounter[0] += 1
71 #self.commandID = "%d %d" % (c, random.randint(0, 1000000))
72 self.commandID = "%d" % c
73 log.msg("%s: RemoteCommand.run [%s]" % (self, self.commandID))
74 self.deferred = defer.Deferred()
75
76 d = defer.maybeDeferred(self.start)
77
78 # _finished is called with an error for unknown commands, errors
79 # that occur while the command is starting (including OSErrors in
80 # exec()), StaleBroker (when the connection was lost before we
81 # started), and pb.PBConnectionLost (when the slave isn't responding
82 # over this connection, perhaps it had a power failure, or NAT
83 # weirdness). If this happens, self.deferred is fired right away.
84 d.addErrback(self._finished)
85
86 # Connections which are lost while the command is running are caught
87 # when our parent Step calls our .lostRemote() method.
88 return self.deferred
89
90 def start(self):
91 """
92 Tell the slave to start executing the remote command.
93
94 @rtype: L{twisted.internet.defer.Deferred}
95 @returns: a deferred that will fire when the remote command is
96 done (with None as the result)
97 """
98
99 # Allow use of WithProperties in logfile path names.
100 cmd_args = self.args
101 if cmd_args.has_key("logfiles") and cmd_args["logfiles"]:
102 cmd_args = cmd_args.copy()
103 properties = self.step.build.getProperties()
104 cmd_args["logfiles"] = properties.render(cmd_args["logfiles"])
105
106 # This method only initiates the remote command.
107 # We will receive remote_update messages as the command runs.
108 # We will get a single remote_complete when it finishes.
109 # We should fire self.deferred when the command is done.
110 d = self.remote.callRemote("startCommand", self, self.commandID,
111 self.remote_command, cmd_args)
112 return d
113
114 def interrupt(self, why):
115 # TODO: consider separating this into interrupt() and stop(), where
116 # stop() unconditionally calls _finished, but interrupt() merely
117 # asks politely for the command to stop soon.
118
119 log.msg("RemoteCommand.interrupt", self, why)
120 if not self.active:
121 log.msg(" but this RemoteCommand is already inactive")
122 return
123 if not self.remote:
124 log.msg(" but our .remote went away")
125 return
126 if isinstance(why, Failure) and why.check(error.ConnectionLost):
127 log.msg("RemoteCommand.disconnect: lost slave")
128 self.remote = None
129 self._finished(why)
130 return
131
132 # tell the remote command to halt. Returns a Deferred that will fire
133 # when the interrupt command has been delivered.
134
135 d = defer.maybeDeferred(self.remote.callRemote, "interruptCommand",
136 self.commandID, str(why))
137 # the slave may not have remote_interruptCommand
138 d.addErrback(self._interruptFailed)
139 return d
140
141 def _interruptFailed(self, why):
142 log.msg("RemoteCommand._interruptFailed", self)
143 # TODO: forcibly stop the Command now, since we can't stop it
144 # cleanly
145 return None
146
147 def remote_update(self, updates):
148 """
149 I am called by the slave's L{buildbot.slave.bot.SlaveBuilder} so
150 I can receive updates from the running remote command.
151
152 @type updates: list of [object, int]
153 @param updates: list of updates from the remote command
154 """
155 self.buildslave.messageReceivedFromSlave()
156 max_updatenum = 0
157 for (update, num) in updates:
158 #log.msg("update[%d]:" % num)
159 try:
160 if self.active: # ignore late updates
161 self.remoteUpdate(update)
162 except:
163 # log failure, terminate build, let slave retire the update
164 self._finished(Failure())
165 # TODO: what if multiple updates arrive? should
166 # skip the rest but ack them all
167 if num > max_updatenum:
168 max_updatenum = num
169 return max_updatenum
170
171 def remoteUpdate(self, update):
172 raise NotImplementedError("You must implement this in a subclass")
173
174 def remote_complete(self, failure=None):
175 """
176 Called by the slave's L{buildbot.slave.bot.SlaveBuilder} to
177 notify me the remote command has finished.
178
179 @type failure: L{twisted.python.failure.Failure} or None
180
181 @rtype: None
182 """
183 self.buildslave.messageReceivedFromSlave()
184 # call the real remoteComplete a moment later, but first return an
185 # acknowledgement so the slave can retire the completion message.
186 if self.active:
187 reactor.callLater(0, self._finished, failure)
188 return None
189
190 def _finished(self, failure=None):
191 self.active = False
192 # call .remoteComplete. If it raises an exception, or returns the
193 # Failure that we gave it, our self.deferred will be errbacked. If
194 # it does not (either it ate the Failure or there the step finished
195 # normally and it didn't raise a new exception), self.deferred will
196 # be callbacked.
197 d = defer.maybeDeferred(self.remoteComplete, failure)
198 # arrange for the callback to get this RemoteCommand instance
199 # instead of just None
200 d.addCallback(lambda r: self)
201 # this fires the original deferred we returned from .run(),
202 # with self as the result, or a failure
203 d.addBoth(self.deferred.callback)
204
205 def remoteComplete(self, maybeFailure):
206 """Subclasses can override this.
207
208 This is called when the RemoteCommand has finished. 'maybeFailure'
209 will be None if the command completed normally, or a Failure
210 instance in one of the following situations:
211
212 - the slave was lost before the command was started
213 - the slave didn't respond to the startCommand message
214 - the slave raised an exception while starting the command
215 (bad command name, bad args, OSError from missing executable)
216 - the slave raised an exception while finishing the command
217 (they send back a remote_complete message with a Failure payload)
218
219 and also (for now):
220 - slave disconnected while the command was running
221
222 This method should do cleanup, like closing log files. It should
223 normally return the 'failure' argument, so that any exceptions will
224 be propagated to the Step. If it wants to consume them, return None
225 instead."""
226
227 return maybeFailure
228
229 class LoggedRemoteCommand(RemoteCommand):
230 """
231
232 I am a L{RemoteCommand} which gathers output from the remote command into
233 one or more local log files. My C{self.logs} dictionary contains
234 references to these L{buildbot.status.builder.LogFile} instances. Any
235 stdout/stderr/header updates from the slave will be put into
236 C{self.logs['stdio']}, if it exists. If the remote command uses other log
237 files, they will go into other entries in C{self.logs}.
238
239 If you want to use stdout or stderr, you should create a LogFile named
240 'stdio' and pass it to my useLog() message. Otherwise stdout/stderr will
241 be ignored, which is probably not what you want.
242
243 Unless you tell me otherwise, when my command completes I will close all
244 the LogFiles that I know about.
245
246 @ivar logs: maps logname to a LogFile instance
247 @ivar _closeWhenFinished: maps logname to a boolean. If true, this
248 LogFile will be closed when the RemoteCommand
249 finishes. LogFiles which are shared between
250 multiple RemoteCommands should use False here.
251
252 """
253
254 rc = None
255 debug = False
256
257 def __init__(self, *args, **kwargs):
258 self.logs = {}
259 self.delayedLogs = {}
260 self._closeWhenFinished = {}
261 RemoteCommand.__init__(self, *args, **kwargs)
262
263 def __repr__(self):
264 return "<RemoteCommand '%s' at %d>" % (self.remote_command, id(self))
265
266 def useLog(self, loog, closeWhenFinished=False, logfileName=None):
267 """Start routing messages from a remote logfile to a local LogFile
268
269 I take a local ILogFile instance in 'loog', and arrange to route
270 remote log messages for the logfile named 'logfileName' into it. By
271 default this logfileName comes from the ILogFile itself (using the
272 name by which the ILogFile will be displayed), but the 'logfileName'
273 argument can be used to override this. For example, if
274 logfileName='stdio', this logfile will collect text from the stdout
275 and stderr of the command.
276
277 @param loog: an instance which implements ILogFile
278 @param closeWhenFinished: a boolean, set to False if the logfile
279 will be shared between multiple
280 RemoteCommands. If True, the logfile will
281 be closed when this ShellCommand is done
282 with it.
283 @param logfileName: a string, which indicates which remote log file
284 should be routed into this ILogFile. This should
285 match one of the keys of the logfiles= argument
286 to ShellCommand.
287
288 """
289
290 assert interfaces.ILogFile.providedBy(loog)
291 if not logfileName:
292 logfileName = loog.getName()
293 assert logfileName not in self.logs
294 assert logfileName not in self.delayedLogs
295 self.logs[logfileName] = loog
296 self._closeWhenFinished[logfileName] = closeWhenFinished
297
298 def useLogDelayed(self, logfileName, activateCallBack, closeWhenFinished=Fal se):
299 assert logfileName not in self.logs
300 assert logfileName not in self.delayedLogs
301 self.delayedLogs[logfileName] = (activateCallBack, closeWhenFinished)
302
303 def start(self):
304 log.msg("LoggedRemoteCommand.start")
305 if 'stdio' not in self.logs:
306 log.msg("LoggedRemoteCommand (%s) is running a command, but "
307 "it isn't being logged to anything. This seems unusual."
308 % self)
309 self.updates = {}
310 return RemoteCommand.start(self)
311
312 def addStdout(self, data):
313 if 'stdio' in self.logs:
314 self.logs['stdio'].addStdout(data)
315 def addStderr(self, data):
316 if 'stdio' in self.logs:
317 self.logs['stdio'].addStderr(data)
318 def addHeader(self, data):
319 if 'stdio' in self.logs:
320 self.logs['stdio'].addHeader(data)
321
322 def addToLog(self, logname, data):
323 # Activate delayed logs on first data.
324 if logname in self.delayedLogs:
325 (activateCallBack, closeWhenFinished) = self.delayedLogs[logname]
326 del self.delayedLogs[logname]
327 loog = activateCallBack(self)
328 self.logs[logname] = loog
329 self._closeWhenFinished[logname] = closeWhenFinished
330
331 if logname in self.logs:
332 self.logs[logname].addStdout(data)
333 else:
334 log.msg("%s.addToLog: no such log %s" % (self, logname))
335
336 def remoteUpdate(self, update):
337 if self.debug:
338 for k,v in update.items():
339 log.msg("Update[%s]: %s" % (k,v))
340 if update.has_key('stdout'):
341 # 'stdout': data
342 self.addStdout(update['stdout'])
343 if update.has_key('stderr'):
344 # 'stderr': data
345 self.addStderr(update['stderr'])
346 if update.has_key('header'):
347 # 'header': data
348 self.addHeader(update['header'])
349 if update.has_key('log'):
350 # 'log': (logname, data)
351 logname, data = update['log']
352 self.addToLog(logname, data)
353 if update.has_key('rc'):
354 rc = self.rc = update['rc']
355 log.msg("%s rc=%s" % (self, rc))
356 self.addHeader("program finished with exit code %d\n" % rc)
357
358 for k in update:
359 if k not in ('stdout', 'stderr', 'header', 'rc'):
360 if k not in self.updates:
361 self.updates[k] = []
362 self.updates[k].append(update[k])
363
364 def remoteComplete(self, maybeFailure):
365 for name,loog in self.logs.items():
366 if self._closeWhenFinished[name]:
367 if maybeFailure:
368 loog.addHeader("\nremoteFailed: %s" % maybeFailure)
369 else:
370 log.msg("closing log %s" % loog)
371 loog.finish()
372 return maybeFailure
373
374
375 class LogObserver:
376 implements(interfaces.ILogObserver)
377
378 def setStep(self, step):
379 self.step = step
380
381 def setLog(self, loog):
382 assert interfaces.IStatusLog.providedBy(loog)
383 loog.subscribe(self, True)
384
385 def logChunk(self, build, step, log, channel, text):
386 if channel == interfaces.LOG_CHANNEL_STDOUT:
387 self.outReceived(text)
388 elif channel == interfaces.LOG_CHANNEL_STDERR:
389 self.errReceived(text)
390
391 # TODO: add a logEnded method? er, stepFinished?
392
393 def outReceived(self, data):
394 """This will be called with chunks of stdout data. Override this in
395 your observer."""
396 pass
397
398 def errReceived(self, data):
399 """This will be called with chunks of stderr data. Override this in
400 your observer."""
401 pass
402
403
404 class LogLineObserver(LogObserver):
405 def __init__(self):
406 self.stdoutParser = basic.LineOnlyReceiver()
407 self.stdoutParser.delimiter = "\n"
408 self.stdoutParser.lineReceived = self.outLineReceived
409 self.stdoutParser.transport = self # for the .disconnecting attribute
410 self.disconnecting = False
411
412 self.stderrParser = basic.LineOnlyReceiver()
413 self.stderrParser.delimiter = "\n"
414 self.stderrParser.lineReceived = self.errLineReceived
415 self.stderrParser.transport = self
416
417 def setMaxLineLength(self, max_length):
418 """
419 Set the maximum line length: lines longer than max_length are
420 dropped. Default is 16384 bytes. Use sys.maxint for effective
421 infinity.
422 """
423 self.stdoutParser.MAX_LENGTH = max_length
424 self.stderrParser.MAX_LENGTH = max_length
425
426 def outReceived(self, data):
427 self.stdoutParser.dataReceived(data)
428
429 def errReceived(self, data):
430 self.stderrParser.dataReceived(data)
431
432 def outLineReceived(self, line):
433 """This will be called with complete stdout lines (not including the
434 delimiter). Override this in your observer."""
435 pass
436
437 def errLineReceived(self, line):
438 """This will be called with complete lines of stderr (not including
439 the delimiter). Override this in your observer."""
440 pass
441
442
443 class RemoteShellCommand(LoggedRemoteCommand):
444 """This class helps you run a shell command on the build slave. It will
445 accumulate all the command's output into a Log named 'stdio'. When the
446 command is finished, it will fire a Deferred. You can then check the
447 results of the command and parse the output however you like."""
448
449 def __init__(self, workdir, command, env=None,
450 want_stdout=1, want_stderr=1,
451 timeout=20*60, maxTime=None, logfiles={},
452 usePTY="slave-config", logEnviron=True, **kwargs):
453 """
454 @type workdir: string
455 @param workdir: directory where the command ought to run,
456 relative to the Builder's home directory. Defaults to
457 '.': the same as the Builder's homedir. This should
458 probably be '.' for the initial 'cvs checkout'
459 command (which creates a workdir), and the Build-wide
460 workdir for all subsequent commands (including
461 compiles and 'cvs update').
462
463 @type command: list of strings (or string)
464 @param command: the shell command to run, like 'make all' or
465 'cvs update'. This should be a list or tuple
466 which can be used directly as the argv array.
467 For backwards compatibility, if this is a
468 string, the text will be given to '/bin/sh -c
469 %s'.
470
471 @type env: dict of string->string
472 @param env: environment variables to add or change for the
473 slave. Each command gets a separate
474 environment; all inherit the slave's initial
475 one. TODO: make it possible to delete some or
476 all of the slave's environment.
477
478 @type want_stdout: bool
479 @param want_stdout: defaults to True. Set to False if stdout should
480 be thrown away. Do this to avoid storing or
481 sending large amounts of useless data.
482
483 @type want_stderr: bool
484 @param want_stderr: False if stderr should be thrown away
485
486 @type timeout: int
487 @param timeout: tell the remote that if the command fails to
488 produce any output for this number of seconds,
489 the command is hung and should be killed. Use
490 None to disable the timeout.
491
492 @param logEnviron: whether to log env vars on the slave side
493
494 @type maxTime: int
495 @param maxTime: tell the remote that if the command fails to complete
496 in this number of seconds, the command should be
497 killed. Use None to disable maxTime.
498 """
499
500 self.command = command # stash .command, set it later
501 if env is not None:
502 # avoid mutating the original master.cfg dictionary. Each
503 # ShellCommand gets its own copy, any start() methods won't be
504 # able to modify the original.
505 env = env.copy()
506 args = {'workdir': workdir,
507 'env': env,
508 'want_stdout': want_stdout,
509 'want_stderr': want_stderr,
510 'logfiles': logfiles,
511 'timeout': timeout,
512 'maxTime': maxTime,
513 'usePTY': usePTY,
514 'logEnviron': logEnviron,
515 }
516 LoggedRemoteCommand.__init__(self, "shell", args)
517
518 def start(self):
519 self.args['command'] = self.command
520 if self.remote_command == "shell":
521 # non-ShellCommand slavecommands are responsible for doing this
522 # fixup themselves
523 if self.step.slaveVersion("shell", "old") == "old":
524 self.args['dir'] = self.args['workdir']
525 what = "command '%s' in dir '%s'" % (self.args['command'],
526 self.args['workdir'])
527 log.msg(what)
528 return LoggedRemoteCommand.start(self)
529
530 def __repr__(self):
531 return "<RemoteShellCommand '%s'>" % repr(self.command)
532
533 class BuildStep:
534 """
535 I represent a single step of the build process. This step may involve
536 zero or more commands to be run in the build slave, as well as arbitrary
537 processing on the master side. Regardless of how many slave commands are
538 run, the BuildStep will result in a single status value.
539
540 The step is started by calling startStep(), which returns a Deferred that
541 fires when the step finishes. See C{startStep} for a description of the
542 results provided by that Deferred.
543
544 __init__ and start are good methods to override. Don't forget to upcall
545 BuildStep.__init__ or bad things will happen.
546
547 To launch a RemoteCommand, pass it to .runCommand and wait on the
548 Deferred it returns.
549
550 Each BuildStep generates status as it runs. This status data is fed to
551 the L{buildbot.status.builder.BuildStepStatus} listener that sits in
552 C{self.step_status}. It can also feed progress data (like how much text
553 is output by a shell command) to the
554 L{buildbot.status.progress.StepProgress} object that lives in
555 C{self.progress}, by calling C{self.setProgress(metric, value)} as it
556 runs.
557
558 @type build: L{buildbot.process.base.Build}
559 @ivar build: the parent Build which is executing this step
560
561 @type progress: L{buildbot.status.progress.StepProgress}
562 @ivar progress: tracks ETA for the step
563
564 @type step_status: L{buildbot.status.builder.BuildStepStatus}
565 @ivar step_status: collects output status
566 """
567
568 # these parameters are used by the parent Build object to decide how to
569 # interpret our results. haltOnFailure will affect the build process
570 # immediately, the others will be taken into consideration when
571 # determining the overall build status.
572 #
573 # steps that are makred as alwaysRun will be run regardless of the outcome
574 # of previous steps (especially steps with haltOnFailure=True)
575 haltOnFailure = False
576 flunkOnWarnings = False
577 flunkOnFailure = False
578 warnOnWarnings = False
579 warnOnFailure = False
580 alwaysRun = False
581
582 # 'parms' holds a list of all the parameters we care about, to allow
583 # users to instantiate a subclass of BuildStep with a mixture of
584 # arguments, some of which are for us, some of which are for the subclass
585 # (or a delegate of the subclass, like how ShellCommand delivers many
586 # arguments to the RemoteShellCommand that it creates). Such delegating
587 # subclasses will use this list to figure out which arguments are meant
588 # for us and which should be given to someone else.
589 parms = ['name', 'locks',
590 'haltOnFailure',
591 'flunkOnWarnings',
592 'flunkOnFailure',
593 'warnOnWarnings',
594 'warnOnFailure',
595 'alwaysRun',
596 'progressMetrics',
597 'doStepIf',
598 ]
599
600 name = "generic"
601 locks = []
602 progressMetrics = () # 'time' is implicit
603 useProgress = True # set to False if step is really unpredictable
604 build = None
605 step_status = None
606 progress = None
607 # doStepIf can be False, True, or a function that returns False or True
608 doStepIf = True
609
610 def __init__(self, **kwargs):
611 self.factory = (self.__class__, dict(kwargs))
612 for p in self.__class__.parms:
613 if kwargs.has_key(p):
614 setattr(self, p, kwargs[p])
615 del kwargs[p]
616 if kwargs:
617 why = "%s.__init__ got unexpected keyword argument(s) %s" \
618 % (self, kwargs.keys())
619 raise TypeError(why)
620 self._pendingLogObservers = []
621
622 def setBuild(self, build):
623 # subclasses which wish to base their behavior upon qualities of the
624 # Build (e.g. use the list of changed files to run unit tests only on
625 # code which has been modified) should do so here. The Build is not
626 # available during __init__, but setBuild() will be called just
627 # afterwards.
628 self.build = build
629
630 def setBuildSlave(self, buildslave):
631 self.buildslave = buildslave
632
633 def setDefaultWorkdir(self, workdir):
634 # The Build calls this just after __init__(). ShellCommand
635 # and variants use a slave-side workdir, but some other steps
636 # do not. Subclasses which use a workdir should use the value
637 # set by this method unless they were constructed with
638 # something more specific.
639 pass
640
641 def addFactoryArguments(self, **kwargs):
642 self.factory[1].update(kwargs)
643
644 def getStepFactory(self):
645 return self.factory
646
647 def setStepStatus(self, step_status):
648 self.step_status = step_status
649
650 def setupProgress(self):
651 if self.useProgress:
652 sp = progress.StepProgress(self.name, self.progressMetrics)
653 self.progress = sp
654 self.step_status.setProgress(sp)
655 return sp
656 return None
657
658 def setProgress(self, metric, value):
659 """BuildSteps can call self.setProgress() to announce progress along
660 some metric."""
661 if self.progress:
662 self.progress.setProgress(metric, value)
663
664 def getProperty(self, propname):
665 return self.build.getProperty(propname)
666
667 def setProperty(self, propname, value, source="Step"):
668 self.build.setProperty(propname, value, source)
669
670 def startStep(self, remote):
671 """Begin the step. This returns a Deferred that will fire when the
672 step finishes.
673
674 This deferred fires with a tuple of (result, [extra text]), although
675 older steps used to return just the 'result' value, so the receiving
676 L{base.Build} needs to be prepared to handle that too. C{result} is
677 one of the SUCCESS/WARNINGS/FAILURE/SKIPPED constants from
678 L{buildbot.status.builder}, and the extra text is a list of short
679 strings which should be appended to the Build's text results. This
680 text allows a test-case step which fails to append B{17 tests} to the
681 Build's status, in addition to marking the build as failing.
682
683 The deferred will errback if the step encounters an exception,
684 including an exception on the slave side (or if the slave goes away
685 altogether). Failures in shell commands (rc!=0) will B{not} cause an
686 errback, in general the BuildStep will evaluate the results and
687 decide whether to treat it as a WARNING or FAILURE.
688
689 @type remote: L{twisted.spread.pb.RemoteReference}
690 @param remote: a reference to the slave's
691 L{buildbot.slave.bot.SlaveBuilder} instance where any
692 RemoteCommands may be run
693 """
694
695 self.remote = remote
696 self.deferred = defer.Deferred()
697 # convert all locks into their real form
698 lock_list = []
699 for access in self.locks:
700 if not isinstance(access, locks.LockAccess):
701 # Buildbot 0.7.7 compability: user did not specify access
702 access = access.defaultAccess()
703 lock = self.build.builder.botmaster.getLockByID(access.lockid)
704 lock_list.append((lock, access))
705 self.locks = lock_list
706 # then narrow SlaveLocks down to the slave that this build is being
707 # run on
708 self.locks = [(l.getLock(self.build.slavebuilder), la) for l, la in self .locks]
709 for l, la in self.locks:
710 if l in self.build.locks:
711 log.msg("Hey, lock %s is claimed by both a Step (%s) and the"
712 " parent Build (%s)" % (l, self, self.build))
713 raise RuntimeError("lock claimed by both Step and Build")
714 d = self.acquireLocks()
715 d.addCallback(self._startStep_2)
716 return self.deferred
717
718 def acquireLocks(self, res=None):
719 log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
720 if not self.locks:
721 return defer.succeed(None)
722 for lock, access in self.locks:
723 if not lock.isAvailable(access):
724 log.msg("step %s waiting for lock %s" % (self, lock))
725 d = lock.waitUntilMaybeAvailable(self, access)
726 d.addCallback(self.acquireLocks)
727 return d
728 # all locks are available, claim them all
729 for lock, access in self.locks:
730 lock.claim(self, access)
731 return defer.succeed(None)
732
733 def _startStep_2(self, res):
734 if self.progress:
735 self.progress.start()
736 self.step_status.stepStarted()
737 try:
738 skip = None
739 if isinstance(self.doStepIf, bool):
740 if not self.doStepIf:
741 skip = SKIPPED
742 elif not self.doStepIf(self):
743 skip = SKIPPED
744
745 if skip is None:
746 skip = self.start()
747
748 if skip == SKIPPED:
749 # this return value from self.start is a shortcut
750 # to finishing the step immediately
751 reactor.callLater(0, self.finished, SKIPPED)
752 except:
753 log.msg("BuildStep.startStep exception in .start")
754 self.failed(Failure())
755
756 def start(self):
757 """Begin the step. Override this method and add code to do local
758 processing, fire off remote commands, etc.
759
760 To spawn a command in the buildslave, create a RemoteCommand instance
761 and run it with self.runCommand::
762
763 c = RemoteCommandFoo(args)
764 d = self.runCommand(c)
765 d.addCallback(self.fooDone).addErrback(self.failed)
766
767 As the step runs, it should send status information to the
768 BuildStepStatus::
769
770 self.step_status.setText(['compile', 'failed'])
771 self.step_status.setText2(['4', 'warnings'])
772
773 To have some code parse stdio (or other log stream) in realtime, add
774 a LogObserver subclass. This observer can use self.step.setProgress()
775 to provide better progress notification to the step.::
776
777 self.addLogObserver('stdio', MyLogObserver())
778
779 To add a LogFile, use self.addLog. Make sure it gets closed when it
780 finishes. When giving a Logfile to a RemoteShellCommand, just ask it
781 to close the log when the command completes::
782
783 log = self.addLog('output')
784 cmd = RemoteShellCommand(args)
785 cmd.useLog(log, closeWhenFinished=True)
786
787 You can also create complete Logfiles with generated text in a single
788 step::
789
790 self.addCompleteLog('warnings', text)
791
792 When the step is done, it should call self.finished(result). 'result'
793 will be provided to the L{buildbot.process.base.Build}, and should be
794 one of the constants defined above: SUCCESS, WARNINGS, FAILURE, or
795 SKIPPED.
796
797 If the step encounters an exception, it should call self.failed(why).
798 'why' should be a Failure object. This automatically fails the whole
799 build with an exception. It is a good idea to add self.failed as an
800 errback to any Deferreds you might obtain.
801
802 If the step decides it does not need to be run, start() can return
803 the constant SKIPPED. This fires the callback immediately: it is not
804 necessary to call .finished yourself. This can also indicate to the
805 status-reporting mechanism that this step should not be displayed.
806
807 A step can be configured to only run under certain conditions. To
808 do this, set the step's doStepIf to a boolean value, or to a function
809 that returns a boolean value. If the value or function result is
810 False, then the step will return SKIPPED without doing anything,
811 otherwise the step will be executed normally. If you set doStepIf
812 to a function, that function should accept one parameter, which will
813 be the Step object itself."""
814
815 raise NotImplementedError("your subclass must implement this method")
816
817 def interrupt(self, reason):
818 """Halt the command, either because the user has decided to cancel
819 the build ('reason' is a string), or because the slave has
820 disconnected ('reason' is a ConnectionLost Failure). Any further
821 local processing should be skipped, and the Step completed with an
822 error status. The results text should say something useful like
823 ['step', 'interrupted'] or ['remote', 'lost']"""
824 pass
825
826 def releaseLocks(self):
827 log.msg("releaseLocks(%s): %s" % (self, self.locks))
828 for lock, access in self.locks:
829 lock.release(self, access)
830
831 def finished(self, results):
832 if self.progress:
833 self.progress.finish()
834 self.step_status.stepFinished(results)
835 self.releaseLocks()
836 self.deferred.callback(results)
837
838 def failed(self, why):
839 # if isinstance(why, pb.CopiedFailure): # a remote exception might
840 # only have short traceback, so formatFailure is not as useful as
841 # you'd like (no .frames, so no traceback is displayed)
842 log.msg("BuildStep.failed, traceback follows")
843 log.err(why)
844 try:
845 if self.progress:
846 self.progress.finish()
847 self.addHTMLLog("err.html", formatFailure(why))
848 self.addCompleteLog("err.text", why.getTraceback())
849 # could use why.getDetailedTraceback() for more information
850 self.step_status.setText([self.name, "exception"])
851 self.step_status.setText2([self.name])
852 self.step_status.stepFinished(EXCEPTION)
853 except:
854 log.msg("exception during failure processing")
855 log.err()
856 # the progress stuff may still be whacked (the StepStatus may
857 # think that it is still running), but the build overall will now
858 # finish
859 try:
860 self.releaseLocks()
861 except:
862 log.msg("exception while releasing locks")
863 log.err()
864
865 log.msg("BuildStep.failed now firing callback")
866 self.deferred.callback(EXCEPTION)
867
868 # utility methods that BuildSteps may find useful
869
870 def slaveVersion(self, command, oldversion=None):
871 """Return the version number of the given slave command. For the
872 commands defined in buildbot.slave.commands, this is the value of
873 'cvs_ver' at the top of that file. Non-existent commands will return
874 a value of None. Buildslaves running buildbot-0.5.0 or earlier did
875 not respond to the version query: commands on those slaves will
876 return a value of OLDVERSION, so you can distinguish between old
877 buildslaves and missing commands.
878
879 If you know that <=0.5.0 buildslaves have the command you want (CVS
880 and SVN existed back then, but none of the other VC systems), then it
881 makes sense to call this with oldversion='old'. If the command you
882 want is newer than that, just leave oldversion= unspecified, and the
883 command will return None for a buildslave that does not implement the
884 command.
885 """
886 return self.build.getSlaveCommandVersion(command, oldversion)
887
888 def slaveVersionIsOlderThan(self, command, minversion):
889 sv = self.build.getSlaveCommandVersion(command, None)
890 if sv is None:
891 return True
892 # the version we get back is a string form of the CVS version number
893 # of the slave's buildbot/slave/commands.py, something like 1.39 .
894 # This might change in the future (I might move away from CVS), but
895 # if so I'll keep updating that string with suitably-comparable
896 # values.
897 if sv.split(".") < minversion.split("."):
898 return True
899 return False
900
901 def getSlaveName(self):
902 return self.build.getSlaveName()
903
904 def addLog(self, name):
905 loog = self.step_status.addLog(name)
906 self._connectPendingLogObservers()
907 return loog
908
909 def getLog(self, name):
910 for l in self.step_status.getLogs():
911 if l.getName() == name:
912 return l
913 raise KeyError("no log named '%s'" % (name,))
914
915 def addCompleteLog(self, name, text):
916 log.msg("addCompleteLog(%s)" % name)
917 loog = self.step_status.addLog(name)
918 size = loog.chunkSize
919 for start in range(0, len(text), size):
920 loog.addStdout(text[start:start+size])
921 loog.finish()
922 self._connectPendingLogObservers()
923
924 def addHTMLLog(self, name, html):
925 log.msg("addHTMLLog(%s)" % name)
926 self.step_status.addHTMLLog(name, html)
927 self._connectPendingLogObservers()
928
929 def addLogObserver(self, logname, observer):
930 assert interfaces.ILogObserver.providedBy(observer)
931 observer.setStep(self)
932 self._pendingLogObservers.append((logname, observer))
933 self._connectPendingLogObservers()
934
935 def _connectPendingLogObservers(self):
936 if not self._pendingLogObservers:
937 return
938 if not self.step_status:
939 return
940 current_logs = {}
941 for loog in self.step_status.getLogs():
942 current_logs[loog.getName()] = loog
943 for logname, observer in self._pendingLogObservers[:]:
944 if logname in current_logs:
945 observer.setLog(current_logs[logname])
946 self._pendingLogObservers.remove((logname, observer))
947
948 def addURL(self, name, url):
949 """Add a BuildStep URL to this step.
950
951 An HREF to this URL will be added to any HTML representations of this
952 step. This allows a step to provide links to external web pages,
953 perhaps to provide detailed HTML code coverage results or other forms
954 of build status.
955 """
956 self.step_status.addURL(name, url)
957
958 def runCommand(self, c):
959 c.buildslave = self.buildslave
960 d = c.run(self, self.remote)
961 return d
962
963
964 class OutputProgressObserver(LogObserver):
965 length = 0
966
967 def __init__(self, name):
968 self.name = name
969
970 def logChunk(self, build, step, log, channel, text):
971 self.length += len(text)
972 self.step.setProgress(self.name, self.length)
973
974 class LoggingBuildStep(BuildStep):
975 """This is an abstract base class, suitable for inheritance by all
976 BuildSteps that invoke RemoteCommands which emit stdout/stderr messages.
977 """
978
979 progressMetrics = ('output',)
980 logfiles = {}
981
982 parms = BuildStep.parms + ['logfiles', 'lazylogfiles']
983
984 def __init__(self, logfiles={}, lazylogfiles=False, *args, **kwargs):
985 BuildStep.__init__(self, *args, **kwargs)
986 self.addFactoryArguments(logfiles=logfiles,
987 lazylogfiles=lazylogfiles)
988 # merge a class-level 'logfiles' attribute with one passed in as an
989 # argument
990 self.logfiles = self.logfiles.copy()
991 self.logfiles.update(logfiles)
992 self.lazylogfiles = lazylogfiles
993 self.addLogObserver('stdio', OutputProgressObserver("output"))
994
995 def describe(self, done=False):
996 raise NotImplementedError("implement this in a subclass")
997
998 def addLogFile(self, logname, filename):
999 """
1000 This allows to add logfiles after construction, but before calling
1001 startCommand().
1002 """
1003 self.logfiles[logname] = filename
1004
1005 def startCommand(self, cmd, errorMessages=[]):
1006 """
1007 @param cmd: a suitable RemoteCommand which will be launched, with
1008 all output being put into our self.stdio_log LogFile
1009 """
1010 log.msg("ShellCommand.startCommand(cmd=%s)" % (cmd,))
1011 args = cmd.args
1012 if cmd.args.get("patch", None):
1013 # Don't print the patch in the logs, it's often too large and not
1014 # useful.
1015 args = cmd.args.copy()
1016 # This is usually a tuple so convert it to a list to be able to modify
1017 # it.
1018 patch = list(args['patch'][:])
1019 if patch[1]:
1020 patch[1] = "(%s bytes)" % len(patch[1])
1021 args['patch'] = patch
1022 log.msg(" cmd.args = %r" % (args))
1023 self.cmd = cmd # so we can interrupt it
1024 self.step_status.setText(self.describe(False))
1025
1026 # stdio is the first log
1027 self.stdio_log = stdio_log = self.addLog("stdio")
1028 cmd.useLog(stdio_log, True)
1029 for em in errorMessages:
1030 stdio_log.addHeader(em)
1031 # TODO: consider setting up self.stdio_log earlier, and have the
1032 # code that passes in errorMessages instead call
1033 # self.stdio_log.addHeader() directly.
1034
1035 # there might be other logs
1036 self.setupLogfiles(cmd, self.logfiles)
1037
1038 d = self.runCommand(cmd) # might raise ConnectionLost
1039 d.addCallback(lambda res: self.commandComplete(cmd))
1040 d.addCallback(lambda res: self.createSummary(cmd.logs['stdio']))
1041 d.addCallback(lambda res: self.evaluateCommand(cmd)) # returns results
1042 def _gotResults(results):
1043 self.setStatus(cmd, results)
1044 return results
1045 d.addCallback(_gotResults) # returns results
1046 d.addCallbacks(self.finished, self.checkDisconnect)
1047 d.addErrback(self.failed)
1048
1049 def setupLogfiles(self, cmd, logfiles):
1050 """Set up any additional logfiles= logs.
1051 """
1052 for logname,remotefilename in logfiles.items():
1053 if self.lazylogfiles:
1054 # Ask LoggedRemoteCommand to watch a logfile, but only add
1055 # it when/if we see any data.
1056 #
1057 # The dummy default argument local_logname is a work-around for
1058 # Python name binding; default values are bound by value, but
1059 # captured variables in the body are bound by name.
1060 callback = lambda cmd_arg, local_logname=logname: self.addLog(lo cal_logname)
1061 cmd.useLogDelayed(logname, callback, True)
1062 else:
1063 # tell the BuildStepStatus to add a LogFile
1064 newlog = self.addLog(logname)
1065 # and tell the LoggedRemoteCommand to feed it
1066 cmd.useLog(newlog, True)
1067
1068 def interrupt(self, reason):
1069 # TODO: consider adding an INTERRUPTED or STOPPED status to use
1070 # instead of FAILURE, might make the text a bit more clear.
1071 # 'reason' can be a Failure, or text
1072 self.addCompleteLog('interrupt', str(reason))
1073 d = self.cmd.interrupt(reason)
1074 return d
1075
1076 def checkDisconnect(self, f):
1077 f.trap(error.ConnectionLost)
1078 self.step_status.setText(self.describe(True) +
1079 ["failed", "slave", "lost"])
1080 self.step_status.setText2(["failed", "slave", "lost"])
1081 return self.finished(EXCEPTION)
1082
1083 # to refine the status output, override one or more of the following
1084 # methods. Change as little as possible: start with the first ones on
1085 # this list and only proceed further if you have to
1086 #
1087 # createSummary: add additional Logfiles with summarized results
1088 # evaluateCommand: decides whether the step was successful or not
1089 #
1090 # getText: create the final per-step text strings
1091 # describeText2: create the strings added to the overall build status
1092 #
1093 # getText2: only adds describeText2() when the step affects build status
1094 #
1095 # setStatus: handles all status updating
1096
1097 # commandComplete is available for general-purpose post-completion work.
1098 # It is a good place to do one-time parsing of logfiles, counting
1099 # warnings and errors. It should probably stash such counts in places
1100 # like self.warnings so they can be picked up later by your getText
1101 # method.
1102
1103 # TODO: most of this stuff should really be on BuildStep rather than
1104 # ShellCommand. That involves putting the status-setup stuff in
1105 # .finished, which would make it hard to turn off.
1106
1107 def commandComplete(self, cmd):
1108 """This is a general-purpose hook method for subclasses. It will be
1109 called after the remote command has finished, but before any of the
1110 other hook functions are called."""
1111 pass
1112
1113 def createSummary(self, log):
1114 """To create summary logs, do something like this:
1115 warnings = grep('^Warning:', log.getText())
1116 self.addCompleteLog('warnings', warnings)
1117 """
1118 pass
1119
1120 def evaluateCommand(self, cmd):
1121 """Decide whether the command was SUCCESS, WARNINGS, or FAILURE.
1122 Override this to, say, declare WARNINGS if there is any stderr
1123 activity, or to say that rc!=0 is not actually an error."""
1124
1125 if cmd.rc != 0:
1126 return FAILURE
1127 # if cmd.log.getStderr(): return WARNINGS
1128 return SUCCESS
1129
1130 def getText(self, cmd, results):
1131 if results == SUCCESS:
1132 return self.describe(True)
1133 elif results == WARNINGS:
1134 return self.describe(True) + ["warnings"]
1135 else:
1136 return self.describe(True) + ["failed"]
1137
1138 def getText2(self, cmd, results):
1139 """We have decided to add a short note about ourselves to the overall
1140 build description, probably because something went wrong. Return a
1141 short list of short strings. If your subclass counts test failures or
1142 warnings of some sort, this is a good place to announce the count."""
1143 # return ["%d warnings" % warningcount]
1144 # return ["%d tests" % len(failedTests)]
1145 return [self.name]
1146
1147 def maybeGetText2(self, cmd, results):
1148 if results == SUCCESS:
1149 # successful steps do not add anything to the build's text
1150 pass
1151 elif results == WARNINGS:
1152 if (self.flunkOnWarnings or self.warnOnWarnings):
1153 # we're affecting the overall build, so tell them why
1154 return self.getText2(cmd, results)
1155 else:
1156 if (self.haltOnFailure or self.flunkOnFailure
1157 or self.warnOnFailure):
1158 # we're affecting the overall build, so tell them why
1159 return self.getText2(cmd, results)
1160 return []
1161
1162 def setStatus(self, cmd, results):
1163 # this is good enough for most steps, but it can be overridden to
1164 # get more control over the displayed text
1165 self.step_status.setText(self.getText(cmd, results))
1166 self.step_status.setText2(self.maybeGetText2(cmd, results))
1167
1168 # (WithProperties used to be available in this module)
1169 from buildbot.process.properties import WithProperties
1170 _hush_pyflakes = [WithProperties]
1171 del _hush_pyflakes
1172
OLDNEW
« no previous file with comments | « third_party/buildbot_7_12/buildbot/process/builder.py ('k') | third_party/buildbot_7_12/buildbot/process/factory.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698