org.gytheio.util.exec.RuntimeExec.java Source code

Java tutorial

Introduction

Here is the source code for org.gytheio.util.exec.RuntimeExec.java

Source

/*
 * Copyright (C) 2005-2014 Alfresco Software Limited.
 *
 * This file is part of Gytheio
 *
 * Gytheio is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Gytheio is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Gytheio. If not, see <http://www.gnu.org/licenses/>.
 */
package org.gytheio.util.exec;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gytheio.error.GytheioRuntimeException;

/**
 * This acts as a session similar to the <code>java.lang.Process</code>, but
 * logs the system standard and error streams.
 * <p>
 * The bean can be configured to execute a command directly, or be given a map
 * of commands keyed by the <i>os.name</i> Java system property.  In this map,
 * the default key that is used when no match is found is the
 * <b>{@link #KEY_OS_DEFAULT *}</b> key.
 * <p>
 * Use the {@link #setProcessDirectory(String) processDirectory} property to change the default location
 * from which the command executes.  The process's environment can be configured using the
 * {@link #setProcessProperties(Map) processProperties} property.
 * <p>
 * Commands may use placeholders, e.g.
 * <pre><code>
 *    find
 *    -name
 *    ${filename}
 * </code></pre>
 * The <b>filename</b> property will be substituted for any supplied value prior to
 * each execution of the command.  Currently, no checks are made to get or check the
 * properties contained within the command string.  It is up to the client code to
 * dynamically extract the properties required if the required properties are not
 * known up front.
 * <p>
 * Sometimes, a variable may contain several arguments.  .  In this case, the arguments
 * need to be tokenized using a standard <tt>StringTokenizer</tt>.  To force tokenization
 * of a value, use:
 * <pre><code>
 *    SPLIT:${userArgs}
 * </code></pre>
 * You should not use this just to split up arguments that are known to require tokenization
 * up front.  The <b>SPLIT:</b> directive works for the entire argument and will not do anything
 * if it is not at the beginning of the argument.  Do not use <b>SPLIT:</b> to break up arguments
 * that are fixed, so avoid doing this:
 * <pre><code>
 *    SPLIT:ls -lih
 * </code></pre>
 * Instead, break the command up explicitly:
 * <pre><code>
 *    ls
 *    -lih
 * </code></pre>
 * 
 * Tokenization of quoted parameter values is handled by {@link ExecParameterTokenizer}, which
 * describes the support in more detail.
 * 
 * @author Derek Hulley
 */
public class RuntimeExec {
    /** the key to use when specifying a command for any other OS: <b>*</b> */
    public static final String KEY_OS_DEFAULT = "*";

    private static final String KEY_OS_NAME = "os.name";
    private static final int BUFFER_SIZE = 1024;
    private static final String VAR_OPEN = "${";
    private static final String VAR_CLOSE = "}";
    private static final String DIRECTIVE_SPLIT = "SPLIT:";

    private static Log logger = LogFactory.getLog(RuntimeExec.class);
    private static Log transformerDebugLogger = LogFactory.getLog("org.gytheio.content.transform.TransformerDebug");

    private String[] command;
    private Charset charset;
    private boolean waitForCompletion;
    private Map<String, String> defaultProperties;
    private String[] processProperties;
    private File processDirectory;
    private Set<Integer> errCodes;
    private InputStreamReaderThreadFactory defaultInputStreamReaderThreadFactory;
    private Timer timer = new Timer(true);

    /**
     * Default constructor.  Initialize this instance by setting individual properties.
     */
    public RuntimeExec() {
        this.charset = Charset.defaultCharset();
        this.waitForCompletion = true;
        defaultProperties = Collections.emptyMap();
        processProperties = null;
        processDirectory = null;

        // set default error codes
        this.errCodes = new HashSet<Integer>(2);
        errCodes.add(1);
        errCodes.add(2);

        this.defaultInputStreamReaderThreadFactory = new InputStreamReaderThreadFactory();
    }

