com.microsoft.tfs.util.process.ProcessRunner.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.util.process.ProcessRunner.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.util.process;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.TypesafeEnum;

/**
 * Runs an external process, especially designed to be run in a new thread via
 * {@link ProcessRunner#runAsync(ProcessRunner)}, but can be run directly via
 * {@link ProcessRunner#run()}.
 * <p>
 * {@link #run()} blocks until the process completes.
 * {@link #runAsync(ProcessRunner)} runs the given runner in a new thread that
 * can be interrupted via {@link ProcessRunner#interrupt()}. Its state may also
 * be queried while it is running.
 * <p>
 *
 * @threadsafety thread-safe
 */
public class ProcessRunner implements Runnable {
    private static final Log log = LogFactory.getLog(ProcessRunner.class);

    /**
     * An {@link OutputStream} that simply writes everything to
     * {@link System#err}.
     */
    public static class SystemErrorOutputStream extends OutputStream {
        public SystemErrorOutputStream() {
        }

        @Override
        public void close() {
        }

        @Override
        public void flush() {
            System.err.flush();
        }

        @Override
        public void write(final byte[] b) throws IOException {
            System.err.write(b);
        }

        @Override
        public void write(final byte[] b, final int off, final int len) throws IOException {
            System.err.write(b, off, len);
        }

        @Override
        public void write(final int b) throws IOException {
            System.err.write(b);
        }
    }

    /**
     * An {@link OutputStream} that simply writes everything to
     * {@link System#out}.
     */
    public static class SystemOutputOutputStream extends OutputStream {
        public SystemOutputOutputStream() {
        }

        @Override
        public void close() {
        }

        @Override
        public void flush() {
            System.out.flush();
        }

        @Override
        public void write(final byte[] b) throws IOException {
            System.out.write(b);
        }

        @Override
        public void write(final byte[] b, final int off, final int len) throws IOException {
            System.out.write(b, off, len);
        }

        @Override
        public void write(final int b) throws IOException {
            System.out.write(b);
        }
    }

    public static class ProcessRunnerState extends TypesafeEnum {
        public ProcessRunnerState(final int value) {
            super(value);
        }

        /**
         * The process runner has been constructed but
         * {@link ProcessRunner#run()} has not been invoked.
         * <p>
         * This is the initial state of all process runners.
         */
        public final static ProcessRunnerState NEW = new ProcessRunnerState(0);

        /**
         * {@link ProcessRunner#run()} has been invoked, but the process has not
         * completed.
         * <p>
         * This is an intermediate state.
         */
        public final static ProcessRunnerState RUNNING = new ProcessRunnerState(1);

        /**
         * {@link ProcessRunner#run()} was invoked but could not create the
         * process. Call {@link ProcessRunner#getError()} to get the exception
         * that resulted in this state.
         * <p>
         * This is a terminal state.
         */
        public final static ProcessRunnerState EXEC_FAILED = new ProcessRunnerState(2);

        /**
         * The process was created and run but the thread that is running this
         * {@link ProcessRunner} has been interrupted, so no exit code was read
         * from the process.
         * <p>
         * This is a terminal state.
         */
        public final static ProcessRunnerState INTERRUPTED = new ProcessRunnerState(3);

        /**
         * The process was created and run and exited with a valid exit code
         * (which may be an error). Call {@link ProcessRunner#getExitCode()} to
         * get the exit code.
         * <p>
         * This is a terminal state.
         */
        public final static ProcessRunnerState COMPLETED = new ProcessRunnerState(4);
    }

    /**
     * The external process we're running (which is null until this runnable is
     * run).
     */
    private Process process;

    /**
     * The state of this runner.
     */
    private ProcessRunnerState state;

    /**
     * The commands that will be run via {@link Runtime#exec(String[])}.
     */
    private final String[] commands;

    /**
     * Environment strings to pass to {@link Runtime#exec(String[])}. May be
     * null.
     */
    private final String[] environment;

    /**
     * Working directory to pass to {@link Runtime#exec(String[])}. May be null.
     */
    private final File workingDirectory;

