1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import re
17
18 from zope.interface import implements
19 from twisted.internet import defer, error
20 from twisted.protocols import basic
21 from twisted.spread import pb
22 from twisted.python import log, components
23 from twisted.python.failure import Failure
24 from twisted.web.util import formatFailure
25 from twisted.python.reflect import accumulateClassList
26
27 from buildbot import interfaces, util, config
28 from buildbot.status import progress
29 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED, \
30 EXCEPTION, RETRY, worst_status
31 from buildbot.process import metrics, properties
32 from buildbot.util.eventual import eventually
33 from buildbot.interfaces import BuildSlaveTooOldError
37
39
40
41 _commandCounter = 0
42
43 active = False
44 rc = None
45 debug = False
46
47 - def __init__(self, remote_command, args, ignore_updates=False,
48 collectStdout=False, collectStderr=False, decodeRC={0:SUCCESS}):
49 self.logs = {}
50 self.delayedLogs = {}
51 self._closeWhenFinished = {}
52 self.collectStdout = collectStdout
53 self.collectStderr = collectStderr
54 self.stdout = ''
55 self.stderr = ''
56
57 self._startTime = None
58 self._remoteElapsed = None
59 self.remote_command = remote_command
60 self.args = args
61 self.ignore_updates = ignore_updates
62 self.decodeRC = decodeRC
63
65 return "<RemoteCommand '%s' at %d>" % (self.remote_command, id(self))
66
67 - def run(self, step, remote):
93
94 - def useLog(self, log, closeWhenFinished=False, logfileName=None):
95 assert interfaces.ILogFile.providedBy(log)
96 if not logfileName:
97 logfileName = log.getName()
98 assert logfileName not in self.logs
99 assert logfileName not in self.delayedLogs
100 self.logs[logfileName] = log
101 self._closeWhenFinished[logfileName] = closeWhenFinished
102
103 - def useLogDelayed(self, logfileName, activateCallBack, closeWhenFinished=False):
104 assert logfileName not in self.logs
105 assert logfileName not in self.delayedLogs
106 self.delayedLogs[logfileName] = (activateCallBack, closeWhenFinished)
107
109 self.updates = {}
110 self._startTime = util.now()
111
112
113
114
115
116 d = self.remote.callRemote("startCommand", self, self.commandID,
117 self.remote_command, self.args)
118 return d
119
121 self.active = False
122
123
124
125
126
127 d = defer.maybeDeferred(self.remoteComplete, failure)
128
129
130 d.addCallback(lambda r: self)
131
132
133 d.addBoth(self.deferred.callback)
134
136 log.msg("RemoteCommand.interrupt", self, why)
137 if not self.active:
138 log.msg(" but this RemoteCommand is already inactive")
139 return defer.succeed(None)
140 if not self.remote:
141 log.msg(" but our .remote went away")
142 return defer.succeed(None)
143 if isinstance(why, Failure) and why.check(error.ConnectionLost):
144 log.msg("RemoteCommand.disconnect: lost slave")
145 self.remote = None
146 self._finished(why)
147 return defer.succeed(None)
148
149
150
151
152 d = defer.maybeDeferred(self.remote.callRemote, "interruptCommand",
153 self.commandID, str(why))
154
155 d.addErrback(self._interruptFailed)
156 return d
157
159 log.msg("RemoteCommand._interruptFailed", self)
160
161
162 return None
163
165 """
166 I am called by the slave's L{buildbot.slave.bot.SlaveBuilder} so
167 I can receive updates from the running remote command.
168
169 @type updates: list of [object, int]
170 @param updates: list of updates from the remote command
171 """
172 self.buildslave.messageReceivedFromSlave()
173 max_updatenum = 0
174 for (update, num) in updates:
175
176 try:
177 if self.active and not self.ignore_updates:
178 self.remoteUpdate(update)
179 except:
180
181 self._finished(Failure())
182
183
184 if num > max_updatenum:
185 max_updatenum = num
186 return max_updatenum
187
189 """
190 Called by the slave's L{buildbot.slave.bot.SlaveBuilder} to
191 notify me the remote command has finished.
192
193 @type failure: L{twisted.python.failure.Failure} or None
194
195 @rtype: None
196 """
197 self.buildslave.messageReceivedFromSlave()
198
199
200 if self.active:
201 eventually(self._finished, failure)
202 return None
203
205 if 'stdio' in self.logs:
206 self.logs['stdio'].addStdout(data)
207 if self.collectStdout:
208 self.stdout += data
209
211 if 'stdio' in self.logs:
212 self.logs['stdio'].addStderr(data)
213 if self.collectStderr:
214 self.stderr += data
215
219
221
222 if logname in self.delayedLogs:
223 (activateCallBack, closeWhenFinished) = self.delayedLogs[logname]
224 del self.delayedLogs[logname]
225 loog = activateCallBack(self)
226 self.logs[logname] = loog
227 self._closeWhenFinished[logname] = closeWhenFinished
228
229 if logname in self.logs:
230 self.logs[logname].addStdout(data)
231 else:
232 log.msg("%s.addToLog: no such log %s" % (self, logname))
233
234 @metrics.countMethod('RemoteCommand.remoteUpdate()')
265
267 if self._startTime and self._remoteElapsed:
268 delta = (util.now() - self._startTime) - self._remoteElapsed
269 metrics.MetricTimeEvent.log("RemoteCommand.overhead", delta)
270
271 for name,loog in self.logs.items():
272 if self._closeWhenFinished[name]:
273 if maybeFailure:
274 loog.addHeader("\nremoteFailed: %s" % maybeFailure)
275 else:
276 log.msg("closing log %s" % loog)
277 loog.finish()
278 return maybeFailure
279
281 if self.rc in self.decodeRC:
282 return self.decodeRC[self.rc]
283 return FAILURE
284
287 LoggedRemoteCommand = RemoteCommand
291 implements(interfaces.ILogObserver)
292
295
299
300 - def logChunk(self, build, step, log, channel, text):
305
306
307
309 """This will be called with chunks of stdout data. Override this in
310 your observer."""
311 pass
312
314 """This will be called with chunks of stderr data. Override this in
315 your observer."""
316 pass
317
331
333 """
334 Set the maximum line length: lines longer than max_length are
335 dropped. Default is 16384 bytes. Use sys.maxint for effective
336 infinity.
337 """
338 self.stdoutParser.MAX_LENGTH = max_length
339 self.stderrParser.MAX_LENGTH = max_length
340
342 self.stdoutParser.dataReceived(data)
343
345 self.stderrParser.dataReceived(data)
346
348 """This will be called with complete stdout lines (not including the
349 delimiter). Override this in your observer."""
350 pass
351
353 """This will be called with complete lines of stderr (not including
354 the delimiter). Override this in your observer."""
355 pass
356
359 - def __init__(self, workdir, command, env=None,
360 want_stdout=1, want_stderr=1,
361 timeout=20*60, maxTime=None, logfiles={},
362 usePTY="slave-config", logEnviron=True,
363 collectStdout=False,collectStderr=False,
364 interruptSignal=None,
365 initialStdin=None, decodeRC={0:SUCCESS},
366 user=None):
367
368 self.command = command
369 if env is not None:
370
371
372
373 env = env.copy()
374 args = {'workdir': workdir,
375 'env': env,
376 'want_stdout': want_stdout,
377 'want_stderr': want_stderr,
378 'logfiles': logfiles,
379 'timeout': timeout,
380 'maxTime': maxTime,
381 'usePTY': usePTY,
382 'logEnviron': logEnviron,
383 'initial_stdin': initialStdin
384 }
385 if interruptSignal is not None:
386 args['interruptSignal'] = interruptSignal
387 if user is not None:
388 args['user'] = user
389 RemoteCommand.__init__(self, "shell", args, collectStdout=collectStdout,
390 collectStderr=collectStderr,
391 decodeRC=decodeRC)
392
394 self.args['command'] = self.command
395 if self.remote_command == "shell":
396
397
398 if self.step.slaveVersion("shell", "old") == "old":
399 self.args['dir'] = self.args['workdir']
400 if ('user' in self.args and
401 self.step.slaveVersionIsOlderThan("shell", "2.16")):
402 m = "slave does not support the 'user' parameter"
403 raise BuildSlaveTooOldError(m)
404 what = "command '%s' in dir '%s'" % (self.args['command'],
405 self.args['workdir'])
406 log.msg(what)
407 return RemoteCommand._start(self)
408
410 return "<RemoteShellCommand '%s'>" % repr(self.command)
411
413 """
414 This is a wrapper to record the arguments passed to as BuildStep subclass.
415 We use an instance of this class, rather than a closure mostly to make it
416 easier to test that the right factories are getting created.
417 """
418 compare_attrs = ['factory', 'args', 'kwargs' ]
419 implements(interfaces.IBuildStepFactory)
420
421 - def __init__(self, factory, *args, **kwargs):
422 self.factory = factory
423 self.args = args
424 self.kwargs = kwargs
425
427 try:
428 return self.factory(*self.args, **self.kwargs)
429 except:
430 log.msg("error while creating step, factory=%s, args=%s, kwargs=%s"
431 % (self.factory, self.args, self.kwargs))
432 raise
433
434 -class BuildStep(object, properties.PropertiesMixin):
435
436 haltOnFailure = False
437 flunkOnWarnings = False
438 flunkOnFailure = False
439 warnOnWarnings = False
440 warnOnFailure = False
441 alwaysRun = False
442 doStepIf = True
443 hideStepIf = False
444
445
446 set_runtime_properties = True
447
448
449
450
451
452
453
454
455 parms = ['name', 'locks',
456 'haltOnFailure',
457 'flunkOnWarnings',
458 'flunkOnFailure',
459 'warnOnWarnings',
460 'warnOnFailure',
461 'alwaysRun',
462 'progressMetrics',
463 'useProgress',
464 'doStepIf',
465 'hideStepIf',
466 ]
467
468 name = "generic"
469 locks = []
470 progressMetrics = ()
471 useProgress = True
472 build = None
473 step_status = None
474 progress = None
475
477 for p in self.__class__.parms:
478 if kwargs.has_key(p):
479 setattr(self, p, kwargs[p])
480 del kwargs[p]
481 if kwargs:
482 config.error("%s.__init__ got unexpected keyword argument(s) %s" \
483 % (self.__class__, kwargs.keys()))
484 self._pendingLogObservers = []
485
486 if not isinstance(self.name, str):
487 config.error("BuildStep name must be a string: %r" % (self.name,))
488
489 self._acquiringLock = None
490 self.stopped = False
491
492 - def __new__(klass, *args, **kwargs):
493 self = object.__new__(klass)
494 self._factory = _BuildStepFactory(klass, *args, **kwargs)
495 return self
496
499
502
505
508
512
515
518
526
530
557
559 self._acquiringLock = None
560 if not self.locks:
561 return defer.succeed(None)
562 if self.stopped:
563 return defer.succeed(None)
564 log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
565 for lock, access in self.locks:
566 if not lock.isAvailable(self, access):
567 self.step_status.setWaitingForLocks(True)
568 log.msg("step %s waiting for lock %s" % (self, lock))
569 d = lock.waitUntilMaybeAvailable(self, access)
570 d.addCallback(self.acquireLocks)
571 self._acquiringLock = (lock, access, d)
572 return d
573
574 for lock, access in self.locks:
575 lock.claim(self, access)
576 self.step_status.setWaitingForLocks(False)
577 return defer.succeed(None)
578
597
598 dl = [ doStep ]
599 for renderable in renderables:
600 d = self.build.render(getattr(self, renderable))
601 d.addCallback(setRenderable, renderable)
602 dl.append(d)
603 dl = defer.gatherResults(dl)
604
605 dl.addCallback(self._startStep_3)
606 return dl
607
608 @defer.inlineCallbacks
628
630 raise NotImplementedError("your subclass must implement this method")
631
638
647
661
682
721
722
723
726
734
737
742
748
757
762
764 assert interfaces.ILogObserver.providedBy(observer)
765 observer.setStep(self)
766 self._pendingLogObservers.append((logname, observer))
767 self._connectPendingLogObservers()
768
770 if not self._pendingLogObservers:
771 return
772 if not self.step_status:
773 return
774 current_logs = {}
775 for loog in self.step_status.getLogs():
776 current_logs[loog.getName()] = loog
777 for logname, observer in self._pendingLogObservers[:]:
778 if logname in current_logs:
779 observer.setLog(current_logs[logname])
780 self._pendingLogObservers.remove((logname, observer))
781
784
789
790 @staticmethod
792 if callable(value):
793 value = value(*args, **kwargs)
794 return value
795
796 components.registerAdapter(
797 BuildStep._getStepFactory,
798 BuildStep, interfaces.IBuildStepFactory)
799 components.registerAdapter(
800 lambda step : interfaces.IProperties(step.build),
801 BuildStep, interfaces.IProperties)
805 length = 0
806
809
810 - def logChunk(self, build, step, log, channel, text):
813
815
816 progressMetrics = ('output',)
817 logfiles = {}
818
819 parms = BuildStep.parms + ['logfiles', 'lazylogfiles', 'log_eval_func']
820 cmd = None
821
822 renderables = [ 'logfiles', 'lazylogfiles' ]
823
824 - def __init__(self, logfiles={}, lazylogfiles=False, log_eval_func=None,
825 *args, **kwargs):
842
845
847 kwargs = dict()
848 kwargs['logfiles'] = self.logfiles
849 return kwargs
850
880 d.addCallback(_gotResults)
881 d.addCallbacks(self.finished, self.checkDisconnect)
882 d.addErrback(self.failed)
883
885 for logname,remotefilename in logfiles.items():
886 if self.lazylogfiles:
887
888
889
890
891
892
893 callback = lambda cmd_arg, local_logname=logname: self.addLog(local_logname)
894 cmd.useLogDelayed(logname, callback, True)
895 else:
896
897 newlog = self.addLog(logname)
898
899 cmd.useLog(newlog, True)
900
914
921
924
927
932
933 - def getText(self, cmd, results):
934 if results == SUCCESS:
935 return self.describe(True)
936 elif results == WARNINGS:
937 return self.describe(True) + ["warnings"]
938 elif results == EXCEPTION:
939 return self.describe(True) + ["exception"]
940 else:
941 return self.describe(True) + ["failed"]
942
943 - def getText2(self, cmd, results):
945
946 - def maybeGetText2(self, cmd, results):
947 if results == SUCCESS:
948
949 pass
950 elif results == WARNINGS:
951 if (self.flunkOnWarnings or self.warnOnWarnings):
952
953 return self.getText2(cmd, results)
954 else:
955 if (self.haltOnFailure or self.flunkOnFailure
956 or self.warnOnFailure):
957
958 return self.getText2(cmd, results)
959 return []
960
966
976 worst = cmd.results()
977 for err, possible_status in regexes:
978
979
980
981 if worst_status(worst, possible_status) == possible_status:
982 if isinstance(err, (basestring)):
983 err = re.compile(".*%s.*" % err, re.DOTALL)
984 for l in cmd.logs.values():
985 if err.search(l.getText()):
986 worst = possible_status
987 return worst
988
989
990 from buildbot.process.properties import WithProperties
991 _hush_pyflakes = [WithProperties]
992 del _hush_pyflakes
993