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.util import now
29 from buildslave.pbutil import ReconnectingPBClientFactory
30 from buildslave.commands import registry, base
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
450
451
453 - def __init__(self, buildmaster_host, port, name, passwd, basedir,
454 keepalive, usePTY, keepaliveTimeout=30, umask=None,
455 maxdelay=300, unicode_encoding=None, allow_shutdown=None):
456 log.msg("Creating BuildSlave -- version: %s" % buildslave.version)
457 self.recordHostname(basedir)
458 service.MultiService.__init__(self)
459 bot = Bot(basedir, usePTY, unicode_encoding=unicode_encoding)
460 bot.setServiceParent(self)
461 self.bot = bot
462 if keepalive == 0:
463 keepalive = None
464 self.umask = umask
465
466 if allow_shutdown == 'signal':
467 if not hasattr(signal, 'SIGHUP'):
468 raise ValueError("Can't install signal handler")
469 elif allow_shutdown == 'file':
470 self.shutdown_file = os.path.join(basedir, 'shutdown.stamp')
471 self.shutdown_mtime = 0
472
473 self.allow_shutdown = allow_shutdown
474 bf = self.bf = BotFactory(buildmaster_host, port, keepalive, keepaliveTimeout, maxdelay)
475 bf.startLogin(credentials.UsernamePassword(name, passwd), client=bot)
476 self.connection = c = internet.TCPClient(buildmaster_host, port, bf)
477 c.setServiceParent(self)
478
480 "Record my hostname in twistd.hostname, for user convenience"
481 log.msg("recording hostname in twistd.hostname")
482 filename = os.path.join(basedir, "twistd.hostname")
483 try:
484 open(filename, "w").write("%s\n" % socket.getfqdn())
485 except:
486 log.msg("failed - ignoring")
487
489 if self.umask is not None:
490 os.umask(self.umask)
491 service.MultiService.startService(self)
492
493 if self.allow_shutdown == 'signal':
494 log.msg("Setting up SIGHUP handler to initiate shutdown")
495 signal.signal(signal.SIGHUP, self._handleSIGHUP)
496 elif self.allow_shutdown == 'file':
497 log.msg("Watching %s's mtime to initiate shutdown" % self.shutdown_file)
498 if os.path.exists(self.shutdown_file):
499 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
500 l = task.LoopingCall(self._checkShutdownFile)
501 l.start(interval=10)
502
504 self.bf.continueTrying = 0
505 self.bf.stopTrying()
506 service.MultiService.stopService(self)
507
511
513 if os.path.exists(self.shutdown_file) and \
514 os.path.getmtime(self.shutdown_file) > self.shutdown_mtime:
515 log.msg("Initiating shutdown because %s was touched" % self.shutdown_file)
516 self.gracefulShutdown()
517
518
519
520
521
522 self.shutdown_mtime = os.path.getmtime(self.shutdown_file)
523
525 """Start shutting down"""
526 if not self.bf.perspective:
527 log.msg("No active connection, shutting down NOW")
528 reactor.stop()
529
530 log.msg("Telling the master we want to shutdown after any running builds are finished")
531 d = self.bf.perspective.callRemote("shutdown")
532 def _shutdownfailed(err):
533 if err.check(AttributeError):
534 log.msg("Master does not support slave initiated shutdown. Upgrade master to 0.8.3 or later to use this feature.")
535 else:
536 log.msg('callRemote("shutdown") failed')
537 log.err(err)
538
539 d.addErrback(_shutdownfailed)
540 return d
541