1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import os
18 from base64 import b64encode
19 import sys
20 import shutil
21
22 from zope.interface import implements
23 from twisted.internet import reactor, defer
24 from twisted.python import log, failure, runtime
25
26 from buildslave.interfaces import ISlaveCommand
27 from buildslave import runprocess
28 from buildslave.exceptions import AbandonChain
29 from buildslave.commands import utils
30
31
32
33
34 command_version = "2.11"
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
64 implements(ISlaveCommand)
65
66 """This class defines one command that can be invoked by the build master.
67 The command is executed on the slave side, and always sends back a
68 completion message when it finishes. It may also send intermediate status
69 as it runs (by calling builder.sendStatus). Some commands can be
70 interrupted (either by the build master or a local timeout), in which
71 case the step is expected to complete normally with a status message that
72 indicates an error occurred.
73
74 These commands are used by BuildSteps on the master side. Each kind of
75 BuildStep uses a single Command. The slave must implement all the
76 Commands required by the set of BuildSteps used for any given build:
77 this is checked at startup time.
78
79 All Commands are constructed with the same signature:
80 c = CommandClass(builder, stepid, args)
81 where 'builder' is the parent SlaveBuilder object, and 'args' is a
82 dict that is interpreted per-command.
83
84 The setup(args) method is available for setup, and is run from __init__.
85
86 The Command is started with start(). This method must be implemented in a
87 subclass, and it should return a Deferred. When your step is done, you
88 should fire the Deferred (the results are not used). If the command is
89 interrupted, it should fire the Deferred anyway.
90
91 While the command runs. it may send status messages back to the
92 buildmaster by calling self.sendStatus(statusdict). The statusdict is
93 interpreted by the master-side BuildStep however it likes.
94
95 A separate completion message is sent when the deferred fires, which
96 indicates that the Command has finished, but does not carry any status
97 data. If the Command needs to return an exit code of some sort, that
98 should be sent as a regular status message before the deferred is fired .
99 Once builder.commandComplete has been run, no more status messages may be
100 sent.
101
102 If interrupt() is called, the Command should attempt to shut down as
103 quickly as possible. Child processes should be killed, new ones should
104 not be started. The Command should send some kind of error status update,
105 then complete as usual by firing the Deferred.
106
107 .interrupted should be set by interrupt(), and can be tested to avoid
108 sending multiple error status messages.
109
110 If .running is False, the bot is shutting down (or has otherwise lost the
111 connection to the master), and should not send any status messages. This
112 is checked in Command.sendStatus .
113
114 """
115
116
117
118
119
120 debug = False
121 interrupted = False
122 running = False
123
124
125 _reactor = reactor
126
127 - def __init__(self, builder, stepId, args):
132
134 """Override this in a subclass to extract items from the args dict."""
135 pass
136
138 self.running = True
139 d = defer.maybeDeferred(self.start)
140 def commandComplete(res):
141 self.running = False
142 return res
143 d.addBoth(commandComplete)
144 return d
145
147 """Start the command. This method should return a Deferred that will
148 fire when the command has completed. The Deferred's argument will be
149 ignored.
150
151 This method should be overridden by subclasses."""
152 raise NotImplementedError, "You must implement this in a subclass"
153
162
166
168 """Override this in a subclass to allow commands to be interrupted.
169 May be called multiple times, test and set self.interrupted=True if
170 this matters."""
171 pass
172
173
174
176 if type(rc) is not int:
177 log.msg("weird, _abandonOnFailure was given rc=%s (%s)" % \
178 (rc, type(rc)))
179 assert isinstance(rc, int)
180 if rc != 0:
181 raise AbandonChain(rc)
182 return rc
183
186
188 log.msg("_checkAbandoned", why)
189 why.trap(AbandonChain)
190 log.msg(" abandoning chain", why.value)
191 self.sendStatus({'rc': why.value.args[0]})
192 return None
193
195 """Abstract base class for Version Control System operations (checkout
196 and update). This class extracts the following arguments from the
197 dictionary received from the master:
198
199 - ['workdir']: (required) the subdirectory where the buildable sources
200 should be placed
201
202 - ['mode']: one of update/copy/clobber/export, defaults to 'update'
203
204 - ['revision']: (required) If not None, this is an int or string which indicates
205 which sources (along a time-like axis) should be used.
206 It is the thing you provide as the CVS -r or -D
207 argument.
208
209 - ['patch']: If not None, this is a tuple of (striplevel, patch)
210 which contains a patch that should be applied after the
211 checkout has occurred. Once applied, the tree is no
212 longer eligible for use with mode='update', and it only
213 makes sense to use this in conjunction with a
214 ['revision'] argument. striplevel is an int, and patch
215 is a string in standard unified diff format. The patch
216 will be applied with 'patch -p%d <PATCH', with
217 STRIPLEVEL substituted as %d. The command will fail if
218 the patch process fails (rejected hunks).
219
220 - ['timeout']: seconds of silence tolerated before we kill off the
221 command
222
223 - ['maxTime']: seconds before we kill off the command
224
225 - ['retry']: If not None, this is a tuple of (delay, repeats)
226 which means that any failed VC updates should be
227 reattempted, up to REPEATS times, after a delay of
228 DELAY seconds. This is intended to deal with slaves
229 that experience transient network failures.
230 """
231
232 sourcedata = ""
233
235
236
237
238 self.env = os.environ.copy()
239 self.env['LC_MESSAGES'] = "C"
240
241 self.workdir = args['workdir']
242 self.mode = args.get('mode', "update")
243 self.revision = args.get('revision')
244 self.patch = args.get('patch')
245 self.timeout = args.get('timeout', 120)
246 self.maxTime = args.get('maxTime', None)
247 self.retry = args.get('retry')
248 self._commandPaths = {}
249
250
251
253 """Wrapper around utils.getCommand that will output a resonable
254 error message and raise AbandonChain if the command cannot be
255 found"""
256 if name not in self._commandPaths:
257 try:
258 self._commandPaths[name] = utils.getCommand(name)
259 except RuntimeError:
260 self.sendStatus({'stderr' : "could not find '%s'\n" % name})
261 self.sendStatus({'stderr' : "PATH is '%s'\n" % os.environ.get('PATH', '')})
262 raise AbandonChain(-1)
263 return self._commandPaths[name]
264
266 self.sendStatus({'header': "starting " + self.header + "\n"})
267 self.command = None
268
269
270 if self.mode == "copy":
271 self.srcdir = "source"
272 else:
273 self.srcdir = self.workdir
274
275 self.sourcedatafile = os.path.join(self.builder.basedir,
276 ".buildbot-sourcedata-" + b64encode(self.srcdir))
277
278
279 old_sd_path = os.path.join(self.builder.basedir, self.srcdir, ".buildbot-sourcedata")
280 if os.path.exists(old_sd_path) and not os.path.exists(self.sourcedatafile):
281 os.rename(old_sd_path, self.sourcedatafile)
282
283
284
285 old_sd_path = os.path.join(self.builder.basedir, ".buildbot-sourcedata")
286 if os.path.exists(old_sd_path) and not os.path.exists(self.sourcedatafile):
287 os.rename(old_sd_path, self.sourcedatafile)
288
289 d = defer.succeed(None)
290 self.maybeClobber(d)
291 if not (self.sourcedirIsUpdateable() and self.sourcedataMatches()):
292
293
294
295 d.addCallback(self.doClobber, self.srcdir)
296
297 d.addCallback(self.doVC)
298
299 if self.mode == "copy":
300 d.addCallback(self.doCopy)
301 if self.patch:
302 d.addCallback(self.doPatch)
303 d.addCallbacks(self._sendRC, self._checkAbandoned)
304 return d
305
307
308 if self.mode in ("copy", "clobber", "export"):
309 d.addCallback(self.doClobber, self.workdir)
310
315
316 - def doVC(self, res):
329
331 try:
332 olddata = self.readSourcedata()
333 if olddata != self.sourcedata:
334 return False
335 except IOError:
336 return False
337 return True
338
343
345 d = defer.maybeDeferred(self.parseGotRevision)
346 d.addCallback(lambda got_revision:
347 self.sendStatus({'got_revision': got_revision}))
348 return d
349
351 """Override this in a subclass. It should return a string that
352 represents which revision was actually checked out, or a Deferred
353 that will fire with such a string. If, in a future build, you were to
354 pass this 'got_revision' string in as the 'revision' component of a
355 SourceStamp, you should wind up with the same source code as this
356 checkout just obtained.
357
358 It is probably most useful to scan self.command.stdout for a string
359 of some sort. Be sure to set keepStdout=True on the VC command that
360 you run, so that you'll have something available to look at.
361
362 If this information is unavailable, just return None."""
363
364 return None
365
367 return open(self.sourcedatafile, "r").read()
368
372
374 """Returns True if the tree can be updated."""
375 raise NotImplementedError("this must be implemented in a subclass")
376
378 """Returns a deferred with the steps to update a checkout."""
379 raise NotImplementedError("this must be implemented in a subclass")
380
382 """Returns a deferred with the steps to do a fresh checkout."""
383 raise NotImplementedError("this must be implemented in a subclass")
384
399
401 msg = "now retrying VC operation"
402 self.sendStatus({'header': msg + "\n"})
403 log.msg(msg)
404 d = self.doVCFull()
405 d.addBoth(self.maybeDoVCRetry)
406 d.addCallback(self._abandonOnFailure)
407 return d
408
410 """Override this in a subclass if you want to detect unrecoverable
411 checkout errors where clobbering the repo wouldn't help, and stop
412 the current VC chain before it clobbers the repo for future builds.
413
414 Use 'raise AbandonChain' to pass up a halt if you do detect such."""
415 pass
416
418 """We get here somewhere after a VC chain has finished. res could
419 be::
420
421 - 0: the operation was successful
422 - nonzero: the operation failed. retry if possible
423 - AbandonChain: the operation failed, someone else noticed. retry.
424 - Failure: some other exception, re-raise
425 """
426
427 if isinstance(res, failure.Failure):
428 if self.interrupted:
429 return res
430 res.trap(AbandonChain)
431 else:
432 if type(res) is int and res == 0:
433 return res
434 if self.interrupted:
435 raise AbandonChain(1)
436
437 if self.retry:
438 delay, repeats = self.retry
439 if repeats >= 0:
440 self.retry = (delay, repeats-1)
441 msg = ("update failed, trying %d more times after %d seconds"
442 % (repeats, delay))
443 self.sendStatus({'header': msg + "\n"})
444 log.msg(msg)
445 d = defer.Deferred()
446
447
448 self.doClobber(d, self.workdir)
449 if self.srcdir:
450 self.doClobber(d, self.srcdir)
451 d.addCallback(lambda res: self.doVCFull())
452 d.addBoth(self.maybeDoVCRetry)
453 self._reactor.callLater(delay, d.callback, None)
454 return d
455 return res
456
457 - def doClobber(self, dummy, dirname, chmodDone=False):
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476 d = os.path.join(self.builder.basedir, dirname)
477 if runtime.platformType != "posix":
478
479
480 utils.rmdirRecursive(d)
481 return defer.succeed(0)
482 command = ["rm", "-rf", d]
483 c = runprocess.RunProcess(self.builder, command, self.builder.basedir,
484 sendRC=0, timeout=self.timeout, maxTime=self.maxTime,
485 usePTY=False)
486
487 self.command = c
488
489
490
491 d = c.start()
492
493
494
495 if chmodDone:
496 d.addCallback(self._abandonOnFailure)
497 else:
498 d.addCallback(lambda rc: self.doClobberTryChmodIfFail(rc, dirname))
499 return d
500
502 assert isinstance(rc, int)
503 if rc == 0:
504 return defer.succeed(0)
505
506
507 command = ["chmod", "-Rf", "u+rwx", os.path.join(self.builder.basedir, dirname)]
508 if sys.platform.startswith('freebsd'):
509
510
511
512 command = ["find", os.path.join(self.builder.basedir, dirname),
513 '-exec', 'chmod', 'u+rwx', '{}', ';' ]
514 c = runprocess.RunProcess(self.builder, command, self.builder.basedir,
515 sendRC=0, timeout=self.timeout, maxTime=self.maxTime,
516 usePTY=False)
517
518 self.command = c
519 d = c.start()
520 d.addCallback(self._abandonOnFailure)
521 d.addCallback(lambda dummy: self.doClobber(dummy, dirname, True))
522 return d
523
525
526 fromdir = os.path.join(self.builder.basedir, self.srcdir)
527 todir = os.path.join(self.builder.basedir, self.workdir)
528 if runtime.platformType != "posix":
529 self.sendStatus({'header': "Since we're on a non-POSIX platform, "
530 "we're not going to try to execute cp in a subprocess, but instead "
531 "use shutil.copytree(), which will block until it is complete. "
532 "fromdir: %s, todir: %s\n" % (fromdir, todir)})
533 shutil.copytree(fromdir, todir)
534 return defer.succeed(0)
535
536 if not os.path.exists(os.path.dirname(todir)):
537 os.makedirs(os.path.dirname(todir))
538 if os.path.exists(todir):
539
540 log.msg("cp target '%s' already exists -- cp will not do what you think!" % todir)
541
542 command = ['cp', '-R', '-P', '-p', fromdir, todir]
543 c = runprocess.RunProcess(self.builder, command, self.builder.basedir,
544 sendRC=False, timeout=self.timeout, maxTime=self.maxTime,
545 usePTY=False)
546 self.command = c
547 d = c.start()
548 d.addCallback(self._abandonOnFailure)
549 return d
550
552 patchlevel = self.patch[0]
553 diff = self.patch[1]
554 root = None
555 if len(self.patch) >= 3:
556 root = self.patch[2]
557 command = [
558 utils.getCommand("patch"),
559 '-p%d' % patchlevel,
560 '--remove-empty-files',
561 '--force',
562 '--forward',
563 '-i', '.buildbot-diff',
564 ]
565 dir = os.path.join(self.builder.basedir, self.workdir)
566
567
568 open(os.path.join(dir, ".buildbot-patched"), "w").write("patched\n")
569
570
571 open(os.path.join(dir, ".buildbot-diff"), "w").write(diff)
572
573
574
575 if (root and
576 os.path.abspath(os.path.join(dir, root)
577 ).startswith(os.path.abspath(dir))):
578 dir = os.path.join(dir, root)
579
580
581 c = runprocess.RunProcess(self.builder, command, dir,
582 sendRC=False, timeout=self.timeout,
583 maxTime=self.maxTime, usePTY=False)
584 self.command = c
585 d = c.start()
586
587
588 def cleanup(x):
589 try:
590 os.unlink(os.path.join(dir, ".buildbot-diff"))
591 except:
592 pass
593 return x
594 d.addBoth(cleanup)
595
596 d.addCallback(self._abandonOnFailure)
597 return d
598