org.fusesource.meshkeeper.distribution.provisioner.embedded.Execute.java Source code

Java tutorial

Introduction

Here is the source code for org.fusesource.meshkeeper.distribution.provisioner.embedded.Execute.java

Source

/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.fusesource.meshkeeper.distribution.provisioner.embedded;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.fusesource.meshkeeper.distribution.provisioner.ProvisionerException;
import org.fusesource.meshkeeper.util.internal.FileSupport;
import org.fusesource.meshkeeper.util.internal.Os;

/**
 * Runs an external program.
 *
 * This is a modified version copied from ant to assist in spawning processes
 * that can outlive the launching jvm.
 *
 */
public class Execute {
    private static Log LOG = LogFactory.getLog(Execute.class);

    private static final int ONE_SECOND = 1000;

    /** Invalid exit code.
     * set to {@link Integer#MAX_VALUE}
     */
    public static final int INVALID = Integer.MAX_VALUE;

    private String[] cmdl = null;
    private String[] env = null;
    private int exitValue = INVALID;
    ExecuteStreamHandler streamHandler;
    //private ExecuteWatchdog watchdog;
    private File workingDirectory = null;
    //private Project project = null;
    private boolean newEnvironment = false;

    /** Controls whether the VM is used to launch commands, where possible. */
    private boolean useVMLauncher = true;

    private static String antWorkingDirectory = System.getProperty("user.dir");
    private static CommandLauncher vmLauncher = null;
    private static CommandLauncher shellLauncher = null;
    private static Vector<String> procEnvironment = null;

    /** Used to destroy processes when the VM exits. */
    //private static ProcessDestroyer processDestroyer = new ProcessDestroyer();

    /** Used for replacing env variables */
    private static boolean environmentCaseInSensitive = false;

    private String commandDirectory = "";

    /*
     * Builds a command launcher for the OS and JVM we are running under.
     */
    static {
        // Try using a JDK 1.3 launcher
        try {
            if (!Os.isFamily("os/2")) {
                vmLauncher = new Java13CommandLauncher();
            }
        } catch (NoSuchMethodException exc) {
            // Ignore and keep trying
        }
        if (Os.isFamily("mac") && !Os.isFamily("unix")) {
            // Mac
            shellLauncher = new MacCommandLauncher(new CommandLauncher());
        } else if (Os.isFamily("os/2")) {
            // OS/2
            shellLauncher = new OS2CommandLauncher(new CommandLauncher());
        } else if (Os.isFamily("windows")) {
            environmentCaseInSensitive = true;
            CommandLauncher baseLauncher = new CommandLauncher();

            if (!Os.isFamily("win9x")) {
                // Windows XP/2000/NT
                shellLauncher = new WinNTCommandLauncher(baseLauncher);
            } else {
                // Windows 98/95 - need to use an auxiliary script
                shellLauncher = new ScriptCommandLauncher("bin/antRun.bat", baseLauncher);
            }
        } else if (Os.isFamily("netware")) {

            CommandLauncher baseLauncher = new CommandLauncher();

            shellLauncher = new PerlScriptCommandLauncher("bin/antRun.pl", baseLauncher);
        } else if (Os.isFamily("openvms")) {
            // OpenVMS
            try {
                shellLauncher = new VmsCommandLauncher();
            } catch (NoSuchMethodException exc) {
                // Ignore and keep trying
            }
        } else {
            // Generic
            shellLauncher = new ScriptCommandLauncher("bin/antRun", new CommandLauncher());
        }
    }