    public String toString() {

        StringBuffer sb = new StringBuffer(256);
        sb.append("RuntimeExec:\n").append("   command:    ");
        if (command == null) {
            // command is 'null', so there's nothing to toString
            sb.append("'null'\n");
        } else {
            for (String cmdStr : command) {
                sb.append(cmdStr).append(" ");
            }
            sb.append("\n");
        }
        sb.append("   env props:  ").append(Arrays.toString(processProperties)).append("\n")
                .append("   dir:        ").append(processDirectory).append("\n").append("   os:         ")
                .append(System.getProperty(KEY_OS_NAME)).append("\n");
        return sb.toString();
    }

    /**
     * Set the command to execute regardless of operating system
     * 
     * @param command       an array of strings representing the command (first entry) and arguments
     * 
     * @since 3.0
     */
    public void setCommand(String[] command) {
        this.command = command;
    }

    /**
     * Sets the assumed charset of OUT and ERR streams generated by the executed command.
     * This defaults to the system default charset: {@link Charset#defaultCharset()}.
     * 
     * @param charsetCode                           a supported character set code
     * @throws UnsupportedCharsetException          if the characterset code is not recognised by Java
     */
    public void setCharset(String charsetCode) {
        this.charset = Charset.forName(charsetCode);
    }

    /**
     * Set whether to wait for completion of the command or not.  If there is no wait for completion,
     * then the return value of <i>out</i> and <i>err</i> buffers cannot be relied upon as the
     * command may still be in progress.  Failure is therefore not possible unless the calling thread
     * waits for execution.
     * 
     * @param waitForCompletion     <tt>true</tt> (default) is to wait for the command to exit,
     *                              or <tt>false</tt> to just return an exit code of 0 and whatever
     *                              output is available at that point.
     * 
     * @since 2.1
     */
    public void setWaitForCompletion(boolean waitForCompletion) {
        this.waitForCompletion = waitForCompletion;
    }

    /**
     * Supply a choice of commands to execute based on a mapping from the <i>os.name</i> system
     * property to the command to execute.  The {@link #KEY_OS_DEFAULT *} key can be used
     * to get a command where there is not direct match to the operating system key.
     * <p>
     * Each command is an array of strings, the first of which represents the command and all subsequent
     * entries in the array represent the arguments.  All elements of the array will be checked for
     * the presence of any substitution parameters (e.g. '{dir}').  The parameters can be set using the
     * {@link #setDefaultProperties(Map) defaults} or by passing the substitution values into the
     * {@link #execute(Map)} command.
     * <p>
     * If parameters passed may be multiple arguments, or if the values provided in the map are themselves
     * collections of arguments (not recommended), then prefix the value with <b>SPLIT:</b> to ensure that
     * the value is tokenized before being passed to the command.  Any values that are not split, will be
     * passed to the command as single arguments.  For example:<br>
     * '<b>SPLIT: dir . ..</b>' becomes '<b>dir</b>', '<b>.</b>' and '<b>..</b>'.<br>
     * '<b>SPLIT: dir ${path}</b>' (if path is '<b>. ..</b>') becomes '<b>dir</b>', '<b>.</b>' and '<b>..</b>'.<br>
     * The splitting occurs post-subtitution.  Where the arguments are known, it is advisable to avoid
     * <b>SPLIT:</b>.
     * 
     * @param commandsByOS          a map of command string arrays, keyed by operating system names
     * 
     * @see #setDefaultProperties(Map)
     * 
     * @since 3.0
     */
    public void setCommandsAndArguments(Map<String, String[]> commandsByOS) {
        // get the current OS
        String serverOs = System.getProperty(KEY_OS_NAME);
        // attempt to find a match
        String[] command = commandsByOS.get(serverOs);
        if (command == null) {
            // go through the commands keys, looking for one that matches by regular expression matching
            for (String osName : commandsByOS.keySet()) {
                // Ignore * options.  It is dealt with later.
                if (osName.equals(KEY_OS_DEFAULT)) {
                    continue;
                }
                // Do regex match
                if (serverOs.matches(osName)) {
                    command = commandsByOS.get(osName);
                    break;
                }
            }
            // if there is still no command, then check for the wildcard
            if (command == null) {
                command = commandsByOS.get(KEY_OS_DEFAULT);
            }
        }
        // check
        if (command == null) {
            throw new GytheioRuntimeException("No command found for OS " + serverOs + " or '" + KEY_OS_DEFAULT
                    + "': \n" + "   commands: " + commandsByOS);
        }
        this.command = command;
    }

