Skip to content

Commit

Permalink
Add MasterShellCommand
Browse files Browse the repository at this point in the history
  • Loading branch information
Dustin J. Mitchell committed Feb 16, 2009
1 parent 4573cc2 commit 77b3864
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 4 deletions.
76 changes: 76 additions & 0 deletions buildbot/steps/master.py
@@ -0,0 +1,76 @@
import os, types
from twisted.python import log, failure, runtime
from twisted.internet import reactor, defer, task
from buildbot.process.buildstep import RemoteCommand, BuildStep
from buildbot.process.buildstep import SUCCESS, FAILURE
from twisted.internet.protocol import ProcessProtocol

class MasterShellCommand(BuildStep):
"""
Run a shell command locally - on the buildmaster. The shell command
COMMAND is specified just as for a RemoteShellCommand. Note that extra
logfiles are not sopported.
"""
name='MasterShellCommand'
description='Running'
descriptionDone='Ran'

def __init__(self, command, **kwargs):
BuildStep.__init__(self, **kwargs)
self.addFactoryArguments(command=command)
self.command=command

class LocalPP(ProcessProtocol):
def __init__(self, step):
self.step = step

def outReceived(self, data):
self.step.stdio_log.addStdout(data)

def errReceived(self, data):
self.step.stdio_log.addStderr(data)

def processEnded(self, status_object):
self.step.stdio_log.addHeader("exit status %d\n" % status_object.value.exitCode)
self.step.processEnded(status_object)

def start(self):
# set up argv
if type(self.command) in types.StringTypes:
if runtime.platformType == 'win32':
argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args
if '/c' not in argv: argv += ['/c']
argv += [self.command]
else:
# for posix, use /bin/sh. for other non-posix, well, doesn't
# hurt to try
argv = ['/bin/sh', '-c', self.command]
else:
if runtime.platformType == 'win32':
argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args
if '/c' not in argv: argv += ['/c']
argv += list(self.command)
else:
argv = self.command

self.stdio_log = stdio_log = self.addLog("stdio")

if type(self.command) in types.StringTypes:
stdio_log.addHeader(self.command.strip() + "\n\n")
else:
stdio_log.addHeader(" ".join(self.command) + "\n\n")
stdio_log.addHeader("** RUNNING ON BUILDMASTER **\n")
stdio_log.addHeader(" in dir %s\n" % os.getcwd())
stdio_log.addHeader(" argv: %s\n" % (argv,))

# TODO add a timeout?
proc = reactor.spawnProcess(self.LocalPP(self), argv[0], argv)
# (the LocalPP object will call processEnded for us)

def processEnded(self, status_object):
if status_object.value.exitCode != 0:
self.step_status.setText(["failed (%d)" % status_object.value.exitCode])
self.finished(FAILURE)
else:
self.step_status.setText(["succeeded"])
self.finished(SUCCESS)
43 changes: 42 additions & 1 deletion buildbot/test/test_steps.py
Expand Up @@ -22,7 +22,7 @@
from buildbot.sourcestamp import SourceStamp
from buildbot.process import buildstep, base, factory
from buildbot.buildslave import BuildSlave
from buildbot.steps import shell, source, python
from buildbot.steps import shell, source, python, master
from buildbot.status import builder
from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE
from buildbot.test.runutils import RunMixin, rmtree
Expand Down Expand Up @@ -744,3 +744,44 @@ def testFailures_NewTestHarness(self):
self.failUnlessEqual(ss.getStatistic('tests-failed'), 287)
self.failUnlessEqual(ss.getStatistic('tests-total'), 264809)
self.failUnlessEqual(ss.getStatistic('tests-passed'), 264522)

class MasterShellCommand(StepTester, unittest.TestCase):
def testMasterShellCommand(self):
self.slavebase = "testMasterShellCommand.slave"
self.masterbase = "testMasterShellCommand.master"
sb = self.makeSlaveBuilder()
step = self.makeStep(master.MasterShellCommand, command=['echo', 'hi'])