    /**
     * Invoked when the process's state is set to one of terminal states. May be
     * null.
     */
    private final ProcessFinishedHandler finishedHandler;

    /**
     * Contains the stream the user wants to fill with the captured standard
     * output. May be null (then standard output from the child process is
     * consumed but not stored).
     */
    private final OutputStream capturedStandardOutput;

    /**
     * Contains the stream the user wants to fill with the captured standard
     * error. May be null (then standard error from the child process is
     * consumed but not stored).
     */
    private final OutputStream capturedStandardError;

    /**
     * The new thread this runner is running in (is null if no new thread was
     * created to run this process runner).
     */
    private Thread asyncThread;

    /**
     * Holds a command line string for display to the user in error or log
     * cases. This string is not actually run via {@link Runtime#exec(String)};
     * only the String[] version is invoked by this class.
     */
    final private String commandLineForDisplay;

    /**
     * Filled in if run() resulted in an exception that caused the external
     * process to fail to run or caused the exit code to be unreadable.
     */
    private Throwable error;

    /**
     * The exit code of the process.
     */
    private int exitCode = -1;

    /**
     * Used to name the IO reader threads we spawn for better debug log
     * messages. Incremented on every access.
     */
    private static long threadID = 0;

    /**
     * Constructs a process runner that will run the given commands via
     * {@link Runtime#exec(String[])} when {@link ProcessRunner#run()} is
     * invoked.
     *
     * @param commands
     *        the commands and arguments (passed directly to
     *        {@link Runtime#exec(String[])}) to run. Not null but may be empty
     *        (no process is created but command state evolves normally).
     * @param environment
     *        an optional array of environment variables to use in the new
     *        process. See {@link Runtime#exec(String[], String[], File)}). May
     *        be null.
     * @param workingDirectory
     *        an optional working directory to use in the new process. See
     *        {@link Runtime#exec(String[], String[], File)}). May be null.
     * @param completedHandler
     *        a {@link ProcessFinishedHandler} whose methods are invoked when
     *        the process runner reaches one of its terminal states.
     *        <p>
     *        This method is invoked in the context of the thread running the
     *        ProcessRunner, which may be the calling thread (
     *        {@link ProcessRunner#run()} was called directly) or may be another
     *        ({@link ProcessRunner#runAsync(ProcessRunner)} was called).
     *        <p>
     *        Pass null if you wish to poll for state or don't care about state
     *        at all.
     */
    public ProcessRunner(final String[] commands, final String[] environment, final File workingDirectory,
            final ProcessFinishedHandler completedHandler) {
        Check.notNull(commands, "commands"); //$NON-NLS-1$

        this.commands = commands;
        this.environment = environment;
        this.workingDirectory = workingDirectory;
        finishedHandler = completedHandler;
        capturedStandardOutput = null;
        capturedStandardError = null;

        commandLineForDisplay = makeCommandLineForDisplay(commands);
        state = ProcessRunnerState.NEW;
    }