    /**
     * Supply a choice of commands to execute based on a mapping from the <i>os.name</i> system
     * property to the command to execute.  The {@link #KEY_OS_DEFAULT *} key can be used
     * to get a command where there is not direct match to the operating system key.
     * 
     * @param commandsByOS a map of command string keyed by operating system names
     * 
     * @deprecated          Use {@link #setCommandsAndArguments(Map)}
     */
    public void setCommandMap(Map<String, String> commandsByOS) {
        // This is deprecated, so issue a warning
        logger.warn("The bean RuntimeExec property 'commandMap' has been deprecated;"
                + " use 'commandsAndArguments' instead.  See https://issues.alfresco.com/jira/browse/ETHREEOH-579.");
        Map<String, String[]> fixed = new LinkedHashMap<String, String[]>(7);
        for (Map.Entry<String, String> entry : commandsByOS.entrySet()) {
            String os = entry.getKey();
            String unparsedCmd = entry.getValue();
            StringTokenizer tokenizer = new StringTokenizer(unparsedCmd);
            String[] cmd = new String[tokenizer.countTokens()];
            for (int i = 0; i < cmd.length; i++) {
                cmd[i] = tokenizer.nextToken();
            }
            fixed.put(os, cmd);
        }
        setCommandsAndArguments(fixed);
    }

    /**
     * Set the default command-line properties to use when executing the command.
     * These are properties that substitute variables defined in the command string itself.
     * Properties supplied during execution will overwrite the default properties.
     * <p>
     * <code>null</code> properties will be treated as an empty string for substitution
     * purposes.
     * 
     * @param defaultProperties property values
     */
    public void setDefaultProperties(Map<String, String> defaultProperties) {
        this.defaultProperties = defaultProperties;
    }

    /**
     * Set additional runtime properties (environment properties) that will used
     * by the executing process.
     * <p>
     * Any keys or properties that start and end with <b>${...}</b> will be removed on the assumption
     * that these are unset properties.  <tt>null</tt> values are translated to empty strings.
     * All keys and values are trimmed of leading and trailing whitespace.
     * 
     * @param processProperties     Runtime process properties
     * 
     * @see Runtime#exec(String, String[], java.io.File)
     */
    public void setProcessProperties(Map<String, String> processProperties) {
        ArrayList<String> processPropList = new ArrayList<String>(processProperties.size());
        boolean hasPath = false;
        String systemPath = System.getenv("PATH");
        for (Map.Entry<String, String> entry : processProperties.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (key == null) {
                continue;
            }
            if (value == null) {
                value = "";
            }
            key = key.trim();
            value = value.trim();
            if (key.startsWith(VAR_OPEN) && key.endsWith(VAR_CLOSE)) {
                continue;
            }
            if (value.startsWith(VAR_OPEN) && value.endsWith(VAR_CLOSE)) {
                continue;
            }
            // If a path is specified, prepend it to the existing path
            if (key.equals("PATH")) {
                if (systemPath != null && systemPath.length() > 0) {
                    processPropList.add(key + "=" + value + File.pathSeparator + systemPath);
                } else {
                    processPropList.add(key + "=" + value);
                }
                hasPath = true;
            } else {
                processPropList.add(key + "=" + value);
            }
        }
        // If a path was not specified, inherit the current one
        if (!hasPath && systemPath != null && systemPath.length() > 0) {
            processPropList.add("PATH=" + systemPath);
        }
        this.processProperties = processPropList.toArray(new String[processPropList.size()]);
    }

    /**
     * Adds a property to existed processProperties. 
     * Property should not be null or empty.
     * If property with the same value already exists then no change is made.
     * If property exists with a different value then old value is replaced with the new one. 
     * @param name - property name
     * @param value - property value 
     */
    public void setProcessProperty(String name, String value) {
        boolean set = false;

        if (name == null || value == null)
            return;

        name = name.trim();
        value = value.trim();

        if (name.isEmpty() || value.isEmpty())
            return;

        String property = name + "=" + value;

        for (String prop : this.processProperties) {
            if (prop.equals(property)) {
                set = true;
                break;
            }

            if (prop.startsWith(name)) {
                String oldValue = prop.split("=")[1];
                prop.replace(oldValue, value);
                set = true;
            }
        }

        if (!set) {
            String[] existedProperties = this.processProperties;
            int epl = existedProperties.length;
            String[] newProperties = Arrays.copyOf(existedProperties, epl + 1);
            newProperties[epl] = property;
            this.processProperties = newProperties;
            set = true;
        }
    }

