1   
   2  import os, signal, types, re, traceback 
   3  from stat import ST_CTIME, ST_MTIME, ST_SIZE 
   4  from collections import deque 
   5   
   6  from zope.interface import implements 
   7  from twisted.internet.protocol import ProcessProtocol 
   8  from twisted.internet import reactor, defer, task 
   9  from twisted.python import log, runtime 
  10   
  11  from buildslave.interfaces import ISlaveCommand 
  12  from buildslave.commands.registry import registerSlaveCommand 
  13  from buildslave import util 
  14   
  15   
  16   
  17   
  18  command_version = "2.9" 
  19   
  20   
  21   
  22   
  23   
  24   
  25   
  26   
  27   
  28   
  29   
  30   
  31   
  32   
  33   
  34   
  35   
  36   
  37   
  38   
  39   
  40   
  41   
  42   
  43   
  44   
  45   
  46 -class CommandInterrupted(Exception): 
   50   
  52      """An obfuscated string in a command""" 
  54          self.real = real 
  55          self.fake = fake 
   56   
  59   
  62   
  63      @staticmethod 
  65          if isinstance(s, (str, unicode)): 
  66              return s 
  67          else: 
  68              return str(s) 
   69   
  70      @staticmethod 
  81   
  82      @staticmethod 
   93   
  95      """A series of chained steps can raise this exception to indicate that 
  96      one of the intermediate ShellCommands has failed, such that there is no 
  97      point in running the remainder. 'rc' should be the non-zero exit code of 
  98      the failing ShellCommand.""" 
  99   
 101          return "<AbandonChain rc=%s>" % self.args[0] 
   102   
 104      debug = False 
 105   
 107          self.command = command 
 108          self.pending_stdin = "" 
 109          self.stdin_finished = False 
  110   
 112          assert not self.stdin_finished 
 113          if self.connected: 
 114              self.transport.write(data) 
 115          else: 
 116              self.pending_stdin += data 
  117   
 123   
 125          if self.debug: 
 126              log.msg("ShellCommandPP.connectionMade") 
 127          if not self.command.process: 
 128              if self.debug: 
 129                  log.msg(" assigning self.command.process: %s" % 
 130                          (self.transport,)) 
 131              self.command.process = self.transport 
 132   
 133           
 134           
 135           
 136           
 137           
 138           
 139           
 140           
 141           
 142           
 143   
 144          if self.pending_stdin: 
 145              if self.debug: log.msg(" writing to stdin") 
 146              self.transport.write(self.pending_stdin) 
 147          if self.stdin_finished: 
 148              if self.debug: log.msg(" closing stdin") 
 149              self.transport.closeStdin() 
  150   
 155   
 160   
 162          if self.debug: 
 163              log.msg("ShellCommandPP.processEnded", status_object) 
 164           
 165           
 166           
 167          sig = status_object.value.signal 
 168          rc = status_object.value.exitCode 
 169          self.command.finished(sig, rc) 
   170   
 172      POLL_INTERVAL = 2 
 173   
 174 -    def __init__(self, command, name, logfile, follow=False): 
  175          self.command = command 
 176          self.name = name 
 177          self.logfile = logfile 
 178   
 179          log.msg("LogFileWatcher created to watch %s" % logfile) 
 180           
 181           
 182           
 183          self.old_logfile_stats = self.statFile() 
 184          self.started = False 
 185   
 186           
 187           
 188          self.follow = follow 
 189   
 190           
 191          self.poller = task.LoopingCall(self.poll) 
  192   
 195   
 197          log.err(err, msg="Polling error") 
 198          self.poller = None 
  199   
 201          self.poll() 
 202          if self.poller is not None: 
 203              self.poller.stop() 
 204          if self.started: 
 205              self.f.close() 
  206   
 208          if os.path.exists(self.logfile): 
 209              s = os.stat(self.logfile) 
 210              return (s[ST_CTIME], s[ST_MTIME], s[ST_SIZE]) 
 211          return None 
  212   
 214          if not self.started: 
 215              s = self.statFile() 
 216              if s == self.old_logfile_stats: 
 217                  return  
 218              if not s: 
 219                   
 220                   
 221                   
 222                  self.old_logfile_stats = None 
 223                  return  
 224              self.f = open(self.logfile, "rb") 
 225               
 226               
 227               
 228              if self.follow: 
 229                  self.f.seek(s[2], 0) 
 230              self.started = True 
 231          self.f.seek(self.f.tell(), 0) 
 232          while True: 
 233              data = self.f.read(10000) 
 234              if not data: 
 235                  return 
 236              self.command.addLogfile(self.name, data) 
   237   
 240       
 241       
 242   
 243      notreally = False 
 244      BACKUP_TIMEOUT = 5 
 245      KILL = "KILL" 
 246      CHUNK_LIMIT = 128*1024 
 247   
 248       
 249       
 250      BUFFER_SIZE = 64*1024 
 251      BUFFER_TIMEOUT = 5 
 252   
 253       
 254      startTime = None 
 255      elapsedTime = None 
 256   
 257       
 258      _reactor = reactor 
 259   
 260       
 261       
 262       
 263       
 264   
 265 -    def __init__(self, builder, command, 
 266                   workdir, environ=None, 
 267                   sendStdout=True, sendStderr=True, sendRC=True, 
 268                   timeout=None, maxTime=None, initialStdin=None, 
 269                   keepStdinOpen=False, keepStdout=False, keepStderr=False, 
 270                   logEnviron=True, logfiles={}, usePTY="slave-config"): 
  271          """ 
 272   
 273          @param keepStdout: if True, we keep a copy of all the stdout text 
 274                             that we've seen. This copy is available in 
 275                             self.stdout, which can be read after the command 
 276                             has finished. 
 277          @param keepStderr: same, for stderr 
 278   
 279          @param usePTY: "slave-config" -> use the SlaveBuilder's usePTY; 
 280              otherwise, true to use a PTY, false to not use a PTY. 
 281          """ 
 282   
 283          self.builder = builder 
 284          self.command = Obfuscated.get_real(command) 
 285   
 286           
 287           
 288           
 289           
 290           
 291           
 292           
 293           
 294           
 295           
 296          if isinstance(self.command, (tuple, list)): 
 297              for i, a in enumerate(self.command): 
 298                  if isinstance(a, unicode): 
 299                      self.command[i] = a.encode(self.builder.unicode_encoding) 
 300          elif isinstance(self.command, unicode): 
 301              self.command = self.command.encode(self.builder.unicode_encoding) 
 302   
 303          self.fake_command = Obfuscated.get_fake(command) 
 304          self.sendStdout = sendStdout 
 305          self.sendStderr = sendStderr 
 306          self.sendRC = sendRC 
 307          self.logfiles = logfiles 
 308          self.workdir = workdir 
 309          if not os.path.exists(workdir): 
 310              os.makedirs(workdir) 
 311          if environ: 
 312              if environ.has_key('PYTHONPATH'): 
 313                  ppath = environ['PYTHONPATH'] 
 314                   
 315                   
 316                   
 317                  if not isinstance(ppath, str): 
 318                       
 319                       
 320                      ppath = os.pathsep.join(ppath) 
 321   
 322                  environ['PYTHONPATH'] = ppath + os.pathsep + "${PYTHONPATH}" 
 323   
 324               
 325              p = re.compile('\${([0-9a-zA-Z_]*)}') 
 326              def subst(match): 
 327                  return os.environ.get(match.group(1), "") 
  328              newenv = {} 
 329              for key in os.environ.keys(): 
 330                   
 331                  if key not in environ or environ[key] is not None: 
 332                      newenv[key] = os.environ[key] 
 333              for key in environ.keys(): 
 334                  if environ[key] is not None: 
 335                      newenv[key] = p.sub(subst, environ[key]) 
 336   
 337              self.environ = newenv 
 338          else:  
 339              self.environ = os.environ.copy() 
 340          self.initialStdin = initialStdin 
 341          self.keepStdinOpen = keepStdinOpen 
 342          self.logEnviron = logEnviron 
 343          self.timeout = timeout 
 344          self.timer = None 
 345          self.maxTime = maxTime 
 346          self.maxTimer = None 
 347          self.keepStdout = keepStdout 
 348          self.keepStderr = keepStderr 
 349   
 350          self.buffered = deque() 
 351          self.buflen = 0 
 352          self.buftimer = None 
 353   
 354          if usePTY == "slave-config": 
 355              self.usePTY = self.builder.usePTY 
 356          else: 
 357              self.usePTY = usePTY 
 358   
 359           
 360           
 361           
 362           
 363          if runtime.platformType != "posix" or initialStdin is not None: 
 364              if self.usePTY and usePTY != "slave-config": 
 365                  self.sendStatus({'header': "WARNING: disabling usePTY for this command"}) 
 366              self.usePTY = False 
 367   
 368          self.logFileWatchers = [] 
 369          for name,filevalue in self.logfiles.items(): 
 370              filename = filevalue 
 371              follow = False 
 372   
 373               
 374               
 375              if type(filevalue) == dict: 
 376                  filename = filevalue['filename'] 
 377                  follow = filevalue.get('follow', False) 
 378   
 379              w = LogFileWatcher(self, name, 
 380                                 os.path.join(self.workdir, filename), 
 381                                 follow=follow) 
 382              self.logFileWatchers.append(w) 
  383   
 385          return "<slavecommand.ShellCommand '%s'>" % self.fake_command 
  386   
 389   
 391           
 392           
 393          if self.keepStdout: 
 394              self.stdout = "" 
 395          if self.keepStderr: 
 396              self.stderr = "" 
 397          self.deferred = defer.Deferred() 
 398          try: 
 399              self._startCommand() 
 400          except: 
 401              log.msg("error in ShellCommand._startCommand") 
 402              log.err() 
 403              self._addToBuffers('stderr', "error in ShellCommand._startCommand\n") 
 404              self._addToBuffers('stderr', traceback.format_exc()) 
 405              self._sendBuffers() 
 406               
 407              self.deferred.errback(AbandonChain(-1)) 
 408          return self.deferred 
  409   
 411           
 412          if not os.path.isdir(self.workdir): 
 413              os.makedirs(self.workdir) 
 414          log.msg("ShellCommand._startCommand") 
 415          if self.notreally: 
 416              self._addToBuffers('header', "command '%s' in dir %s" % \ 
 417                               (self.fake_command, self.workdir)) 
 418              self._addToBuffers('header', "(not really)\n") 
 419              self.finished(None, 0) 
 420              return 
 421   
 422          self.pp = ShellCommandPP(self) 
 423   
 424          if type(self.command) in types.StringTypes: 
 425              if runtime.platformType  == 'win32': 
 426                  argv = os.environ['COMSPEC'].split()  
 427                  if '/c' not in argv: argv += ['/c'] 
 428                  argv += [self.command] 
 429              else: 
 430                   
 431                   
 432                  argv = ['/bin/sh', '-c', self.command] 
 433              display = self.fake_command 
 434          else: 
 435               
 436               
 437               
 438               
 439               
 440              if runtime.platformType == 'win32' and not \ 
 441                      (self.command[0].lower().endswith(".exe") and os.path.isabs(self.command[0])): 
 442                  argv = os.environ['COMSPEC'].split()  
 443                  if '/c' not in argv: argv += ['/c'] 
 444                  argv += list(self.command) 
 445              else: 
 446                  argv = self.command 
 447              display = " ".join(self.fake_command) 
 448   
 449           
 450           
 451           
 452          if not self.environ.get('MACHTYPE', None) == 'i686-pc-msys': 
 453              self.environ['PWD'] = os.path.abspath(self.workdir) 
 454   
 455           
 456   
 457           
 458           
 459           
 460           
 461          log.msg(" " + display) 
 462          self._addToBuffers('header', display+"\n") 
 463   
 464           
 465          msg = " in dir %s" % (self.workdir,) 
 466          if self.timeout: 
 467              msg += " (timeout %d secs)" % (self.timeout,) 
 468          log.msg(" " + msg) 
 469          self._addToBuffers('header', msg+"\n") 
 470   
 471          msg = " watching logfiles %s" % (self.logfiles,) 
 472          log.msg(" " + msg) 
 473          self._addToBuffers('header', msg+"\n") 
 474   
 475           
 476          msg = " argv: %s" % (self.fake_command,) 
 477          log.msg(" " + msg) 
 478          self._addToBuffers('header', msg+"\n") 
 479   
 480           
 481          if self.logEnviron: 
 482              msg = " environment:\n" 
 483              env_names = self.environ.keys() 
 484              env_names.sort() 
 485              for name in env_names: 
 486                  msg += "  %s=%s\n" % (name, self.environ[name]) 
 487              log.msg(" environment: %s" % (self.environ,)) 
 488              self._addToBuffers('header', msg) 
 489   
 490          if self.initialStdin: 
 491              msg = " writing %d bytes to stdin" % len(self.initialStdin) 
 492              log.msg(" " + msg) 
 493              self._addToBuffers('header', msg+"\n") 
 494   
 495          if self.keepStdinOpen: 
 496              msg = " leaving stdin open" 
 497          else: 
 498              msg = " closing stdin" 
 499          log.msg(" " + msg) 
 500          self._addToBuffers('header', msg+"\n") 
 501   
 502          msg = " using PTY: %s" % bool(self.usePTY) 
 503          log.msg(" " + msg) 
 504          self._addToBuffers('header', msg+"\n") 
 505   
 506           
 507          if self.initialStdin: 
 508              self.pp.writeStdin(self.initialStdin) 
 509          if not self.keepStdinOpen: 
 510              self.pp.closeStdin() 
 511   
 512           
 513           
 514           
 515           
 516           
 517           
 518           
 519           
 520           
 521          self.process = None 
 522          self.startTime = util.now(self._reactor) 
 523   
 524          p = reactor.spawnProcess(self.pp, argv[0], argv, 
 525                                   self.environ, 
 526                                   self.workdir, 
 527                                   usePTY=self.usePTY) 
 528           
 529          if not self.process: 
 530              self.process = p 
 531   
 532           
 533           
 534           
 535           
 536           
 537   
 538          if self.timeout: 
 539              self.timer = self._reactor.callLater(self.timeout, self.doTimeout) 
 540   
 541          if self.maxTime: 
 542              self.maxTimer = self._reactor.callLater(self.maxTime, self.doMaxTimeout) 
 543   
 544          for w in self.logFileWatchers: 
 545              w.start() 
  546   
 547   
 549          """ 
 550          limit the chunks that we send over PB to 128k, since it has a hardwired 
 551          string-size limit of 640k. 
 552          """ 
 553          LIMIT = self.CHUNK_LIMIT 
 554          for i in range(0, len(data), LIMIT): 
 555              yield data[i:i+LIMIT] 
  556   
 558          """ 
 559          Take msg, which is a dictionary of lists of output chunks, and 
 560          concatentate all the chunks into a single string 
 561          """ 
 562          retval = {} 
 563          for log in msg: 
 564              data = "".join(msg[log]) 
 565              if isinstance(log, tuple) and log[0] == 'log': 
 566                  retval['log'] = (log[1], data) 
 567              else: 
 568                  retval[log] = data 
 569          return retval 
  570   
 572          """ 
 573          Collapse and send msg to the master 
 574          """ 
 575          if not msg: 
 576              return 
 577          msg = self._collapseMsg(msg) 
 578          self.sendStatus(msg) 
  579   
 581          self.buftimer = None 
 582          self._sendBuffers() 
  583   
 585          """ 
 586          Send all the content in our buffers. 
 587          """ 
 588          msg = {} 
 589          msg_size = 0 
 590          lastlog = None 
 591          logdata = [] 
 592          while self.buffered: 
 593               
 594              logname, data = self.buffered.popleft() 
 595   
 596               
 597               
 598               
 599               
 600               
 601               
 602               
 603               
 604              if lastlog is None: 
 605                  lastlog = logname 
 606              elif logname != lastlog: 
 607                  self._sendMessage(msg) 
 608                  msg = {} 
 609                  msg_size = 0 
 610              lastlog = logname 
 611   
 612              logdata = msg.setdefault(logname, []) 
 613   
 614               
 615               
 616              for chunk in self._chunkForSend(data): 
 617                  if len(chunk) == 0: continue 
 618                  logdata.append(chunk) 
 619                  msg_size += len(chunk) 
 620                  if msg_size >= self.CHUNK_LIMIT: 
 621                       
 622                       
 623                       
 624                      self._sendMessage(msg) 
 625                      msg = {} 
 626                      logdata = msg.setdefault(logname, []) 
 627                      msg_size = 0 
 628          self.buflen = 0 
 629          if logdata: 
 630              self._sendMessage(msg) 
 631          if self.buftimer: 
 632              if self.buftimer.active(): 
 633                  self.buftimer.cancel() 
 634              self.buftimer = None 
  635   
 637          """ 
 638          Add data to the buffer for logname 
 639          Start a timer to send the buffers if BUFFER_TIMEOUT elapses. 
 640          If adding data causes the buffer size to grow beyond BUFFER_SIZE, then 
 641          the buffers will be sent. 
 642          """ 
 643          n = len(data) 
 644   
 645          self.buflen += n 
 646          self.buffered.append((logname, data)) 
 647          if self.buflen > self.BUFFER_SIZE: 
 648              self._sendBuffers() 
 649          elif not self.buftimer: 
 650              self.buftimer = self._reactor.callLater(self.BUFFER_TIMEOUT, self._bufferTimeout) 
  651   
 653          if self.sendStdout: 
 654              self._addToBuffers('stdout', data) 
 655   
 656          if self.keepStdout: 
 657              self.stdout += data 
 658          if self.timer: 
 659              self.timer.reset(self.timeout) 
  660   
 662          if self.sendStderr: 
 663              self._addToBuffers('stderr', data) 
 664   
 665          if self.keepStderr: 
 666              self.stderr += data 
 667          if self.timer: 
 668              self.timer.reset(self.timeout) 
  669   
 675   
 677          self.elapsedTime = util.now(self._reactor) - self.startTime 
 678          log.msg("command finished with signal %s, exit code %s, elapsedTime: %0.6f" % (sig,rc,self.elapsedTime)) 
 679          for w in self.logFileWatchers: 
 680               
 681              w.stop() 
 682          self._sendBuffers() 
 683          if sig is not None: 
 684              rc = -1 
 685          if self.sendRC: 
 686              if sig is not None: 
 687                  self.sendStatus( 
 688                      {'header': "process killed by signal %d\n" % sig}) 
 689              self.sendStatus({'rc': rc}) 
 690          self.sendStatus({'header': "elapsedTime=%0.6f\n" % self.elapsedTime}) 
 691          if self.timer: 
 692              self.timer.cancel() 
 693              self.timer = None 
 694          if self.maxTimer: 
 695              self.maxTimer.cancel() 
 696              self.maxTimer = None 
 697          if self.buftimer: 
 698              self.buftimer.cancel() 
 699              self.buftimer = None 
 700          d = self.deferred 
 701          self.deferred = None 
 702          if d: 
 703              d.callback(rc) 
 704          else: 
 705              log.msg("Hey, command %s finished twice" % self) 
  706   
 708          self._sendBuffers() 
 709          log.msg("ShellCommand.failed: command failed: %s" % (why,)) 
 710          if self.timer: 
 711              self.timer.cancel() 
 712              self.timer = None 
 713          if self.maxTimer: 
 714              self.maxTimer.cancel() 
 715              self.maxTimer = None 
 716          if self.buftimer: 
 717              self.buftimer.cancel() 
 718              self.buftimer = None 
 719          d = self.deferred 
 720          self.deferred = None 
 721          if d: 
 722              d.errback(why) 
 723          else: 
 724              log.msg("Hey, command %s finished twice" % self) 
  725   
 727          self.timer = None 
 728          msg = "command timed out: %d seconds without output" % self.timeout 
 729          self.kill(msg) 
  730   
 732          self.maxTimer = None 
 733          msg = "command timed out: %d seconds elapsed" % self.maxTime 
 734          self.kill(msg) 
  735   
 736 -    def kill(self, msg): 
  737           
 738           
 739          self._sendBuffers() 
 740          if self.timer: 
 741              self.timer.cancel() 
 742              self.timer = None 
 743          if self.maxTimer: 
 744              self.maxTimer.cancel() 
 745              self.maxTimer = None 
 746          if self.buftimer: 
 747              self.buftimer.cancel() 
 748              self.buftimer = None 
 749          if hasattr(self.process, "pid") and self.process.pid is not None: 
 750              msg += ", killing pid %s" % self.process.pid 
 751          log.msg(msg) 
 752          self.sendStatus({'header': "\n" + msg + "\n"}) 
 753   
 754          hit = 0 
 755          if runtime.platformType == "posix": 
 756              try: 
 757                   
 758                   
 759                   
 760                   
 761   
 762                   
 763                   
 764                   
 765   
 766                  sig = None 
 767                  if self.KILL is not None: 
 768                      sig = getattr(signal, "SIG"+ self.KILL, None) 
 769   
 770                  if self.KILL == None: 
 771                      log.msg("self.KILL==None, only pretending to kill child") 
 772                  elif sig is None: 
 773                      log.msg("signal module is missing SIG%s" % self.KILL) 
 774                  elif not hasattr(os, "kill"): 
 775                      log.msg("os module is missing the 'kill' function") 
 776                  elif not hasattr(self.process, "pid") or self.process.pid is None: 
 777                      log.msg("self.process has no pid") 
 778                  else: 
 779                      log.msg("trying os.kill(-pid, %d)" % (sig,)) 
 780                       
 781                      os.kill(-self.process.pid, sig) 
 782                      log.msg(" signal %s sent successfully" % sig) 
 783                      hit = 1 
 784              except OSError: 
 785                   
 786                   
 787                  pass 
 788          if not hit: 
 789              try: 
 790                  if self.KILL is None: 
 791                      log.msg("self.KILL==None, only pretending to kill child") 
 792                  else: 
 793                      log.msg("trying process.signalProcess('KILL')") 
 794                      self.process.signalProcess(self.KILL) 
 795                      log.msg(" signal %s sent successfully" % (self.KILL,)) 
 796                      hit = 1 
 797              except OSError: 
 798                   
 799                  pass 
 800          if not hit: 
 801              log.msg("signalProcess/os.kill failed both times") 
 802   
 803          if runtime.platformType == "posix": 
 804               
 805               
 806               
 807              self.pp.transport.loseConnection() 
 808   
 809           
 810           
 811          self.timer = self._reactor.callLater(self.BACKUP_TIMEOUT, 
 812                                         self.doBackupTimeout) 
  813   
 815          log.msg("we tried to kill the process, and it wouldn't die.." 
 816                  " finish anyway") 
 817          self.timer = None 
 818          self.sendStatus({'header': "SIGKILL failed to kill process\n"}) 
 819          if self.sendRC: 
 820              self.sendStatus({'header': "using fake rc=-1\n"}) 
 821              self.sendStatus({'rc': -1}) 
 822          self.failed(TimeoutError("SIGKILL failed to kill process")) 
  823   
 824   
 827   
 830   
 833      implements(ISlaveCommand) 
 834   
 835      """This class defines one command that can be invoked by the build master. 
 836      The command is executed on the slave side, and always sends back a 
 837      completion message when it finishes. It may also send intermediate status 
 838      as it runs (by calling builder.sendStatus). Some commands can be 
 839      interrupted (either by the build master or a local timeout), in which 
 840      case the step is expected to complete normally with a status message that 
 841      indicates an error occurred. 
 842   
 843      These commands are used by BuildSteps on the master side. Each kind of 
 844      BuildStep uses a single Command. The slave must implement all the 
 845      Commands required by the set of BuildSteps used for any given build: 
 846      this is checked at startup time. 
 847   
 848      All Commands are constructed with the same signature: 
 849       c = CommandClass(builder, args) 
 850      where 'builder' is the parent SlaveBuilder object, and 'args' is a 
 851      dict that is interpreted per-command. 
 852   
 853      The setup(args) method is available for setup, and is run from __init__. 
 854   
 855      The Command is started with start(). This method must be implemented in a 
 856      subclass, and it should return a Deferred. When your step is done, you 
 857      should fire the Deferred (the results are not used). If the command is 
 858      interrupted, it should fire the Deferred anyway. 
 859   
 860      While the command runs. it may send status messages back to the 
 861      buildmaster by calling self.sendStatus(statusdict). The statusdict is 
 862      interpreted by the master-side BuildStep however it likes. 
 863   
 864      A separate completion message is sent when the deferred fires, which 
 865      indicates that the Command has finished, but does not carry any status 
 866      data. If the Command needs to return an exit code of some sort, that 
 867      should be sent as a regular status message before the deferred is fired . 
 868      Once builder.commandComplete has been run, no more status messages may be 
 869      sent. 
 870   
 871      If interrupt() is called, the Command should attempt to shut down as 
 872      quickly as possible. Child processes should be killed, new ones should 
 873      not be started. The Command should send some kind of error status update, 
 874      then complete as usual by firing the Deferred. 
 875   
 876      .interrupted should be set by interrupt(), and can be tested to avoid 
 877      sending multiple error status messages. 
 878   
 879      If .running is False, the bot is shutting down (or has otherwise lost the 
 880      connection to the master), and should not send any status messages. This 
 881      is checked in Command.sendStatus . 
 882   
 883      """ 
 884   
 885       
 886       
 887       
 888   
 889      debug = False 
 890      interrupted = False 
 891      running = False  
 892                       
 893   
 894      _reactor = reactor 
 895   
 896 -    def __init__(self, builder, stepId, args): 
  901   
 903          """Override this in a subclass to extract items from the args dict.""" 
 904          pass 
  905   
 911   
 913          """Start the command. This method should return a Deferred that will 
 914          fire when the command has completed. The Deferred's argument will be 
 915          ignored. 
 916   
 917          This method should be overridden by subclasses.""" 
 918          raise NotImplementedError, "You must implement this in a subclass" 
  919   
 928   
 932   
 934          """Override this in a subclass to allow commands to be interrupted. 
 935          May be called multiple times, test and set self.interrupted=True if 
 936          this matters.""" 
 937          pass 
  938   
 940          self.running = False 
 941          return res 
  942   
 943       
 944   
 946          if type(rc) is not int: 
 947              log.msg("weird, _abandonOnFailure was given rc=%s (%s)" % \ 
 948                      (rc, type(rc))) 
 949          assert isinstance(rc, int) 
 950          if rc != 0: 
 951              raise AbandonChain(rc) 
 952          return rc 
  953   
 956   
 958          log.msg("_checkAbandoned", why) 
 959          why.trap(AbandonChain) 
 960          log.msg(" abandoning chain", why.value) 
 961          self.sendStatus({'rc': why.value.args[0]}) 
 962          return None 
   963   
 967      """This is a Command which runs a shell command. The args dict contains 
 968      the following keys: 
 969   
 970          - ['command'] (required): a shell command to run. If this is a string, 
 971                                    it will be run with /bin/sh (['/bin/sh', 
 972                                    '-c', command]). If it is a list 
 973                                    (preferred), it will be used directly. 
 974          - ['workdir'] (required): subdirectory in which the command will be 
 975                                    run, relative to the builder dir 
 976          - ['env']: a dict of environment variables to augment/replace 
 977                     os.environ . PYTHONPATH is treated specially, and 
 978                     should be a list of path components to be prepended to 
 979                     any existing PYTHONPATH environment variable. 
 980          - ['initial_stdin']: a string which will be written to the command's 
 981                               stdin as soon as it starts 
 982          - ['keep_stdin_open']: unless True, the command's stdin will be 
 983                                 closed as soon as initial_stdin has been 
 984                                 written. Set this to True if you plan to write 
 985                                 to stdin after the command has been started. 
 986          - ['want_stdout']: 0 if stdout should be thrown away 
 987          - ['want_stderr']: 0 if stderr should be thrown away 
 988          - ['usePTY']: True or False if the command should use a PTY (defaults to 
 989                        configuration of the slave) 
 990          - ['not_really']: 1 to skip execution and return rc=0 
 991          - ['timeout']: seconds of silence to tolerate before killing command 
 992          - ['maxTime']: seconds before killing command 
 993          - ['logfiles']: dict mapping LogFile name to the workdir-relative 
 994                          filename of a local log file. This local file will be 
 995                          watched just like 'tail -f', and all changes will be 
 996                          written to 'log' status updates. 
 997          - ['logEnviron']: False to not log the environment variables on the slave 
 998   
 999      ShellCommand creates the following status messages: 
1000          - {'stdout': data} : when stdout data is available 
1001          - {'stderr': data} : when stderr data is available 
1002          - {'header': data} : when headers (command start/stop) are available 
1003          - {'log': (logfile_name, data)} : when log files have new contents 
1004          - {'rc': rc} : when the process has terminated 
1005      """ 
1006   
1008          args = self.args 
1009           
1010          assert args['workdir'] is not None 
1011          workdir = os.path.join(self.builder.basedir, args['workdir']) 
1012   
1013          c = ShellCommand(self.builder, args['command'], 
1014                           workdir, environ=args.get('env'), 
1015                           timeout=args.get('timeout', None), 
1016                           maxTime=args.get('maxTime', None), 
1017                           sendStdout=args.get('want_stdout', True), 
1018                           sendStderr=args.get('want_stderr', True), 
1019                           sendRC=True, 
1020                           initialStdin=args.get('initial_stdin'), 
1021                           keepStdinOpen=args.get('keep_stdin_open'), 
1022                           logfiles=args.get('logfiles', {}), 
1023                           usePTY=args.get('usePTY', "slave-config"), 
1024                           logEnviron=args.get('logEnviron', True), 
1025                           ) 
1026          c._reactor = self._reactor 
1027          self.command = c 
1028          d = self.command.start() 
1029          return d 
 1030   
