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