1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import os.path
17 import socket
18 import sys
19 import signal
20
21 from twisted.spread import pb
22 from twisted.python import log
23 from twisted.internet import error, reactor, task, defer
24 from twisted.application import service, internet
25 from twisted.cred import credentials
26
27 import buildslave
28 from buildslave.pbutil import ReconnectingPBClientFactory
29 from buildslave.commands import registry, base
30 from buildslave import monkeypatches
34
36
37 """This is the local representation of a single Builder: it handles a
38 single kind of build (like an all-warnings build). It has a name and a
39 home directory. The rest of its behavior is determined by the master.
40 """
41
42 stopCommandOnShutdown = True
43
44
45
46
47 remote = None
48
49
50
51 command = None
52
53
54
55 remoteStep = None
56
60
62 return "<SlaveBuilder '%s' at %d>" % (self.name, id(self))
63
67
68
69
70
71
78
83
91
95
97 log.msg("SlaveBuilder.remote_print(%s): message from master: %s" %
98 (self.name, message))
99
101 log.msg("lost remote")
102 self.remote = None
103
109
110
111
113 """This is invoked before the first step of any new build is run. It
114 doesn't do much, but masters call it so it's still here."""
115 pass
116
118 """
119 This gets invoked by L{buildbot.process.step.RemoteCommand.start}, as
120 part of various master-side BuildSteps, to start various commands
121 that actually do the build. I return nothing. Eventually I will call
122 .commandComplete() to notify the master-side RemoteCommand that I'm
123 done.
124 """
125
126 self.activity()
127
128 if self.command:
129 log.msg("leftover command, dropping it")
130 self.stopCommand()
131
132 try:
133 factory = registry.getFactory(command)
134 except KeyError:
135 raise UnknownCommand, "unrecognized SlaveCommand '%s'" % command
136 self.command = factory(self, stepId, args)
137
138 log.msg(" startCommand:%s [id %s]" % (command,stepId))
139 self.remoteStep = stepref
140 self.remoteStep.notifyOnDisconnect(self.lostRemoteStep)
141 d = self.command.doStart()
142 d.addCallback(lambda res: None)
143 d.addBoth(self.commandComplete)
144 return None
145
147 """Halt the current step."""
148 log.msg("asked to interrupt current command: %s" % why)
149 self.activity()
150 if not self.command:
151
152
153 log.msg(" .. but none was running")
154 return
155 self.command.doInterrupt()
156
157
159 """Make any currently-running command die, with no further status
160 output. This is used when the buildslave is shutting down or the
161 connection to the master has been lost. Interrupt the command,
162 silence it, and then forget about it."""
163 if not self.command:
164 return
165 log.msg("stopCommand: halting current command %s" % self.command)
166 self.command.doInterrupt()
167 self.command = None
168
169
171 """This sends the status update to the master-side
172 L{buildbot.process.step.RemoteCommand} object, giving it a sequence
173 number in the process. It adds the update to a queue, and asks the
174 master to acknowledge the update so it can be removed from that
175 queue."""
176
177 if not self.running:
178
179
180
181 return
182
183
184
185 if self.remoteStep:
186 update = [data, 0]
187 updates = [update]
188 d = self.remoteStep.callRemote("update", updates)
189 d.addCallback(self.ackUpdate)
190 d.addErrback(self._ackFailed, "SlaveBuilder.sendUpdate")
191
194
197
199 log.msg("SlaveBuilder._ackFailed:", where)
200 log.err(why)
201
202
203
225
226
228 log.msg("slave shutting down on command from master")
229 log.msg("NOTE: master is using deprecated slavebuilder.shutdown method")
230 reactor.stop()
231
232
233 -class Bot(pb.Referenceable, service.MultiService):
234 """I represent the slave-side bot."""
235 usePTY = None
236 name = "bot"
237
238 - def __init__(self, basedir, usePTY, unicode_encoding=None):
239 service.MultiService.__init__(self)
240 self.basedir = basedir
241 self.usePTY = usePTY
242 self.unicode_encoding = unicode_encoding or sys.getfilesystemencoding() or 'ascii'
243 self.builders = {}
244
248
255
256 @defer.deferredGenerator
258 retval = {}
259 wanted_names = set([ name for (name, builddir) in wanted ])
260 wanted_dirs = set([ builddir for (name, builddir) in wanted ])
261 wanted_dirs.add('info')
262 for (name, builddir) in wanted:
263 b = self.builders.get(name, None)
264 if b:
265 if b.builddir != builddir:
266 log.msg("changing builddir for builder %s from %s to %s" \
267 % (name, b.builddir, builddir))
268 b.setBuilddir(builddir)
269 else:
270 b = SlaveBuilder(name)
271 b.usePTY = self.usePTY
272 b.unicode_encoding = self.unicode_encoding
273 b.setServiceParent(self)
274 b.setBuilddir(builddir)
275 self.builders[name] = b
276 retval[name] = b
277
278
279 to_remove = list(set(self.builders.keys()) - wanted_names)
280 dl = defer.DeferredList([
281 defer.maybeDeferred(self.builders[name].disownServiceParent)
282 for name in to_remove ])
283 wfd = defer.waitForDeferred(dl)
284 yield wfd
285 wfd.getResult()
286
287
288 for name in to_remove:
289 del self.builders[name]
290
291
292 for dir in os.listdir(self.basedir):
293 if os.path.isdir(os.path.join(self.basedir, dir)):
294 if dir not in wanted_dirs:
295 log.msg("I have a leftover directory '%s' that is not "
296 "being used by the buildmaster: you can delete "
297 "it now" % dir)
298
299 yield retval
300
302 log.msg("message from master:", message)
303
305 """This command retrieves data from the files in SLAVEDIR/info/* and
306 sends the contents to the buildmaster. These are used to describe
307 the slave and its configuration, and should be created and
308 maintained by the slave administrator. They will be retrieved each
309 time the master-slave connection is established.
310 """
311
312 files = {}
313 basedir = os.path.join(self.basedir, "info")
314 if os.path.isdir(basedir):
315 for f in os.listdir(basedir):
316 filename = os.path.join(basedir, f)
317 if os.path.isfile(filename):
318 files[f] = open(filename, "r").read()
319 files['environ'] = os.environ.copy()
320 files['system'] = os.name
321 files['basedir'] = self.basedir
322 return files
323
327
329 log.msg("slave shutting down on command from master")
330
331
332
333
334 reactor.callLater(0.2, reactor.stop)
335
419
424
426 """Subclass or monkey-patch this method to be alerted whenever there is
427 active communication between the master and slave."""
428 pass
429
433
436 - def __init__(self, buildmaster_host, port, name, passwd, basedir,
437 keepalive, usePTY, keepaliveTimeout=None, umask=None,
438 maxdelay=300, unicode_encoding=None, allow_shutdown=None):
439
440
441
442
443 service.MultiService.__init__(self)
444 bot = Bot(basedir, usePTY, unicode_encoding=unicode_encoding)
445 bot.setServiceParent(self)
446 self.bot = bot
447 if keepalive == 0:
448 keepalive = None
449 self.umask = umask
450 self.basedir = basedir
451
452 self.shutdown_loop = None
453
454 if allow_shutdown == 'signal':
455 if not hasattr(signal, 'SIGHUP'):
456 raise ValueError("Can't install signal handler")
457 elif allow_shutdown == 'file':
458 self.shutdown_file = os.path.join(basedir, 'shutdown.stamp')
459 self.shutdown_mtime = 0
460
461 self.allow_shutdown = allow_shutdown
462 bf = self.bf = BotFactory(buildmaster_host, port, keepalive, maxdelay)
463 bf.startLogin(credentials.UsernamePassword(name, passwd), client=bot)
464 self.connection = c = internet.TCPClient(buildmaster_host, port, bf)
465 c.setServiceParent(self)
466
468
469 monkeypatches.patch_all()
470
471 log.msg("Starting BuildSlave -- version: %s" % buildslave.version)
472
473 self.recordHostname(self.basedir)
474 if self.umask is not None:
475 os.umask(self.umask)
476
477 service.MultiService.startService(self)
478
479 if self.allow_shutdown == 'signal':
480 log.msg("Setting up SIGHUP handler to initiate shutdown")
481 signal.signal(signal.SIGHUP, self._handleSIGHUP)
482 elif self.allow_shutdown == 'file':
483 log.msg("Watching %s's mtime to initiate shutdown" % self.shutdown_file)
484 if os.path.exists(self.shutdown_file):
485 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
486 self.shutdown_loop = l = task.LoopingCall(self._checkShutdownFile)
487 l.start(interval=10)
488
490 self.bf.continueTrying = 0
491 self.bf.stopTrying()
492 if self.shutdown_loop:
493 self.shutdown_loop.stop()
494 self.shutdown_loop = None
495 return service.MultiService.stopService(self)
496
498 "Record my hostname in twistd.hostname, for user convenience"
499 log.msg("recording hostname in twistd.hostname")
500 filename = os.path.join(basedir, "twistd.hostname")
501
502 try:
503 hostname = os.uname()[1]
504 except AttributeError:
505
506
507 hostname = socket.getfqdn()
508
509 try:
510 open(filename, "w").write("%s\n" % hostname)
511 except:
512 log.msg("failed - ignoring")
513
517
519 if os.path.exists(self.shutdown_file) and \
520 os.path.getmtime(self.shutdown_file) > self.shutdown_mtime:
521 log.msg("Initiating shutdown because %s was touched" % self.shutdown_file)
522 self.gracefulShutdown()
523
524
525
526
527
528 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
529
531 """Start shutting down"""
532 if not self.bf.perspective:
533 log.msg("No active connection, shutting down NOW")
534 reactor.stop()
535 return
536
537 log.msg("Telling the master we want to shutdown after any running builds are finished")
538 d = self.bf.perspective.callRemote("shutdown")
539 def _shutdownfailed(err):
540 if err.check(AttributeError):
541 log.msg("Master does not support slave initiated shutdown. Upgrade master to 0.8.3 or later to use this feature.")
542 else:
543 log.msg('callRemote("shutdown") failed')
544 log.err(err)
545
546 d.addErrback(_shutdownfailed)
547 return d
548