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
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
31
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
257 retval = {}
258 wanted_dirs = ["info"]
259 for (name, builddir) in wanted:
260 wanted_dirs.append(builddir)
261 b = self.builders.get(name, None)
262 if b:
263 if b.builddir != builddir:
264 log.msg("changing builddir for builder %s from %s to %s" \
265 % (name, b.builddir, builddir))
266 b.setBuilddir(builddir)
267 else:
268 b = SlaveBuilder(name)
269 b.usePTY = self.usePTY
270 b.unicode_encoding = self.unicode_encoding
271 b.setServiceParent(self)
272 b.setBuilddir(builddir)
273 self.builders[name] = b
274 retval[name] = b
275 for name in self.builders.keys():
276 if not name in map(lambda a: a[0], wanted):
277 log.msg("removing old builder %s" % name)
278 self.builders[name].disownServiceParent()
279 del(self.builders[name])
280
281 for d in os.listdir(self.basedir):
282 if os.path.isdir(os.path.join(self.basedir, d)):
283 if d not in wanted_dirs:
284 log.msg("I have a leftover directory '%s' that is not "
285 "being used by the buildmaster: you can delete "
286 "it now" % d)
287 return retval
288
290 log.msg("message from master:", message)
291
293 """This command retrieves data from the files in SLAVEDIR/info/* and
294 sends the contents to the buildmaster. These are used to describe
295 the slave and its configuration, and should be created and
296 maintained by the slave administrator. They will be retrieved each
297 time the master-slave connection is established.
298 """
299
300 files = {}
301 basedir = os.path.join(self.basedir, "info")
302 if os.path.isdir(basedir):
303 for f in os.listdir(basedir):
304 filename = os.path.join(basedir, f)
305 if os.path.isfile(filename):
306 files[f] = open(filename, "r").read()
307 files['environ'] = os.environ.copy()
308 files['system'] = os.name
309 files['basedir'] = self.basedir
310 return files
311
315
317 log.msg("slave shutting down on command from master")
318
319
320
321
322 reactor.callLater(0.2, reactor.stop)
323
407
412
414 """Subclass or monkey-patch this method to be alerted whenever there is
415 active communication between the master and slave."""
416 pass
417
421
422
424 - def __init__(self, buildmaster_host, port, name, passwd, basedir,
425 keepalive, usePTY, keepaliveTimeout=None, umask=None,
426 maxdelay=300, unicode_encoding=None, allow_shutdown=None):
427
428
429
430
431 service.MultiService.__init__(self)
432 bot = Bot(basedir, usePTY, unicode_encoding=unicode_encoding)
433 bot.setServiceParent(self)
434 self.bot = bot
435 if keepalive == 0:
436 keepalive = None
437 self.umask = umask
438 self.basedir = basedir
439
440 self.shutdown_loop = None
441
442 if allow_shutdown == 'signal':
443 if not hasattr(signal, 'SIGHUP'):
444 raise ValueError("Can't install signal handler")
445 elif allow_shutdown == 'file':
446 self.shutdown_file = os.path.join(basedir, 'shutdown.stamp')
447 self.shutdown_mtime = 0
448
449 self.allow_shutdown = allow_shutdown
450 bf = self.bf = BotFactory(buildmaster_host, port, keepalive, maxdelay)
451 bf.startLogin(credentials.UsernamePassword(name, passwd), client=bot)
452 self.connection = c = internet.TCPClient(buildmaster_host, port, bf)
453 c.setServiceParent(self)
454
456
457 monkeypatches.patch_all()
458
459 log.msg("Starting BuildSlave -- version: %s" % buildslave.version)
460
461 self.recordHostname(self.basedir)
462 if self.umask is not None:
463 os.umask(self.umask)
464
465 service.MultiService.startService(self)
466
467 if self.allow_shutdown == 'signal':
468 log.msg("Setting up SIGHUP handler to initiate shutdown")
469 signal.signal(signal.SIGHUP, self._handleSIGHUP)
470 elif self.allow_shutdown == 'file':
471 log.msg("Watching %s's mtime to initiate shutdown" % self.shutdown_file)
472 if os.path.exists(self.shutdown_file):
473 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
474 self.shutdown_loop = l = task.LoopingCall(self._checkShutdownFile)
475 l.start(interval=10)
476
478 self.bf.continueTrying = 0
479 self.bf.stopTrying()
480 if self.shutdown_loop:
481 self.shutdown_loop.stop()
482 self.shutdown_loop = None
483 return service.MultiService.stopService(self)
484
486 "Record my hostname in twistd.hostname, for user convenience"
487 log.msg("recording hostname in twistd.hostname")
488 filename = os.path.join(basedir, "twistd.hostname")
489
490 try:
491 hostname = os.uname()[1]
492 except AttributeError:
493
494
495 hostname = socket.getfqdn()
496
497 try:
498 open(filename, "w").write("%s\n" % hostname)
499 except:
500 log.msg("failed - ignoring")
501
505
507 if os.path.exists(self.shutdown_file) and \
508 os.path.getmtime(self.shutdown_file) > self.shutdown_mtime:
509 log.msg("Initiating shutdown because %s was touched" % self.shutdown_file)
510 self.gracefulShutdown()
511
512
513
514
515
516 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
517
519 """Start shutting down"""
520 if not self.bf.perspective:
521 log.msg("No active connection, shutting down NOW")
522 reactor.stop()
523 return
524
525 log.msg("Telling the master we want to shutdown after any running builds are finished")
526 d = self.bf.perspective.callRemote("shutdown")
527 def _shutdownfailed(err):
528 if err.check(AttributeError):
529 log.msg("Master does not support slave initiated shutdown. Upgrade master to 0.8.3 or later to use this feature.")
530 else:
531 log.msg('callRemote("shutdown") failed')
532 log.err(err)
533
534 d.addErrback(_shutdownfailed)
535 return d
536