Commits

Bryan Turner  committed b666842 Merge

Merging in the 1.3.1 branch

  • Participants
  • Parent commits 579dc4d, 3385fa9
  • Branches 1.5.x

Comments (0)

Files changed (15)

+<?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>

File .hgignore

File contents unchanged.
 f583ef26fc4ceeefa968683749fa55f22afc59d2 atlassian-processutils-1.5-beta11
 c11b314b02f1c1490024dbc87623cfa0fc5f9e51 atlassian-processutils-1.5-rc1
 0006b2ff8f692eef47b9f5b149ddf02280d8abb6 atlassian-processutils-1.5-rc2
+1715c02500dc3d99fa1baf5bc361b15b726e03dd atlassian-processutils-1.3.1
+93e69ea723cf82bb98fa2c78a26ad27a441a2efb atlassian-processutils-1.3.2
+974342911be56b2fd61e80a8aab8ee776106fe7f atlassian-processutils-1.3.3
     <name>Atlassian Process Utils</name>
 
     <scm>
-        <connection>scm:hg:ssh://hg@bitbucket.org/i386/atlassian-processutils</connection>
-        <developerConnection>scm:hg:ssh://hg@bitbucket.org/i386/atlassian-processutils</developerConnection>
-        <url>scm:hg:ssh://hg@bitbucket.org/i386/atlassian-processutils</url>
+        <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>

File 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();

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

 package com.atlassian.utils.process;
 
-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 interface ExternalProcess extends Watchdog {
 
-import org.apache.log4j.Logger;
-import org.jvnet.winp.WinProcess;
+    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);
-    }
-
-    /**
-     * Attempts to terminate 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 terminate() {
-        log.debug("Attempting to shutdown ThreadPoolExecutor");
-
-        pumpThreadPool.shutdown();
-        try {
-            pumpThreadPool.awaitTermination(1, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            //The thread pool did not shutdown within the timeout. We can't wait forever, though.
-        } finally {
-            if (pumpThreadPool.isTerminated()) {
-                log.debug("ThreadPoolExecutor shutdown gracefully");
-            } else {
-                log.warn("ThreadPoolExecutor did not shutdown within the timeout; forcing shutdown");
-
-                pumpThreadPool.shutdownNow();
-                try {
-                    pumpThreadPool.awaitTermination(1, TimeUnit.SECONDS);
-                } catch (InterruptedException ie) {
-                    log.warn("ThreadPoolExecutor not shutdown; it will be abandoned");
-                }
-            }
-        }
-    }
-
-    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);
-        }
-        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);
-            }
-
-    		return processBuilder.start();
-    	}
-    }
-    
-    /**
-     * 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() {
-                protected void doTask() {
-                    handler.provideInput(process.getOutputStream());
-                }
-            };
-        }
-
-        errorPump = new LatchedRunnable() {
-            protected void doTask() {
-                try {
-                    handler.processError(process.getErrorStream());
-                } catch (Throwable e) {
-                    if (!isCancelled()) {
-                        processException = new ProcessException(e);
-                    }
-                }
-            }
-        };
-
-        outputPump = new LatchedRunnable() {
-            protected void doTask() {
-                try {
-                    handler.processOutput(process.getInputStream());
-                } catch (Throwable e) {
-                    if (!isCancelled()) {
-                        processException = new ProcessException(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());
-
-                if (Thread.currentThread().isInterrupted() && arePumpsRunning()) {
-                    cancel();
-                }
-            } finally {
-                int exitCode = 0;
-                if (!cancelled) {
-                    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 boolean isCanceled() {
-        return cancelled;
-    }
-
-    public void setTimeout(long timeout) {
-        this.timeout = timeout;
-    }
-    
-    public static void shutdown() {
-        if (pumpThreadPool != null) {
-            pumpThreadPool.shutdown();
-        }
-    }
+    public String getCommandLine();
 }

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

 package com.atlassian.utils.process;
 
+import org.apache.log4j.Logger;
+import org.apache.log4j.Priority;
+
 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;
-
-
 /**
  * Utility class to simplify the building of an ExternalProcess instance
- *
  */
 public class ExternalProcessBuilder {
     private ProcessHandler handler;
     private final Map<String, String> environment = new HashMap<String, String>();
     private File workingDir;
     private long timeout;
-    private boolean disableWindowsEncodingWorkaround;
+    private boolean useWindowsEncodingWorkaround;
+    private boolean suppressSpecialWindowsBehaviour;
+    private static ExternalProcessFactory externalProcessFactory = new ExternalProcessFactory.Default();
 
     public ExternalProcessBuilder handlers(InputHandler input, OutputHandler output, OutputHandler error) {
         this.input = input;
     }
 
     public ExternalProcessBuilder handlers(OutputHandler output, OutputHandler error) {
-        return this.handlers(null, output, error);
+        return handlers(null, output, error);
     }
 
     public ExternalProcessBuilder handlers(OutputHandler output) {
-        return this.handlers(null, output, null);
+        return handlers(null, output, null);
     }
 
     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) {
     }
 
     public ExternalProcessBuilder env(String variable, String value) {
-        this.environment.put(variable, value);
+        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;
+    public ExternalProcessBuilder useWindowsEncodingWorkaround() {
+        useWindowsEncodingWorkaround = true;
         return this;
     }
-    
+
+    /**
+     * Processes created using process-utils on Windows platforms default to performing a special workaround for lossy
+     * code page conversion issues. See the implementation of {@link ExternalProcessImpl#createWinProcess(List, Map, File)}}
+     * for the gory details. This method can be used to suppress this 'special' behaviour and behave as if it were
+     * executing on a non-Windows platform.
+     *
+     * @return this {@link ExternalProcessBuilder} instance
+     */
+    public ExternalProcessBuilder suppressSpecialWindowsBehaviour() {
+        suppressSpecialWindowsBehaviour = true;
+        return this;
+    }
+
     public ExternalProcess build() {
-        ProcessHandler h = this.handler;
-        if (this.handler == null) {
+        ProcessHandler h = handler;
+        if (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);
+            plugHandler.setInputHandler(input);
+            plugHandler.setOutputHandler(output);
+            if (error != null) {
+                plugHandler.setErrorHandler(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));
-        }
+        return externalProcessFactory.createExternalProcess(command, timeout, workingDir, monitors, environment,
+                suppressSpecialWindowsBehaviour, useWindowsEncodingWorkaround, h);
+    }
 
-        if (disableWindowsEncodingWorkaround)
-        {
-            process.setUseWindowsEncodingWorkaround(true);
-        }
+    public static void setExternalProcessFactory(ExternalProcessFactory factory) {
+        externalProcessFactory = factory;
+    }
 
-        return process;
+    public static void resetExternalProcessFactory() {
+        externalProcessFactory = new ExternalProcessFactory.Default();
     }
 }

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

