1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16from __future__ import absolute_import 17from __future__ import print_function 18 19from twisted.internet import defer 20from twisted.internet import reactor 21from twisted.python import log 22from zope.interface import implementer 23 24from buildbot_worker import util 25from buildbot_worker.exceptions import AbandonChain 26from buildbot_worker.interfaces import IWorkerCommand 27 28# The following identifier should be updated each time this file is changed 29command_version = "3.1" 30 31# version history: 32# >=1.17: commands are interruptable 33# >=1.28: Arch understands 'revision', added Bazaar 34# >=1.33: Source classes understand 'retry' 35# >=1.39: Source classes correctly handle changes in branch (except Git) 36# Darcs accepts 'revision' (now all do but Git) (well, and P4Sync) 37# Arch/Baz should accept 'build-config' 38# >=1.51: (release 0.7.3) 39# >= 2.1: SlaveShellCommand now accepts 'initial_stdin', 'keep_stdin_open', 40# and 'logfiles'. It now sends 'log' messages in addition to 41# stdout/stdin/header/rc. It acquired writeStdin/closeStdin methods, 42# but these are not remotely callable yet. 43# (not externally visible: ShellCommandPP has writeStdin/closeStdin. 44# ShellCommand accepts new arguments (logfiles=, initialStdin=, 45# keepStdinOpen=) and no longer accepts stdin=) 46# (release 0.7.4) 47# >= 2.2: added monotone, uploadFile, and downloadFile (release 0.7.5) 48# >= 2.3: added bzr (release 0.7.6) 49# >= 2.4: Git understands 'revision' and branches 50# >= 2.5: workaround added for remote 'hg clone --rev REV' when hg<0.9.2 51# >= 2.6: added uploadDirectory 52# >= 2.7: added usePTY option to SlaveShellCommand 53# >= 2.8: added username and password args to SVN class 54# >= 2.9: add depth arg to SVN class 55# >= 2.10: CVS can handle 'extra_options' and 'export_options' 56# >= 2.11: Arch, Bazaar, and Monotone removed 57# >= 2.12: SlaveShellCommand no longer accepts 'keep_stdin_open' 58# >= 2.13: SlaveFileUploadCommand supports option 'keepstamp' 59# >= 2.14: RemoveDirectory can delete multiple directories 60# >= 2.15: 'interruptSignal' option is added to SlaveShellCommand 61# >= 2.16: 'sigtermTime' option is added to SlaveShellCommand 62# >= 2.16: runprocess supports obfuscation via tuples (#1748) 63# >= 2.16: listdir command added to read a directory 64# >= 3.0: new buildbot-worker package: 65# * worker-side usePTY configuration (usePTY='slave-config') support 66# dropped, 67# * remote method getSlaveInfo() renamed to getWorkerInfo(). 68# * "slavesrc" command argument renamed to "workersrc" in uploadFile and 69# uploadDirectory commands. 70# * "slavedest" command argument renamed to "workerdest" in downloadFile 71# command. 72# >= 3.1: rmfile command added to remove a file 73 74 75@implementer(IWorkerCommand) 76class Command(object): 77 78 """This class defines one command that can be invoked by the build master. 79 The command is executed on the worker side, and always sends back a 80 completion message when it finishes. It may also send intermediate status 81 as it runs (by calling builder.sendStatus). Some commands can be 82 interrupted (either by the build master or a local timeout), in which 83 case the step is expected to complete normally with a status message that 84 indicates an error occurred. 85 86 These commands are used by BuildSteps on the master side. Each kind of 87 BuildStep uses a single Command. The worker must implement all the 88 Commands required by the set of BuildSteps used for any given build: 89 this is checked at startup time. 90 91 All Commands are constructed with the same signature: 92 c = CommandClass(builder, stepid, args) 93 where 'builder' is the parent WorkerForBuilder object, and 'args' is a 94 dict that is interpreted per-command. 95 96 The setup(args) method is available for setup, and is run from __init__. 97 Mandatory args can be declared by listing them in the requiredArgs property. 98 They will be checked before calling the setup(args) method. 99 100 The Command is started with start(). This method must be implemented in a 101 subclass, and it should return a Deferred. When your step is done, you 102 should fire the Deferred (the results are not used). If the command is 103 interrupted, it should fire the Deferred anyway. 104 105 While the command runs. it may send status messages back to the 106 buildmaster by calling self.sendStatus(statusdict). The statusdict is 107 interpreted by the master-side BuildStep however it likes. 108 109 A separate completion message is sent when the deferred fires, which 110 indicates that the Command has finished, but does not carry any status 111 data. If the Command needs to return an exit code of some sort, that 112 should be sent as a regular status message before the deferred is fired . 113 Once builder.commandComplete has been run, no more status messages may be 114 sent. 115 116 If interrupt() is called, the Command should attempt to shut down as 117 quickly as possible. Child processes should be killed, new ones should 118 not be started. The Command should send some kind of error status update, 119 then complete as usual by firing the Deferred. 120 121 .interrupted should be set by interrupt(), and can be tested to avoid 122 sending multiple error status messages. 123 124 If .running is False, the bot is shutting down (or has otherwise lost the 125 connection to the master), and should not send any status messages. This 126 is checked in Command.sendStatus . 127 128 """ 129 130 # builder methods: 131 # sendStatus(dict) (zero or more) 132 # commandComplete() or commandInterrupted() (one, at end) 133 134 requiredArgs = [] 135 debug = False 136 interrupted = False 137 # set by Builder, cleared on shutdown or when the Deferred fires 138 running = False 139 140 _reactor = reactor 141 142 def __init__(self, builder, stepId, args): 143 self.builder = builder 144 self.stepId = stepId # just for logging 145 self.args = args 146 self.startTime = None 147 148 missingArgs = [arg for arg in self.requiredArgs if arg not in args] 149 if missingArgs: 150 raise ValueError("{0} is missing args: {1}".format( 151 self.__class__.__name__, ", ".join(missingArgs))) 152 self.setup(args) 153 154 def setup(self, args): 155 """Override this in a subclass to extract items from the args dict.""" 156 157 def doStart(self): 158 self.running = True 159 self.startTime = util.now(self._reactor) 160 d = defer.maybeDeferred(self.start) 161 162 def commandComplete(res): 163 self.sendStatus( 164 {"elapsed": util.now(self._reactor) - self.startTime}) 165 self.running = False 166 return res 167 d.addBoth(commandComplete) 168 return d 169 170 def start(self): 171 """Start the command. This method should return a Deferred that will 172 fire when the command has completed. The Deferred's argument will be 173 ignored. 174 175 This method should be overridden by subclasses.""" 176 raise NotImplementedError("You must implement this in a subclass") 177 178 def sendStatus(self, status): 179 """Send a status update to the master.""" 180 if self.debug: 181 log.msg("sendStatus", status) 182 if not self.running: 183 log.msg("would sendStatus but not .running") 184 return 185 self.builder.sendUpdate(status) 186 187 def doInterrupt(self): 188 self.running = False 189 self.interrupt() 190 191 def interrupt(self): 192 """Override this in a subclass to allow commands to be interrupted. 193 May be called multiple times, test and set self.interrupted=True if 194 this matters.""" 195 196 # utility methods, mostly used by WorkerShellCommand and the like 197 198 def _abandonOnFailure(self, rc): 199 if not isinstance(rc, int): 200 log.msg("weird, _abandonOnFailure was given rc={0} ({1})".format( 201 rc, type(rc))) 202 assert isinstance(rc, int) 203 if rc != 0: 204 raise AbandonChain(rc) 205 return rc 206 207 def _sendRC(self, res): 208 self.sendStatus({'rc': 0}) 209 210 def _checkAbandoned(self, why): 211 log.msg("_checkAbandoned", why) 212 why.trap(AbandonChain) 213 log.msg(" abandoning chain", why.value) 214 self.sendStatus({'rc': why.value.args[0]}) 215 return None 216