Package buildbot :: Package process :: Module buildstep
[frames] | no frames]

Source Code for Module buildbot.process.buildstep

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