Commits

Bryan Turner committed 862dbb8 Merge

Merging 1.5.x branch back into default to prepare for the rc7 release

Comments (0)

Files changed (26)

+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java"/>
+	<classpathentry kind="src" path="src/test/java"/>
+	<classpathentry kind="src" path="src/test/resources"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+	<classpathentry kind="con" path="org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER"/>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>
 syntax: glob
+.idea
 target
 *.ipr
 *.iws
 *.iml
-.idea
 d34dfd863d5cc878103539bc20e96673524bc12d 2.4.3-build-20101126023954
 e8ae626e51acd90fe18ce5d6d603c5d1c6ca6e62 2.4.0-M5-build-20100907010907
 22864ef1ef3c72a60c5e7c3e3f8295b318f7ff00 atlassian-processutils-1.3
+1715c02500dc3d99fa1baf5bc361b15b726e03dd atlassian-processutils-1.3.1
+93e69ea723cf82bb98fa2c78a26ad27a441a2efb atlassian-processutils-1.3.2
+974342911be56b2fd61e80a8aab8ee776106fe7f atlassian-processutils-1.3.3
 7d7caf63ccf3fc2b5048d2bff82ee429e4910cff atlassian-processutils-1.4
 34ccca357fab47cf89cfb1801786636108f4647f atlassian-processutils-1.5-beta1
 71b8336a54b14faaec15e09c62b6ea9a2662f191 atlassian-processutils-1.5-beta2
 323e97095d3a837844b5c8037f1ef158e965d8d8 atlassian-processutils-1.5-rc4
 e9d64cf7f65991c0fee369b37f0268d888d6d025 atlassian-processutils-1.5-rc5
 9a018567077cff394c9985146613db5b675d7240 atlassian-processutils-1.5-rc5.stash
+3b4642858f31be4d6ee1fcab5064dbe6a05393a5 atlassian-processutils-1.5-rc6.stash
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <parent>
-    <groupId>com.atlassian.pom</groupId>
-    <artifactId>atlassian-public-pom</artifactId>
-    <version>23</version>
-  </parent>
+    <modelVersion>4.0.0</modelVersion>
 
-  <modelVersion>4.0.0</modelVersion>
-  <groupId>com.atlassian.utils</groupId>
-  <artifactId>atlassian-processutils</artifactId>
-  <version>1.5-rc6-SNAPSHOT</version>
+    <parent>
+        <groupId>com.atlassian.pom</groupId>
+        <artifactId>atlassian-public-pom</artifactId>
+        <version>29.2</version>
+    </parent>
 
-  <name>Atlassian Process Utils</name>
+    <groupId>com.atlassian.utils</groupId>
+    <artifactId>atlassian-processutils</artifactId>
+    <version>1.5-rc7-SNAPSHOT</version>
+    <name>Atlassian Process Utils</name>
 
-  <scm>
-    <connection>scm:hg:ssh://hg@bitbucket.org/atlassian/atlassian-processutils</connection>
-    <developerConnection>scm:hg:ssh://hg@bitbucket.org/atlassian/atlassian-processutils</developerConnection>
-    <url>scm:hg:ssh://hg@bitbucket.org/atlassian/atlassian-processutils</url>
-  </scm>
+    <scm>
+        <connection>scm:hg:ssh://hg@bitbucket.org/atlassian/atlassian-processutils</connection>
+        <developerConnection>scm:hg:ssh://hg@bitbucket.org/atlassian/atlassian-processutils</developerConnection>
+        <url>https://bitbucket.org/atlassian/atlassian-processutils/overview</url>
+    </scm>
 
-
-  <dependencies>
-    <dependency>
-      <groupId>org.jvnet.winp</groupId>
-      <artifactId>winp</artifactId>
-      <version>1.15-atlassian-1</version>
-    </dependency>
-
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-      <version>1.2.15</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.sun.jdmk</groupId>
-          <artifactId>jmxtools</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.sun.jmx</groupId>
-          <artifactId>jmxri</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>javax.jms</groupId>
-          <artifactId>jms</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>javax.mail</groupId>
-          <artifactId>mail</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>javax.activation</groupId>
-          <artifactId>activation</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <version>4.5</version>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
+    <dependencies>
+        <dependency>
+            <groupId>org.jvnet.winp</groupId>
+            <artifactId>winp</artifactId>
+            <version>1.15-atlassian-1</version>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.15</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.sun.jdmk</groupId>
+                    <artifactId>jmxtools</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.sun.jmx</groupId>
+                    <artifactId>jmxri</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>javax.jms</groupId>
+                    <artifactId>jms</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>javax.mail</groupId>
+                    <artifactId>mail</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>javax.activation</groupId>
+                    <artifactId>activation</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.10</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
 </project>