+package com.atlassian.utils.process;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+public interface ExternalProcessFactory {
+
+    ExternalProcess createExternalProcess(List<String> command, long timeout, File workingDir,
+                                          List<ProcessMonitor> monitors, Map<String, String> environment,
+                                          boolean suppressSpecialWindowsBehaviour, boolean useWindowsEncodingWorkaround,
+                                          ProcessHandler handler);
+
+    void shutdown();
+
+    public static class Default implements ExternalProcessFactory {
+
+        public ExternalProcess createExternalProcess(List<String> command, long timeout, File workingDir,
+                                                     List<ProcessMonitor> monitors, Map<String, String> environment,
+                                                     boolean suppressSpecialWindowsBehaviour,
+                                                     boolean useWindowsEncodingWorkaround, ProcessHandler handler) {
+            ExternalProcessImpl process =  new ExternalProcessImpl(command, handler);
+            return configureProcess(process, timeout, workingDir, monitors, environment,
+                    suppressSpecialWindowsBehaviour, useWindowsEncodingWorkaround);
+        }
+
+        public void shutdown() {
+            ExternalProcessImpl.shutdown();
+        }
+
+        protected ExternalProcess configureProcess(ExternalProcessImpl process, long timeout, File workingDir,
+                                                   List<ProcessMonitor> monitors, Map<String, String> environment,
+                                                   boolean suppressSpecialWindowsBehaviour,
+                                                   boolean useWindowsEncodingWorkaround) {
+            if (timeout > 0L) {
+                process.setTimeout(timeout);
+            }
+            process.setWorkingDir(workingDir);
+            for (ProcessMonitor monitor: monitors) {
+                if (monitor != null) {
+                    process.addMonitor(monitor);
+                }
+            }
+            process.setEnvironment(environment);
+            process.setSuppressSpecialWindowsBehaviour(suppressSpecialWindowsBehaviour);
+            process.setUseWindowsEncodingWorkaround(useWindowsEncodingWorkaround);
+            return process;
+        }
+    }
+}

