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

Source Code for Module buildbot.process.buildstep

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