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          try: 
490              open(filename, "w").write("%s\n" % socket.getfqdn()) 
491          except: 
492              log.msg("failed - ignoring") 
 493   
497   
499          if os.path.exists(self.shutdown_file) and \ 
500                  os.path.getmtime(self.shutdown_file) > self.shutdown_mtime: 
501              log.msg("Initiating shutdown because %s was touched" % self.shutdown_file) 
502              self.gracefulShutdown() 
503   
504               
505               
506               
507               
508              self.shutdown_mtime = os.path.getmtime(self.shutdown_file) 
 509   
511          """Start shutting down""" 
512          if not self.bf.perspective: 
513              log.msg("No active connection, shutting down NOW") 
514              reactor.stop() 
515              return 
516   
517          log.msg("Telling the master we want to shutdown after any running builds are finished") 
518          d = self.bf.perspective.callRemote("shutdown") 
519          def _shutdownfailed(err): 
520              if err.check(AttributeError): 
521                  log.msg("Master does not support slave initiated shutdown.  Upgrade master to 0.8.3 or later to use this feature.") 
522              else: 
523                  log.msg('callRemote("shutdown") failed') 
524                  log.err(err) 
 525   
526          d.addErrback(_shutdownfailed) 
527          return d 
 528