blob: 8a9cde07e2414ffcd2b02fb011da743be4135bdd [file] [log] [blame]
# pylint: disable=C0103
"""This class knows how to understand annotations.
Here are a list of the currently supported annotations:
@@@BUILD_STEP <stepname>@@@
Add a new step <stepname>. End the current step, marking with last available
status.
@@@STEP_LINK@<label>@<url>@@@
Add a link with label <label> linking to <url> to the current stage.
@@@STEP_WARNINGS@@@
Mark the current step as having warnings (oragnge).
@@@STEP_FAILURE@@@
Mark the current step as having failed (red).
@@@STEP_EXCEPTION@@@
Mark the current step as having exceptions (magenta).
@@@STEP_CLEAR@@@
Reset the text description of the current step.
@@@STEP_SUMMARY_CLEAR@@@
Reset the text summary of the current step.
@@@STEP_TEXT@<msg>@@@
Append <msg> to the current step text.
@@@STEP_SUMMARY_TEXT@<msg>@@@
Append <msg> to the step summary (appears on top of the waterfall).
@@@HALT_ON_FAILURE@@@
Halt if exception or failure steps are encountered (default is not).
@@@HONOR_ZERO_RETURN_CODE@@@
Honor the return code being zero (success), even if steps have other results.
Deprecated annotations:
TODO(bradnelson): drop these when all users have been tracked down.
@@@BUILD_WARNINGS@@@
Equivalent to @@@STEP_WARNINGS@@@
@@@BUILD_FAILED@@@
Equivalent to @@@STEP_FAILURE@@@
@@@BUILD_EXCEPTION@@@
Equivalent to @@@STEP_EXCEPTION@@@
@@@link@<label>@<url>@@@
Equivalent to @@@STEP_LINK@<label>@<url>@@@
"""
import re
from twisted.internet import defer
from twisted.internet import error
from twisted.python import failure
from twisted.python import log as logging
from buildbot.process import buildstep
from buildbot.process import results
from buildbot.process import logobserver
from buildbot.plugins import util
from buildbot.util import asyncSleep
from buildbot.util.misc import deferredLocked
if False: # for debugging
debuglog = logging.msg
else:
debuglog = lambda m: None
class AnnotatedBuildStep(buildstep.BuildStep):
def __init__(self, parent_step, *args, **kwargs):
self.parent_step = parent_step
buildstep.BuildStep.__init__(self, *args, **kwargs)
# Step is really unpredictable
self.useProgress = False
self.build = self.parent_step.build
self.master = self.parent_step.build.master
self.worker = self.parent_step.worker
self.stdio_log = None
self._loglines = []
self._logLock = defer.DeferredLock()
self._request_finish = False
@defer.inlineCallbacks
def addStep(self):
debuglog("AnnotatedBuildStep: adding step '%s': buildid=%s" % (
self.name, self.build.buildid))
yield buildstep.BuildStep.addStep(self)
# Put ourselves into a list of processed steps of the build object.
self.build.executedSteps.append(self)
# Init result to SUCCESS by default.
self.results = results.SUCCESS
self.description = [self.name]
self.stdio_log = yield self.addLog('stdio')
debuglog("AnnotatedBuildStep: added step '%s': stepid=%s, buildid=%s" % (
self.name, self.stepid, self.build.buildid))
@defer.inlineCallbacks
def finishStep(self):
hidden = False
debuglog("AnnotatedBuildStep: finish step '%s': stepid=%s, buildid=%s, results=%s" % (
self.name, self.stepid, self.build.buildid, self.results))
yield self.master.data.updates.finishStep(self.stepid, self.results,
hidden)
# finish unfinished logs
yield self.finishUnfinishedLogs()
@defer.inlineCallbacks
def startStep(self, remote, done=False):
debuglog("AnnotatedBuildStep::startStep() starting '%s': "
"buildid=%s, done=%s" % (
self.name, self.build.buildid, done))
try:
yield self.addStep()
try:
self.realUpdateSummary()
# The "main" step has finished already.
# Do only necessary work.
#NOTE: we can get 'request finish' flag before the step started.
self._request_finish = self._request_finish or done
self._running = True
self.results = yield self.run()
finally:
self._running = False
except buildstep.BuildStepCancelled:
self.results = results.CANCELLED
except buildstep.BuildStepFailed:
self.results = results.FAILURE
except error.ConnectionLost:
self.results = results.RETRY
except Exception:
self.results = results.EXCEPTION
why = failure.Failure()
logging.err(why, "AnnotatedBuildStep.failed; traceback follows")
yield self.addLogWithFailure(why)
finally:
# update the summary one last time, make sure that completes,
# and then don't update it any more.
self.realUpdateSummary()
yield self.realUpdateSummary.stop()
# Update step status in the database.
yield self.finishStep()
debuglog("AnnotatedBuildStep::startStep() completed '%s': "
"stepid=%s, buildid=%s, results=%s" % (
self.name, self.stepid, self.build.buildid, self.results))
def requestFinish(self, status=None):
debuglog("AnnotatedBuildStep::requestFinish(%r): '%s': "
"stepid=%s, buildid=%s, results=%r" % (
status, self.name, self.stepid, self.build.buildid, self.results))
# Update the current step status with the worst result.
if status is not None:
self.updateStatus(status)
self._request_finish = True
def setStepText(self, text=None):
if text is not None:
if self.description is not None:
self.description.append(text)
else:
self.description = []
self.updateSummary()
def setStepSummary(self, text=None):
if text is not None:
if self.descriptionDone is not None:
self.descriptionDone.append(text)
else:
self.descriptionDone = []
self.updateSummary()
@deferredLocked("_logLock")
def _flushLogs(self):
if self._loglines:
ll = "".join(self._loglines)
self._loglines = []
self.stdio_log.addStdout(ll)
return defer.succeed(None)
@deferredLocked("_logLock")
def _addLogs(self, text):
if text is not None:
self._loglines.append(text)
return defer.succeed(None)
@defer.inlineCallbacks
def run(self):
debuglog("AnnotatedBuildStep::run() starting '%s' step: "
"stepid=%s, buildid=%s" % (
self.name, self.stepid, self.build.buildid))
# Save previously collected log lines.
yield self._flushLogs()
while not self._request_finish:
# Sleep for .1 second. Let arrive more logs.
yield asyncSleep(.1)
if self._loglines:
yield self._flushLogs()
# and the last one time.
if self._loglines:
yield self._flushLogs()
debuglog("AnnotatedBuildStep::run() exiting '%s' step: "
"stepid=%s, buildid=%s, results=%s" % (
self.name, self.stepid, self.build.buildid, self.results))
return self.results
@defer.inlineCallbacks
def scheduleStdout(self, text):
if text is None:
return
# Not running yet, just store a log line.
if not self._running:
self._loglines.append(text)
return
# Already executed, add the log line with locking.
yield self._addLogs(text=text)
def updateStatus(self, status):
self.results = results.worst_status(self.results, status)
ALL_COMMANDS = list(range(11))
STEP_LINK, STEP_WARNINGS, STEP_FAILURE, STEP_EXCEPTION, HALT_ON_FAILURE, HONOR_ZERO_RC, STEP_CLEAR, STEP_SUMMARY_CLEAR, STEP_TEXT, STEP_SUMMARY_TEXT, BUILD_STEP = ALL_COMMANDS
class AnnotatedCommand(buildstep.ShellMixin, buildstep.BuildStep):
"""Buildbot command that knows how to display annotations."""
_re_step_link = re.compile(r'^@@@(STEP_LINK|link)@(?P<link_label>.*)@(?P<link_url>.*)@@@', re.M)
_re_step_warnings = re.compile(r'^@@@(STEP_WARNINGS|BUILD_WARNINGS)@@@', re.M)
_re_step_failure = re.compile(r'^@@@(STEP_FAILURE|BUILD_FAILED)@@@', re.M)
_re_step_exception = re.compile(r'^@@@(STEP_EXCEPTION|BUILD_EXCEPTION)@@@', re.M)
_re_halt_on_failure = re.compile(r'^@@@HALT_ON_FAILURE@@@', re.M)
_re_honor_zero_rc = re.compile(r'^@@@HONOR_ZERO_RETURN_CODE@@@', re.M)
_re_step_clear = re.compile(r'^@@@STEP_CLEAR@@@', re.M)
_re_step_summary_clear = re.compile(r'^@@@STEP_SUMMARY_CLEAR@@@', re.M)
_re_step_text = re.compile(r'^@@@STEP_TEXT@(?P<text>.*)@@@', re.M)
_re_step_summary_step = re.compile(r'^@@@STEP_SUMMARY_TEXT@(?P<text>.*)@@@', re.M)
_re_build_step = re.compile(r'^@@@BUILD_STEP (?P<name>.*)@@@', re.M)
_re_set = [
# Support: @@@STEP_LINK@<name>@<url>@@@ (emit link)
# Also support depreceated @@@link@<name>@<url>@@@
( STEP_LINK, _re_step_link),
# Support: @@@STEP_WARNINGS@@@ (warn on a stage)
# Also support deprecated @@@BUILD_WARNINGS@@@
( STEP_WARNINGS, _re_step_warnings ),
# Support: @@@STEP_FAILURE@@@ (fail a stage)
# Also support deprecated @@@BUILD_FAILED@@@
( STEP_FAILURE, _re_step_failure ),
# Support: @@@STEP_EXCEPTION@@@ (exception on a stage)
# Also support deprecated @@@BUILD_FAILED@@@
( STEP_EXCEPTION, _re_step_exception ),
# Support: @@@HALT_ON_FAILURE@@@ (halt if a step fails immediately)
( HALT_ON_FAILURE, _re_halt_on_failure ),
# Support: @@@HONOR_ZERO_RETURN_CODE@@@ (succeed on 0 return, even if some
# steps have failed)
( HONOR_ZERO_RC, _re_honor_zero_rc ),
# Support: @@@STEP_CLEAR@@@ (reset step description)
( STEP_CLEAR, _re_step_clear ),
# Support: @@@STEP_SUMMARY_CLEAR@@@ (reset step summary)
( STEP_SUMMARY_CLEAR, _re_step_summary_clear ),
# Support: @@@STEP_TEXT@<msg>@@@
( STEP_TEXT, _re_step_text ),
# Support: @@@STEP_SUMMARY_TEXT@<msg>@@@
( STEP_SUMMARY_TEXT, _re_step_summary_step ),
# Support: @@@BUILD_STEP <step_name>@@@ (start a new section)
( BUILD_STEP, _re_build_step ),
]
def __init__(self, **kwargs):
# Inject standard tags into the environment.
env = {
'BUILDBOT_BLAMELIST': util.Interpolate('%(prop:blamelist:-[])s'),
'BUILDBOT_BRANCH': util.Interpolate('%(prop:branch:-None)s'),
'BUILDBOT_BUILDERNAME': util.Interpolate('%(prop:buildername:-None)s'),
'BUILDBOT_BUILDNUMBER': util.Interpolate('%(prop:buildnumber:-None)s'),
'BUILDBOT_CLOBBER': util.Interpolate('%(prop:clobber:+1)s'),
'BUILDBOT_GOT_REVISION': util.Interpolate('%(prop:got_revision:-None)s'),
'BUILDBOT_REVISION': util.Interpolate('%(prop:revision:-None)s'),
'BUILDBOT_SCHEDULER': util.Interpolate('%(prop:scheduler:-None)s'),
'BUILDBOT_SLAVENAME': util.Interpolate('%(prop:slavename:-None)s'),
'BUILDBOT_MSAN_ORIGINS': util.Interpolate('%(prop:msan_origins:-)s'),
}
# Apply the passed in environment on top.
old_env = kwargs.get('env') or {}
env.update(old_env)
# Change passed in args (ok as a copy is made internally).
kwargs['env'] = env
kwargs = self.setupShellMixin(kwargs)
super().__init__(**kwargs)
self.addLogObserver('stdio', logobserver.LineConsumerLogObserver(self.processAnnotations))
# A step log to store all logs, which we got before the first @@@BUILD_STEP @@@ command.
self.preamble_log = None
self.annotate_status = results.SUCCESS
self.halt_on_failure = False
self.honor_zero_return_code = False
self.annotated_steps = []
# Use this lock to "run" the annotated steps.
self.initLock = defer.DeferredLock()
self._annotated_finished = False
def _getLastAnnotatedStep(self):
return self.annotated_steps[-1] if self.annotated_steps else None
def _updateLastAnnotatedStepStatus(self, status):
debuglog(">>> AnnotatedCommand::_updateLastAnnotatedStepStatus(%r)" % status)
# Alway update the common annotate command status.
self.annotate_status = results.worst_status(self.annotate_status, status)
s = self._getLastAnnotatedStep()
if s is not None:
s.updateStatus(status)
if self.halt_on_failure and status in [results.FAILURE, results.EXCEPTION]:
if self.cmd:
self.cmd.interrupt("Failed annotated step")
def _updateLastAnnotatedStepText(self, text = None):
"""Updating a step text. None to clean up."""
debuglog(">>> AnnotatedCommand::_updateLastAnnotatedStepText('%s')" % (text if text else ''))
s = self._getLastAnnotatedStep()
if s is not None:
s.setStepText(text)
def _updateLastAnnotatedStepSummaryInfo(self, text = None):
"""Updating a step summary info. None to clean up."""
debuglog(">>> AnnotatedCommand::_updateLastAnnotatedStepSummaryInfo('%s')" % (text if text else ''))
s = self._getLastAnnotatedStep()
if s is not None:
s.setStepSummary(text)
def _fixupActiveAnnotatedStep(self):
debuglog(">>> AnnotatedCommand::_fixupActiveAnnotatedStep()")
# Fixup the active step
s = self._getLastAnnotatedStep()
if s is not None:
# Finish the active step with its current status.
s.requestFinish()
def _scheduleNewAnnotatedStep(self, name, logline):
debuglog(">>> AnnotatedCommand::_scheduleNewAnnotatedStep('%s')" % name)
self._fixupActiveAnnotatedStep()
s = AnnotatedBuildStep(parent_step=self, name=name)
# Doing this last so that @@@BUILD_STEP... occurs in the log of the new
# step.
s.scheduleStdout(logline)
# Make it ready to consume the logs and status updates.
self.annotated_steps.append(s)
@deferredLocked("initLock")
@defer.inlineCallbacks
def _execStep(self, step, done):
yield step.startStep(remote=None, done=done)
@defer.inlineCallbacks
def _walkOverScheduledAnnotatedSteps(self):
debuglog(">>> AnnotatedCommand::_walkOverScheduledAnnotatedSteps: started")
while self.annotated_steps or not self._annotated_finished:
if self.annotated_steps:
yield self._execStep(self.annotated_steps[0], done=self._annotated_finished)
last_step = self.annotated_steps.pop(0)
if last_step.results == results.EXCEPTION:
raise Exception("Annotated step exception")
if not self._annotated_finished:
yield asyncSleep(.1)
debuglog(">>> AnnotatedCommand::_walkOverScheduledAnnotatedSteps: finished")
def run(self):
return self.runAnnotatedCommands()
@defer.inlineCallbacks
def runAnnotatedCommands(self):
try:
# Create a preamble log for primary annotate step.
self.preamble_log = yield self.addLog('preamble')
cmd = yield self.makeRemoteShellCommand(command=self.command,
stdioLogName='stdio')
d1 = self.runCommand(cmd)
d2 = defer.maybeDeferred(self._walkOverScheduledAnnotatedSteps)
@d1.addBoth
def cb(r):
self._annotated_finished = True
try:
# In some cases we can get the empty queue after the check.
# Just catch and pass the exception.
if self.annotated_steps:
# Current processing step.
last_step = self.annotated_steps[0]
cmd_results = cmd.results()
# We can get CANCELLED for two cases:
# 1. when UI stop button was pressed. In that case just pass this status
# into the annotated step as the result status.
# 2. and when the remote command was canceled because of failed annotated step.
# For that situation we finish the annotated step with its current status.
if cmd_results == results.CANCELLED and last_step.results in [results.FAILURE, results.EXCEPTION]:
# Finish the annotated step with its current status.
last_step.requestFinish()
else:
last_step.requestFinish(cmd_results)
except IndexError:
pass
return r
@d2.addErrback
def cbErrback(r):
debuglog("+++ AnnotatedCommand::runAnnotatedCommands(): error callback with exception. "
"Terminate remote command.")
self.annotate_status = results.EXCEPTION
if self.cmd:
self.cmd.interrupt("Annotated step exception")
return r
# Wait until both -- the remote command and the annotated step processing loop -- get finished.
yield defer.DeferredList([d1, d2])
except Exception:
why = failure.Failure()
logging.err(why, "AnnotatedCommand.failed; traceback follows")
yield self.addLogWithFailure(why)
# This case when the remote command has been canceled by failed annotated command.
if cmd.results() == results.CANCELLED and self.annotate_status in [results.FAILURE, results.EXCEPTION]:
return self.annotate_status
# Regular case.
return results.worst_status(cmd.results(), self.annotate_status)
def processAnnotations(self):
while True:
try:
stream, ln = yield
# Add \n if not there, which seems to be the case for log lines from
# windows agents, but not others.
if not ln.endswith('\n'):
ln += '\n'
self.processAnnotatedCommand(ln)
except GeneratorExit:
return
def processAnnotatedCommand(self, line):
#pylint: disable=too-many-branches
# returns: op, args
def parse_annotate_cmd(ln):
for ancmd, anre in AnnotatedCommand._re_set:
ro = anre.search(ln)
if ro is not None:
# Get the regex named group values.
# We can get the empty set, but it is ok.
args = ro.groupdict() or {}
# Store the current log line within the arguments.
# We will need to save it in some cases.
args['logline'] = ln
return ancmd, args
return None, None
ancmd, args = parse_annotate_cmd(line)
if ancmd:
debuglog(">>> AnnotatedCommand::processAnnotatedCommand(): %s, %r" % (ancmd, args))
try:
if ancmd == STEP_LINK:
s = self._getLastAnnotatedStep()
if s is not None:
s.addURL(args['link_label'], args['link_url'])
else:
logging.msg("Warning: missed link for annotated step. "
"No active steps. (%s/%s)" % (args['link_label'], args['link_url']))
elif ancmd == STEP_WARNINGS:
self._updateLastAnnotatedStepStatus(results.WARNINGS)
elif ancmd == STEP_FAILURE:
self._updateLastAnnotatedStepStatus(results.FAILURE)
elif ancmd == STEP_EXCEPTION:
self._updateLastAnnotatedStepStatus(results.EXCEPTION)
elif ancmd == HALT_ON_FAILURE:
self.halt_on_failure = True
elif ancmd == HONOR_ZERO_RC:
self.honor_zero_return_code = True
elif ancmd == STEP_CLEAR:
self._updateLastAnnotatedStepText()
elif ancmd == STEP_SUMMARY_CLEAR:
self._updateLastAnnotatedStepSummaryInfo()
elif ancmd == STEP_TEXT:
self._updateLastAnnotatedStepText(text=args['text'])
elif ancmd == STEP_SUMMARY_TEXT:
self._updateLastAnnotatedStepSummaryInfo(text=args['text'])
elif ancmd == BUILD_STEP:
self._scheduleNewAnnotatedStep(name=args['name'].strip(),
logline=args['logline'])
else:
# Regular log line, forward it to the active step.
s = self._getLastAnnotatedStep()
if s is None and self.preamble_log is not None:
self.preamble_log.addStdout(line)
else:
s.scheduleStdout(line)
except Exception:
logging.err(failure.Failure(), 'error while processing annotgated command log:')