File 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 pb = new ProcessBuilder(newCommand)
+                .directory(workingDir);
+        if (environment != null) {
+            pb.environment().putAll(environment);
+        }
+        return pb.start();
+    }
+
+    protected Process createProcess(List<String> cmdArray, Map<String, String> environment, File workingDir)
+            throws IOException {
+        if (!suppressSpecialWindowsBehaviour && isWindows()) {
+            return createWinProcess(cmdArray, environment, workingDir);
+        } else {
+            ProcessBuilder builder = new ProcessBuilder(cmdArray).directory(workingDir);
+            if (environment != null) {
+                builder.environment().putAll(environment);
+            }
+            return builder.start();
+        }
+    }
+
+    /**
+     * 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() {
+                @Override
+                public String getName() {
+                    return "StdInHandler " + process;
+                }
+
+                protected void doTask() {
+                    handler.provideInput(process.getOutputStream());
+                }
+            };
+        }
+
+        errorPump = new LatchedRunnable() {
+            @Override
+            public String getName() {
+                return "StdErrHandler " + process;
+            }
+
+            protected void doTask() {
+                try {
+                    handler.processError(process.getErrorStream());
+                } catch (Throwable e) {
+                    if (!isCancelled()) {
+                        processException = new ProcessException(e);
+                    }
+                }
+            }
+        };
+
+        outputPump = new LatchedRunnable() {
+            @Override
+            public String getName() {
+                return "StdOutHandler " + process;
+            }
+
+            protected void doTask() {
+                try {
+                    handler.processOutput(process.getInputStream());
+                } catch (Throwable e) {
+                    if (!isCancelled()) {
+                        processException = new ProcessException(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());
+            } finally {
+                int exitCode = 0;
+                if (!canceled) {
+                    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
+                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, 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 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) {
+                    interrupted = true;
+                    break;
+                } catch (IllegalThreadStateException e) {
+                    // ignore and try in the next loop
+                }
+            }
+        } finally {
+            process.destroy();
+        }
+
+        // make sure pumps are done
+        if (arePumpsRunning()) {
+            cancel();
+        }
+
+        if (interrupted) {
+            processException = new ProcessException("Process interrupted");
+        } else if (processIncomplete) {
+            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 (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");
+                }
+            }
+        }
+    }
+}

File src/main/java/com/atlassian/utils/process/LatchedRunnable.java

     public boolean isCancelled() {
         return cancelled;
     }
+
+    public String getName() {
+        return "LatchedRunnable";
+    }
 }

File src/main/java/com/atlassian/utils/process/StringInputHandler.java

 package com.atlassian.utils.process;
 
+import java.io.InterruptedIOException;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.IOException;
 
 /**
- * An input handler which provides the input from the given string.
+ * An {@link InputHandler} which provides the input from the given string.
  */
 public class StringInputHandler implements InputHandler {
     private String encoding;
         this.content = content;
     }
 
+    public void complete() {
+        // nothing to do here
+    }
+
     public void process(OutputStream input) {
         OutputStreamWriter writer = null;
         try {
                 writer = new OutputStreamWriter(input, encoding);
             }
             writer.write(content);
+        } catch (InterruptedIOException e) {
+            // This means the process was asked to stop which can be normal so we just finish
         } catch (IOException e) {
             throw new RuntimeException(e);
         } finally {
         }
     }
 
-    public void complete() {
-        // nothing to do here
-    }
-
     public void setWatchdog(Watchdog watchdog) {
     }
 }