    /**
     * Constructs a process runner that will run the given commands via
     * {@link Runtime#exec(String[])} when {@link ProcessRunner#run()} is
     * invoked.
     * <p>
     * <b>Buffer Warning</b>
     * <p>
     * If you pass OutputStreams to accept the child process's output, take care
     * not to use it in a way that causes the process runner to be unable to
     * write to it for the duration of the runner's life (until it has reached a
     * terminal state). Doing so might cause deadlock (for example, process
     * runner is waiting to write to the stream while you're waiting to read
     * from it).
     *
     * @param commands
     *        the commands and arguments (passed directly to
     *        {@link Runtime#exec(String[])}) to run. Not null but may be empty
     *        (no process is created but command state evolves normally).
     * @param environment
     *        an optional array of environment variables to use in the new
     *        process. See {@link Runtime#exec(String[], String[], File)}). May
     *        be null.
     * @param workingDirectory
     *        an optional working directory to use in the new process. See
     *        {@link Runtime#exec(String[], String[], File)}). May be null.
     * @param completedHandler
     *        a {@link ProcessFinishedHandler} whose methods are invoked when
     *        the process runner reaches one of its terminal states.
     *        <p>
     *        This method is invoked in the context of the thread running the
     *        ProcessRunner, which may be the calling thread (
     *        {@link ProcessRunner#run()} was called directly) or may be another
     *        ({@link ProcessRunner#runAsync(ProcessRunner)} was called).
     *        <p>
     *        Pass null if you wish to poll for state or don't care about state
     *        at all.
     * @param capturedStandardOutput
     *        a stream to capture the text written by the child process to its
     *        standard output stream. Pass null if you don't want this output.
     *        <p>
     *        <b>See the warning in this method's Javadoc about deadlock.</b>
     * @param capturedStandardError
     *        a stream to capture the text written by the child process to its
     *        standard error stream. Pass null if you don't want this output.
     *        <p>
     *        <b>See the warning in this method's Javadoc about deadlock.</b>
     */
    public ProcessRunner(final String[] commands, final String[] environment, final File workingDirectory,
            final ProcessFinishedHandler completedHandler, final OutputStream capturedStandardOutput,
            final OutputStream capturedStandardError) {
        Check.notNull(commands, "commands"); //$NON-NLS-1$

        this.commands = commands;
        this.environment = environment;
        this.workingDirectory = workingDirectory;
        finishedHandler = completedHandler;
        this.capturedStandardOutput = capturedStandardOutput;
        this.capturedStandardError = capturedStandardError;

        commandLineForDisplay = makeCommandLineForDisplay(commands);
        state = ProcessRunnerState.NEW;
    }

    /**
     * Causes the process runner to stop waiting for the external process to
     * complete if it is currently running in another thread. Does not terminate
     * the external process.
     * <p>
     * Do not call this method if you did not start the process runner via
     * {@link #runAsync(ProcessRunner)}. You will get an
     * {@link IllegalStateException} if you do.
     * <p>
     * If the process is still in the initial state (
     * {@link ProcessRunnerState#NEW}), an {@link IllegalStateException} is
     * thrown. If the process is in any other state, no exception is thrown.
     * <p>
     * The terminal state of the runner may not be
     * {@link ProcessRunnerState#INTERRUPTED} even if this method is invoked.
     * For example, the process might complete before this method completes, and
     * the state of the runner would become {@link ProcessRunnerState#COMPLETED}
     * instead. If the process failed to start in the first place, the state
     * would remain {@link ProcessRunnerState#EXEC_FAILED}.
     *
     * @throws IllegalStateException
     *         if this method was invoked when the runner's state was (the
     *         runner's state is {@link ProcessRunnerState#NEW}, or if the
     *         runner was not run via {@link #runAsync(ProcessRunner)}.
     */
    public synchronized void interrupt() {
        if (asyncThread == null) {
            throw new IllegalStateException(
                    "This runner was not started via runAsync() so it may not be interrupted"); //$NON-NLS-1$
        }

        if (state == ProcessRunnerState.NEW) {
            throw new IllegalStateException("A process runner cannot be interrupted before it is started"); //$NON-NLS-1$
        }

        /*
         * Interrupting our controlling thread will cause an interrupted
         * exception in Process.waitFor(), if the thread is currently blocking
         * there.
         *
         * If waitFor() has already exited, then the status will end up being
         * COMPLETED.
         */
        asyncThread.interrupt();
    }

    /**
     * Waits for the external process to finish running or be interrupted.
     * Returns only when the runner's state is one of the terminal states.
     * Returns immediately if the process has already finished.
     *
     * @throws IllegalStateException
     *         if this method was invoked when the runner was not started via
     *         {@link #runAsync(ProcessRunner)}.
     */
    public void waitForFinish() {
        synchronized (this) {
            if (asyncThread == null) {
                throw new IllegalStateException(
                        "This runner was not started via runAsync() so you can't wait for it to finish"); //$NON-NLS-1$
            }

            if (isFinished()) {
                return;
            }

            try {
                this.wait();
            } catch (final InterruptedException e) {
                /*
                 * Ignore. Since this method is never run in the context of the
                 * async thread, this is not the interrupted exception that's
                 * caused when interrupt() is called.
                 */
            }
        }
    }