src/main/java/com/atlassian/utils/process/BaseInputHandler.java

 package com.atlassian.utils.process;
 
 /**
+ * A base implementation of the {@link InputHandler} interface which maintains a reference to the {@link Watchdog}.
  */
 public abstract class BaseInputHandler implements InputHandler {
+
     private Watchdog watchdog;
 
     public void complete() {
         this.watchdog = watchdog;
     }
 
-    public void cancelProcess() {
+    protected void cancelProcess() {
         watchdog.cancel();
     }
 
+    /**
+     * Retrieves a flag indicating whether the underlying process has been canceled.
+     *
+     * @return {@code true} if the process has been canceled; otherwise, {@code false}
+     * @since 1.5
+     */
+    protected boolean isCanceled() {
+        return watchdog.isCanceled();
+    }
+
     protected void resetWatchdog() {
-        watchdog.resetWatchdog();
+        if (watchdog != null) {
+            watchdog.resetWatchdog();
+        }
     }
 }

src/main/java/com/atlassian/utils/process/BaseOutputHandler.java

 package com.atlassian.utils.process;
 
 /**
+ * A base implementation of the {@link OutputHandler} interface which maintains a reference to the {@link Watchdog}.
  */
 public abstract class BaseOutputHandler implements OutputHandler {
+
     private Watchdog watchdog;
 
+    public void complete() throws ProcessException {
+    }
+
     public void setWatchdog(Watchdog watchdog) {
         this.watchdog = watchdog;
     }
 
+    protected void cancelProcess() {
+        watchdog.cancel();
+    }
+
+    /**
+     * Retrieves a flag indicating whether the underlying process has been canceled.
+     *
+     * @return {@code true} if the process has been canceled; otherwise, {@code false}
+     * @since 1.5
+     */
+    protected boolean isCanceled() {
+        return watchdog.isCanceled();
+    }
+
     protected void resetWatchdog() {
         if (watchdog != null) {
             watchdog.resetWatchdog();
         }
     }
-
-    public void cancelProcess() {
-        watchdog.cancel();
-    }
-
-    public void complete() throws ProcessException {
-    }
 }

src/main/java/com/atlassian/utils/process/CopyOutputHandler.java

  * An Output Handler which copies the process output to a give output stream
  */
 public class CopyOutputHandler extends BaseOutputHandler {
+    
     private final OutputStream dest;
+    private final int bufferSize;
 
     /**
-     * Create a CopyOutputHandler to redirect output from the process to the given stream
+     * Create a CopyOutputHandler to redirect output from the process to the given stream using the default buffer size
+     * of 1024 bytes
      *
      * @param dest the stream to which output is to be written
      */
     public CopyOutputHandler(OutputStream dest) {
+        this(dest, 1024);
+    }
+
+    /**
+     * Create a CopyOutputHandler to redirect output from the process to the given stream using the specified
+     * buffer size
+     *
+     * @param dest the stream to which output is to be written
+     * @param bufferSize the buffer size to use for redirecting the output
+     */
+    public CopyOutputHandler(OutputStream dest, int bufferSize) {
         this.dest = dest;
+        this.bufferSize = bufferSize;
     }
 
     public void process(InputStream output) throws ProcessException {
         try {
-            byte buffer[] = new byte[1024];
+            byte buffer[] = new byte[bufferSize];
             int num;
             while ((num = output.read(buffer)) != -1) {
                 resetWatchdog();

src/main/java/com/atlassian/utils/process/DefaultExternalProcessFactory.java

+package com.atlassian.utils.process;
+
+/**
+ * A default implementation of the {@link ExternalProcessFactory} which builds and configures instances of
+ * {@link ExternalProcessImpl}.
+ */
+public class DefaultExternalProcessFactory implements ExternalProcessFactory {
+
+    private boolean shutdown;
+
+    public ExternalProcess create(ExternalProcessSettings settings) {
+        if (shutdown) {
+            throw new IllegalStateException("The DefaultExternalProcessFactory has been shutdown; new external processes cannot be created");
+        }
+        settings.validate();
+
+        ExternalProcessImpl process =  new ExternalProcessImpl(settings.getCommand(), settings.getProcessHandler());
+        configureProcess(process, settings);
+
+        return process;
+    }
+
+    /**
+     * Shuts down the thread pool managed by {@link ExternalProcessImpl}.
+     *
+     * @see com.atlassian.utils.process.ExternalProcessImpl#shutdown()
+     */
+    public void shutdown() {
+        shutdown = true;
+
+        ExternalProcessImpl.shutdown();
+    }
+
+    protected void configureProcess(ExternalProcessImpl process, ExternalProcessSettings settings) {
+        process.setSuppressSpecialWindowsBehaviour(settings.isSuppressSpecialWindowsBehaviour());
+        process.setUseWindowsEncodingWorkaround(settings.isUseWindowsEncodingWorkaround());
+        process.setWorkingDir(settings.getWorkingDirectory());
+        for (ProcessMonitor monitor : settings.getMonitors()) {
+            if (monitor == null) {
+                throw new IllegalArgumentException("Null ProcessMonitor in the monitors collection");
+            }
+            process.addMonitor(monitor);
+        }
+        if (settings.hasEnvironment()) {
+            process.setEnvironment(settings.getEnvironment());
+        }
+        if (settings.hasTimeout()) {
+            process.setTimeout(settings.getTimeout());
+        }
+    }
+}

src/main/java/com/atlassian/utils/process/ExternalProcess.java

 package com.atlassian.utils.process;
 
-import org.apache.log4j.Logger;
-import org.jvnet.winp.WinProcess;
+public interface ExternalProcess extends Watchdog {
 
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
+    public ProcessHandler getHandler();
 
-/**
- * This class manages the execution of an external process, using separate threads to process
- * the process' IO requirements.
- */
-public class ExternalProcess implements Watchdog {
-    private static final Logger log = Logger.getLogger(ExternalProcess.class);
-    private static final String OS_NAME = System.getProperty("os.name");
-    
-    private boolean useWindowsEncodingWorkaround = false;
-    private List<String> command;
-    private File workingDir;
-    private Map<String, String> environment;
-    private ProcessHandler handler;
-    private Process process;
+    public Long getStartTime();
 
-    private final List<ProcessMonitor> monitors = new ArrayList<ProcessMonitor>();
-    
-    private ProcessException processException;
+    public void start();
 
-    private LatchedRunnable outputPump;
-    private LatchedRunnable errorPump;
-    private LatchedRunnable inputPump;
+    public void finish();
 
-    private static final ExecutorService pumpThreadPool;
+    public boolean finish(int maxWait);
 
-    private long lastWatchdogReset;
-    private long timeout = 60000L;
-    private Long startTime;
-    private boolean cancelled;
-    
-    public void resetWatchdog() {
-        lastWatchdogReset = System.currentTimeMillis();
-    }
+    public void execute();
 
-    public long getTimeoutTime() {
-        return lastWatchdogReset + timeout;
-    }
+    public void executeWhile(Runnable runnable);
 
-    public boolean isTimedOut() {
-        return getTimeoutTime() < System.currentTimeMillis();
-    }
-
-    static {
-        ThreadFactory threadFactory = new ThreadFactory() {
-            public Thread newThread(Runnable r) {
-                return new Thread(r, "ExtProcess IO Pump");
-            }
-        };
-        pumpThreadPool = new ThreadPoolExecutor(6, Integer.MAX_VALUE, 120, TimeUnit.SECONDS,
-                                                new SynchronousQueue<Runnable>(), threadFactory);
-    }
-
-    /**
-     * Process an external command.
-     * @param command the command and its arguments as separate elements
-     * @param handler The handler for this execution. The handler supports the required IO
-     *                operations
-     */
-    public ExternalProcess(String[] command, ProcessHandler handler) {
-        setCommand(Arrays.asList(command));
-        setHandler(handler);
-    }
-
-    /**
-     * Process an external command (the command and arguments are given as a list)
-     * @param command A list containing the command and its arguments
-     * @param handler The process handler to manage the execution of this process.
-     */
-    public ExternalProcess(List<String> command, ProcessHandler handler) {
-        setCommand(command);
-        setHandler(handler);
-    }
-
-    /**
-     * Process an external command. The command is given as a single command line and parsed into
-     * the command and its arguments. Spaces are used as argument delimiters so if any command arguments
-     * need to contain spaces, the array or list based constructors should be used.
-     *
-     * @param commandLine the command and its arguments in a single line. If any arguments
-     *                    need to contain spaces, the array or list based constructors should be used.
-     * @param handler The handler for this execution. The handler supports the required IO
-     *                operations
-     */
-    public ExternalProcess(String commandLine, ProcessHandler handler) {
-        String[] cmdArray = ProcessUtils.tokenizeCommand(commandLine);
-        setCommand(Arrays.asList(cmdArray));
-        setHandler(handler);
-    }
-
-    private void setHandler(ProcessHandler handler) {
-        this.handler = handler;
-    }
-
-    private void setCommand(List<String> command) {
-        this.command = command;
-    }
-
-    public void setWorkingDir(File workingDir) {
-        this.workingDir = workingDir;
-    }
-
-    public void setEnvironment(Map<String, String> environment) {
-        this.environment = environment;
-    }
-
-    private boolean arePumpsRunning() {
-        return outputPump.isRunning() || errorPump.isRunning()
-                || (inputPump != null && inputPump.isRunning());
-    }
-
-    public void setUseWindowsEncodingWorkaround(final boolean useWindowsEncodingWorkaround) {
-        this.useWindowsEncodingWorkaround = useWindowsEncodingWorkaround;
-    }
-
-    /**
-     * Get the process handler for this process execution
-     *
-     * @return the ProcessHandler instance associated with this process execution.
-     */
-    public ProcessHandler getHandler() {
-        return handler;
-    }
-
-    /**
-     * @return the time process execution started. null if the process has not yet started.
-     */
-    public Long getStartTime() {
-        return this.startTime;
-    }
-    
-    public void addMonitor(ProcessMonitor monitor) {
-        this.monitors.add(monitor);
-    }
-    
-    public void removeMonitor(ProcessMonitor monitor) {
-        this.monitors.remove(monitor);
-    }
-    
-    private boolean isWindows() {
-    	return OS_NAME.toLowerCase().contains("windows");
-    }
-    
-    private String quoteString(String value) {
-        StringBuilder builder = new StringBuilder()
-            .append("\"")
-            .append(value.replace("\"", "\\\""))
-            .append("\"");
-        return builder.toString();
-    }
-    
-    /*
-      * This method provides a workaround for a JVM bug on windows (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4947220). The bug
-      * is that the Sun/Oracle JVM uses the (8 bit) OEM codepage for encoding commandline arguments that are passed to an external process.  Any
-      * characters in a command line argument that can't be represented in the OEM codepage will be replaced by the '?' character, which will probably
-      * cause the command that's being called to fail.
-      * 
-      * A bit of background information is helpful to understand what's going on. Windows uses 2 code pages: the OEM code page and the ANSI code page 
-      * (or 'Windows code page'). The OEM code page is always limited to 8 bit character encodings, whereas some windows code pages are 8 bit and some
-      * are larger. The OEM code page is typically used for console applications, whereas the windows code pages are used for native non-Unicode application
-      * using a GUI on Windows systems.  The system-wide settings can be found in the windows registry:
-      * 
-      * 
-      * More info about the history of OEM vs ANSI code pages: http://blogs.msdn.com/b/michkap/archive/2005/02/08/369197.aspx
-      * 
-      * The workaround is to store the command-line arguments in environment variables and refer to the environment vars on the command line. Windows
-      * cmd will expand the command line arguments without using to the OEM code page, which usually has more correlation with the locale of the 
-      * producer of the hardware (it's derived from the BIOS code page) than with the regional settings configured in Windows. The ANSI code page is derived from
-      * the regional settings.
-      * 
-      * When cmd expands the %JENV_XXX% vars on the command line it uses the ANSI code page instead of the OEM code page (or that is what testing
-      * seems to indicate, can't find any definitive answer in the cmd docos). While this still isn't a true fix to the problem, for most situations this will be sufficient 
-      * as the user typically won't use characters that aren't defined for his locale. But then again, they might..
-     */
-    private Process createWinProcess(List<String> command, Map<String, String> environment, File workingDir) throws IOException {
-        final List<String> newCommand = new ArrayList<String>();
-        newCommand.add("cmd");
-        newCommand.add("/A");
-        newCommand.add("/C");
-        newCommand.add("call");
-
-        if (useWindowsEncodingWorkaround) {
-            Map<String, String> newEnv = environment != null ? new HashMap<String, String>(environment) : new HashMap<String, String>();
-            for (int counter = 1; counter < command.size(); counter++) {
-                final String envName = "JENV_" + counter;
-                newCommand.add("%" + envName + "%");
-                newEnv.put(envName, quoteString(command.get(counter)));
-            }
-            environment = newEnv;
-        } else {
-            newCommand.addAll(command);
-        }
-
-        ProcessBuilder pb = new ProcessBuilder(newCommand);
-        pb.directory(workingDir);
-        if (environment != null)
-        {
-            pb.environment().putAll(environment);
-        }
-
-        if (log.isDebugEnabled())
-        {
-            logProcessDetails(pb);
-        }
-
-        return pb.start();
-    }
-
-    private Process createProcess(List<String> command, Map<String, String> environment, File workingDir) throws IOException {
-    	if (isWindows()) {
-    		return createWinProcess(command, environment, workingDir);
-    	} else {
-            //But what about BeOS?!
-            final ProcessBuilder processBuilder = new ProcessBuilder()
-                    .command(command)
-                    .directory(workingDir);
-
-            if (environment != null)
-            {
-                processBuilder.environment().putAll(environment);
-            }
-
-            if (log.isDebugEnabled())
-            {
-                logProcessDetails(processBuilder);
-            }
-
-            return processBuilder.start();
-    	}
-    }
-
-    private void logProcessDetails(ProcessBuilder processBuilder)
-    {
-        String divider = "---------------------------";
-        log.debug(divider);
-        log.debug("Start Process Debug Information");
-        log.debug(divider);
-        log.debug("Command");
-        log.debug(processBuilder.command());
-        log.debug(divider);
-        log.debug("Working Dir");
-        log.debug(processBuilder.directory());
-        log.debug(divider);
-        log.debug("Environment");
-        for (Map.Entry entry : processBuilder.environment().entrySet())
-        {
-            log.debug(entry.getKey() + ": " + entry.getValue());
-        }
-        log.debug(divider);
-        log.debug("Redirect Error Stream?");
-        log.debug(processBuilder.redirectErrorStream());
-        log.debug(divider);
-        log.debug("End Process Debug Information");
-        log.debug(divider);
-    }
-
-    /**
-     * Start the external process and setup the IO pump threads needed to
-     * manage the process IO. If you call this method you must eventually call the
-     * finish() method. Using this method you may execute additional code between process
-     * start and finish.
-     */
-    public void start() {
-        try {
-            this.startTime = System.currentTimeMillis();
-            this.process = createProcess(command, environment, workingDir);
-            setupIOPumps();
-        } catch (IOException e) {
-            processException = new ProcessException(e);
-        }
-    }
-
-    private void setupIOPumps() {
-        // set up threads to feed data to and extract data from the process
-        if (handler.hasInput()) {
-            inputPump = new LatchedRunnable("inputPump") {
-                protected void doTask() {
-                    handler.provideInput(process.getOutputStream());
-                }
-            };
-        }
-
-        errorPump = new LatchedRunnable("errorPump") {
-            protected void doTask() {
-                try {
-                    handler.processError(process.getErrorStream());
-                } catch (Throwable e) {
-                    if (!isCancelled()) {
-                        log.debug(name + ": Process wasn't cancelled, storing exception", e);
-                        processException = new ProcessException(e);
-                    }
-                    else {
-                        log.debug(name + ": Process cancelled ignoring exception", e);
-                    }
-                }
-            }
-        };
-
-        outputPump = new LatchedRunnable("outputPump") {
-            protected void doTask() {
-                try {
-                    handler.processOutput(process.getInputStream());
-                } catch (Throwable e) {
-                    if (!isCancelled()) {
-                        log.debug(name + ": Process wasn't cancelled, storing exception", e);
-                        processException = new ProcessException(e);
-                    }
-                    else {
-                        log.debug(name + ": Process cancelled ignoring exception", e);
-                    }
-                }
-            }
-        };
-
-        // tickle the dog initially
-        resetWatchdog();
-        handler.setWatchdog(this);
-
-        pumpThreadPool.execute(errorPump);
-        pumpThreadPool.execute(outputPump);
-        if (inputPump != null) {
-            pumpThreadPool.execute(inputPump);
-        }
-    }
-
-    /**
-     * Finish process execution. This method should be called after you have called the
-     * start() method.
-     */
-    public void finish() {
-        if (process != null) {
-            try {
-                do {
-                    long checkTime = getTimeoutTime();
-                    awaitPump(outputPump, checkTime);
-                    awaitPump(inputPump, checkTime);
-                    awaitPump(errorPump, checkTime);
-                } while (!isTimedOut() && arePumpsRunning() && !Thread.currentThread().isInterrupted());
-            } finally {
-
-                if (Thread.currentThread().isInterrupted()) {
-                    cancel();
-
-                    // All is good, now clearing interrupted state of current thread.
-                    Thread.interrupted();
-                }
-
-                int exitCode  = wrapUpProcess();
-                handler.complete(exitCode, processException);
-            }
-        } else {
-            handler.complete(-1, processException);
-        }
-    }
-    
-    /**
-     * Notifies all ProcessMonitors of the 'beforeStart' event.
-     */
-    private void notifyBeforeStart() {
-        for (ProcessMonitor monitor: monitors) {
-            try {
-                monitor.onBeforeStart(this);
-            } catch (Exception e) {
-                // catch and log error, but continue
-                Logger.getLogger(ExternalProcess.class).error("Error while processing 'beforeStarted' event:", e);
-            }
-        }
-    }
-
-    /**
-     * Notifies all ProcessMonitors of the 'afterFinished' event.
-     */
-    private void notifyAfterFinished() {
-        for (ProcessMonitor monitor: monitors) {
-            try {
-                monitor.onAfterFinished(this);
-            } catch (Exception e) {
-                Logger.getLogger(ExternalProcess.class).error("Error while processing 'afterFinished' event:", e);
-            }
-        }
-    }
-    
-    /**
-     * Execute the external command. When this method returns, the process handler
-     * provided at construction time should be consulted to collect exit code, exceptions,
-     * process output, etc.
-     */
-    public void execute() {
-        notifyBeforeStart();
-        try {
-            start();
-            finish();
-        } finally {
-            notifyAfterFinished();
-        }
-    }
-
-    /**
-     * Executes the external command. While it is running, the given runnable is executed.
-     * The external command is not checked until the runnable completes
-     *
-     * @param runnable A task to perform while the external command is running.
-     */
-    public void executeWhile(Runnable runnable) {
-        start();
-        if (runnable != null) {
-            runnable.run();
-        }
-        finish();
-    }
-
-    public String getCommandLine() {
-        StringBuilder sb = new StringBuilder();
-        for (String s : command) {
-            sb.append(s);
-            sb.append(" ");
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Wait a given time for the process to finish
-     *
-     * @param maxWait the maximum amount of time in milliseconds to wait for the process to finish
-     *
-     * @return true if the process has finished.
-     */
-    public boolean finish(int maxWait) {
-        if (process != null) {
-            boolean finished = false;
-            try {
-                long endTime = System.currentTimeMillis() + maxWait;
-                awaitPump(outputPump, endTime);
-                awaitPump(inputPump, endTime);
-                awaitPump(errorPump, endTime);
-            } finally {
-                if (!arePumpsRunning()) {
-                    // process finished
-                    finished = true;
-                    int exitCode = wrapUpProcess();
-                    handler.complete(exitCode, processException);
-                }
-            }
-            return finished;
-        } else {
-            handler.complete(-1, processException);
-            return true;
-        }
-    }
-
-    private int wrapUpProcess() {
-        int exitCode = -1;
-        boolean processIncomplete = true;
-        boolean interrupted = false;
-        try {
-            exitCode = process.exitValue();
-            processIncomplete = false;
-        } catch (IllegalThreadStateException e) {
-            // process still running - could be a race to have the process finish so wait a little to be sure
-            while (processIncomplete && System.currentTimeMillis() - getTimeoutTime() < 10) {
-                // we are currently before the end of the period (within 10ms slack), so process probably not ready yet
-                try {
-                    Thread.sleep(100);
-                    exitCode = process.exitValue();
-                    processIncomplete = false;
-                } catch (InterruptedException e1) {
-                    processIncomplete = true;
-                    interrupted = true;
-                    break;
-                } catch (IllegalThreadStateException e2) {
-                    // ignore and try in the next loop
-                }
-            }
-        } finally {
-            cancel();
-        }
-
-        if (processIncomplete && !interrupted) {
-            processException = new ProcessTimeoutException("process timed out");
-        }
-        return exitCode;
-    }
-
-    private void awaitPump(LatchedRunnable runnable, long latestTime) {
-        if (runnable != null) {
-            long timeout = latestTime - System.currentTimeMillis();
-            if (timeout < 1) {
-                timeout = 1;
-            }
-            runnable.await(timeout);
-        }
-    }
-
-    /**
-     * Cancel should be called if you wish to interrupt process execution.
-     */
-    public void cancel() {
-        this.cancelled = true;
-        if (outputPump != null) {
-            outputPump.cancel();
-        }
-
-        if (inputPump != null) {
-            inputPump.cancel();
-        }
-
-        if (errorPump != null) {
-            errorPump.cancel();
-        }
-
-        if (process != null)
-        {
-            if (isWindows())
-            {
-                try
-                {
-                    final WinProcess winProcess = new WinProcess(process);
-                    winProcess.killRecursively();
-                }
-                catch (Throwable e)
-                {
-                    log.error("Could not recursively kill windows process using winp; falling back to java.lang.Process#destroy", e);
-                    process.destroy();
-                }
-            }
-            else
-            {
-                process.destroy();
-            }
-        }
-    }
-
-    public void setTimeout(long timeout) {
-        this.timeout = timeout;
-    }
-    
-    public static void shutdown() {
-        if (pumpThreadPool != null) {
-            pumpThreadPool.shutdown();
-        }
-    }
+    public String getCommandLine();
 }

src/main/java/com/atlassian/utils/process/ExternalProcessBuilder.java

 package com.atlassian.utils.process;
 
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.log4j.Logger;
 import org.apache.log4j.Priority;
 
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 
 /**
- * Utility class to simplify the building of an ExternalProcess instance
- *
+ * Utility class to simplify the building of an {@link ExternalProcess} instance.
  */
 public class ExternalProcessBuilder {
-    private ProcessHandler handler;
-    private InputHandler input;
-    private OutputHandler output;
-    private OutputHandler error;
-    private final List<ProcessMonitor> monitors = new ArrayList<ProcessMonitor>();
-    private List<String> command;
-    private final Map<String, String> environment = new HashMap<String, String>();
-    private File workingDir;
-    private long timeout;
-    private boolean disableWindowsEncodingWorkaround;
 
-    public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output, OutputHandler error) {
-        this.input = input;
-        this.output = output;
-        this.error = error;
+    private static ExternalProcessFactory externalProcessFactory = new DefaultExternalProcessFactory();
+
+    private final ExternalProcessSettings settings = new ExternalProcessSettings();
+
+    public static void setExternalProcessFactory(ExternalProcessFactory factory) {
+        if (factory == null) {
+            throw new NullPointerException("factory");
+        }
+        externalProcessFactory = factory;
+    }
+
+    public ExternalProcessBuilder addMonitor(ProcessMonitor... monitors) {
+        settings.getMonitors().addAll(Arrays.asList(monitors));
+
+        return this;
+    }
+
+    public ExternalProcess build() {
+        return externalProcessFactory.create(settings);
+    }
+
+    public ExternalProcessBuilder command(List<String> command) {
+        settings.setCommand(command);
+
+        return this;
+    }
+
+    public ExternalProcessBuilder command(List<String> command, File workingDir) {
+        settings.setWorkingDirectory(workingDir);
+
+        return command(command);
+    }
+
+    public ExternalProcessBuilder command(List<String> command, File workingDir, long timeout) {
+        timeout(timeout);
+
+        return command(command, workingDir);
+    }
+
+    public ExternalProcessBuilder env(String variable, String value) {
+        settings.getEnvironment().put(variable, value);
+
+        return this;
+    }
+
+    public ExternalProcessBuilder env(Map<String, String> environment) {
+        settings.getEnvironment().putAll(environment);
+
         return this;
     }
 
     public ExternalProcessBuilder handler(ProcessHandler handler) {
-        this.handler = handler;
+        settings.setProcessHandler(handler);
+
         return this;
     }
 
-    public ExternalProcessBuilder handlers(OutputHandler output, OutputHandler error) {
-        return this.handlers(null, output, error);
+    public ExternalProcessBuilder handlers(OutputHandler output) {
+        return handlers(null, output, null);
     }
 
-    public ExternalProcessBuilder handlers(OutputHandler output) {
-        return this.handlers(null, output, null);
+    public ExternalProcessBuilder handlers(OutputHandler output, OutputHandler error) {
+        return handlers(null, output, error);
     }
 
     public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output) {
-        return this.handlers(input, output, null);
+        return handlers(input, output, null);
     }
 
-    public ExternalProcessBuilder command(List<String> command, File workingDir, long timeout) {
-        this.command = command;
-        this.workingDir = workingDir;
-        this.timeout = timeout;
-        return this;
+    public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output, OutputHandler error) {
+        if (error == null) {
+            error = new StringOutputHandler();
+        }
+
+        return handler(new PluggableProcessHandler(input, output, error));
     }
 
-    public ExternalProcessBuilder command(List<String> command, File workingDir) {
-        this.command = command;
-        this.workingDir = workingDir;
-        return this;
+    public ExternalProcessBuilder log(Logger logger, Priority priority) {
+        return log(logger, priority, null);
     }
 
-    public ExternalProcessBuilder command(List<String> command) {
-        this.command = command;
+    public ExternalProcessBuilder log(Logger logger, Priority priority, StringObfuscator obfuscator) {
+        return addMonitor(new LoggingProcessMonitor(logger, priority, obfuscator));
+    }
+
+    /**
+     * By default, on Windows, commands for {@link ExternalProcess}es are prefixed with {@code cmd /A /C} before they
+     * are started. This flag can be used to suppress that behaviour, causing the commands to be started as-is.
+     * <p/>
+     * Note: If this special Windows behaviour is suppressed, the {@link #useWindowsEncodingWorkaround() workaround}
+     * for encoding is disabled even if it has been specifically requested.
+     *
+     * @return {@code this}
+     */
+    public ExternalProcessBuilder suppressSpecialWindowsBehaviour() {
+        settings.setSuppressSpecialWindowsBehaviour(true);
+
         return this;
     }
 
     public ExternalProcessBuilder timeout(long timeout) {
-        this.timeout = timeout;
+        settings.setTimeout(timeout);
+
         return this;
     }
 
-    public ExternalProcessBuilder log(Logger logger, Priority priority) {
-        addMonitor(new LoggingProcessMonitor(logger, priority));
+    /**
+     * Processes created using process-utils on Windows platforms can optionally perform a special workaround for code
+     * page conversion issues. See the implementation of {@link ExternalProcessImpl#createWinProcess(List, Map, File)}}
+     * for the gory details. This method can be used to request this workaround.
+     * <p/>
+     * Note: This workaround is only available if {@link #suppressSpecialWindowsBehaviour() special Windows behaviour}
+     * is not suppressed. Otherwise, setting this flag has no effect.
+     * 
+     * @return {@code this}
+     */
+    public ExternalProcessBuilder useWindowsEncodingWorkaround() {
+        settings.setUseWindowsEncodingWorkaround(true);
+        
         return this;
     }
-
-    public ExternalProcessBuilder log(Logger logger, Priority priority, StringObfuscator obfuscator) {
-        addMonitor(new LoggingProcessMonitor(logger, priority, obfuscator));
-        return this;
-    }
-
-    public ExternalProcessBuilder addMonitor(ProcessMonitor... monitors) {
-        this.monitors.addAll(Arrays.asList(monitors));
-        return this;
-    }
-
-    public ExternalProcessBuilder env(String variable, String value) {
-        this.environment.put(variable, value);
-        return this;
-    }
-    
-    public ExternalProcessBuilder env(Map<String, String> environment) {
-        this.environment.putAll(environment);
-        return this;
-    }
-
-    public ExternalProcessBuilder useWindowsEncodingWorkaround()
-    {
-        this.disableWindowsEncodingWorkaround = true;
-        return this;
-    }
-    
-    public ExternalProcess build() {
-        ProcessHandler h = this.handler;
-        if (this.handler == null) {
-            // no processHandler defined, create a pluggableprocesshandler
-            PluggableProcessHandler plugHandler = new PluggableProcessHandler();
-            plugHandler.setInputHandler(this.input);
-            plugHandler.setOutputHandler(this.output);
-            if (this.error != null) {
-                plugHandler.setErrorHandler(this.error);
-            } else {
-                plugHandler.setErrorHandler(new StringOutputHandler());
-            }
-
-            h = plugHandler;
-        }
-
-        ExternalProcess process = new ExternalProcess(new LinkedList<String>(command), h);
-        if (timeout > 0L) {
-            process.setTimeout(timeout);
-        }
-        process.setWorkingDir(workingDir);
-        for (ProcessMonitor monitor: monitors) {
-            if (monitor != null) {
-                process.addMonitor(monitor);
-            }
-        }
-        if (!environment.isEmpty()) {
-            process.setEnvironment(new HashMap<String, String>(environment));
-        }
-
-        if (disableWindowsEncodingWorkaround)
-        {
-            process.setUseWindowsEncodingWorkaround(true);
-        }
-
-        return process;
-    }
 }

src/main/java/com/atlassian/utils/process/ExternalProcessFactory.java

+package com.atlassian.utils.process;
+
+/**
+ * Defines a factory which accepts {@link ExternalProcessSettings} and constructs instances of some
+ * {@link ExternalProcess} implementation based on them.
+ */
+public interface ExternalProcessFactory {
+
+    /**
+     * Creates an {@link ExternalProcess} configured by the provided settings.
+     *
+     * @param settings the settings to apply to the process
+     * @return the created and configured process
+     */
+    ExternalProcess create(ExternalProcessSettings settings);
+
+    /**
+     * Shuts down the factory, indicating no more processes will be created.
+     */
+    void shutdown();
+}

src/main/java/com/atlassian/utils/process/ExternalProcessImpl.java

+package com.atlassian.utils.process;
+
+import org.apache.log4j.Logger;
+import org.jvnet.winp.WinProcess;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * This class manages the execution of an external process, using separate threads to process
+ * the process' IO requirements.
+ */
+public class ExternalProcessImpl implements ExternalProcess {
+
+    private static final Logger LOG = Logger.getLogger(ExternalProcessImpl.class);
+    private static final String OS_NAME = System.getProperty("os.name").toLowerCase();
+    private static final ExecutorService POOL;
+
+    private List<String> command;
+    private File workingDir;
+    private Map<String, String> environment;
+    private ProcessHandler handler;
+    private Process process;
+    private boolean suppressSpecialWindowsBehaviour;
+    private boolean useWindowsEncodingWorkaround;
+
+    private List<ProcessMonitor> monitors = new ArrayList<ProcessMonitor>();
+
+    private ProcessException processException;
+
+    private LatchedRunnable outputPump;
+    private LatchedRunnable errorPump;
+    private LatchedRunnable inputPump;
+
+    private long lastWatchdogReset;
+    private long timeout = 60000L;
+    private Long startTime;
+    private boolean canceled;
+
+    public void resetWatchdog() {
+        lastWatchdogReset = System.currentTimeMillis();
+    }
+
+    public long getTimeoutTime() {
+        return lastWatchdogReset + timeout;
+    }
+
+    public boolean isTimedOut() {
+        return getTimeoutTime() < System.currentTimeMillis();
+    }
+
+    static {
+        final String pooledThreadName = "ExtProcess IO Pump";
+        ThreadFactory threadFactory = new ThreadFactory() {
+            public Thread newThread(Runnable r) {
+                return new Thread(r, pooledThreadName);
+            }
+        };
+        POOL = new ThreadPoolExecutor(6, Integer.MAX_VALUE, 120, TimeUnit.SECONDS,
+                new SynchronousQueue<Runnable>(), threadFactory) {
+
+            @Override
+            protected void beforeExecute(Thread thread, Runnable runnable) {
+                thread.setName(thread.getId() + ":" + ((LatchedRunnable) runnable).getName());
+                super.beforeExecute(thread, runnable);
+            }
+
+            @Override
+            protected void afterExecute(Runnable runnable, Throwable throwable) {
+                Thread.currentThread().setName(pooledThreadName);
+                super.afterExecute(runnable, throwable);
+            }
+        };
+    }
+
+    /**
+     * Process an external command.
+     *
+     * @param command the command and its arguments as separate elements
+     * @param handler the process handler to manage the execution of this process
+     */
+    public ExternalProcessImpl(String[] command, ProcessHandler handler) {
+        this(Arrays.asList(command), handler);
+    }
+
+    /**
+     * Process an external command.
+     *
+     * @param command the command and its arguments as separate elements
+     * @param handler the process handler to manage the execution of this process
+     */
+    public ExternalProcessImpl(List<String> command, ProcessHandler handler) {
+        setCommand(command);
+        setHandler(handler);
+    }
+
+    /**
+     * Process an external command. The command is given as a single command line and parsed into
+     * the command and its arguments. Spaces are used as argument delimiters so if any command arguments
+     * need to contain spaces, the array or list based constructors should be used.
+     *
+     * @param commandLine the command and its arguments in a single line. If any arguments
+     *                    need to contain spaces, the array or list based constructors should be used.
+     * @param handler     The handler for this execution. The handler supports the required IO
+     *                    operations
+     */
+    public ExternalProcessImpl(String commandLine, ProcessHandler handler) {
+        this(ProcessUtils.tokenizeCommand(commandLine), handler);
+    }
+
+    protected void setHandler(ProcessHandler handler) {
+        this.handler = handler;
+    }
+
+    private void setCommand(List<String> command) {
+        this.command = command;
+    }
+
+    public void setWorkingDir(File workingDir) {
+        this.workingDir = workingDir;
+    }
+
+    public void setEnvironment(Map<String, String> environment) {
+        this.environment = environment;
+    }
+
+    public void setSuppressSpecialWindowsBehaviour(boolean suppressSpecialWindowsBehaviour) {
+        this.suppressSpecialWindowsBehaviour = suppressSpecialWindowsBehaviour;
+    }
+
+    public void setUseWindowsEncodingWorkaround(boolean useWindowsEncodingWorkaround) {
+        this.useWindowsEncodingWorkaround = useWindowsEncodingWorkaround;
+    }
+
+    private boolean arePumpsRunning() {
+        return outputPump.isRunning() || errorPump.isRunning()
+                || (inputPump != null && inputPump.isRunning());
+    }
+
+    /**
+     * Get the process handler for this process execution
+     *
+     * @return the ProcessHandler instance associated with this process execution.
+     */
+    public ProcessHandler getHandler() {
+        return handler;
+    }
+
+    /**
+     * @return the time process execution started. null if the process has not yet started.
+     */
+    public Long getStartTime() {
+        return this.startTime;
+    }
+
+    public void addMonitor(ProcessMonitor monitor) {
+        this.monitors.add(monitor);
+    }
+
+    public void removeMonitor(ProcessMonitor monitor) {
+        this.monitors.remove(monitor);
+    }
+
+    private boolean isWindows() {
+        return OS_NAME.contains("windows");
+    }
+
+    private String quoteString(String value) {
+        StringBuilder builder = new StringBuilder()
+                .append("\"")
+                .append(value.replace("\"", "\\\""))
+                .append("\"");
+        return builder.toString();
+    }
+
+    /*
+      * This method provides a workaround for a JVM bug on windows (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4947220). The bug
+      * is that the Sun/Oracle JVM uses the (8 bit) OEM codepage for encoding commandline arguments that are passed to an external process.  Any
+      * characters in a command line argument that can't be represented in the OEM codepage will be replaced by the '?' character, which will probably
+      * cause the command that's being called to fail.
+      *
+      * A bit of background information is helpful to understand what's going on. Windows uses 2 code pages: the OEM code page and the ANSI code page
+      * (or 'Windows code page'). The OEM code page is always limited to 8 bit character encodings, whereas some windows code pages are 8 bit and some
+      * are larger. The OEM code page is typically used for console applications, whereas the windows code pages are used for native non-Unicode application
+      * using a GUI on Windows systems.  The system-wide settings can be found in the windows registry:
+      *
+      * More info about the history of OEM vs ANSI code pages: http://blogs.msdn.com/b/michkap/archive/2005/02/08/369197.aspx
+      *
+      * The workaround is to store the command-line arguments in environment variables and refer to the environment vars on the command line. Windows
+      * cmd will expand the command line arguments without using to the OEM code page, which usually has more correlation with the locale of the
+      * producer of the hardware (it's derived from the BIOS code page) than with the regional settings configured in Windows. The ANSI code page is derived from
+      * the regional settings.
+      *
+      * When cmd expands the %JENV_XXX% vars on the command line it uses the ANSI code page instead of the OEM code page (or that is what testing
+      * seems to indicate, can't find any definitive answer in the cmd docos). While this still isn't a true fix to the problem, for most situations this will be sufficient
+      * as the user typically won't use characters that aren't defined for his locale. But then again, they might..
+     */
+    private Process createWinProcess(List<String> command, Map<String, String> environment, File workingDir)
+            throws IOException {
+        List<String> newCommand = new ArrayList<String>(command.size() + 3);
+        newCommand.add("cmd");
+        newCommand.add("/A"); // use ANSI encoding
+        newCommand.add("/C");
+
+        if (useWindowsEncodingWorkaround) {
+            newCommand.add(command.get(0));
+
+            Map<String, String> i18nEnvironment = environment == null ? new HashMap<String, String>() : new HashMap<String, String>(environment);
+            for (int counter = 1; counter < command.size(); counter++) {
+                String envName = "JENV_" + counter;
+                newCommand.add("%" + envName + "%");
+                i18nEnvironment.put(envName, quoteString(command.get(counter)));
+            }
+            environment = i18nEnvironment;
+        } else {
+            newCommand.addAll(command);
+        }
+
+        ProcessBuilder builder = new ProcessBuilder(newCommand)
+                .directory(workingDir);
+        if (environment != null) {
+            builder.environment().putAll(environment);
+        }
+        if (LOG.isDebugEnabled()) {
+            logProcessDetails(builder);
+        }
+        return builder.start();
+    }
+
+    protected Process createProcess(List<String> command, Map<String, String> environment, File workingDir)
+            throws IOException {
+        if (!suppressSpecialWindowsBehaviour && isWindows()) {
+            return createWinProcess(command, environment, workingDir);
+        } else {
+            ProcessBuilder builder = new ProcessBuilder(command).directory(workingDir);
+            if (environment != null) {
+                builder.environment().putAll(environment);
+            }
+            if (LOG.isDebugEnabled()) {
+                logProcessDetails(builder);
+            }
+            return builder.start();
+        }
+    }
+
+    private void logProcessDetails(ProcessBuilder processBuilder) {
+        String divider = "---------------------------";
+        LOG.debug(divider);
+        LOG.debug("Start Process Debug Information");
+        LOG.debug(divider);
+        LOG.debug("Command");
+        LOG.debug(processBuilder.command());
+        LOG.debug(divider);
+        LOG.debug("Working Dir");
+        LOG.debug(processBuilder.directory());
+        LOG.debug(divider);
+        LOG.debug("Environment");
+        for (Map.Entry entry : processBuilder.environment().entrySet()) {
+            LOG.debug(entry.getKey() + ": " + entry.getValue());
+        }
+        LOG.debug(divider);
+        LOG.debug("Redirect Error Stream?");
+        LOG.debug(processBuilder.redirectErrorStream());
+        LOG.debug(divider);
+        LOG.debug("End Process Debug Information");
+        LOG.debug(divider);
+    }
+
+    /**
+     * Start the external process and setup the IO pump threads needed to
+     * manage the process IO. If you call this method you must eventually call the
+     * finish() method. Using this method you may execute additional code between process
+     * start and finish.
+     */
+    public void start() {
+        try {
+            this.startTime = System.currentTimeMillis();
+            this.process = createProcess(command, environment, workingDir);
+            setupIOPumps();
+        } catch (IOException e) {
+            processException = new ProcessException(e);
+        }
+    }
+
+    private void setupIOPumps() {
+        // set up threads to feed data to and extract data from the process
+        if (handler.hasInput()) {
+            inputPump = new LatchedRunnable("StdInHandler " + process) {
+
+                protected void doTask() {
+                    handler.provideInput(process.getOutputStream());
+                }
+            };
+        }
+
+        errorPump = new LatchedRunnable("StdErrHandler " + process) {
+
+            protected void doTask() {
+                try {
+                    handler.processError(process.getErrorStream());
+                } catch (Throwable e) {
+                    if (!isCancelled()) {
+                        LOG.debug(name + ": Process wasn't canceled; storing exception", e);
+                        processException = new ProcessException(e);
+                    } else {
+                        LOG.debug(name + ": Process canceled; ignoring exception", e);
+                    }
+                }
+            }
+        };
+
+        outputPump = new LatchedRunnable("StdOutHandler " + process) {
+
+            protected void doTask() {
+                try {
+                    handler.processOutput(process.getInputStream());
+                } catch (Throwable e) {
+                    if (!isCancelled()) {
+                        LOG.debug(name + ": Process wasn't canceled; storing exception", e);
+                        processException = new ProcessException(e);
+                    } else {
+                        LOG.debug(name + ": Process canceled; ignoring exception", e);
+                    }
+                }
+            }
+        };
+
+        // tickle the dog initially
+        resetWatchdog();
+        handler.setWatchdog(this);
+
+        POOL.execute(errorPump);
+        POOL.execute(outputPump);
+        if (inputPump != null) {
+            POOL.execute(inputPump);
+        }
+    }
+
+    /**
+     * Finish process execution. This method should be called after you have called the
+     * start() method.
+     */
+    public void finish() {
+        if (process != null) {
+            try {
+                do {
+                    long checkTime = getTimeoutTime();
+                    awaitPump(outputPump, checkTime);
+                    awaitPump(inputPump, checkTime);
+                    awaitPump(errorPump, checkTime);
+                } while (!isTimedOut() && arePumpsRunning() && !Thread.currentThread().isInterrupted());
+            } finally {
+                if (Thread.currentThread().isInterrupted()) {
+                    cancel();
+
+                    // All is good, now clearing interrupted state of current thread.
+                    Thread.interrupted();
+                }
+
+                int exitCode = wrapUpProcess();
+                handler.complete(exitCode, canceled, processException);
+            }
+        } else {
+            handler.complete(-1, false, processException);
+        }
+    }
+
+    /**
+     * Notifies all ProcessMonitors of the 'beforeStart' event.
+     */
+    private void notifyBeforeStart() {
+        for (ProcessMonitor monitor : monitors) {
+            try {
+                monitor.onBeforeStart(this);
+            } catch (Exception e) {
+                // catch and log error, but continue
+                LOG.error("Error while processing 'beforeStarted' event:", e);
+            }
+        }
+    }
+
+    /**
+     * Notifies all ProcessMonitors of the 'afterFinished' event.
+     */
+    private void notifyAfterFinished() {
+        for (ProcessMonitor monitor : monitors) {
+            try {
+                monitor.onAfterFinished(this);
+            } catch (Exception e) {
+                LOG.error("Error while processing 'afterFinished' event:", e);
+            }
+        }
+    }
+
+    /**
+     * Execute the external command. When this method returns, the process handler
+     * provided at construction time should be consulted to collect exit code, exceptions,
+     * process output, etc.
+     */
+    public void execute() {
+        notifyBeforeStart();
+        try {
+            start();
+            finish();
+        } finally {
+            notifyAfterFinished();
+        }
+    }
+
+    /**
+     * Executes the external command. While it is running, the given runnable is executed.
+     * The external command is not checked until the runnable completes
+     *
+     * @param runnable A task to perform while the external command is running.
+     */
+    public void executeWhile(Runnable runnable) {
+        start();
+        if (runnable != null) {
+            runnable.run();
+        }
+        finish();
+    }
+
+    public String getCommandLine() {
+        StringBuilder builder = new StringBuilder();
+        for (String s : command) {
+            if (builder.length() > 0) {
+                builder.append(" ");
+            }
+            builder.append(s);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Wait a given time for the process to finish
+     *
+     * @param maxWait the maximum amount of time in milliseconds to wait for the process to finish
+     * @return true if the process has finished.
+     */
+    public boolean finish(int maxWait) {
+        if (process != null) {
+            boolean finished = false;
+            try {
+                long endTime = System.currentTimeMillis() + maxWait;
+                awaitPump(outputPump, endTime);
+                awaitPump(inputPump, endTime);
+                awaitPump(errorPump, endTime);
+            } finally {
+                if (!arePumpsRunning()) {
+                    // process finished
+                    finished = true;
+                    int exitCode = wrapUpProcess();
+                    handler.complete(exitCode, canceled, processException);
+                }
+            }
+            return finished;
+        } else {
+            handler.complete(-1, false, processException);
+            return true;
+        }
+    }
+
+    private int wrapUpProcess() {
+        int exitCode = -1;
+        boolean processIncomplete = true;
+        boolean interrupted = false;
+        try {
+            exitCode = process.exitValue();
+            processIncomplete = false;
+        } catch (IllegalThreadStateException itse) {
+            // process still running - could be a race to have the process finish so wait a little to be sure
+            while (processIncomplete && System.currentTimeMillis() - getTimeoutTime() < 10) {
+                // we are currently before the end of the period (within 10ms slack), so process probably not ready yet
+                try {
+                    Thread.sleep(100);
+                    exitCode = process.exitValue();
+                    processIncomplete = false;
+                } catch (InterruptedException ie) {
+                    processIncomplete = true;
+                    interrupted = true;
+                    break;
+                } catch (IllegalThreadStateException e) {
+                    // ignore and try in the next loop
+                }
+            }
+        } finally {
+            cancel();
+        }
+
+        if (processIncomplete && !interrupted) {
+            processException = new ProcessTimeoutException("process timed out");
+        }
+        return exitCode;
+    }
+
+    private void awaitPump(LatchedRunnable runnable, long latestTime) {
+        if (runnable != null) {
+            long timeout = latestTime - System.currentTimeMillis();
+            if (timeout < 1) {
+                timeout = 1;
+            }
+            runnable.await(timeout);
+        }
+    }
+
+    /**
+     * Cancel should be called if you wish to interrupt process execution.
+     */
+    public void cancel() {
+        this.canceled = true;
+        if (outputPump != null) {
+            outputPump.cancel();
+        }
+        if (inputPump != null) {
+            inputPump.cancel();
+        }
+        if (errorPump != null) {
+            errorPump.cancel();
+        }
+        if (process != null) {
+            if (isWindows()) {
+                try {
+                    new WinProcess(process).killRecursively();
+                } catch (Throwable t) {
+                    LOG.error("Failed to kill Windows process; falling back on Process.destroy()", t);
+                    process.destroy();
+                }
+            } else {
+                process.destroy();
+            }
+        }
+    }
+
+    public boolean isCanceled() {
+        return canceled;
+    }
+
+    public void setTimeout(long timeout) {
+        this.timeout = timeout;
+    }
+
+    /**
+     * Attempts to shutdown the internal {@code ThreadPoolExecutor} which manages the I/O pumps for external processes.
+     * To prevent memory leaks, web applications which use process-utils should always call this when they terminate.
+     * <p/>
+     * On termination, an attempt is made to shutdown the thread pool gracefully. If that does not complete within one
+     * second, shutdown is forced. That is given another second to complete before the thread pool is abandoned.
+     *
+     * @since 1.5
+     */
+    public static void shutdown() {
+        if (POOL == null) {
+            return;
+        }
+
+        LOG.debug("Attempting to shutdown ThreadPoolExecutor");
+
+        POOL.shutdown();
+        try {
+            POOL.awaitTermination(1, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            //The thread pool did not shutdown within the timeout. We can't wait forever, though.
+        } finally {
+            if (POOL.isTerminated()) {
+                LOG.debug("ThreadPoolExecutor shutdown gracefully");
+            } else {
+                LOG.warn("ThreadPoolExecutor did not shutdown within the timeout; forcing shutdown");
+
+                POOL.shutdownNow();
+                try {
+                    POOL.awaitTermination(1, TimeUnit.SECONDS);
+                } catch (InterruptedException ie) {
+                    LOG.warn("ThreadPoolExecutor not shutdown; it will be abandoned");
+                }
+            }
+        }
+    }
+}

src/main/java/com/atlassian/utils/process/ExternalProcessSettings.java

+package com.atlassian.utils.process;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper class containing all of the settings which should be applied to an {@link ExternalProcess} when it is
+ * created by the {@link ExternalProcessFactory}.
+ * <p/>
+ * This class is <i>intentionally mutable</i>. Instances of it are bound to the {@link ExternalProcessBuilder}, and
+ * they have the same thread-safety considerations it does--neither class is reentrant.
+ *
+ * @since 1.5
+ */
+public class ExternalProcessSettings {
+
+    private final Map<String, String> environment;
+    private final List<ProcessMonitor> monitors;
+
+    private List<String> command;
+    private ProcessHandler processHandler;
+    private boolean suppressSpecialWindowsBehaviour;
+    private long timeout;
+    private boolean useWindowsEncodingWorkaround;
+    private File workingDirectory;
+
+    public ExternalProcessSettings() {
+        environment = new HashMap<String, String>();
+        monitors = new ArrayList<ProcessMonitor>();
+    }
+
+    public Map<String, String> getEnvironment() {
+        return environment;
+    }
+
+    public List<ProcessMonitor> getMonitors() {
+        return monitors;
+    }
+
+    public List<String> getCommand() {
+        return command;
+    }
+
+    public ProcessHandler getProcessHandler() {
+        return processHandler;
+    }
+