File src/main/java/com/atlassian/utils/process/StringOutputHandler.java

 import java.io.StringWriter;
 
 /**
- * An Output Handler which captures the output stream to a string
+ * An {@link OutputHandler} which captures the output stream as a string.
  */
 public class StringOutputHandler extends BaseOutputHandler {
     private final String encoding;
-    private final StringWriter sw = new StringWriter();
+    private final StringWriter writer;
 
     public StringOutputHandler() {
         this(null);
 
     public StringOutputHandler(String encoding) {
         this.encoding = encoding;
+        
+        writer = new StringWriter();
+    }
+
+    @Override
+    public void complete() {
+        IOUtils.closeQuietly(writer);
+    }
+
+    public String getOutput() {
+        return writer.toString();
     }
 
     public void process(InputStream output) throws ProcessException {
-        InputStreamReader reader;
+        InputStreamReader reader = null;
         try {
             if (encoding == null) {
                 reader = new InputStreamReader(output);
             int num;
             while ((num = reader.read(buffer)) != -1) {
                 resetWatchdog();
-                sw.write(buffer, 0, num);
+                writer.write(buffer, 0, num);
             }
         } catch (InterruptedIOException e) {
             // This means the process was asked to stop which can be normal so we just finish
         } catch (IOException e) {
             throw new ProcessException(e);
+        } finally {
+            IOUtils.closeQuietly(reader);
         }
     }
-
-    public String getOutput() {
-        return sw.toString();
-    }
-
-    @Override
-    public void complete() {
-        IOUtils.closeQuietly(sw);
-    }
 }

File src/test/java/com/atlassian/utils/process/CallEcho.java

 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Properties;
 
 /**
  * Utility class that calls 'echo'. This is implemented as a separate java application (with a main) to allow it to be called
- * with different -Dfile.encoding settings. See {@link ExternalProcessTest} for more info. 
+ * with different -Dfile.encoding settings. See {@link ProcessBuilderEncodingTest} for more info.
  */
 public class CallEcho {
 
     static public String byteToHex(byte b) {
         // Returns hex String representation of byte b
-        char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-        char[] array = { hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f] };
+        char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+        char[] array = {hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f]};
         return new String(array);
     }
 
 
     /**
      * Helper method to print out a string as a list of bytes (integers)
-     * 
-     * @param value
-     * @return
+     *
+     * @param value -
+     * @return -
+     * @throws UnsupportedEncodingException -
      */
     private static String getBytes(String value) throws UnsupportedEncodingException {
         if (value == null) {
         return builder.toString();
     }
 
-    /**
-     * @param args
-     */
     public static void main(String[] args) throws IOException, InterruptedException {
         Properties testStrings = new Properties();
         testStrings.load(CallEcho.class.getResourceAsStream("/echostrings.properties"));
             // we're calling yet another java process here because the echo application on windows is doing character conversions as well
             // using a java process at least gives us control over the encodings it uses. Java will use the default encoding (-Dfile.encoding) 
             // to convert command line arguments into Java Strings 
-            List<String> echoCmd = Arrays.asList("java", "-Dfile.encoding=" + System.getProperty("file.encoding"), "com.atlassian.utils.process.Echo", echo);
-            Map<String, String> echoEnv = new HashMap<String, String>()
-            {{
-                put("CLASSPATH", System.getProperty("java.class.path"));
-            }};
+            String encoding = System.getProperty("file.encoding");
+            List<String> echoCmd = Arrays.asList("java", "-Dfile.encoding=" + encoding, "com.atlassian.utils.process.Echo", echo);
 
             // the command line arguments will be converted using the default encoding (file.encoding). 
             // The lineoutputhandler needs to use the same encoding to process the results. Otherwise, encoding errors will occur.
-            ExternalProcess process = new ExternalProcessBuilder().command(echoCmd).timeout(2000)
-                    .handlers(new LineOutputHandler(System.getProperty("file.encoding")) {
-                        @Override
-                        protected void processLine(int lineNum, String line) {
-                            result[0] = line;
-                        }
-                    }, new LineOutputHandler() {
-                        @Override
-                        protected void processLine(int lineNum, String line) {
-                            System.err.println(line);
-                        }
-                    })
+            ExternalProcess process = new ExternalProcessBuilder()
+                    .command(echoCmd)
+                    .timeout(2000)
+                    .handlers(
+                            new LineOutputHandler(encoding) {
+                                @Override
+                                protected void processLine(int lineNum, String line) {
+                                    result[0] = line;
+                                }
+                            },
+                            new LineOutputHandler() {
+                                @Override
+                                protected void processLine(int lineNum, String line) {
+                                    System.err.println(line);
+                                }
+                            }
+                    )
+                    .env("CLASSPATH", System.getProperty("java.class.path"))
                     .useWindowsEncodingWorkaround()
                     .build();
-            process.setEnvironment(echoEnv);
             process.execute();
 
             // output the expected and actual strings as a list of bytes to
-            // prevent encodings to mess up the results
+            // prevent encodings from messing up the results
             System.out.println(getBytes(echo));
             System.out.println(getBytes(result[0]));
         }
-        ExternalProcess.shutdown();
+        ExternalProcessImpl.shutdown();
     }
 }

File src/test/java/com/atlassian/utils/process/ExternalProcessTest.java