    /**
     * Starts the given process runner in its own thread, returning immediately.
     * Check on the runner's state through its public methods or pass a
     * {@link ProcessFinishedHandler} to the runner on creation.
     *
     * @param runner
     *        the runner to start in its own thread (not null).
     */
    public static void runAsync(final ProcessRunner runner) {
        Check.notNull(runner, "runner"); //$NON-NLS-1$

        final Thread thread = new Thread(runner);
        thread.setName("Process Runner"); //$NON-NLS-1$

        /*
         * Sneak in a reference to the new thread. This must be done before
         * start() so that calls to interrupt() cause the correct exception
         * (because the runner is in the NEW state).
         */
        runner.setThread(thread);

        thread.start();
    }

    /**
     * Starts another process to run the commands this runner was constructed
     * with. Blocks until the process exits or {@link #interrupt()} is invoked.
     *
     * @see Runnable#run()
     */
    @Override
    public void run() {
        synchronized (this) {
            if (state != ProcessRunnerState.NEW) {
                throw new IllegalStateException("Can only run a ProcessRunner once"); //$NON-NLS-1$
            }
        }

        /*
         * If the commands were empty, we can skip the process creation which
         * may be heavy on some platforms and simply report a success.
         */
        if (commands.length == 0) {
            synchronized (this) {
                exitCode = 0;
                state = ProcessRunnerState.COMPLETED;
            }

            notifyTerminalState();
            return;
        }

        try {
            synchronized (this) {
                process = Runtime.getRuntime().exec(commands, environment, workingDirectory);
                state = ProcessRunnerState.RUNNING;
            }
        } catch (final IOException e) {
            synchronized (this) {
                error = e;
                state = ProcessRunnerState.EXEC_FAILED;
            }

            notifyTerminalState();
            return;
        }

        /*
         * If we do not pump (read) the child process's streams (standard out
         * and standard error), Windows will cause the child to block if it
         * writes more than a small amount of output (512 bytes or chars [not
         * sure which] in our testing).
         *
         * If the user of this runner is interested in the child's output, we
         * have to service the streams in other threads in order to prevent
         * becoming blind to an interruption delivered to this thread.
         * Specifically, reading from these streams is a blocking task, and
         * there is no way for the user to interrupt us while we block. If we
         * launch other threads, we arrive at process.waitFor() quickly in this
         * thread and can accept the interruption, then interrupt the readers.
         */

        final Thread outputReaderThread = new Thread(
                new ProcessOutputReader(process.getInputStream(), capturedStandardOutput));

        String messageFormat = "Standard Output Reader {0}"; //$NON-NLS-1$
        String message = MessageFormat.format(messageFormat, Long.toString(getNewThreadID()));
        outputReaderThread.setName(message);
        outputReaderThread.start();

        messageFormat = "Started IO waiter thread '{0}'"; //$NON-NLS-1$
        message = MessageFormat.format(messageFormat, outputReaderThread.getName());
        log.debug(message);

        final Thread errorReaderThread = new Thread(
                new ProcessOutputReader(process.getErrorStream(), capturedStandardError));

        messageFormat = "Standard Error Reader {0}"; //$NON-NLS-1$
        message = MessageFormat.format(messageFormat, Long.toString(getNewThreadID()));
        errorReaderThread.setName(message);
        errorReaderThread.start();

        messageFormat = "Started IO waiter thread '{0}'"; //$NON-NLS-1$
        message = MessageFormat.format(messageFormat, errorReaderThread.getName());
        log.debug(message);

        int ret;
        try {
            /*
             * We must not hold the lock on this while we wait on the child, or
             * we could not be interrupted.
             */
            ret = process.waitFor();
        } catch (final InterruptedException e) {
            log.debug("Normal interruption, interrupting all IO readers"); //$NON-NLS-1$

            /*
             * We must join on all IO readers before entering a terminal state
             * to prevent the reader threads from later writing to their
             * streams. This method also performs the immediate interrupt.
             *
             * Ignore if there was an error joining because we just want to
             * terminate as INTERRUPTED anyway.
             */
            joinReaders(new Thread[] { outputReaderThread, errorReaderThread }, true);

            /*
             * This is the normal abort scenario. No exit code is available and
             * no error occurred.
             */
            synchronized (this) {
                state = ProcessRunnerState.INTERRUPTED;
            }

            notifyTerminalState();
            return;
        }

        /*
         * If we launched output reader threads, we have to wait for them to
         * complete here. This is usually a short wait because once we're this
         * far, process.waitFor() has finished so the readers will be reaching
         * the end of their input streams soon (and terminating).
         *
         * If we get an error back from the join, we want to consider this
         * entire runner INTERRUPTED because we can't trust the output streams
         * to have the entire contents of the process.
         */

        if (joinReaders(new Thread[] { outputReaderThread, errorReaderThread }, false) == false) {
            log.error("Error joining IO reader threads, setting INTERRUPTED"); //$NON-NLS-1$

            synchronized (this) {
                state = ProcessRunnerState.INTERRUPTED;
            }

            notifyTerminalState();
            return;
        }

        /*
         * Now that we have joined the IO reader threads, we can close the close
         * the streams in order to prevent Java from leaking the handles.
         */

        try {
            process.getOutputStream().close();
            process.getInputStream().close();
            process.getErrorStream().close();
        } catch (final IOException e) {
            /*
             * This exception is from Stream.close().
             *
             * A failure to configure the output streams is a critical error and
             * should be treated as a failure to launch the process. Setting
             * different state could cause the user to trust that his process
             * which returned a 0 exit code also printed no error text when it
             * actually did (and therefore failed).
             */

            log.error("Error closing child process's output streams after join, setting INTERRUPTED", e); //$NON-NLS-1$

            synchronized (this) {
                state = ProcessRunnerState.INTERRUPTED;
            }

            notifyTerminalState();
            return;
        }

        synchronized (this) {
            exitCode = ret;
            state = ProcessRunnerState.COMPLETED;
        }

        notifyTerminalState();
    }

