Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/process/buildstep.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/process/buildstep.py')
-rw-r--r--buildbot/buildbot/process/buildstep.py1097
1 files changed, 1097 insertions, 0 deletions
diff --git a/buildbot/buildbot/process/buildstep.py b/buildbot/buildbot/process/buildstep.py
new file mode 100644
index 0000000..2cfc157
--- /dev/null
+++ b/buildbot/buildbot/process/buildstep.py
@@ -0,0 +1,1097 @@
+# -*- test-case-name: buildbot.test.test_steps -*-
+
+from zope.interface import implements
+from twisted.internet import reactor, defer, error
+from twisted.protocols import basic
+from twisted.spread import pb
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.web.util import formatFailure
+
+from buildbot import interfaces, locks
+from buildbot.status import progress
+from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, \
+ EXCEPTION
+
+"""
+BuildStep and RemoteCommand classes for master-side representation of the
+build process
+"""
+
+class RemoteCommand(pb.Referenceable):
+ """
+ I represent a single command to be run on the slave. I handle the details
+ of reliably gathering status updates from the slave (acknowledging each),
+ and (eventually, in a future release) recovering from interrupted builds.
+ This is the master-side object that is known to the slave-side
+ L{buildbot.slave.bot.SlaveBuilder}, to which status updates are sent.
+
+ My command should be started by calling .run(), which returns a
+ Deferred that will fire when the command has finished, or will
+ errback if an exception is raised.
+
+ Typically __init__ or run() will set up self.remote_command to be a
+ string which corresponds to one of the SlaveCommands registered in
+ the buildslave, and self.args to a dictionary of arguments that will
+ be passed to the SlaveCommand instance.
+
+ start, remoteUpdate, and remoteComplete are available to be overridden
+
+ @type commandCounter: list of one int
+ @cvar commandCounter: provides a unique value for each
+ RemoteCommand executed across all slaves
+ @type active: boolean
+ @ivar active: whether the command is currently running
+ """
+ commandCounter = [0] # we use a list as a poor man's singleton
+ active = False
+
+ def __init__(self, remote_command, args):
+ """
+ @type remote_command: string
+ @param remote_command: remote command to start. This will be
+ passed to
+ L{buildbot.slave.bot.SlaveBuilder.remote_startCommand}
+ and needs to have been registered
+ slave-side by
+ L{buildbot.slave.registry.registerSlaveCommand}
+ @type args: dict
+ @param args: arguments to send to the remote command
+ """
+
+ self.remote_command = remote_command
+ self.args = args
+
+ def __getstate__(self):
+ dict = self.__dict__.copy()
+ # Remove the remote ref: if necessary (only for resumed builds), it
+ # will be reattached at resume time
+ if dict.has_key("remote"):
+ del dict["remote"]
+ return dict
+
+ def run(self, step, remote):
+ self.active = True
+ self.step = step
+ self.remote = remote
+ c = self.commandCounter[0]
+ self.commandCounter[0] += 1
+ #self.commandID = "%d %d" % (c, random.randint(0, 1000000))
+ self.commandID = "%d" % c
+ log.msg("%s: RemoteCommand.run [%s]" % (self, self.commandID))
+ self.deferred = defer.Deferred()
+
+ d = defer.maybeDeferred(self.start)
+
+ # _finished is called with an error for unknown commands, errors
+ # that occur while the command is starting (including OSErrors in
+ # exec()), StaleBroker (when the connection was lost before we
+ # started), and pb.PBConnectionLost (when the slave isn't responding
+ # over this connection, perhaps it had a power failure, or NAT
+ # weirdness). If this happens, self.deferred is fired right away.
+ d.addErrback(self._finished)
+
+ # Connections which are lost while the command is running are caught
+ # when our parent Step calls our .lostRemote() method.
+ return self.deferred
+
+ def start(self):
+ """
+ Tell the slave to start executing the remote command.
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @returns: a deferred that will fire when the remote command is
+ done (with None as the result)
+ """
+ # This method only initiates the remote command.
+ # We will receive remote_update messages as the command runs.
+ # We will get a single remote_complete when it finishes.
+ # We should fire self.deferred when the command is done.
+ d = self.remote.callRemote("startCommand", self, self.commandID,
+ self.remote_command, self.args)
+ return d
+
+ def interrupt(self, why):
+ # TODO: consider separating this into interrupt() and stop(), where
+ # stop() unconditionally calls _finished, but interrupt() merely
+ # asks politely for the command to stop soon.
+
+ log.msg("RemoteCommand.interrupt", self, why)
+ if not self.active:
+ log.msg(" but this RemoteCommand is already inactive")
+ return
+ if not self.remote:
+ log.msg(" but our .remote went away")
+ return
+ if isinstance(why, Failure) and why.check(error.ConnectionLost):
+ log.msg("RemoteCommand.disconnect: lost slave")
+ self.remote = None
+ self._finished(why)
+ return
+
+ # tell the remote command to halt. Returns a Deferred that will fire
+ # when the interrupt command has been delivered.
+
+ d = defer.maybeDeferred(self.remote.callRemote, "interruptCommand",
+ self.commandID, str(why))
+ # the slave may not have remote_interruptCommand
+ d.addErrback(self._interruptFailed)
+ return d
+
+ def _interruptFailed(self, why):
+ log.msg("RemoteCommand._interruptFailed", self)
+ # TODO: forcibly stop the Command now, since we can't stop it
+ # cleanly
+ return None
+
+ def remote_update(self, updates):
+ """
+ I am called by the slave's L{buildbot.slave.bot.SlaveBuilder} so
+ I can receive updates from the running remote command.
+
+ @type updates: list of [object, int]
+ @param updates: list of updates from the remote command
+ """
+ self.buildslave.messageReceivedFromSlave()
+ max_updatenum = 0
+ for (update, num) in updates:
+ #log.msg("update[%d]:" % num)
+ try:
+ if self.active: # ignore late updates
+ self.remoteUpdate(update)
+ except:
+ # log failure, terminate build, let slave retire the update
+ self._finished(Failure())
+ # TODO: what if multiple updates arrive? should
+ # skip the rest but ack them all
+ if num > max_updatenum:
+ max_updatenum = num
+ return max_updatenum
+
+ def remoteUpdate(self, update):
+ raise NotImplementedError("You must implement this in a subclass")
+
+ def remote_complete(self, failure=None):
+ """
+ Called by the slave's L{buildbot.slave.bot.SlaveBuilder} to
+ notify me the remote command has finished.
+
+ @type failure: L{twisted.python.failure.Failure} or None
+
+ @rtype: None
+ """
+ self.buildslave.messageReceivedFromSlave()
+ # call the real remoteComplete a moment later, but first return an
+ # acknowledgement so the slave can retire the completion message.
+ if self.active:
+ reactor.callLater(0, self._finished, failure)
+ return None
+
+ def _finished(self, failure=None):
+ self.active = False
+ # call .remoteComplete. If it raises an exception, or returns the
+ # Failure that we gave it, our self.deferred will be errbacked. If
+ # it does not (either it ate the Failure or there the step finished
+ # normally and it didn't raise a new exception), self.deferred will
+ # be callbacked.
+ d = defer.maybeDeferred(self.remoteComplete, failure)
+ # arrange for the callback to get this RemoteCommand instance
+ # instead of just None
+ d.addCallback(lambda r: self)
+ # this fires the original deferred we returned from .run(),
+ # with self as the result, or a failure
+ d.addBoth(self.deferred.callback)
+
+ def remoteComplete(self, maybeFailure):
+ """Subclasses can override this.
+
+ This is called when the RemoteCommand has finished. 'maybeFailure'
+ will be None if the command completed normally, or a Failure
+ instance in one of the following situations:
+
+ - the slave was lost before the command was started
+ - the slave didn't respond to the startCommand message
+ - the slave raised an exception while starting the command
+ (bad command name, bad args, OSError from missing executable)
+ - the slave raised an exception while finishing the command
+ (they send back a remote_complete message with a Failure payload)
+
+ and also (for now):
+ - slave disconnected while the command was running
+
+ This method should do cleanup, like closing log files. It should
+ normally return the 'failure' argument, so that any exceptions will
+ be propagated to the Step. If it wants to consume them, return None
+ instead."""
+
+ return maybeFailure
+
+class LoggedRemoteCommand(RemoteCommand):
+ """
+
+ I am a L{RemoteCommand} which gathers output from the remote command into
+ one or more local log files. My C{self.logs} dictionary contains
+ references to these L{buildbot.status.builder.LogFile} instances. Any
+ stdout/stderr/header updates from the slave will be put into
+ C{self.logs['stdio']}, if it exists. If the remote command uses other log
+ files, they will go into other entries in C{self.logs}.
+
+ If you want to use stdout or stderr, you should create a LogFile named
+ 'stdio' and pass it to my useLog() message. Otherwise stdout/stderr will
+ be ignored, which is probably not what you want.
+
+ Unless you tell me otherwise, when my command completes I will close all
+ the LogFiles that I know about.
+
+ @ivar logs: maps logname to a LogFile instance
+ @ivar _closeWhenFinished: maps logname to a boolean. If true, this
+ LogFile will be closed when the RemoteCommand
+ finishes. LogFiles which are shared between
+ multiple RemoteCommands should use False here.
+
+ """
+
+ rc = None
+ debug = False
+
+ def __init__(self, *args, **kwargs):
+ self.logs = {}
+ self._closeWhenFinished = {}
+ RemoteCommand.__init__(self, *args, **kwargs)
+
+ def __repr__(self):
+ return "<RemoteCommand '%s' at %d>" % (self.remote_command, id(self))
+
+ def useLog(self, loog, closeWhenFinished=False, logfileName=None):
+ """Start routing messages from a remote logfile to a local LogFile
+
+ I take a local ILogFile instance in 'loog', and arrange to route
+ remote log messages for the logfile named 'logfileName' into it. By
+ default this logfileName comes from the ILogFile itself (using the
+ name by which the ILogFile will be displayed), but the 'logfileName'
+ argument can be used to override this. For example, if
+ logfileName='stdio', this logfile will collect text from the stdout
+ and stderr of the command.
+
+ @param loog: an instance which implements ILogFile
+ @param closeWhenFinished: a boolean, set to False if the logfile
+ will be shared between multiple
+ RemoteCommands. If True, the logfile will
+ be closed when this ShellCommand is done
+ with it.
+ @param logfileName: a string, which indicates which remote log file
+ should be routed into this ILogFile. This should
+ match one of the keys of the logfiles= argument
+ to ShellCommand.
+
+ """
+
+ assert interfaces.ILogFile.providedBy(loog)
+ if not logfileName:
+ logfileName = loog.getName()
+ assert logfileName not in self.logs
+ self.logs[logfileName] = loog
+ self._closeWhenFinished[logfileName] = closeWhenFinished
+
+ def start(self):
+ log.msg("LoggedRemoteCommand.start")
+ if 'stdio' not in self.logs:
+ log.msg("LoggedRemoteCommand (%s) is running a command, but "
+ "it isn't being logged to anything. This seems unusual."
+ % self)
+ self.updates = {}
+ return RemoteCommand.start(self)
+
+ def addStdout(self, data):
+ if 'stdio' in self.logs:
+ self.logs['stdio'].addStdout(data)
+ def addStderr(self, data):
+ if 'stdio' in self.logs:
+ self.logs['stdio'].addStderr(data)
+ def addHeader(self, data):
+ if 'stdio' in self.logs:
+ self.logs['stdio'].addHeader(data)
+
+ def addToLog(self, logname, data):
+ if logname in self.logs:
+ self.logs[logname].addStdout(data)
+ else:
+ log.msg("%s.addToLog: no such log %s" % (self, logname))
+
+ def remoteUpdate(self, update):
+ if self.debug:
+ for k,v in update.items():
+ log.msg("Update[%s]: %s" % (k,v))
+ if update.has_key('stdout'):
+ # 'stdout': data
+ self.addStdout(update['stdout'])
+ if update.has_key('stderr'):
+ # 'stderr': data
+ self.addStderr(update['stderr'])
+ if update.has_key('header'):
+ # 'header': data
+ self.addHeader(update['header'])
+ if update.has_key('log'):
+ # 'log': (logname, data)
+ logname, data = update['log']
+ self.addToLog(logname, data)
+ if update.has_key('rc'):
+ rc = self.rc = update['rc']
+ log.msg("%s rc=%s" % (self, rc))
+ self.addHeader("program finished with exit code %d\n" % rc)
+
+ for k in update:
+ if k not in ('stdout', 'stderr', 'header', 'rc'):
+ if k not in self.updates:
+ self.updates[k] = []
+ self.updates[k].append(update[k])
+
+ def remoteComplete(self, maybeFailure):
+ for name,loog in self.logs.items():
+ if self._closeWhenFinished[name]:
+ if maybeFailure:
+ loog.addHeader("\nremoteFailed: %s" % maybeFailure)
+ else:
+ log.msg("closing log %s" % loog)
+ loog.finish()
+ return maybeFailure
+
+
+class LogObserver:
+ implements(interfaces.ILogObserver)
+
+ def setStep(self, step):
+ self.step = step
+
+ def setLog(self, loog):
+ assert interfaces.IStatusLog.providedBy(loog)
+ loog.subscribe(self, True)
+
+ def logChunk(self, build, step, log, channel, text):
+ if channel == interfaces.LOG_CHANNEL_STDOUT:
+ self.outReceived(text)
+ elif channel == interfaces.LOG_CHANNEL_STDERR:
+ self.errReceived(text)
+
+ # TODO: add a logEnded method? er, stepFinished?
+
+ def outReceived(self, data):
+ """This will be called with chunks of stdout data. Override this in
+ your observer."""
+ pass
+
+ def errReceived(self, data):
+ """This will be called with chunks of stderr data. Override this in
+ your observer."""
+ pass
+
+
+class LogLineObserver(LogObserver):
+ def __init__(self):
+ self.stdoutParser = basic.LineOnlyReceiver()
+ self.stdoutParser.delimiter = "\n"
+ self.stdoutParser.lineReceived = self.outLineReceived
+ self.stdoutParser.transport = self # for the .disconnecting attribute
+ self.disconnecting = False
+
+ self.stderrParser = basic.LineOnlyReceiver()
+ self.stderrParser.delimiter = "\n"
+ self.stderrParser.lineReceived = self.errLineReceived
+ self.stderrParser.transport = self
+
+ def setMaxLineLength(self, max_length):
+ """
+ Set the maximum line length: lines longer than max_length are
+ dropped. Default is 16384 bytes. Use sys.maxint for effective
+ infinity.
+ """
+ self.stdoutParser.MAX_LENGTH = max_length
+ self.stderrParser.MAX_LENGTH = max_length
+
+ def outReceived(self, data):
+ self.stdoutParser.dataReceived(data)
+
+ def errReceived(self, data):
+ self.stderrParser.dataReceived(data)
+
+ def outLineReceived(self, line):
+ """This will be called with complete stdout lines (not including the
+ delimiter). Override this in your observer."""
+ pass
+
+ def errLineReceived(self, line):
+ """This will be called with complete lines of stderr (not including
+ the delimiter). Override this in your observer."""
+ pass
+
+
+class RemoteShellCommand(LoggedRemoteCommand):
+ """This class helps you run a shell command on the build slave. It will
+ accumulate all the command's output into a Log named 'stdio'. When the
+ command is finished, it will fire a Deferred. You can then check the
+ results of the command and parse the output however you like."""
+
+ def __init__(self, workdir, command, env=None,
+ want_stdout=1, want_stderr=1,
+ timeout=20*60, logfiles={}, usePTY="slave-config"):
+ """
+ @type workdir: string
+ @param workdir: directory where the command ought to run,
+ relative to the Builder's home directory. Defaults to
+ '.': the same as the Builder's homedir. This should
+ probably be '.' for the initial 'cvs checkout'
+ command (which creates a workdir), and the Build-wide
+ workdir for all subsequent commands (including
+ compiles and 'cvs update').
+
+ @type command: list of strings (or string)
+ @param command: the shell command to run, like 'make all' or
+ 'cvs update'. This should be a list or tuple
+ which can be used directly as the argv array.
+ For backwards compatibility, if this is a
+ string, the text will be given to '/bin/sh -c
+ %s'.
+
+ @type env: dict of string->string
+ @param env: environment variables to add or change for the
+ slave. Each command gets a separate
+ environment; all inherit the slave's initial
+ one. TODO: make it possible to delete some or
+ all of the slave's environment.
+
+ @type want_stdout: bool
+ @param want_stdout: defaults to True. Set to False if stdout should
+ be thrown away. Do this to avoid storing or
+ sending large amounts of useless data.
+
+ @type want_stderr: bool
+ @param want_stderr: False if stderr should be thrown away
+
+ @type timeout: int
+ @param timeout: tell the remote that if the command fails to
+ produce any output for this number of seconds,
+ the command is hung and should be killed. Use
+ None to disable the timeout.
+ """
+
+ self.command = command # stash .command, set it later
+ if env is not None:
+ # avoid mutating the original master.cfg dictionary. Each
+ # ShellCommand gets its own copy, any start() methods won't be
+ # able to modify the original.
+ env = env.copy()
+ args = {'workdir': workdir,
+ 'env': env,
+ 'want_stdout': want_stdout,
+ 'want_stderr': want_stderr,
+ 'logfiles': logfiles,
+ 'timeout': timeout,
+ 'usePTY': usePTY,
+ }
+ LoggedRemoteCommand.__init__(self, "shell", args)
+
+ def start(self):
+ self.args['command'] = self.command
+ if self.remote_command == "shell":
+ # non-ShellCommand slavecommands are responsible for doing this
+ # fixup themselves
+ if self.step.slaveVersion("shell", "old") == "old":
+ self.args['dir'] = self.args['workdir']
+ what = "command '%s' in dir '%s'" % (self.args['command'],
+ self.args['workdir'])
+ log.msg(what)
+ return LoggedRemoteCommand.start(self)
+
+ def __repr__(self):
+ return "<RemoteShellCommand '%s'>" % repr(self.command)
+
+class BuildStep:
+ """
+ I represent a single step of the build process. This step may involve
+ zero or more commands to be run in the build slave, as well as arbitrary
+ processing on the master side. Regardless of how many slave commands are
+ run, the BuildStep will result in a single status value.
+
+ The step is started by calling startStep(), which returns a Deferred that
+ fires when the step finishes. See C{startStep} for a description of the
+ results provided by that Deferred.
+
+ __init__ and start are good methods to override. Don't forget to upcall
+ BuildStep.__init__ or bad things will happen.
+
+ To launch a RemoteCommand, pass it to .runCommand and wait on the
+ Deferred it returns.
+
+ Each BuildStep generates status as it runs. This status data is fed to
+ the L{buildbot.status.builder.BuildStepStatus} listener that sits in
+ C{self.step_status}. It can also feed progress data (like how much text
+ is output by a shell command) to the
+ L{buildbot.status.progress.StepProgress} object that lives in
+ C{self.progress}, by calling C{self.setProgress(metric, value)} as it
+ runs.
+
+ @type build: L{buildbot.process.base.Build}
+ @ivar build: the parent Build which is executing this step
+
+ @type progress: L{buildbot.status.progress.StepProgress}
+ @ivar progress: tracks ETA for the step
+
+ @type step_status: L{buildbot.status.builder.BuildStepStatus}
+ @ivar step_status: collects output status
+ """
+
+ # these parameters are used by the parent Build object to decide how to
+ # interpret our results. haltOnFailure will affect the build process
+ # immediately, the others will be taken into consideration when
+ # determining the overall build status.
+ #
+ # steps that are makred as alwaysRun will be run regardless of the outcome
+ # of previous steps (especially steps with haltOnFailure=True)
+ haltOnFailure = False
+ flunkOnWarnings = False
+ flunkOnFailure = False
+ warnOnWarnings = False
+ warnOnFailure = False
+ alwaysRun = False
+
+ # 'parms' holds a list of all the parameters we care about, to allow
+ # users to instantiate a subclass of BuildStep with a mixture of
+ # arguments, some of which are for us, some of which are for the subclass
+ # (or a delegate of the subclass, like how ShellCommand delivers many
+ # arguments to the RemoteShellCommand that it creates). Such delegating
+ # subclasses will use this list to figure out which arguments are meant
+ # for us and which should be given to someone else.
+ parms = ['name', 'locks',
+ 'haltOnFailure',
+ 'flunkOnWarnings',
+ 'flunkOnFailure',
+ 'warnOnWarnings',
+ 'warnOnFailure',
+ 'alwaysRun',
+ 'progressMetrics',
+ ]
+
+ name = "generic"
+ locks = []
+ progressMetrics = () # 'time' is implicit
+ useProgress = True # set to False if step is really unpredictable
+ build = None
+ step_status = None
+ progress = None
+
+ def __init__(self, **kwargs):
+ self.factory = (self.__class__, dict(kwargs))
+ for p in self.__class__.parms:
+ if kwargs.has_key(p):
+ setattr(self, p, kwargs[p])
+ del kwargs[p]
+ if kwargs:
+ why = "%s.__init__ got unexpected keyword argument(s) %s" \
+ % (self, kwargs.keys())
+ raise TypeError(why)
+ self._pendingLogObservers = []
+
+ def setBuild(self, build):
+ # subclasses which wish to base their behavior upon qualities of the
+ # Build (e.g. use the list of changed files to run unit tests only on
+ # code which has been modified) should do so here. The Build is not
+ # available during __init__, but setBuild() will be called just
+ # afterwards.
+ self.build = build
+
+ def setBuildSlave(self, buildslave):
+ self.buildslave = buildslave
+
+ def setDefaultWorkdir(self, workdir):
+ # The Build calls this just after __init__(). ShellCommand
+ # and variants use a slave-side workdir, but some other steps
+ # do not. Subclasses which use a workdir should use the value
+ # set by this method unless they were constructed with
+ # something more specific.
+ pass
+
+ def addFactoryArguments(self, **kwargs):
+ self.factory[1].update(kwargs)
+
+ def getStepFactory(self):
+ return self.factory
+
+ def setStepStatus(self, step_status):
+ self.step_status = step_status
+
+ def setupProgress(self):
+ if self.useProgress:
+ sp = progress.StepProgress(self.name, self.progressMetrics)
+ self.progress = sp
+ self.step_status.setProgress(sp)
+ return sp
+ return None
+
+ def setProgress(self, metric, value):
+ """BuildSteps can call self.setProgress() to announce progress along
+ some metric."""
+ if self.progress:
+ self.progress.setProgress(metric, value)
+
+ def getProperty(self, propname):
+ return self.build.getProperty(propname)
+
+ def setProperty(self, propname, value, source="Step"):
+ self.build.setProperty(propname, value, source)
+
+ def startStep(self, remote):
+ """Begin the step. This returns a Deferred that will fire when the
+ step finishes.
+
+ This deferred fires with a tuple of (result, [extra text]), although
+ older steps used to return just the 'result' value, so the receiving
+ L{base.Build} needs to be prepared to handle that too. C{result} is
+ one of the SUCCESS/WARNINGS/FAILURE/SKIPPED constants from
+ L{buildbot.status.builder}, and the extra text is a list of short
+ strings which should be appended to the Build's text results. This
+ text allows a test-case step which fails to append B{17 tests} to the
+ Build's status, in addition to marking the build as failing.
+
+ The deferred will errback if the step encounters an exception,
+ including an exception on the slave side (or if the slave goes away
+ altogether). Failures in shell commands (rc!=0) will B{not} cause an
+ errback, in general the BuildStep will evaluate the results and
+ decide whether to treat it as a WARNING or FAILURE.
+
+ @type remote: L{twisted.spread.pb.RemoteReference}
+ @param remote: a reference to the slave's
+ L{buildbot.slave.bot.SlaveBuilder} instance where any
+ RemoteCommands may be run
+ """
+
+ self.remote = remote
+ self.deferred = defer.Deferred()
+ # convert all locks into their real form
+ lock_list = []
+ for access in self.locks:
+ if not isinstance(access, locks.LockAccess):
+ # Buildbot 0.7.7 compability: user did not specify access
+ access = access.defaultAccess()
+ lock = self.build.builder.botmaster.getLockByID(access.lockid)
+ lock_list.append((lock, access))
+ self.locks = lock_list
+ # then narrow SlaveLocks down to the slave that this build is being
+ # run on
+ self.locks = [(l.getLock(self.build.slavebuilder), la) for l, la in self.locks]
+ for l, la in self.locks:
+ if l in self.build.locks:
+ log.msg("Hey, lock %s is claimed by both a Step (%s) and the"
+ " parent Build (%s)" % (l, self, self.build))
+ raise RuntimeError("lock claimed by both Step and Build")
+ d = self.acquireLocks()
+ d.addCallback(self._startStep_2)
+ return self.deferred
+
+ def acquireLocks(self, res=None):
+ log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
+ if not self.locks:
+ return defer.succeed(None)
+ for lock, access in self.locks:
+ if not lock.isAvailable(access):
+ log.msg("step %s waiting for lock %s" % (self, lock))
+ d = lock.waitUntilMaybeAvailable(self, access)
+ d.addCallback(self.acquireLocks)
+ return d
+ # all locks are available, claim them all
+ for lock, access in self.locks:
+ lock.claim(self, access)
+ return defer.succeed(None)
+
+ def _startStep_2(self, res):
+ if self.progress:
+ self.progress.start()
+ self.step_status.stepStarted()
+ try:
+ skip = self.start()
+ if skip == SKIPPED:
+ # this return value from self.start is a shortcut
+ # to finishing the step immediately
+ reactor.callLater(0, self.finished, SKIPPED)
+ except:
+ log.msg("BuildStep.startStep exception in .start")
+ self.failed(Failure())
+
+ def start(self):
+ """Begin the step. Override this method and add code to do local
+ processing, fire off remote commands, etc.
+
+ To spawn a command in the buildslave, create a RemoteCommand instance
+ and run it with self.runCommand::
+
+ c = RemoteCommandFoo(args)
+ d = self.runCommand(c)
+ d.addCallback(self.fooDone).addErrback(self.failed)
+
+ As the step runs, it should send status information to the
+ BuildStepStatus::
+
+ self.step_status.setText(['compile', 'failed'])
+ self.step_status.setText2(['4', 'warnings'])
+
+ To have some code parse stdio (or other log stream) in realtime, add
+ a LogObserver subclass. This observer can use self.step.setProgress()
+ to provide better progress notification to the step.::
+
+ self.addLogObserver('stdio', MyLogObserver())
+
+ To add a LogFile, use self.addLog. Make sure it gets closed when it
+ finishes. When giving a Logfile to a RemoteShellCommand, just ask it
+ to close the log when the command completes::
+
+ log = self.addLog('output')
+ cmd = RemoteShellCommand(args)
+ cmd.useLog(log, closeWhenFinished=True)
+
+ You can also create complete Logfiles with generated text in a single
+ step::
+
+ self.addCompleteLog('warnings', text)
+
+ When the step is done, it should call self.finished(result). 'result'
+ will be provided to the L{buildbot.process.base.Build}, and should be
+ one of the constants defined above: SUCCESS, WARNINGS, FAILURE, or
+ SKIPPED.
+
+ If the step encounters an exception, it should call self.failed(why).
+ 'why' should be a Failure object. This automatically fails the whole
+ build with an exception. It is a good idea to add self.failed as an
+ errback to any Deferreds you might obtain.
+
+ If the step decides it does not need to be run, start() can return
+ the constant SKIPPED. This fires the callback immediately: it is not
+ necessary to call .finished yourself. This can also indicate to the
+ status-reporting mechanism that this step should not be displayed."""
+
+ raise NotImplementedError("your subclass must implement this method")
+
+ def interrupt(self, reason):
+ """Halt the command, either because the user has decided to cancel
+ the build ('reason' is a string), or because the slave has
+ disconnected ('reason' is a ConnectionLost Failure). Any further
+ local processing should be skipped, and the Step completed with an
+ error status. The results text should say something useful like
+ ['step', 'interrupted'] or ['remote', 'lost']"""
+ pass
+
+ def releaseLocks(self):
+ log.msg("releaseLocks(%s): %s" % (self, self.locks))
+ for lock, access in self.locks:
+ lock.release(self, access)
+
+ def finished(self, results):
+ if self.progress:
+ self.progress.finish()
+ self.step_status.stepFinished(results)
+ self.releaseLocks()
+ self.deferred.callback(results)
+
+ def failed(self, why):
+ # if isinstance(why, pb.CopiedFailure): # a remote exception might
+ # only have short traceback, so formatFailure is not as useful as
+ # you'd like (no .frames, so no traceback is displayed)
+ log.msg("BuildStep.failed, traceback follows")
+ log.err(why)
+ try:
+ if self.progress:
+ self.progress.finish()
+ self.addHTMLLog("err.html", formatFailure(why))
+ self.addCompleteLog("err.text", why.getTraceback())
+ # could use why.getDetailedTraceback() for more information
+ self.step_status.setText([self.name, "exception"])
+ self.step_status.setText2([self.name])
+ self.step_status.stepFinished(EXCEPTION)
+ except:
+ log.msg("exception during failure processing")
+ log.err()
+ # the progress stuff may still be whacked (the StepStatus may
+ # think that it is still running), but the build overall will now
+ # finish
+ try:
+ self.releaseLocks()
+ except:
+ log.msg("exception while releasing locks")
+ log.err()
+
+ log.msg("BuildStep.failed now firing callback")
+ self.deferred.callback(EXCEPTION)
+
+ # utility methods that BuildSteps may find useful
+
+ def slaveVersion(self, command, oldversion=None):
+ """Return the version number of the given slave command. For the
+ commands defined in buildbot.slave.commands, this is the value of
+ 'cvs_ver' at the top of that file. Non-existent commands will return
+ a value of None. Buildslaves running buildbot-0.5.0 or earlier did
+ not respond to the version query: commands on those slaves will
+ return a value of OLDVERSION, so you can distinguish between old
+ buildslaves and missing commands.
+
+ If you know that <=0.5.0 buildslaves have the command you want (CVS
+ and SVN existed back then, but none of the other VC systems), then it
+ makes sense to call this with oldversion='old'. If the command you
+ want is newer than that, just leave oldversion= unspecified, and the
+ command will return None for a buildslave that does not implement the
+ command.
+ """
+ return self.build.getSlaveCommandVersion(command, oldversion)
+
+ def slaveVersionIsOlderThan(self, command, minversion):
+ sv = self.build.getSlaveCommandVersion(command, None)
+ if sv is None:
+ return True
+ # the version we get back is a string form of the CVS version number
+ # of the slave's buildbot/slave/commands.py, something like 1.39 .
+ # This might change in the future (I might move away from CVS), but
+ # if so I'll keep updating that string with suitably-comparable
+ # values.
+ if sv.split(".") < minversion.split("."):
+ return True
+ return False
+
+ def getSlaveName(self):
+ return self.build.getSlaveName()
+
+ def addLog(self, name):
+ loog = self.step_status.addLog(name)
+ self._connectPendingLogObservers()
+ return loog
+
+ def getLog(self, name):
+ for l in self.step_status.getLogs():
+ if l.getName() == name:
+ return l
+ raise KeyError("no log named '%s'" % (name,))
+
+ def addCompleteLog(self, name, text):
+ log.msg("addCompleteLog(%s)" % name)
+ loog = self.step_status.addLog(name)
+ size = loog.chunkSize
+ for start in range(0, len(text), size):
+ loog.addStdout(text[start:start+size])
+ loog.finish()
+ self._connectPendingLogObservers()
+
+ def addHTMLLog(self, name, html):
+ log.msg("addHTMLLog(%s)" % name)
+ self.step_status.addHTMLLog(name, html)
+ self._connectPendingLogObservers()
+
+ def addLogObserver(self, logname, observer):
+ assert interfaces.ILogObserver.providedBy(observer)
+ observer.setStep(self)
+ self._pendingLogObservers.append((logname, observer))
+ self._connectPendingLogObservers()
+
+ def _connectPendingLogObservers(self):
+ if not self._pendingLogObservers:
+ return
+ if not self.step_status:
+ return
+ current_logs = {}
+ for loog in self.step_status.getLogs():
+ current_logs[loog.getName()] = loog
+ for logname, observer in self._pendingLogObservers[:]:
+ if logname in current_logs:
+ observer.setLog(current_logs[logname])
+ self._pendingLogObservers.remove((logname, observer))
+
+ def addURL(self, name, url):
+ """Add a BuildStep URL to this step.
+
+ An HREF to this URL will be added to any HTML representations of this
+ step. This allows a step to provide links to external web pages,
+ perhaps to provide detailed HTML code coverage results or other forms
+ of build status.
+ """
+ self.step_status.addURL(name, url)
+
+ def runCommand(self, c):
+ c.buildslave = self.buildslave
+ d = c.run(self, self.remote)
+ return d
+
+
+class OutputProgressObserver(LogObserver):
+ length = 0
+
+ def __init__(self, name):
+ self.name = name
+
+ def logChunk(self, build, step, log, channel, text):
+ self.length += len(text)
+ self.step.setProgress(self.name, self.length)
+
+class LoggingBuildStep(BuildStep):
+ """This is an abstract base class, suitable for inheritance by all
+ BuildSteps that invoke RemoteCommands which emit stdout/stderr messages.
+ """
+
+ progressMetrics = ('output',)
+ logfiles = {}
+
+ parms = BuildStep.parms + ['logfiles']
+
+ def __init__(self, logfiles={}, *args, **kwargs):
+ BuildStep.__init__(self, *args, **kwargs)
+ self.addFactoryArguments(logfiles=logfiles)
+ # merge a class-level 'logfiles' attribute with one passed in as an
+ # argument
+ self.logfiles = self.logfiles.copy()
+ self.logfiles.update(logfiles)
+ self.addLogObserver('stdio', OutputProgressObserver("output"))
+
+ def describe(self, done=False):
+ raise NotImplementedError("implement this in a subclass")
+
+ def startCommand(self, cmd, errorMessages=[]):
+ """
+ @param cmd: a suitable RemoteCommand which will be launched, with
+ all output being put into our self.stdio_log LogFile
+ """
+ log.msg("ShellCommand.startCommand(cmd=%s)" % (cmd,))
+ log.msg(" cmd.args = %r" % (cmd.args))
+ self.cmd = cmd # so we can interrupt it
+ self.step_status.setText(self.describe(False))
+
+ # stdio is the first log
+ self.stdio_log = stdio_log = self.addLog("stdio")
+ cmd.useLog(stdio_log, True)
+ for em in errorMessages:
+ stdio_log.addHeader(em)
+ # TODO: consider setting up self.stdio_log earlier, and have the
+ # code that passes in errorMessages instead call
+ # self.stdio_log.addHeader() directly.
+
+ # there might be other logs
+ self.setupLogfiles(cmd, self.logfiles)
+
+ d = self.runCommand(cmd) # might raise ConnectionLost
+ d.addCallback(lambda res: self.commandComplete(cmd))
+ d.addCallback(lambda res: self.createSummary(cmd.logs['stdio']))
+ d.addCallback(lambda res: self.evaluateCommand(cmd)) # returns results
+ def _gotResults(results):
+ self.setStatus(cmd, results)
+ return results
+ d.addCallback(_gotResults) # returns results
+ d.addCallbacks(self.finished, self.checkDisconnect)
+ d.addErrback(self.failed)
+
+ def setupLogfiles(self, cmd, logfiles):
+ """Set up any additional logfiles= logs.
+ """
+ for logname,remotefilename in logfiles.items():
+ # tell the BuildStepStatus to add a LogFile
+ newlog = self.addLog(logname)
+ # and tell the LoggedRemoteCommand to feed it
+ cmd.useLog(newlog, True)
+
+ def interrupt(self, reason):
+ # TODO: consider adding an INTERRUPTED or STOPPED status to use
+ # instead of FAILURE, might make the text a bit more clear.
+ # 'reason' can be a Failure, or text
+ self.addCompleteLog('interrupt', str(reason))
+ d = self.cmd.interrupt(reason)
+ return d
+
+ def checkDisconnect(self, f):
+ f.trap(error.ConnectionLost)
+ self.step_status.setText(self.describe(True) +
+ ["failed", "slave", "lost"])
+ self.step_status.setText2(["failed", "slave", "lost"])
+ return self.finished(FAILURE)
+
+ # to refine the status output, override one or more of the following
+ # methods. Change as little as possible: start with the first ones on
+ # this list and only proceed further if you have to
+ #
+ # createSummary: add additional Logfiles with summarized results
+ # evaluateCommand: decides whether the step was successful or not
+ #
+ # getText: create the final per-step text strings
+ # describeText2: create the strings added to the overall build status
+ #
+ # getText2: only adds describeText2() when the step affects build status
+ #
+ # setStatus: handles all status updating
+
+ # commandComplete is available for general-purpose post-completion work.
+ # It is a good place to do one-time parsing of logfiles, counting
+ # warnings and errors. It should probably stash such counts in places
+ # like self.warnings so they can be picked up later by your getText
+ # method.
+
+ # TODO: most of this stuff should really be on BuildStep rather than
+ # ShellCommand. That involves putting the status-setup stuff in
+ # .finished, which would make it hard to turn off.
+
+ def commandComplete(self, cmd):
+ """This is a general-purpose hook method for subclasses. It will be
+ called after the remote command has finished, but before any of the
+ other hook functions are called."""
+ pass
+
+ def createSummary(self, log):
+ """To create summary logs, do something like this:
+ warnings = grep('^Warning:', log.getText())
+ self.addCompleteLog('warnings', warnings)
+ """
+ pass
+
+ def evaluateCommand(self, cmd):
+ """Decide whether the command was SUCCESS, WARNINGS, or FAILURE.
+ Override this to, say, declare WARNINGS if there is any stderr
+ activity, or to say that rc!=0 is not actually an error."""
+
+ if cmd.rc != 0:
+ return FAILURE
+ # if cmd.log.getStderr(): return WARNINGS
+ return SUCCESS
+
+ def getText(self, cmd, results):
+ if results == SUCCESS:
+ return self.describe(True)
+ elif results == WARNINGS:
+ return self.describe(True) + ["warnings"]
+ else:
+ return self.describe(True) + ["failed"]
+
+ def getText2(self, cmd, results):
+ """We have decided to add a short note about ourselves to the overall
+ build description, probably because something went wrong. Return a
+ short list of short strings. If your subclass counts test failures or
+ warnings of some sort, this is a good place to announce the count."""
+ # return ["%d warnings" % warningcount]
+ # return ["%d tests" % len(failedTests)]
+ return [self.name]
+
+ def maybeGetText2(self, cmd, results):
+ if results == SUCCESS:
+ # successful steps do not add anything to the build's text
+ pass
+ elif results == WARNINGS:
+ if (self.flunkOnWarnings or self.warnOnWarnings):
+ # we're affecting the overall build, so tell them why
+ return self.getText2(cmd, results)
+ else:
+ if (self.haltOnFailure or self.flunkOnFailure
+ or self.warnOnFailure):
+ # we're affecting the overall build, so tell them why
+ return self.getText2(cmd, results)
+ return []
+
+ def setStatus(self, cmd, results):
+ # this is good enough for most steps, but it can be overridden to
+ # get more control over the displayed text
+ self.step_status.setText(self.getText(cmd, results))
+ self.step_status.setText2(self.maybeGetText2(cmd, results))
+
+# (WithProeprties used to be available in this module)
+from buildbot.process.properties import WithProperties
+_hush_pyflakes = [WithProperties]
+del _hush_pyflakes
+