    /**
     * Set the runtime location from which the command is executed.
     * <p>
     * If the value is an unsubsititued variable (<b>${...}</b>) then it is ignored.
     * If the location is not visible at the time of setting, a warning is issued only.
     * 
     * @param processDirectory          the runtime location from which to execute the command
     */
    public void setProcessDirectory(String processDirectory) {
        if (processDirectory.startsWith(VAR_OPEN) && processDirectory.endsWith(VAR_CLOSE)) {
            this.processDirectory = null;
        } else {
            this.processDirectory = new File(processDirectory);
            if (!this.processDirectory.exists()) {
                logger.warn(
                        "The runtime process directory is not visible when setting property 'processDirectory': \n"
                                + this);
            }
        }
    }

    /**
     * A comma or space separated list of values that, if returned by the executed command,
     * indicate an error value.  This defaults to <b>"1, 2"</b>.
     * 
     * @param errCodesStr the error codes for the execution
     */
    public void setErrorCodes(String errCodesStr) {
        errCodes.clear();
        StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,");
        while (tokenizer.hasMoreElements()) {
            String errCodeStr = tokenizer.nextToken();
            // attempt to convert it to an integer
            try {
                int errCode = Integer.parseInt(errCodeStr);
                this.errCodes.add(errCode);
            } catch (NumberFormatException e) {
                throw new GytheioRuntimeException(
                        "Property 'errorCodes' must be comma-separated list of integers: " + errCodesStr);
            }
        }
    }

    public void setDefaultInputStreamReaderThreadFactory(
            InputStreamReaderThreadFactory defaultInputStreamReaderThreadFactory) {
        this.defaultInputStreamReaderThreadFactory = defaultInputStreamReaderThreadFactory;
    }

    /**
     * Executes the command using the default properties
     * 
     * @see #execute(Map)
     */
    public ExecutionResult execute() {
        return execute(defaultProperties);
    }

    /**
     * Executes the statement that this instance was constructed with.
     * 
     * @param properties the properties that the command might be executed with.
     * <code>null</code> properties will be treated as an empty string for substitution
     * purposes.
     * 
     * @return Returns the full execution results
     */
    public ExecutionResult execute(Map<String, String> properties) {
        return execute(properties, -1);
    }

    /**
     * Executes the statement that this instance was constructed with an optional
     * timeout after which the command is asked to 
     * 
     * @param properties the properties that the command might be executed with.
     * <code>null</code> properties will be treated as an empty string for substitution
     * purposes.
     * @param timeoutMs a timeout after which {@link Process#destroy()} is called.
     *        ignored if less than or equal to zero. Note this method does not guarantee
     *        to terminate the process (it is not a kill -9).
     * 
     * @return Returns the full execution results
     */
    public ExecutionResult execute(Map<String, String> properties, final long timeoutMs) {
        return execute(properties, defaultInputStreamReaderThreadFactory, defaultInputStreamReaderThreadFactory,
                timeoutMs);
    }