    /**
     * Joins on the given threads in the given order, optionally interrupting
     * them first. If a thread is null, it is ignored.
     * <p>
     * If a thread throws an exception during join, false is returned, but all
     * other threads are still joined.
     *
     * @param threads
     *        the threads to join on (not null, but null elements are ignored).
     * @param immediateInterrupt
     *        if true, the threads are all immediately interrupted in order,
     *        then joined. If false, the threads are only joined.
     *
     * @return true if no error occurred joining the threads, false if an error
     *         occurred.
     */
    private boolean joinReaders(final Thread[] threads, final boolean immediateInterrupt) {
        Check.notNull(threads, "threads"); //$NON-NLS-1$

        boolean hadJoinError = false;

        if (immediateInterrupt) {
            for (int i = 0; i < threads.length; i++) {
                if (threads[i] == null) {
                    continue;
                }

                final String messageFormat = "Normal interruption of reader thread '{0}'"; //$NON-NLS-1$
                final String message = MessageFormat.format(messageFormat, threads[i].getName());
                log.debug(message);

                threads[i].interrupt();
            }
        }

        for (int i = 0; i < threads.length; i++) {
            if (threads[i] == null) {
                continue;
            }

            try {
                threads[i].join();

                final String messageFormat = "Reader thread '{0}' joined"; //$NON-NLS-1$
                final String message = MessageFormat.format(messageFormat, threads[i].getName());
                log.debug(message);
            } catch (final InterruptedException e) {
                final String messageFormat = "Error joining on reader '{0}'"; //$NON-NLS-1$
                final String message = MessageFormat.format(messageFormat, threads[i].getName());
                log.warn(message, e);

                hadJoinError = true;
            }
        }

        return hadJoinError == false;
    }

    /**
     * @return a new, unique numeric thread ID appropriate for an IO reader
     *         thread.
     */
    private synchronized long getNewThreadID() {
        return threadID++;
    }