1034   
1037   
 1040   
1041  registerSlaveCommand("shell", SlaveShellCommand, command_version) 
1045      """ 
1046      I am a dummy no-op command that by default takes 5 seconds to complete. 
1047      See L{buildbot.steps.dummy.RemoteDummy} 
1048      """ 
1049   
1051          self.d = defer.Deferred() 
1052          log.msg("  starting dummy command [%s]" % self.stepId) 
1053          self.timer = self._reactor.callLater(1, self.doStatus) 
1054          return self.d 
 1055   
1063   
1069   
1071          log.msg("  dummy command finished [%s]" % self.stepId) 
1072          if self.interrupted: 
1073              self.sendStatus({'rc': 1}) 
1074          else: 
1075              self.sendStatus({'rc': 0}) 
1076          self.d.callback(0) 
  1077   
1078  registerSlaveCommand("dummy", DummyCommand, command_version) 
1079   
1080   
1081   
1082   
1083   
1084  waitCommandRegistry = {} 
1087      """ 
1088      I am a dummy command used by the buildbot unit test suite. I want for the 
1089      unit test to tell us to finish. See L{buildbot.steps.dummy.Wait} 
1090      """ 
1091   
1093          self.d = defer.Deferred() 
1094          log.msg("  starting wait command [%s]" % self.stepId) 
1095          handle = self.args['handle'] 
1096          cb = waitCommandRegistry[handle] 
1097          del waitCommandRegistry[handle] 
1098          def _called(): 
1099              log.msg(" wait-%s starting" % (handle,)) 
1100              d = cb() 
1101              def _done(res): 
1102                  log.msg(" wait-%s finishing: %s" % (handle, res)) 
1103                  return res 
 1104              d.addBoth(_done) 
1105              d.addCallbacks(self.finished, self.failed) 
 1106          self._reactor.callLater(0, _called) 
1107          return self.d 
1108   
1115   
1117          log.msg("  wait command finished [%s]" % self.stepId) 
1118          if self.interrupted: 
1119              self.sendStatus({'rc': 2}) 
1120          else: 
1121              self.sendStatus({'rc': 0}) 
1122          self.d.callback(0) 
 1124          log.msg("  wait command failed [%s]" % self.stepId) 
1125          self.sendStatus({'rc': 1}) 
1126          self.d.callback(0) 
 1127   
1128  registerSlaveCommand("dummy.wait", WaitCommand, command_version) 
1129