/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2003, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
********************************************************************************/
package net.sourceforge.cruisecontrol.builders;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import net.sourceforge.cruisecontrol.Builder;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Progress;
import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
import net.sourceforge.cruisecontrol.gendoc.annotations.Description;
import net.sourceforge.cruisecontrol.gendoc.annotations.ManualChildName;
import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
import net.sourceforge.cruisecontrol.util.DateUtil;
import net.sourceforge.cruisecontrol.util.OSEnvironment;
import net.sourceforge.cruisecontrol.util.StreamLogger;
import net.sourceforge.cruisecontrol.util.StreamConsumer;
import net.sourceforge.cruisecontrol.util.StreamPumper;
import net.sourceforge.cruisecontrol.util.Util;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
import org.jdom.Attribute;
import org.jdom.Element;
/**
* Piped exec builder class.
*
* Executes a set of command line scripts where one can be piped from another as well as one may
* wait until another finishes. It determines whether each of the scripts was successful or not.
* Each script is configured independently in the same way as scripts run by {@link ExecBuilder},
* with the extension of piping and waiting facility configured through objects returned by
* {@link #createExec()} method.
*
* Individual scripts in the builder are started simultaneously whenever possible.
*
* Configuration example for this plugin:
* <pre>
* {@code
* <cruisecontrol>
* <schedule>
* <pipedexec workingdir="${workingdir.default}" timeout="3000"/>
* <exec id="1" command="exec1" args="-a1 -a2" timeout="10"/>
* <exec id="2" command="exec2" args="-a1" pipefrom="1"/>
* <exec id="3" command="exec3" args="-a1 -a2 -a3" pipefrom="2" workingdir="${workingdir.special}"/>
* <exec id="4" command="exec3" args="-a1 -a2" pipefrom="1" waitfor="2"/>
* <exec id="5" command="exec4" args="-a1 data" pipefrom="4"/>
* </piped_exec>
* </schedule>
* <cruisecontrol>
* }
* </pre>
*
* @author <a href="mailto:dtihelka@kky.zcu.cz">Dan Tihelka</a>
*/
public class PipedExecBuilder extends Builder {
/** Serialization UID */
private static final long serialVersionUID = -6632406315466647230L;
/** Logger. */
private static final Logger LOG = Logger.getLogger(PipedExecBuilder.class);
/** Build timeout in seconds, set by {@link #setTimeout(long)}. */
private long timeout = ScriptRunner.NO_TIMEOUT;
/** Keep STDOUT of all the scripts gzipped? Set by {@link #setGZipStdout(boolean)} */
private boolean gzip;
/** Is STDOUT of all the scripts binary? Set by {@link #setBinaryStdout(boolean)} */
private boolean binary;
/** The working directory where the commands are to be executed, set by
* {@link #setWorkingDir(String)}. */
private String workingDir;
/** The list of scripts to execute during build. Once the script is started, it is moved
* to the list of started scripts. */
private final LinkedList<PipedScript> scripts = new LinkedList<PipedScript>();
/**
* Validate the attributes for the plugin.
*/
@Override
public void validate() throws CruiseControlException {
super.validate();
Set<PipedScript> removeIDs = new HashSet<PipedScript>(scripts.size()); /* Scripts to be removed */
Set<String> uniqueIDs = new HashSet<String>(scripts.size()); /* To check unique IDs */
Set<String> notInLoop = new HashSet<String>(scripts.size()); /* Loops detection */
/*
* Resolve repiped scripts
*/
for (PipedScript s : scripts) {
if (s.getRepipe() == null) {
continue;
}
/* Repipe required, check setting */
ValidationHelper.assertIsSet(s.getID(), "ID", s.getClass());
/* Repipe */
for (PipedScript c : scripts) {
if (c != s && s.getID().equals(c.getID())) { // ID is defined, just checked
c.setPipeFrom(s.getRepipe());
}
}
/* Remove the "repipe" script from the sequence */
removeIDs.add(s);
}
/*
* Remove disabled scripts
*/
for (PipedScript s : scripts) {
if (!s.getDisable()) {
continue;
}
/* Disabled - check setting */
ValidationHelper.assertIsSet(s.getID(), "ID", s.getClass());
/* Remove the command from the sequence */
for (PipedScript c : scripts) {
if (s.getID().equals(c.getID())) { // ID is defined, just checked
removeIDs.add(c);
removeIDs.addAll(findPipedSeq(c.getID(), scripts));
}
}
/* Remove the disabled */
removeIDs.add(s);
}
scripts.removeAll(removeIDs);
removeIDs.clear();
/*
* Check the (remaining) scripts for basic setting
*/
for (PipedScript s : scripts) {
/* Pass config variables to the exec script, if it does not have set them. Must be done
before s.validate(), since it sets the variables to a default value */
if (s.getWorkingDir() == null) {
s.setWorkingDir(workingDir);
}
if (s.getGZipStdout() == null) {
s.setGZipStdout(gzip);
}
if (s.getBinaryOutput() == null) {
s.setBinaryOutput(binary);
}
/* Let it validate itself */
s.validate();
/* Cannot be piped or wait for itself */
ValidationHelper.assertIsSet(s.getID(), "ID", s.getClass());
ValidationHelper.assertFalse(s.getID().equals(s.getPipeFrom()),
"Script " + s.getID() + " cannot pipe from itself");
ValidationHelper.assertFalse(s.getID().equals(s.getWaitFor()),
"Script " + s.getID() + " cannot wait for itself");
/* If the script is piped from for another script, the "another: must exist */
if (s.getPipeFrom() != null) {
ValidationHelper.assertTrue(findToStart(s.getPipeFrom(), scripts) != null,
"Script " + s.getID() + " is piped from non-existing script " + s.getPipeFrom());
}
/* If the script waits for another script, the "another: must exist */
if (s.getWaitFor() != null) {
ValidationHelper.assertTrue(findToStart(s.getWaitFor(), scripts) != null,
"Script " + s.getID() + " waits for non-existing script " + s.getWaitFor());
}
/* ID must be unique */
ValidationHelper.assertFalse(uniqueIDs.contains(s.getID()), "ID " + s.getID() + " is not unique");
uniqueIDs.add(s.getID());
}
/*
* Loops detection
*/
for (PipedScript s : scripts) {
notInLoop = checkLoop(s, notInLoop, new HashSet<String>());
}
}
/**
* Execute the commands and return the results as XML
*/
@Override
public Element build(final Map<String, String> buildProperties, final Progress progressIn)
throws CruiseControlException {
final ThreadPool threads = new ThreadPool();
final long startTime = System.currentTimeMillis();
final LinkedList<PipedScript> tostart = new LinkedList<PipedScript>(scripts);
final LinkedList<PipedScript> started = new LinkedList<PipedScript>();
final Element buildLogElement = new Element("build");
/* Go through the list of scripts until all were started and finished (tostart contains
* those not started yet, started those running or not finished yet) */
while (tostart.size() > 0 || started.size() > 0) {
ListIterator<PipedScript> iter = tostart.listIterator();
/* Go through all scripts to start and look for those which can be started now */
while (iter.hasNext()) {
PipedScript s = iter.next();
boolean canStart;
/* Script can start if:
* - it is not piped from another script
* - it is piped from another script and the script was started
* - it waits for another script and the script is finished
*/
canStart = s.getPipeFrom() == null;
canStart = s.getPipeFrom() != null && findStarted(s.getPipeFrom(), started) != null ? true : canStart;
canStart = s.getWaitFor() != null && !isDone(s.getWaitFor(), tostart, started) ? false : canStart;
/* If cannot be started, try another one */
if (!canStart) {
continue;
}
long remainTime = this.timeout != ScriptRunner.NO_TIMEOUT
? this.timeout - (System.currentTimeMillis() - startTime) / 1000
: Long.MAX_VALUE;
if (s.getTimeout() == ScriptRunner.NO_TIMEOUT || s.getTimeout() > remainTime) {
s.setTimeout(remainTime);
}
/* And stuff for #build() method */
s.setBuildLogParent(buildLogElement);
s.setBuildProperties(buildProperties);
s.setProgress(progressIn);
/* Pipe to the required script */
if (s.getPipeFrom() != null) {
s.setInputProvider(findStarted(s.getPipeFrom(), started).getOutputReader());
}
/* Initialize the script */
s.initialize();
/* Now start the script and set its thread to the pool */
threads.startThread(s, s.getID());
/* And move it from tostart array into started array. Reset the iterator, which
* allows to run all scripts except those waiting for others */
iter.remove();
iter = tostart.listIterator();
started.add(s);
// !!!!!!
// WINDOWS SPECIFIC HACK:
// Under Windows (tested on Windows XP with SP3, but suppose that it affects all
// lower versions as well) we have found problems when several commands are started
// simultaneously - although the process terminates successfully (finish reports 0
// status), the reading from STDOUT/STDERR of a process blocks forever ... It is later
// caught by timeout killer, but the whole pipe does not finish correctly.
// If this does not occur on Windows Vista (and higher], check just for Windows XP
// and lower can be added.
// Preventing very fast concurrent process spawning seems to fix it (tests are OK). But
// if you still find such problem, use threads.join(). It will lead to horrible
// performance of the pipe under the affected windows versions, but it should be safe.
if (Util.isWindows()) {
threads.join(1000);
}
}
/* All scripts which could be started up to now were started ...
* Try to join some scripts */
threads.join(1000);
/* And check if some scripts were finished */
iter = started.listIterator();
while (iter.hasNext()) {
PipedScript s = iter.next();
/* Remove the script from 'started' map when finished and not required by any
* other script not started yet */
if (s.isDone() && null == findPipedFrom(s.getID(), tostart)) {
s.initialize(); // re-init is supposed to clear the inner variables to save memory
iter.remove();
}
}
/* Sanity check - if running time > timeout, leave the loop with error message */
if (System.currentTimeMillis() - startTime > this.timeout * 1000) {
LOG.warn("Build timeout timer of " + timeout + " seconds has expired");
synchronized (buildLogElement) {
buildLogElement.setAttribute("error", "build timeout");
}
break;
}
}
/* Wait for all scripts to finish (they may be killed by their own timeouts) */
threads.join();
for (PipedScript s : scripts) {
s.initialize(); // re-init is supposed to clear the inner variables to save memory
}
/* Set the time it took to exec command */
buildLogElement.setAttribute("time", DateUtil.getDurationAsString((System.currentTimeMillis() - startTime)));
/* Go through children (individual commands), and check if there is an "error" attribute
* in them. Copy it if so */
for (Object e : buildLogElement.getChildren()) {
Attribute a = ((Element) e).getAttribute("error");
if (a != null) {
buildLogElement.setAttribute(a.detach());
break;
}
}
//note: what other attributes/information should be stored in the element?
// ExecScript.setBuildLogHeader()????
return buildLogElement;
} // build
/**
* Execute the commands and return the results as XML
*/
@Override
public Element buildWithTarget(final Map<String, String> properties,
final String target, final Progress progress)
throws CruiseControlException {
// final String origArgs = args;
// try {
// args = target;
return build(properties, progress);
// } finally {
// args = origArgs;
// }
}
/**
* Sets the working directory where all the scripts are to be executed. Can be overridden
* by the configuration of individual scripts.
*
* @param dir the directory where the command is to be executed
*/
public void setWorkingDir(String dir) {
this.workingDir = dir;
} // setWorkingDir
/**
* Sets the working directory where all the scripts are to be executed. Can be overridden
* by the configuration of individual scripts, but only by lower value.
*
* @param timeout build timeout in seconds
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
} // setWorkingDir
/**
* Should the STDOUT content of the scripts be kept gzipped within the builder? It may save
* some memory required by CruiseControl in cases that data piped through scripts are huge, but
* compressible. Can be overridden by the configuration of individual scripts, see
* {@link Script#setGZipStdout(boolean)}.
*
* @param gzip <code>true</code> if STDOUT is required to be stored gzipped, <code>false</code>
* if raw STDOUT contents are kept.
*/
public void setGZipStdout(boolean gzip) {
this.gzip = gzip;
} // setGZipStdout
/**
* Is the STDOUT content of the scripts in binary form? If <code>true</code>, the STDOUT is not
* logged even in debug mode. If <code>false</code>, the STDOUT of the scripts will be logged in
* debug mode. Can be overridden by the configuration of individual scripts, see
* {@link Script#setBinaryStdout(boolean)}.
*
* @param binary <code>true</code> if STDOUT is in binary form, <code>false</code>
* if STDOUT is text.
*/
public void setBinaryStdout(boolean binary) {
this.binary = binary;
} // setBinaryStdout
/**
* Creates object into which <code>{@code <exec />}</code> tag will be set. Each call returns new
* object which is expected to be set by CC. The attribute is not required; if not
* specified, nothing will be executed.
*
* @return new {@link Script} object to configure.
*/
@Cardinality(min = 0, max = -1)
@ManualChildName("ExecBuilder")
public PipedScript createExec() {
scripts.add(new Script());
return scripts.getLast();
} // createExec
/**
* Adds object into the builder. It is similar to {@link #createExec()}, but allows to add any
* 3rd party plugin implementing the {@link PipedScript} interface.
*
* @param execobj the implementation of {@link PipedScript} interface.
*/
@SkipDoc // TODO: should be documented???
public void add(PipedScript execobj) {
scripts.add(execobj);
} // add
/**
* Finds script with the given ID in the array of scripts not started yet.
*
* @param id the ID of the script to look for.
* @param tostart the list of scripts to be started.
* @return the instance of {@link Script} or <code>null</code> if not found.
*/
private static PipedScript findToStart(String id, List<PipedScript> tostart) {
for (PipedScript s : tostart) {
if (id != null && id.equals(s.getID())) {
return s;
}
}
/* No such found */
return null;
} // findToStart
/**
* Checks. if there is a script in {@link #scripts} array which is required to be piped
* to script with given ID.
*
* @param id the ID of the script to look for.
* @param scripts the list of scripts to be looked it.
* @return the instance of {@link Script} or <code>null</code> if not found.
*/
private static PipedScript findPipedFrom(String id, List<PipedScript> scripts) {
for (PipedScript s : scripts) {
if (id != null && id.equals(s.getPipeFrom())) {
return s;
}
}
/* No such found */
return null;
} // findPipedFrom
/**
* Finds script with the given ID among those started (script is 'started' either if it is
* running, or it is finished but piped to other script not started yet).
*
* @param id the ID of the script to look for.
* @param started the list of scripts which were started.
* @return the instance of {@link Script} or <code>null</code> if not found.
*/
private static PipedScript findStarted(String id, List<PipedScript> started) {
for (PipedScript s : started) {
if (s.getID().equals(id)) {
return s;
}
}
/* No such found */
return null;
} // findStarted
/**
* Checks if script with the given ID is finished or not.
*
* @param id the ID of the script to look for.
* @param tostart the list of scripts to be started (see {@link #findToStart(String, List)}
* for more details about what does 'tostart' mean).
* @param started the list of scripts which were started (see {@link #findStarted(String, List)}
* for more details about what does 'started' mean).
* @return <code>true</code> if the script is done, <code>false</code> if it has not been
* started yet, still running.
*/
private static boolean isDone(String id, List<PipedScript> tostart, List<PipedScript> started) {
/* Not started yet */
if (findToStart(id, tostart) != null) {
return false;
}
PipedScript script = findStarted(id, started);
/* Not among started => finished */
if (script == null) {
return true;
}
return script.isDone();
} // isDone
/**
* Method used for the detection of loops in piped commands. It works with two sets. First,
* the set of script IDs which are not in loop (they lead to a script not piped or not
* waiting for another script). The second is the set of scripts under check, but it cannot
* be determined yet, if they are in loop.
*
* The algorithm is as follows:
* <ul>
* <li> if the script is already in 'not-in-loop' set, return immediately
* <li> if the script is not piped from another script, neither it is waiting for another
* script, put it into 'not-in-loop' set and return
* <li> if the script is piped from another script, or it is waiting for another script,
* put it into 'checking' set and check recursively the scripts which this depends
* on
* <li> if the script is found in 'checking' set, loop is detected (it is checked twice
* during the recursive calls)
* <li> if none of the predecessors is in loop (recursion is left), move the script from
* 'checking' to 'not-in-loop' set and return.
* </ul>
*
* @param s the script to check
* @param notInLoop the 'not-in-loop' set
* @param checking the 'checking' set
* @return 'not-in-loop' set with the ID of current script added when it is not in a loop
* @throws CruiseControlException if loop is detected.
*/
private Set<String> checkLoop(final PipedScript s, Set<String> notInLoop, Set<String> checking)
throws CruiseControlException {
final String pipeFrom = s.getPipeFrom();
final String waitFor = s.getWaitFor();
final String id = s.getID();
/* Already determined not in loop */
if (notInLoop.contains(id)) {
return notInLoop;
}
/* Not piped and not waiting - cannot create loop */
if (pipeFrom == null && waitFor == null) {
notInLoop.add(id);
return notInLoop;
}
/* If piped, check recursively the piped sequence */
if (pipeFrom != null) {
/* If the predecessor is in checking set, loop is detected! */
if (checking.contains(pipeFrom)) {
throw new CruiseControlException("Loop detected, ID " + id + " is within loop");
}
/* Cannot detect loop now, check the predecessor */
checking.add(id);
notInLoop = checkLoop(findToStart(pipeFrom, scripts), notInLoop, checking);
}
/* If waiting, check recursively as well */
if (waitFor != null) {
/* Predecessor in checking set, loop detected! */
if (checking.contains(waitFor)) {
throw new CruiseControlException("Loop detected, ID " + id + " is within loop");
}
/* Cannot detect loop now, ... */
checking.add(id);
notInLoop = checkLoop(findToStart(waitFor, scripts), notInLoop, checking);
}
/* Exception was not thrown, not in loop */
checking.remove(id);
notInLoop.add(id);
return notInLoop;
} // checkLoop
/**
* Finds all scripts piped from the given script
*
* @param id the ID of the script to look for.
* @param scripts the list of scripts to be looked it.
* @return collection of scripts piped to the given script
*/
private static Collection<PipedScript> findPipedSeq(String id, List<PipedScript> scripts) {
Collection<PipedScript> piped = new HashSet<PipedScript>(10);
PipedScript found;
/* Copy the collection, since data will be removed from it */
scripts = new ArrayList<PipedScript>(scripts);
while ((found = findPipedFrom(id, scripts)) != null) {
/* If already in sequence, ignore */
if (piped.contains(found)) {
continue;
}
scripts.remove(found);
/* Add the piped and find those piped to it */
piped.add(found);
piped.addAll(findPipedSeq(found.getID(), scripts));
}
/* Get the sequence */
return piped;
} // findPipedSeq
/** Wrapper for {@link #mergeEnv(OSEnvironment)}, just calling the wrapped method. It
* is required for {@link #mergeEnv(OSEnvironment)} be callable from by Script class,
* since it contains the method with the same name */
private void mergeEnv_wrap(final OSEnvironment env) {
super.mergeEnv(env);
}
/* ----------- NESTED CLASSES ----------- */
/**
* Class for the configuration script to execute. It has the same arguments as
* {@link ExecBuilder}, plus the ID of script, the ID of script from which it is supposed to
* read data through STDIN (optional), and the ID of script which the current should wait
* for.
*
* The class is the implementation of {@link Runnable} interface, as several scripts piped
* one with another are started simultaneously.
*/
@Description("Standard exec builder extended with attributes required for a builder to be piped "
+ "into the pipedexec builder. ")
public final class Script extends PipedScriptBase implements PipedScript {
/**
* Override of {@link ScriptRunner} piping STDIN and STDOUT from/to other scripts
* @author dtihelka
*/
private final class PipedScriptRunner extends ScriptRunner {
/**
* Disable script consumption of STDOUT - although errors cannot be found in it now,
* it is expected that errors are printed to STDERR when a sequence of piped
* commands is started. Also, STDOUT of the script may contain binary data - it is
* generally bad idea pass through text-expected classes.
*/
@Override
protected boolean letConsumeOut() {
return false;
}
/**
* Returns the consumer printing STDOUT of the script on
* {@link org.apache.log4j.Level#DEBUG} level.
*/
@Override
protected StreamConsumer getDirectOutLogger() {
/* Log only non-binary output */
if (Boolean.FALSE.equals(getBinaryOutput())) {
return StreamLogger.getDebugLogger(ScriptRunner.LOG);
}
/* Disable logging otherwise */
return new StreamConsumer() {
@Override
public void consumeLine(final String arg0) { /* Ignore data */ }
};
}
/**
* Assign STDOUT of the process directly to the StdoutBuffer (as byte stream) in
* addition to the (text) consumer given.
*/
@Override
protected StreamPumper getOutPumper(final Process p, final StreamConsumer consumer) {
return new StreamPumper(p.getInputStream(), getBinaryOutput().booleanValue(), consumer,
getOutputBuffer());
} // getPumperOut
}
/** The override of {@link ExecBuilder} class customising {@link ExecBuilder#createScriptRunner()}
* and {@link Builder#mergeEnv(OSEnvironment)} methods; see their description for further
* details. */
private final ExecBuilder builder = new ExecBuilder() {
/** Returns script runner which does not allow to consume STDOUT, and it logs STDOUT in
* debug mode only (the output is passed to the piped script, and it may be huge, or it may
* contain binary data ...) */
@Override
protected ScriptRunner createScriptRunner() {
return new PipedScriptRunner();
} // createScriptRunner
/** Override of {@link #mergeEnv(OSEnvironment)}, merging ENV set to {@link PipedExecBuilder}
* first and then ENV set to the script itself. */
@Override
protected void mergeEnv(final OSEnvironment env) {
mergeEnv_wrap(env);
super.mergeEnv(env);
}
/** Serialization UID */
private static final long serialVersionUID = 2452456256173465623L;
};
@Override
public void validate() throws CruiseControlException {
super.validate();
builder.validate();
}
@Override
protected Element build() throws CruiseControlException {
return builder.build(getBuildProperties(), getProgress(), getInputProvider());
}
@Override
protected Logger log() {
return ExecBuilder.LOG;
}
/** Just caller of {@link ExecBuilder#setTimeout(long)} */
@Override
public void setTimeout(long time) {
builder.setTimeout(time);
}
/** Just caller of {@link ExecBuilder#getTimeout()} */
@Override
public long getTimeout() {
return builder.getTimeout();
}
/** Just caller of {@link ExecBuilder#setWorkingDir(String)} */
@Override
public void setWorkingDir(String workingDir) {
builder.setWorkingDir(workingDir);
}
/** Just caller of {@link ExecBuilder#getWorkingDir()} */
@Override
public String getWorkingDir() {
return builder.getWorkingDir();
}
/** Raw caller of {@link ExecBuilder#setCommand(String)} for the script configuration
* purposes */
@SuppressWarnings("javadoc")
public void setCommand(String cmd) {
this.builder.setCommand(cmd);
}
/** Raw caller of {@link ExecBuilder#setArgs(String)} for the script configuration
* purposes */
@SuppressWarnings("javadoc")
public void setArgs(String args) {
this.builder.setArgs(args);
}
/** Raw caller of {@link ExecBuilder#setErrorStr(String)} for the script configuration
* purposes. */
@SuppressWarnings("javadoc")
public void setErrorStr(String errStr) {
this.builder.setErrorStr(errStr);
} // setErrorStr
/** Raw caller of {@link ExecBuilder#createEnv()} for the script configuration
* purposes. */
@SuppressWarnings("javadoc")
public EnvConf createEnv() {
return builder.createEnv();
} // createEnv
/** Prints string representation of the object */
@Override
public String toString() {
return getClass().getName() + "[ID " + getID() + ", piped from "
+ (getPipeFrom() != null ? getPipeFrom() : "-") + ", wait for "
+ (getWaitFor() != null ? getWaitFor() : "-") + " Command: "
+ builder.getCommand() + ' ' + builder.getArgs() + "]";
}
} // PipedExecScript
/**
* Simple class with pool of started threads. It implements {@link #join()} method waiting
* for any (or all) threads in the pool.
*/
private class ThreadPool {
/** The list of threads in the pool. */
private final List<Thread> threads = new ArrayList<Thread>();
/**
* Creates and <b>starts</b> new thread with the given {@link Runnable} implementation.
* @param runnable the implementation of {@link Runnable} to start.
* @param name the name of the thread.
*/
void startThread(Runnable runnable, String name) {
final Thread t = new Thread(runnable, name + " build thread");
t.start();
threads.add(t);
}
/**
* Waits at least some time for some threads to die.
* @param millis the number of milliseconds to wait.
*/
void join(long millis) {
/* Remove the threads not being alive */
for (int i = threads.size() - 1; i >= 0; i--) {
if (!threads.get(i).isAlive()) {
threads.remove(i);
}
}
/* And try to join the others */
millis = millis / (threads.size() > 0 ? threads.size() : 1);
for (Thread t : threads) {
try {
t.join(millis < 10 ? 10 : millis);
} catch (InterruptedException e) {
/* Did not die in the given time ... */
}
}
}
/**
* Waits for all threads to die.
*/
void join() {
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
/* Should not happen */
}
}
threads.clear();
}
} // ThreadPool
} // PipedExecBuilder