# we can't invoke runStep until the reactor is started .. hence this
# little dance
d = defer.Deferred()
def _dotest(_):
return self.runStep(step)
d.addCallback(_dotest)

def _check(results):
self.failUnlessEqual(results, SUCCESS)
logtxt = step.getLog("stdio").getText()
self.failUnlessEqual(logtxt.strip(), "hi")
d.addCallback(_check)
reactor.callLater(0, d.callback, None)
return d

def testMasterShellCommand_badexit(self):
self.slavebase = "testMasterShellCommand_badexit.slave"
self.masterbase = "testMasterShellCommand_badexit.master"
sb = self.makeSlaveBuilder()
step = self.makeStep(master.MasterShellCommand, command="exit 1")

# we can't invoke runStep until the reactor is started .. hence this
# little dance
d = defer.Deferred()
def _dotest(_):
return self.runStep(step)
d.addCallback(_dotest)

def _check(results):
self.failUnlessEqual(results, FAILURE)
d.addCallback(_check)
reactor.callLater(0, d.callback, None)
return d
33 changes: 30 additions & 3 deletions docs/buildbot.texinfo
Expand Up @@ -200,6 +200,7 @@ Build Steps
* Simple ShellCommand Subclasses::
* Python BuildSteps::
* Transferring Files::
* Steps That Run on the Master::
* Triggering Schedulers::
* Writing New BuildSteps::
Expand Down Expand Up @@ -4579,6 +4580,7 @@ control each.
* Simple ShellCommand Subclasses::
* Python BuildSteps::
* Transferring Files::
* Steps That Run on the Master::
* Triggering Schedulers::
* Writing New BuildSteps::
@end menu
Expand Down Expand Up @@ -5674,7 +5676,7 @@ f.addStep(PyFlakes(command=["pyflakes", "src"]))
@end example


@node Transferring Files, Triggering Schedulers, Python BuildSteps, Build Steps
@node Transferring Files
@subsection Transferring Files

@cindex File Transfer
Expand Down Expand Up @@ -5783,8 +5785,33 @@ f.addStep(DirectoryUpload(slavesrc="docs",
The DirectoryUpload step will create all necessary directories and
transfers empty directories, too.

@node Steps That Run on the Master
@subsection Steps That Run on the Master

@node Triggering Schedulers, Writing New BuildSteps, Transferring Files, Build Steps
Occasionally, it is useful to execute some task on the master, for example to
create a directory, deploy a build result, or trigger some other centralized
processing. This is possible, in a limited fashion, with the
@code{MasterShellCommand} step.

This step operates similarly to a regular @code{ShellCommand}, but executes on
the master, instead of the slave. To be clear, the enclosing @code{Build}
object must still have a slave object, just as for any other step -- only, in
this step, the slave does not do anything.

In this example, the step renames a tarball based on the day of the week.

@example
from buildbot.steps.transfer import FileUpload
from buildbot.steps.master import MasterShellCommand
f.addStep(FileUpload(slavesrc="widgetsoft.tar.gz",
masterdest="/var/buildoutputs/widgetsoft-new.tar.gz"))
f.addStep(MasterShellCommand(command="""
cd /var/buildoutputs;
mv widgetsoft-new.tar.gz widgetsoft-`date +%a`.tar.gz"""))
@end example

@node Triggering Schedulers
@subsection Triggering Schedulers

The counterpart to the Triggerable described in section
Expand Down Expand Up @@ -5816,7 +5843,7 @@ useful to ensure that all of the builds use exactly the same
SourceStamp, even if other Changes have occurred while the build was
running.

@node Writing New BuildSteps, , Triggering Schedulers, Build Steps
@node Writing New BuildSteps
@subsection Writing New BuildSteps

While it is a good idea to keep your build process self-contained in
Expand Down

1 comment on commit 77b3864

@bdash
Copy link

@bdash bdash commented on 77b3864 Feb 16, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that extra logfiles are not sopported.

Please sign in to comment.