    /**
     * Builds a string that looks like a command line to show the user how his
     * external tool went wrong (or just for logging).
     *
     * @param commands
     *        first item is the command others are arguments. If null, returns
     *        null.
     * @return null if commands was null, else a string like: "/bin/ls" "-la"
     *         "/tmp"
     */
    private String makeCommandLineForDisplay(final String[] commands) {
        if (commands == null) {
            return null;
        }

        final StringBuffer sb = new StringBuffer();
        for (int i = 0; i < commands.length; i++) {
            if (i > 0) {
                sb.append(" "); //$NON-NLS-1$
            }
            sb.append("\"" + commands[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
        }

        return sb.toString();
    }

    /**
     * @return an approximation of the command line that was run by this runner.
     *         The exact command line is not available becuase the String[]
     *         version of {@link Runtime#exec(String[])} is used. null if no
     *         external process was launched (a null command array was given
     *         during construction).
     */
    public String getCommandLineForDisplay() {
        return commandLineForDisplay;
    }

    /**
     * Gets the error object that caused the process to fail to run. Only call
     * this method if the state of the runner is
     * {@link ProcessRunnerState#EXEC_FAILED}.
     *
     * @return the error that caused the execution to fail.
     * @throws IllegalStateException
     *         if the runner's state is anything other than
     *         {@link ProcessRunnerState#EXEC_FAILED}.
     */
    public synchronized Throwable getExecutionError() {
        if (state != ProcessRunnerState.EXEC_FAILED) {
            throw new IllegalStateException(
                    "No error is available unless the process runner's state is EXEC_FAILED"); //$NON-NLS-1$
        }

        return error;
    }

    /**
     * Gets the exit status of the process that ran and completed. Only call
     * this method if the state of the runner is
     * {@link ProcessRunnerState#COMPLETED}.
     *
     * @return exit status of the process.
     * @throws IllegalStateException
     *         if the runner's state is anything other than
     *         {@link ProcessRunnerState#COMPLETED}.
     */
    public synchronized int getExitCode() {
        if (state != ProcessRunnerState.COMPLETED) {
            throw new IllegalStateException(
                    "No exit code is available unless the process runner's state is COMPLETED"); //$NON-NLS-1$
        }

        return exitCode;
    }

    /**
     * Gets the state of the runner.
     *
     * @return the state of this runner.
     */
    public synchronized ProcessRunnerState getState() {
        return state;
    }

    /**
     * @return true if the process runner has reached one of its terminal
     *         states, false if it has not.
     */
    public synchronized boolean isFinished() {
        return state == ProcessRunnerState.COMPLETED || state == ProcessRunnerState.EXEC_FAILED
                || state == ProcessRunnerState.INTERRUPTED;
    }

    /**
     * Sets the controlling thread for this process runner. The controlling
     * thread only needs to be set when the runner is started via
     * {@link #runAsync(ProcessRunner).
     *
     * @param thread
     *        the newly created thread running this runner (not null).
     */
    private synchronized void setThread(final Thread thread) {
        Check.notNull(thread, "thread"); //$NON-NLS-1$
        asyncThread = thread;
    }

    /**
     * Invoke when we have entered a terminal state. this.state must have been
     * set correctly before this method is invoked.
     * <p>
     * Does two things: invokes the appropriate method on the finished handler
     * if there is one, and notifies all listeners who are waiting on this via
     * {@link #waitForFinish()}.
     * <p>
     * Do not invoke this method inside a synchronized block. That might cause
     * deadlock if the {@link ProcessFinishedHandler} is written to cause
     * another thread to call back into this runner.
     */
    private void notifyTerminalState() {
        // The handler is final so we don't need to synchronize.
        if (finishedHandler != null) {
            if (state == ProcessRunnerState.COMPLETED) {
                finishedHandler.processCompleted(this);
            } else if (state == ProcessRunnerState.EXEC_FAILED) {
                finishedHandler.processExecFailed(this);
            } else if (state == ProcessRunnerState.INTERRUPTED) {
                finishedHandler.processInterrupted(this);
            } else {
                Check.isTrue(false, "State " + state.getClass().getName() + " is not a known terminal state"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        synchronized (this) {
            // Wake all threads in waitForFinish().
            notifyAll();
        }
    }
}