|
# This file is part of Buildbot. Buildbot is free software: you can # redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright Buildbot Team Members
EXCEPTION, RETRY, worst_status
pass
# class-level unique identifier generator for command ids
def __repr__(self): return "<RemoteCommand '%s' at %d>" % (self.remote_command, id(self))
# generate a new command id
# _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.
# Connections which are lost while the command is running are caught # when our parent Step calls our .lostRemote() method.
assert interfaces.ILogFile.providedBy(log) if not logfileName: logfileName = log.getName() assert logfileName not in self.logs assert logfileName not in self.delayedLogs self.logs[logfileName] = log self._closeWhenFinished[logfileName] = closeWhenFinished
# 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. self.remote_command, self.args)
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)
log.msg("RemoteCommand.interrupt", self, why) if not self.active: log.msg(" but this RemoteCommand is already inactive") return defer.succeed(None) if not self.remote: log.msg(" but our .remote went away") return defer.succeed(None) if isinstance(why, Failure) and why.check(error.ConnectionLost): log.msg("RemoteCommand.disconnect: lost slave") self.remote = None self._finished(why) return defer.succeed(None)
# 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
log.msg("RemoteCommand._interruptFailed", self) # TODO: forcibly stop the Command now, since we can't stop it # cleanly return None
""" 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 and not self.ignore_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
""" 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
if 'stdio' in self.logs: self.logs['stdio'].addStdout(data) if self.collectStdout: self.stdout += data
if 'stdio' in self.logs: self.logs['stdio'].addStderr(data)
if 'stdio' in self.logs: self.logs['stdio'].addHeader(data)
# Activate delayed logs on first data. if logname in self.delayedLogs: (activateCallBack, closeWhenFinished) = self.delayedLogs[logname] del self.delayedLogs[logname] loog = activateCallBack(self) self.logs[logname] = loog self._closeWhenFinished[logname] = closeWhenFinished
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) if update.has_key('elapsed'): self._remoteElapsed = update['elapsed']
# TODO: these should be handled at the RemoteCommand level 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])
if self._startTime and self._remoteElapsed: delta = (util.now() - self._startTime) - self._remoteElapsed metrics.MetricTimeEvent.log("RemoteCommand.overhead", delta)
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
assert interfaces.IStatusLog.providedBy(loog) loog.subscribe(self, True)
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?
"""This will be called with chunks of stdout data. Override this in your observer.""" pass
"""This will be called with chunks of stderr data. Override this in your observer.""" pass
""" 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
self.stderrParser.dataReceived(data)
"""This will be called with complete stdout lines (not including the delimiter). Override this in your observer.""" pass
"""This will be called with complete lines of stderr (not including the delimiter). Override this in your observer.""" pass
want_stdout=1, want_stderr=1, timeout=20*60, maxTime=None, logfiles={}, usePTY="slave-config", logEnviron=True, collectStdout=False, interruptSignal=None, initialStdin=None):
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, 'maxTime': maxTime, 'usePTY': usePTY, 'logEnviron': logEnviron, 'initial_stdin': initialStdin } if interruptSignal is not None: args['interruptSignal'] = interruptSignal RemoteCommand.__init__(self, "shell", args, collectStdout=collectStdout)
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 RemoteCommand._start(self)
def __repr__(self): return "<RemoteShellCommand '%s'>" % repr(self.command)
""" This is a wrapper to record the arguments passed to as BuildStep subclass. We use an instance of this class, rather than a closure mostly to make it easier to test that the right factories are getting created. """
except: log.msg("error while creating step, factory=%s, args=%s, kwargs=%s" % (self.factory, self.args, self.kwargs)) raise
# properties set on a build step are, by nature, always runtime properties
# '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. 'haltOnFailure', 'flunkOnWarnings', 'flunkOnFailure', 'warnOnWarnings', 'warnOnFailure', 'alwaysRun', 'progressMetrics', 'useProgress', 'doStepIf', 'hideStepIf', ]
why = "%s.__init__ got unexpected keyword argument(s) %s" \ % (self, kwargs.keys()) raise TypeError(why)
pass
# this is here for backwards compatability pass
return None
# convert all locks into their real form # Buildbot 0.7.7 compability: user did not specify access access = access.defaultAccess() # then narrow SlaveLocks down to the slave that this build is being # run on 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")
# Set the step's text here so that the stepStarted notification sees # the correct description
return defer.succeed(None) self.step_status.setWaitingForLocks(True) log.msg("step %s waiting for lock %s" % (self, lock)) d = lock.waitUntilMaybeAvailable(self, access) d.addCallback(self.acquireLocks) self._acquiringLock = (lock, access, d) return d # all locks are available, claim them all
else:
def _startStep_3(self, doStep): doStep = False
# this return value from self.start is a shortcut to finishing # the step immediately; we skip calling finished() as # subclasses may have overridden that an expect it to be called # after start() (bug #837)
raise NotImplementedError("your subclass must implement this method")
lock, access, d = self._acquiringLock lock.stopWaitingUntilAvailable(self, access, d) d.callback(None)
else: # This should only happen if we've been interrupted assert self.stopped
# We handle this specially because we don't care about # the return code of an interrupted command; we know # that this should just be exception due to interrupt # At the same time we must respect RETRY status because it's used # to retry interrupted build due to some other issues for example # due to slave lost ["interrupted"])
# internal function to indicate that this step is done; this is separated # from finished() so that subclasses can override finished()
# This can either be a BuildStepFailed exception/failure, meaning we # should call self.finished, or it can be a real exception, which should # be recorded as such.
# could use why.getDetailedTraceback() for more information
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 except: log.msg("exception while releasing locks") log.err()
# utility methods that BuildSteps may find useful
return True
return self.build.getSlaveName()
loog = self.step_status.addLog(name) self._connectPendingLogObservers() return loog
raise KeyError("no log named '%s'" % (name,))
log.msg("addHTMLLog(%s)" % name) self.step_status.addHTMLLog(name, html) self._connectPendingLogObservers()
return current_logs[loog.getName()] = loog observer.setLog(current_logs[logname]) self._pendingLogObservers.remove((logname, observer))
def _maybeEvaluate(value, *args, **kwargs):
BuildStep._getStepFactory, BuildStep, interfaces.IBuildStepFactory) lambda step : interfaces.IProperties(step.build), BuildStep, interfaces.IProperties)
self.length += len(text) self.step.setProgress(self.name, self.length)
*args, **kwargs):
config.error( "the ShellCommand 'logfiles' parameter must be a dictionary")
# merge a class-level 'logfiles' attribute with one passed in as an # argument config.error( "the 'log_eval_func' paramater must be a callable")
self.logfiles[logname] = filename
""" @param cmd: a suitable RemoteCommand which will be launched, with all output being put into our self.stdio_log LogFile """
# stdio is the first log # 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
# Ask RemoteCommand to watch a logfile, but only add # it when/if we see any data. # # The dummy default argument local_logname is a work-around for # Python name binding; default values are bound by value, but # captured variables in the body are bound by name. callback = lambda cmd_arg, local_logname=logname: self.addLog(local_logname) cmd.useLogDelayed(logname, callback, True) else: # tell the BuildStepStatus to add a LogFile # and tell the RemoteCommand to feed it
# 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 else: self.addCompleteLog('interrupt', str(reason))
d = self.cmd.interrupt(reason) d.addErrback(log.err, 'while interrupting command')
self.step_status.setText(self.describe(True) + ["exception", "slave", "lost"]) self.step_status.setText2(["exception", "slave", "lost"]) return self.finished(RETRY)
pass
pass
return self.describe(True) + ["exception"] else:
# successful steps do not add anything to the build's text pass # we're affecting the overall build, so tell them why return self.getText2(cmd, results) else: or self.warnOnFailure): # we're affecting the overall build, so tell them why
# this is good enough for most steps, but it can be overridden to # get more control over the displayed text
# Parses the logs for a list of regexs. Meant to be invoked like: # regexes = ((re.compile(...), FAILURE), (re.compile(...), WARNINGS)) # self.addStep(ShellCommand, # command=..., # ..., # log_eval_func=lambda c,s: regex_log_evaluator(c, s, regexs) # ) worst = FAILURE # worst_status returns the worse of the two status' passed to it. # we won't be changing "worst" unless possible_status is worse than it, # so we don't even need to check the log if that's the case
# (WithProperties used to be available in this module)
|