-package com.atlassian.utils.process;
-
-import static org.junit.Assert.*;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import org.junit.Test;
-
-public class ExternalProcessTest {
-    private static class EchoResult {
-        public String input;
-        public String output;
-    }
-    
-    /*
-     * Starts the CallEcho as an separate process. This is necessary because the jvm uses the default encoding (-Dfile.encoding) for encoding commandline
-     * arguments that are provided to external processes. This test tests whether encodings that should be compatible are actually compatible. This is
-     * needed for http://jira.atlassian.com/browse/CRUC-4793 which provides a workaround for a JVM bug on windows.
-     */
-    protected EchoResult spawnEcho(String encoding, String... testStrings) throws Exception {
-        List<String> cmd = new ArrayList<String>(Arrays.asList("java", "-Dfile.encoding=" + encoding, CallEcho.class.getName()));
-        cmd.addAll(Arrays.asList(testStrings));
-        
-        ProcessBuilder processBuilder = new ProcessBuilder(cmd);
-        processBuilder.environment().put("CLASSPATH", System.getProperty("java.class.path"));
-        Process process = processBuilder.start();
-        
-        BufferedReader reader = null;
-        try {
-            reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
-            EchoResult result = new EchoResult();
-            result.input = reader.readLine();
-            result.output = reader.readLine();
-            
-            return result;
-        } finally {
-            reader.close();
-        }
-    }
-    
-    protected void assertNotEquals(String message, String value1, String value2) {
-        assertFalse(message + "; result was : " + value1, (value1 == null && value2 == null) || (value1 != null && value1.equals(value2)));
-    }
-    
-    protected boolean isWindows() {
-        return System.getProperty("os.name").toLowerCase().contains("windows");
-    }
-    
-    @Test
-    public void testIncompatibleCommandLineArgumentsASCII() throws Exception {
-        EchoResult result = spawnEcho("ASCII", "iso-8859-1");
-        assertNotEquals("ASCII - incompatibility expected", result.input, result.output);
-
-        result = spawnEcho("ASCII", "unicode-chinese");
-        assertNotEquals("ASCII - incompatibility expected", result.input, result.output);
-}
-
-    /**
-     * This test will only succeed on windows if the ANSI code page that is defined is compatible with iso-8859-1 (e.g. cp1252)
-     * @see ExternalProcess#createWinProcess for more details
-     */
-    @Test
-    public void testCommandLineArgumentsUTF8() throws Exception {
-        EchoResult result = spawnEcho("UTF-8",  "iso-8859-1");
-        assertEquals("UTF-8 - incompatibility detected:", result.input, result.output);
-    }
-    
-    /**
-     * This test is expected to fail on windows (assuming the ANSI CP is cp1252) because on windows that code page
-     * is used to encode the commandline arguments.
-     */
-    @Test
-    public void testCommandLineArgumentsUTF8IncompatibleWithCp1252() throws Exception {
-        EchoResult result = spawnEcho("UTF-8",  "unicode-chinese");
-        
-        if (isWindows()) {
-        	assertNotEquals("UTF-8 - incompatibility expected", result.input, result.output);
-        } else {
-        	assertEquals("UTF-8 - incompatibility detected", result.input, result.output);
-        }
-    }
-    
-    @Test
-    public void testCommandLineArgumentsWin1252() throws Exception {
-        if (Charset.isSupported("windows-1252")) {
-            EchoResult result = spawnEcho("windows-1252", "iso-8859-1");
-            assertEquals("Windows cp 1252 -  incompatibility detected:", result.input, result.output);
-        }
-    }
-
-    @Test
-    public void testCommandLineArgumentsWin1252Iso88591Incompatibilities() throws Exception {
-        if (Charset.isSupported("windows-1252") && isWindows()) {
-            // other platforms claim to support windows-1252, but it looks like only iso-8859-1 is supported.
-            EchoResult result = spawnEcho("windows-1252", "win-1252-not-in-iso-8859-1");
-            assertEquals("Windows cp 1252 -  incompatibility detected:", result.input, result.output);
-        }
-    }
-
-    @Test
-    public void testCommandLineArgumentsWin437() throws Exception {
-        if (Charset.isSupported("windows-437")) {
-            EchoResult result = spawnEcho("windows-437", "windows-437");
-            assertEquals("Windows cp 437 - incompatibility detected:", result.input, result.output);
-        }
-    }
-    
-    @Test
-    public void testCommandLineArgumentsIso88591() throws Exception {
-        EchoResult result = spawnEcho("iso-8859-1", "iso-8859-1");
-        assertEquals("ISO-8859-1 - incompatibility detected:", result.input, result.output);
-    }
-
-    @Test
-    public void testNativeWindowsKilling()
-    {
-        ExternalProcess process = new ExternalProcessBuilder().command(Arrays.asList("pause")).build();
-        process.start();
-        process.cancel();
-    }
-}

File src/test/java/com/atlassian/utils/process/ProcessBuilderEncodingTest.java

+package com.atlassian.utils.process;
+
+import static org.junit.Assert.*;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+