    /**
     * Executes the statement that this instance was constructed with an optional
     * timeout after which the command is asked to 
     * 
     * @param properties the properties that the command might be executed with.
     * <code>null</code> properties will be treated as an empty string for substitution
     * purposes.
     * @param timeoutMs a timeout after which {@link Process#destroy()} is called.
     *        ignored if less than or equal to zero. Note this method does not guarantee
     *        to terminate the process (it is not a kill -9).
     * @param stdOutGobblerFactory the object used to create the output input stream reader
     *        If null the defaultInputStreamReaderThreadFactory will be used
     * @param stdErrGobblerFactory the object used to create the error input stream reader
     *        If null the defaultInputStreamReaderThreadFactory will be used
     * 
     * @return Returns the full execution results
     */
    public ExecutionResult execute(Map<String, String> properties,
            InputStreamReaderThreadFactory stdOutGobblerFactory,
            InputStreamReaderThreadFactory stdErrGobblerFactory, final long timeoutMs) {
        int defaultFailureExitValue = errCodes.size() > 0 ? ((Integer) errCodes.toArray()[0]) : 1;

        // check that the command has been set
        if (command == null) {
            throw new GytheioRuntimeException("Runtime command has not been set: \n" + this);
        }

        if (stdOutGobblerFactory == null) {
            stdOutGobblerFactory = defaultInputStreamReaderThreadFactory;
        }
        if (stdErrGobblerFactory == null) {
            stdErrGobblerFactory = defaultInputStreamReaderThreadFactory;
        }

        // create the properties
        Runtime runtime = Runtime.getRuntime();
        Process process = null;
        String[] commandToExecute = null;
        try {
            // execute the command with full property replacement
            commandToExecute = getCommand(properties);
            final Process thisProcess = runtime.exec(commandToExecute, processProperties, processDirectory);
            process = thisProcess;
            if (timeoutMs > 0) {
                final String[] command = commandToExecute;
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        // Only try to kill the process if it is still running
                        try {
                            thisProcess.exitValue();
                        } catch (IllegalThreadStateException stillRunning) {
                            if (transformerDebugLogger.isDebugEnabled()) {
                                transformerDebugLogger.debug("Process has taken too long (" + (timeoutMs / 1000)
                                        + " seconds). Killing process " + Arrays.deepToString(command));
                            }
                            thisProcess.destroy();
                        }
                    }
                }, timeoutMs);
            }
        } catch (IOException e) {
            // The process could not be executed here, so just drop out with an appropriate error state
            String execOut = "";
            String execErr = e.getMessage();
            int exitValue = defaultFailureExitValue;
            ExecutionResult result = new ExecutionResult(null, commandToExecute, errCodes, exitValue, execOut,
                    execErr);
            logFullEnvironmentDump(result);
            return result;
        }

        // create the stream gobblers
        InputStreamReaderThread stdOutGobbler = stdOutGobblerFactory.createInstance(process.getInputStream(),
                charset);
        InputStreamReaderThread stdErrGobbler = stdErrGobblerFactory.createInstance(process.getErrorStream(),
                charset);

        // start gobbling
        stdOutGobbler.start();
        stdErrGobbler.start();

        // wait for the process to finish
        int exitValue = 0;
        try {
            if (waitForCompletion) {
                exitValue = process.waitFor();
            }
        } catch (InterruptedException e) {
            // process was interrupted - generate an error message
            stdErrGobbler.addToBuffer(e.toString());
            exitValue = defaultFailureExitValue;
        }

        if (waitForCompletion) {
            // ensure that the stream gobblers get to finish
            stdOutGobbler.waitForCompletion();
            stdErrGobbler.waitForCompletion();
        }

        // get the stream values
        String execOut = stdOutGobbler.getBuffer();
        String execErr = stdErrGobbler.getBuffer();

        // construct the return value
        ExecutionResult result = new ExecutionResult(process, commandToExecute, errCodes, exitValue, execOut,
                execErr);

        // done
        logFullEnvironmentDump(result);
        return result;
    }

    /**
     * Dump the full environment in debug mode
     */
    private void logFullEnvironmentDump(ExecutionResult result) {
        if (logger.isTraceEnabled()) {
            StringBuilder sb = new StringBuilder();
            sb.append(result);

            // Environment variables modified by Alfresco
            if (processProperties != null && processProperties.length > 0) {
                sb.append("\n   modified environment: ");
                for (int i = 0; i < processProperties.length; i++) {
                    String property = processProperties[i];
                    sb.append("\n        ");
                    sb.append(property);
                }
            }

            // Dump the full environment
            sb.append("\n   existing environment: ");
            Map<String, String> envVariables = System.getenv();
            for (Map.Entry<String, String> entry : envVariables.entrySet()) {
                String name = entry.getKey();
                String value = entry.getValue();
                sb.append("\n        ");
                sb.append(name + "=" + value);
            }

            logger.trace(sb);
        } else if (logger.isDebugEnabled()) {
            logger.debug(result);
        }

        // close output stream (connected to input stream of native subprocess) 
    }

    /**
     * @return Returns the command that will be executed if no additional properties
     *      were to be supplied
     */
    public String[] getCommand() {
        return getCommand(defaultProperties);
    }

    /**
     * Get the command that will be executed post substitution.
     * <p>
     * <code>null</code> properties will be treated as an empty string for substitution
     * purposes.
     * 
     * @param properties the properties that the command might be executed with
     * @return Returns the command that will be executed should the additional properties
     *      be supplied
     */
    public String[] getCommand(Map<String, String> properties) {
        Map<String, String> execProperties = null;
        if (properties == defaultProperties) {
            // we are just using the default properties
            execProperties = defaultProperties;
        } else {
            execProperties = new HashMap<String, String>(defaultProperties);
            // overlay the supplied properties
            execProperties.putAll(properties);
        }
        // Perform the substitution for each element of the command
        ArrayList<String> adjustedCommandElements = new ArrayList<String>(20);
        for (int i = 0; i < command.length; i++) {
            StringBuilder sb = new StringBuilder(command[i]);
            for (Map.Entry<String, String> entry : execProperties.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                // ignore null
                if (value == null) {
                    value = "";
                }
                // progressively replace the property in the command
                key = (VAR_OPEN + key + VAR_CLOSE);
                int index = sb.indexOf(key);
                while (index > -1) {
                    // replace
                    sb.replace(index, index + key.length(), value);
                    // get the next one
                    index = sb.indexOf(key, index + 1);
                }
            }
            String adjustedValue = sb.toString();
            // Now SPLIT: it
            if (adjustedValue.startsWith(DIRECTIVE_SPLIT)) {
                String unsplitAdjustedValue = sb.substring(DIRECTIVE_SPLIT.length());

                // There may be quoted arguments here (see ALF-7482)
                ExecParameterTokenizer quoteAwareTokenizer = new ExecParameterTokenizer(unsplitAdjustedValue);
                List<String> tokens = quoteAwareTokenizer.getAllTokens();
                adjustedCommandElements.addAll(tokens);
            } else {
                adjustedCommandElements.add(adjustedValue);
            }
        }
        // done
        return adjustedCommandElements.toArray(new String[adjustedCommandElements.size()]);
    }

    /**
     * Object to carry the results of an execution to the caller.
     * 
     * @author Derek Hulley
     */
    public static class ExecutionResult {
        private final Process process;
        private final String[] command;
        private final Set<Integer> errCodes;
        private final int exitValue;
        private final String stdOut;
        private final String stdErr;

        /**
         * 
         * @param process           the process attached to Java - <tt>null</tt> is allowed
         */
        private ExecutionResult(final Process process, final String[] command, final Set<Integer> errCodes,
                final int exitValue, final String stdOut, final String stdErr) {
            this.process = process;
            this.command = command;
            this.errCodes = errCodes;
            this.exitValue = exitValue;
            this.stdOut = stdOut;
            this.stdErr = stdErr;
        }

        @Override
        public String toString() {
            String out = stdOut.length() > 250 ? stdOut.substring(0, 250) : stdOut;
            String err = stdErr.length() > 250 ? stdErr.substring(0, 250) : stdErr;

            StringBuilder sb = new StringBuilder(128);
            sb.append("Execution result: \n").append("   os:         ").append(System.getProperty(KEY_OS_NAME))
                    .append("\n").append("   command:    ");
            appendCommand(sb, command).append("\n").append("   succeeded:  ").append(getSuccess()).append("\n")
                    .append("   exit code:  ").append(exitValue).append("\n").append("   out:        ").append(out)
                    .append("\n").append("   err:        ").append(err);
            return sb.toString();
        }

        /**
         * Appends the command in a form that make running from the command line simpler.
         * It is not a real attempt at making a command given all the operating system 
         * and shell options, but makes copy, paste and edit a bit simpler.
         */
        private StringBuilder appendCommand(StringBuilder sb, String[] command) {
            boolean arg = false;
            for (String element : command) {
                if (element == null) {
                    continue;
                }

                if (arg) {
                    sb.append(' ');
                } else {
                    arg = true;
                }

                boolean escape = element.indexOf(' ') != -1 || element.indexOf('>') != -1;
                if (escape) {
                    sb.append("\"");
                }
                sb.append(element);
                if (escape) {
                    sb.append("\"");
                }
            }
            return sb;
        }

        /**
         * A helper method to force a kill of the process that generated this result.  This is
         * useful in cases where the process started is not expected to exit, or doesn't exit
         * quickly.  If the {@linkplain RuntimeExec#setWaitForCompletion(boolean) "wait for completion"}
         * flag is <tt>false</tt> then the process may still be running when this result is returned.
         * 
         * @return
         *      <tt>true</tt> if the process was killed, otherwise <tt>false</tt>
         */
        public boolean killProcess() {
            if (process == null) {
                return true;
            }
            try {
                process.destroy();
                return true;
            } catch (Throwable e) {
                logger.warn(e.getMessage());
                return false;
            }
        }

        /**
         * @param exitValue the command exit value
         * @return Returns true if the code is a listed failure code
         * 
         * @see #setErrorCodes(String)
         */
        private boolean isFailureCode(int exitValue) {
            return errCodes.contains((Integer) exitValue);
        }

        /**
         * @return Returns true if the command was deemed to be successful according to the
         *      failure codes returned by the execution.
         */
        public boolean getSuccess() {
            return !isFailureCode(exitValue);
        }

        public int getExitValue() {
            return exitValue;
        }

        public String getStdOut() {
            return stdOut;
        }

        public String getStdErr() {
            return stdErr;
        }
    }

    /**
     * Class for instantiating InputStreamReaderThreads
     */
    public static class InputStreamReaderThreadFactory {
        public InputStreamReaderThread createInstance(InputStream is, Charset charset) {
            return new InputStreamReaderThread(is, charset);
        }
    }

    /**
     * Gobbles an <code>InputStream</code> and writes it into a
     * <code>StringBuffer</code>
     * <p>
     * The reading of the input stream is buffered.
     */
    public static class InputStreamReaderThread extends Thread {
        private final InputStream is;
        protected final Charset charset;
        private final StringBuffer buffer; // we require the synchronization
        private boolean completed;

        /**
         * @param is an input stream to read - it will be wrapped in a buffer
         *        for reading
         */
        public InputStreamReaderThread(InputStream is, Charset charset) {
            super();
            setDaemon(true); // must not hold up the VM if it is terminating
            this.is = is;
            this.charset = charset;
            this.buffer = new StringBuffer(BUFFER_SIZE);
            this.completed = false;
        }

        protected void processBytes(byte[] bytes, int count) throws UnsupportedEncodingException {
            String toWrite = new String(bytes, 0, count, charset.name());
            buffer.append(toWrite);
        }

        public synchronized void run() {
            completed = false;

            byte[] bytes = new byte[BUFFER_SIZE];
            InputStream tempIs = null;
            try {
                tempIs = new BufferedInputStream(is, BUFFER_SIZE);
                int count = -2;
                while (count != -1) {
                    // do we have something previously read?
                    if (count > 0) {
                        processBytes(bytes, count);
                    }
                    // read the next set of bytes
                    count = tempIs.read(bytes);
                }
                // done
            } catch (IOException e) {
                throw new GytheioRuntimeException("Unable to read stream", e);
            } finally {
                // close the input stream
                if (tempIs != null) {
                    try {
                        tempIs.close();
                    } catch (Exception e) {
                    }
                }
                // The thread has finished consuming the stream
                completed = true;
                // Notify waiters
                this.notifyAll(); // Note: Method is synchronized
            }
        }

        /**
         * Waits for the run to complete.
         * <p>
         * <b>Remember to <code>start</code> the thread first
         */
        public synchronized void waitForCompletion() {
            while (!completed) {
                try {
                    // release our lock and wait a bit
                    this.wait(1000L); // 200 ms
                } catch (InterruptedException e) {
                }
            }
        }

        /**
         * @param msg the message to add to the buffer
         */
        public void addToBuffer(String msg) {
            buffer.append(msg);
        }

        public boolean isComplete() {
            return completed;
        }

        /**
         * @return Returns the current state of the buffer
         */
        public String getBuffer() {
            return buffer.toString();
        }
    }
}