    /**
     * Find the list of environment variables for this process.
     *
     * @return a vector containing the environment variables.
     * The vector elements are strings formatted like variable = value.
     */
    public static synchronized Vector<String> getProcEnvironment() {
        if (procEnvironment != null) {
            return procEnvironment;
        }
        procEnvironment = new Vector<String>();
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Execute exe = new Execute(new PumpStreamHandler(out));
            exe.setCommandline(getProcEnvCommand());
            // Make sure we do not recurse forever
            exe.setNewenvironment(true);
            int retval = exe.execute();
            if (retval != 0) {
                // Just try to use what we got
            }
            BufferedReader in = new BufferedReader(new StringReader(toString(out)));

            if (Os.isFamily("openvms")) {
                procEnvironment = addVMSLogicals(procEnvironment, in);
                return procEnvironment;
            }
            String var = null;
            String line, lineSep = System.getProperty("line.separator");
            while ((line = in.readLine()) != null) {
                if (line.indexOf('=') == -1) {
                    // Chunk part of previous env var (UNIX env vars can
                    // contain embedded new lines).
                    if (var == null) {
                        var = lineSep + line;
                    } else {
                        var += lineSep + line;
                    }
                } else {
                    // New env var...append the previous one if we have it.
                    if (var != null) {
                        procEnvironment.addElement(var);
                    }
                    var = line;
                }
            }
            // Since we "look ahead" before adding, there's one last env var.
            if (var != null) {
                procEnvironment.addElement(var);
            }
        } catch (java.io.IOException exc) {
            exc.printStackTrace();
            // Just try to see how much we got
        }
        return procEnvironment;
    }

    /**
     * This is the operation to get our environment.
     * It is a notorious troublespot pre-Java1.5, and should be approached
     * with extreme caution.
     * @return
     */
    private static String[] getProcEnvCommand() {
        if (Os.isFamily("os/2")) {
            // OS/2 - use same mechanism as Windows 2000
            return new String[] { "cmd", "/c", "set" };
        } else if (Os.isFamily("windows")) {
            // Determine if we're running under XP/2000/NT or 98/95
            if (Os.isFamily("win9x")) {
                // Windows 98/95
                return new String[] { "command.com", "/c", "set" };
            } else {
                // Windows XP/2000/NT/2003
                return new String[] { "cmd", "/c", "set" };
            }
        } else if (Os.isFamily("z/os") || Os.isFamily("unix")) {
            // On most systems one could use: /bin/sh -c env

            // Some systems have /bin/env, others /usr/bin/env, just try
            String[] cmd = new String[1];
            if (new File("/bin/env").canRead()) {
                cmd[0] = "/bin/env";
            } else if (new File("/usr/bin/env").canRead()) {
                cmd[0] = "/usr/bin/env";
            } else {
                // rely on PATH
                cmd[0] = "env";
            }
            return cmd;
        } else if (Os.isFamily("netware") || Os.isFamily("os/400")) {
            // rely on PATH
            return new String[] { "env" };
        } else if (Os.isFamily("openvms")) {
            return new String[] { "show", "logical" };
        } else {
            // MAC OS 9 and previous
            //TODO: I have no idea how to get it, someone must fix it
            return null;
        }
    }

    /**
     * ByteArrayOutputStream#toString doesn't seem to work reliably on
     * OS/390, at least not the way we use it in the execution
     * context.
     *
     * @param bos the output stream that one wants to read.
     * @return the output stream as a string, read with
     * special encodings in the case of z/os and os/400.
     *
     * @since Ant 1.5
     */
    public static String toString(ByteArrayOutputStream bos) {
        if (Os.isFamily("z/os")) {
            try {
                return bos.toString("Cp1047");
            } catch (java.io.UnsupportedEncodingException e) {
                //noop default encoding used
            }
        } else if (Os.isFamily("os/400")) {
            try {
                return bos.toString("Cp500");
            } catch (java.io.UnsupportedEncodingException e) {
                //noop default encoding used
            }
        }
        return bos.toString();
    }

    /**
     * Creates a new execute object using <code>PumpStreamHandler</code> for
     * stream handling.
     */
    public Execute(String commandDirectory) {
        this(new PumpStreamHandler());
        this.commandDirectory = commandDirectory;
    }

    /**
     * Creates a new execute object.
     *
     * @param streamHandler the stream handler used to handle the input and
     *        output streams of the subprocess.
     */
    private Execute(ExecuteStreamHandler streamHandler) {
        setStreamHandler(streamHandler);
        //By default, use the shell launcher for VMS
        //
        if (Os.isFamily("openvms")) {
            useVMLauncher = false;
        }
    }

    /**
     * Set the stream handler to use.
     * @param streamHandler ExecuteStreamHandler.
     * @since Ant 1.6
     */
    public void setStreamHandler(ExecuteStreamHandler streamHandler) {
        this.streamHandler = streamHandler;
    }

    /**
     * Returns the commandline used to create a subprocess.
     *
     * @return the commandline used to create a subprocess.
     */
    public String[] getCommandline() {
        return cmdl;
    }

    /**
     * Sets the commandline of the subprocess to launch.
     *
     * @param commandline the commandline of the subprocess to launch.
     */
    public void setCommandline(String[] commandline) {
        cmdl = commandline;
    }

    /**
     * Set whether to propagate the default environment or not.
     *
     * @param newenv whether to propagate the process environment.
     */
    public void setNewenvironment(boolean newenv) {
        newEnvironment = newenv;
    }

    /**
     * Returns the environment used to create a subprocess.
     *
     * @return the environment used to create a subprocess.
     */
    public String[] getEnvironment() {
        return (env == null || newEnvironment) ? env : patchEnvironment();
    }

    /**
     * Sets the environment variables for the subprocess to launch.
     *
     * @param env array of Strings, each element of which has
     * an environment variable settings in format <em>key=value</em>.
     */
    public void setEnvironment(String[] env) {
        this.env = env;
    }

    /**
     * Sets the working directory of the process to execute.
     *
     * <p>This is emulated using the antRun scripts unless the OS is
     * Windows NT in which case a cmd.exe is spawned,
     * or MRJ and setting user.dir works, or JDK 1.3 and there is
     * official support in java.lang.Runtime.
     *
     * @param wd the working directory of the process.
     */
    public void setWorkingDirectory(File wd) {
        workingDirectory = (wd == null || wd.getAbsolutePath().equals(antWorkingDirectory)) ? null : wd;
    }

    /**
     * Return the working directory.
     * @return the directory as a File.
     * @since Ant 1.7
     */
    public File getWorkingDirectory() {
        return workingDirectory == null ? new File(antWorkingDirectory) : workingDirectory;
    }

    /**
     * Launch this execution through the VM, where possible, rather than through
     * the OS's shell. In some cases and operating systems using the shell will
     * allow the shell to perform additional processing such as associating an
     * executable with a script, etc.
     *
     * @param useVMLauncher true if exec should launch through the VM,
     *                   false if the shell should be used to launch the
     *                   command.
     */
    public void setVMLauncher(boolean useVMLauncher) {
        this.useVMLauncher = useVMLauncher;
    }

    /**
     * Creates a process that runs a command.
     *
     * @param project the Project, only used for logging purposes, may be null.
     * @param command the command to run.
     * @param env the environment for the command.
     * @param dir the working directory for the command.
     * @param useVM use the built-in exec command for JDK 1.3 if available.
     * @return the process started.
     * @throws IOException forwarded from the particular launcher used.
     *
     * @since Ant 1.5
     */
    public static Process launch(String[] command, String[] env, File dir, boolean useVM) throws IOException {
        if (dir != null && !dir.exists()) {
            throw new ProvisionerException(dir + " doesn't exist.");
        }
        CommandLauncher launcher = ((useVM && vmLauncher != null) ? vmLauncher : shellLauncher);
        return launcher.exec(command, env, dir);
    }

    /**
     * Runs a process defined by the command line and returns its exit status.
     *
     * @return the exit status of the subprocess or <code>INVALID</code>.
     * @exception java.io.IOException The exception is thrown, if launching
     *            of the subprocess failed.
     */
    public int execute() throws IOException {
        if (workingDirectory != null && !workingDirectory.exists()) {
            throw new ProvisionerException(workingDirectory + " doesn't exist.");
        }
        final Process process = launch(getCommandline(), getEnvironment(), workingDirectory, useVMLauncher);
        try {
            streamHandler.setProcessInputStream(process.getOutputStream());
            streamHandler.setProcessOutputStream(process.getInputStream());
            streamHandler.setProcessErrorStream(process.getErrorStream());
        } catch (IOException e) {
            process.destroy();
            throw e;
        }
        streamHandler.start();

        try {

            waitFor(process);

            streamHandler.stop();
            closeStreams(process);

            return getExitValue();
        } catch (ThreadDeath t) {
            // #31928: forcibly kill it before continuing.
            process.destroy();
            throw t;
        } finally {
        }
    }

    /**
     * Starts a process defined by the command line.
     * Ant will not wait for this process, nor log its output.
     *
     * @throws java.io.IOException The exception is thrown, if launching
     *            of the subprocess failed.
     * @since Ant 1.6
     */
    public void spawn() throws IOException {
        createWorkingDir();
        final Process process = launch(getCommandline(), getEnvironment(), workingDirectory, useVMLauncher);
        if (Os.isFamily("windows")) {
            try {
                Thread.sleep(ONE_SECOND);
            } catch (InterruptedException e) {
                LOG.debug("interruption in the sleep after having spawned a" + " process");
            }
        }
        OutputStream dummyOut = new OutputStream() {
            public void write(int b) throws IOException {
                // Method intended to swallow whatever comes at it
            }
        };

        ExecuteStreamHandler handler = new PumpStreamHandler(dummyOut);
        handler.setProcessErrorStream(process.getErrorStream());
        handler.setProcessOutputStream(process.getInputStream());
        handler.start();
        process.getOutputStream().close();

    }

    private final void createWorkingDir() throws ProvisionerException {
        if (workingDirectory == null) {
            workingDirectory = new File(commandDirectory);
            workingDirectory.mkdirs();
        }

        if (!workingDirectory.exists()) {
            throw new ProvisionerException(workingDirectory + " doesn't exist.");
        }
    }

    /**
     * Wait for a given process.
     *
     * @param process the process one wants to wait for.
     */
    protected void waitFor(Process process) {
        try {
            process.waitFor();
            setExitValue(process.exitValue());
        } catch (InterruptedException e) {
            process.destroy();
        }
    }

    /**
     * Set the exit value.
     *
     * @param value exit value of the process.
     */
    protected void setExitValue(int value) {
        exitValue = value;
    }

    /**
     * Query the exit value of the process.
     * @return the exit value or Execute.INVALID if no exit value has
     * been received.
     */
    public int getExitValue() {
        return exitValue;
    }

    /**
     * Checks whether <code>exitValue</code> signals a failure on the current
     * system (OS specific).
     *
     * <p><b>Note</b> that this method relies on the conventions of
     * the OS, it will return false results if the application you are
     * running doesn't follow these conventions.  One notable
     * exception is the Java VM provided by HP for OpenVMS - it will
     * return 0 if successful (like on any other platform), but this
     * signals a failure on OpenVMS.  So if you execute a new Java VM
     * on OpenVMS, you cannot trust this method.</p>
     *
     * @param exitValue the exit value (return code) to be checked.
     * @return <code>true</code> if <code>exitValue</code> signals a failure.
     */
    public static boolean isFailure(int exitValue) {
        //on openvms even exit value signals failure;
        // for other platforms nonzero exit value signals failure
        return Os.isFamily("openvms") ? (exitValue % 2 == 0) : (exitValue != 0);
    }

    /**
     * Did this execute return in a failure.
     * @see #isFailure(int)
     * @return true if and only if the exit code is interpreted as a failure
     * @since Ant1.7
     */
    public boolean isFailure() {
        return isFailure(getExitValue());
    }

    /**
     * Test for an untimely death of the process.
     * @return true if a watchdog had to kill the process.
     * @since Ant 1.5
         
    public boolean killedProcess() {
    return watchdog != null && watchdog.killedProcess();
    }*/

    /**
     * Patch the current environment with the new values from the user.
     * @return the patched environment.
     */
    @SuppressWarnings("unchecked")
    private String[] patchEnvironment() {
        // On OpenVMS Runtime#exec() doesn't support the environment array,
        // so we only return the new values which then will be set in
        // the generated DCL script, inheriting the parent process environment
        if (Os.isFamily("openvms")) {
            return env;
        }
        Vector<String> osEnv = (Vector<String>) getProcEnvironment().clone();
        for (int i = 0; i < env.length; i++) {
            String keyValue = env[i];
            // Get key including "="
            String key = keyValue.substring(0, keyValue.indexOf('=') + 1);
            if (environmentCaseInSensitive) {
                // Nb: using default locale as key is a env name
                key = key.toLowerCase();
            }
            int size = osEnv.size();
            // Find the key in the current enviroment copy
            // and remove it.
            for (int j = 0; j < size; j++) {
                String osEnvItem = (String) osEnv.elementAt(j);
                String convertedItem = environmentCaseInSensitive ? osEnvItem.toLowerCase() : osEnvItem;
                if (convertedItem.startsWith(key)) {
                    osEnv.removeElementAt(j);
                    if (environmentCaseInSensitive) {
                        // Use the original casiness of the key
                        keyValue = osEnvItem.substring(0, key.length()) + keyValue.substring(key.length());
                    }
                    break;
                }
            }
            // Add the key to the enviromnent copy
            osEnv.addElement(keyValue);
        }
        return (String[]) (osEnv.toArray(new String[osEnv.size()]));
    }

    /**
     * Close the streams belonging to the given Process.
     * @param process   the <code>Process</code>.
     */
    public static void closeStreams(Process process) {
        close(process.getInputStream());
        close(process.getOutputStream());
        close(process.getErrorStream());
    }

    private static void close(OutputStream outputStream) {
        try {
            outputStream.close();
        } catch (IOException e) {
            LOG.warn("Error closing stream", e);
        }
    }

    private static void close(InputStream inputStream) {
        try {
            inputStream.close();
        } catch (IOException e) {
            LOG.warn("Error closing stream", e);
        }
    }

    /**
     * This method is VMS specific and used by getProcEnvironment().
     *
     * Parses VMS logicals from <code>in</code> and adds them to
     * <code>environment</code>.  <code>in</code> is expected to be the
     * output of "SHOW LOGICAL".  The method takes care of parsing the output
     * correctly as well as making sure that a logical defined in multiple
     * tables only gets added from the highest order table.  Logicals with
     * multiple equivalence names are mapped to a variable with multiple
     * values separated by a comma (,).
     */
    private static Vector<String> addVMSLogicals(Vector<String> environment, BufferedReader in) throws IOException {
        HashMap<String, String> logicals = new HashMap<String, String>();
        String logName = null, logValue = null, newLogName;
        String line = null;
        // CheckStyle:MagicNumber OFF
        while ((line = in.readLine()) != null) {
            // parse the VMS logicals into required format ("VAR=VAL[,VAL2]")
            if (line.startsWith("\t=")) {
                // further equivalence name of previous logical
                if (logName != null) {
                    logValue += "," + line.substring(4, line.length() - 1);
                }
            } else if (line.startsWith("  \"")) {
                // new logical?
                if (logName != null) {
                    logicals.put(logName, logValue);
                }
                int eqIndex = line.indexOf('=');
                newLogName = line.substring(3, eqIndex - 2);
                if (logicals.containsKey(newLogName)) {
                    // already got this logical from a higher order table
                    logName = null;
                } else {
                    logName = newLogName;
                    logValue = line.substring(eqIndex + 3, line.length() - 1);
                }
            }
        }
        // CheckStyle:MagicNumber ON
        // Since we "look ahead" before adding, there's one last env var.
        if (logName != null) {
            logicals.put(logName, logValue);
        }
        for (Iterator<String> i = logicals.keySet().iterator(); i.hasNext();) {
            String logical = (String) i.next();
            environment.add(logical + "=" + logicals.get(logical));
        }
        return environment;
    }

    /**
     * A command launcher for a particular JVM/OS platform.  This class is
     * a general purpose command launcher which can only launch commands in
     * the current working directory.
     */
    private static class CommandLauncher {
        /**
         * Launches the given command in a new process.
         *
         * @param project       The project that the command is part of.
         * @param cmd           The command to execute.
         * @param env           The environment for the new process.  If null,
         *                      the environment of the current process is used.
         * @return the created Process.
         * @throws IOException if attempting to run a command in a
         * specific directory.
         */
        public Process exec(String[] cmd, String[] env) throws IOException {
            return Runtime.getRuntime().exec(cmd, env);
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         *
         * @param project       The project that the command is part of.
         * @param cmd           The command to execute.
         * @param env           The environment for the new process.  If null,
         *                      the environment of the current process is used.
         * @param workingDir    The directory to start the command in.  If null,
         *                      the current directory is used.
         * @return the created Process.
         * @throws IOException  if trying to change directory.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            if (workingDir == null) {
                return exec(cmd, env);
            }
            throw new IOException("Cannot execute a process in different " + "directory under this JVM");
        }
    }

    /**
     * A command launcher for JDK/JRE 1.3 (and higher).  Uses the built-in
     * Runtime.exec() command.
     */
    private static class Java13CommandLauncher extends CommandLauncher {

        public Java13CommandLauncher() throws NoSuchMethodException {
            // Used to verify if Java13 is available, is prerequisite in ant 1.8
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir the working directory where the command
         * should run.
         * @return the created Process.
         * @throws IOException probably forwarded from Runtime#exec.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            try {
                return Runtime.getRuntime().exec(cmd, env, workingDir);
            } catch (IOException ioex) {
                throw ioex;
            } catch (Exception exc) {
                // IllegalAccess, IllegalArgument, ClassCast
                throw new ProvisionerException("Unable to execute command", exc);
            }
        }
    }

    /**
     * A command launcher that proxies another command launcher.
     *
     * Sub-classes override exec(args, env, workdir).
     */
    private static class CommandLauncherProxy extends CommandLauncher {
        private CommandLauncher myLauncher;

        CommandLauncherProxy(CommandLauncher launcher) {
            myLauncher = launcher;
        }

        /**
         * Launches the given command in a new process.  Delegates this
         * method to the proxied launcher.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env) throws IOException {
            return myLauncher.exec(cmd, env);
        }
    }

    /**
     * A command launcher for OS/2 that uses 'cmd.exe' when launching
     * commands in directories other than the current working
     * directory.
     *
     * <p>Unlike Windows NT and friends, OS/2's cd doesn't support the
     * /d switch to change drives and directories in one go.</p>
     */
    private static class OS2CommandLauncher extends CommandLauncherProxy {
        OS2CommandLauncher(CommandLauncher launcher) {
            super(launcher);
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            File commandDir = workingDir;
            // Use cmd.exe to change to the specified drive and
            // directory before running the command
            final int preCmdLength = 7;
            final String cmdDir = commandDir.getAbsolutePath();
            String[] newcmd = new String[cmd.length + preCmdLength];
            // CheckStyle:MagicNumber OFF - do not bother
            newcmd[0] = "cmd";
            newcmd[1] = "/c";
            newcmd[2] = cmdDir.substring(0, 2);
            newcmd[3] = "&&";
            newcmd[4] = "cd";
            newcmd[5] = cmdDir.substring(2);
            newcmd[6] = "&&";
            // CheckStyle:MagicNumber ON
            System.arraycopy(cmd, 0, newcmd, preCmdLength, cmd.length);

            return exec(newcmd, env);
        }
    }

    /**
     * A command launcher for Windows XP/2000/NT that uses 'cmd.exe' when
     * launching commands in directories other than the current working
     * directory.
     */
    private static class WinNTCommandLauncher extends CommandLauncherProxy {
        WinNTCommandLauncher(CommandLauncher launcher) {
            super(launcher);
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            File commandDir = workingDir;

            // Use cmd.exe to change to the specified directory before running
            // the command
            final int preCmdLength = 6;
            String[] newcmd = new String[cmd.length + preCmdLength];
            // CheckStyle:MagicNumber OFF - do not bother
            newcmd[0] = "cmd";
            newcmd[1] = "/c";
            newcmd[2] = "cd";
            newcmd[3] = "/d";
            newcmd[4] = commandDir.getAbsolutePath();
            newcmd[5] = "&&";
            // CheckStyle:MagicNumber ON
            System.arraycopy(cmd, 0, newcmd, preCmdLength, cmd.length);

            return exec(newcmd, env);
        }
    }

    /**
     * A command launcher for Mac that uses a dodgy mechanism to change
     * working directory before launching commands.
     */
    private static class MacCommandLauncher extends CommandLauncherProxy {
        MacCommandLauncher(CommandLauncher launcher) {
            super(launcher);
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            System.getProperties().put("user.dir", workingDir.getAbsolutePath());
            try {
                return exec(cmd, env);
            } finally {
                System.getProperties().put("user.dir", antWorkingDirectory);
            }
        }
    }

    /**
     * A command launcher that uses an auxiliary script to launch commands
     * in directories other than the current working directory.
     */
    private static class ScriptCommandLauncher extends CommandLauncherProxy {
        ScriptCommandLauncher(String script, CommandLauncher launcher) {
            super(launcher);
            myScript = script;
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {

            // Locate the auxiliary script

            String antRun = workingDir.getAbsolutePath() + File.separator + myScript;

            // Build the command
            String[] newcmd = new String[cmd.length + 2];
            newcmd[0] = antRun;
            newcmd[1] = workingDir.getAbsolutePath();
            System.arraycopy(cmd, 0, newcmd, 2, cmd.length);

            return exec(newcmd, env);
        }

        private String myScript;
    }

    /**
     * A command launcher that uses an auxiliary perl script to launch commands
     * in directories other than the current working directory.
     */
    private static class PerlScriptCommandLauncher extends CommandLauncherProxy {
        private String myScript;

        PerlScriptCommandLauncher(String script, CommandLauncher launcher) {
            super(launcher);
            myScript = script;
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            String antRun = workingDir.getAbsolutePath() + File.separator + myScript;

            // CheckStyle:MagicNumber OFF
            String[] newcmd = new String[cmd.length + 3];
            newcmd[0] = "perl";
            newcmd[1] = antRun;
            newcmd[2] = workingDir.getAbsolutePath();
            System.arraycopy(cmd, 0, newcmd, 3, cmd.length);
            // CheckStyle:MagicNumber ON

            return exec(newcmd, env);
        }
    }

    /**
     * A command launcher for VMS that writes the command to a temporary DCL
     * script before launching commands.  This is due to limitations of both
     * the DCL interpreter and the Java VM implementation.
     */
    private static class VmsCommandLauncher extends Java13CommandLauncher {

        public VmsCommandLauncher() throws NoSuchMethodException {
            super();
        }

        /**
         * Launches the given command in a new process.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env) throws IOException {
            File cmdFile = createCommandFile(cmd, env);
            Process p = super.exec(new String[] { cmdFile.getPath() }, env);
            deleteAfter(cmdFile, p);
            return p;
        }

        /**
         * Launches the given command in a new process, in the given working
         * directory.  Note that under Java 1.4.0 and 1.4.1 on VMS this
         * method only works if <code>workingDir</code> is null or the logical
         * JAVA$FORK_SUPPORT_CHDIR needs to be set to TRUE.
         * @param project the Ant project.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @param workingDir working directory where the command should run.
         * @return the created Process.
         * @throws IOException forwarded from the exec method of the
         * command launcher.
         */
        public Process exec(String[] cmd, String[] env, File workingDir) throws IOException {
            File cmdFile = createCommandFile(cmd, env);
            Process p = super.exec(new String[] { cmdFile.getPath() }, env, workingDir);
            deleteAfter(cmdFile, p);
            return p;
        }

        /*
         * Writes the command into a temporary DCL script and returns the
         * corresponding File object.  The script will be deleted on exit.
         * @param cmd the command line to execute as an array of strings.
         * @param env the environment to set as an array of strings.
         * @return the command File.
         * @throws IOException if errors are encountered creating the file.
         */
        private File createCommandFile(String[] cmd, String[] env) throws IOException {
            File script = File.createTempFile("meshkeeper", ".COM");
            BufferedWriter out = null;
            try {
                out = new BufferedWriter(new FileWriter(script));

                // add the environment as logicals to the DCL script
                if (env != null) {
                    int eqIndex;
                    for (int i = 0; i < env.length; i++) {
                        eqIndex = env[i].indexOf('=');
                        if (eqIndex != -1) {
                            out.write("$ DEFINE/NOLOG ");
                            out.write(env[i].substring(0, eqIndex));
                            out.write(" \"");
                            out.write(env[i].substring(eqIndex + 1));
                            out.write('\"');
                            out.newLine();
                        }
                    }
                }
                out.write("$ " + cmd[0]);
                for (int i = 1; i < cmd.length; i++) {
                    out.write(" -");
                    out.newLine();
                    out.write(cmd[i]);
                }
            } finally {
                out.close();
            }
            return script;
        }

        private void deleteAfter(final File f, final Process p) {
            new Thread() {
                public void run() {
                    try {
                        p.waitFor();
                    } catch (InterruptedException e) {
                        //ignore
                    }
                    FileSupport.recursiveDelete(f);
                }
            }.start();
        }
    }
}