All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.io.ProcessRunner Maven / Gradle / Ivy

/*
 * 02/14/2006
 *
 * ProcessRunner.java - Runs an external process as safely as possible.
 * This code is a modified form of the following JavaWorld article:
 * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps_p.html
 *
 * This class is public domain.  Use however you see fit.
 */
package org.fife.io;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


/**
 * Runs an external process (a program, batch file, shell script, etc.)
 * as safely as possible.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class ProcessRunner implements Runnable {

	private File dir;
	private String[] commandLine;
	private Map envVars;
	private boolean appendEnv;
	private String stdout;
	private String stderr;
	private ProcessRunnerOutputListener outputListener;
	private int rc;
	private Throwable lastError;


	/**
	 * Constructor.
	 * 
	 * @param commandLine The command line to run, with each item in the
	 *        array being a single parameter.
	 * @throws IllegalArgumentException If commandLine has
	 *         length 0.
	 * @throws NullPointerException If commandLine is
	 *         null.
	 */
	public ProcessRunner(String[] commandLine) {
		setCommandLine(commandLine);
		appendEnv = true;
	}


	/**
	 * Clears the stdout and stderr variables.
	 */
	private void clearLastOutput() {
		stdout = stderr = null;
		rc = Integer.MIN_VALUE;
		lastError = null;
	}


	/**
	 * Creates an array of "name=value" elements, suitable for
	 * Runtime.getRuntime().exec().
	 *
	 * @return The array of environment variables.
	 */
	private String[] createEnvVarArray() {

		Map env = new HashMap();

		// If we want to append our environment to that of the parent process...
		if (appendEnv) {

			// This class works with Java 1.4+, but System.getenv() was only
			// added in Java 5, so we take extra care here.
			Class clazz = System.class;
			try {
				Method getenv = clazz.getMethod("getenv", null);
				Map parentEnv = (Map)getenv.invoke(clazz, null);
				env.putAll(parentEnv);
			} catch (NoSuchMethodException nsme) { // Java 1.4
				getEnvironmentNative(env);
			} catch (RuntimeException re) {
				throw re;
			} catch (Exception e) {
				e.printStackTrace();
				getEnvironmentNative(env); // Fallback
			}

		}

		// If we have any environment variables to append...
		if (this.envVars!=null) {
			env.putAll(this.envVars);
		}

		// Create an array of "name=value" elements.
		List temp = new ArrayList(env.size());
		for (Iterator i=env.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry entry = (Map.Entry)i.next();
			temp.add(entry.getKey() + "=" + entry.getValue());
		}
		String[] envp = new String[temp.size()];
		envp = (String[])temp.toArray(envp);

		return envp;

	}


	/**
	 * Returns whether any extra environment variables defined for this process
	 * to run with should be appended to the parent process's environment (as
	 * opposed to overwriting it).
	 *
	 * @return Whether to append the parent process's environment.
	 * @see #getEnvironmentVars()
	 * @see #setEnvironmentVars(Map, boolean)
	 */
	public boolean getAppendEnvironmentVars() {
		return appendEnv;
	}


	/**
	 * Returns the command line this external process runner will run as a
	 * string.  Parameters are wrapped in quotes.
	 *
	 * @return The command line this object will run.
	 * @see #setCommandLine(String[])
	 */
	public String getCommandLineString() {
		int count = commandLine.length;
		StringBuffer sb = new StringBuffer();
		for (int i=0; inull, then the process will run in the same
	 *         directory as this Java process.
	 * @see #setDirectory(File)
	 */
	public File getDirectory() {
		return dir;
	}


	/**
	 * Uses "cmd /c set" on Windows and
	 * "/bin/sh -c env" on *nix to determine the current
	 * environment.  This method is used when an application is running in a
	 * 1.4 JVM, meaning System.getenv() is not available.
	 *
	 * @param env The map to append the environment variables to.
	 */
	private void getEnvironmentNative(Map env) {

		String command = File.separatorChar=='\\' ?
									"cmd /c set" : "/bin/sh -c env";

		Process p = null;
		StreamReaderThread stdoutThread = null;
		Thread stderrThread = null;
		String vars = null;

		try {

			p = Runtime.getRuntime().exec(command);

			// Create threads to read the stdout and stderr of the external
			// process.  If we do not do it this way, the process may
			// deadlock.
			InputStream errStream = p.getErrorStream();
			InputStream outStream = p.getInputStream();
			stdoutThread = new StreamReaderThread(p, outStream, null, true);
			stderrThread = new StreamReaderThread(p, errStream, null, false);
			stdoutThread.start();
			stderrThread.start();

			rc = p.waitFor();
			p = null;

			// Don't interrupt reader threads;
			// just wait for them to terminate normally.
			//stdoutThread.interrupt();
			//stderrThread.interrupt();
			stdoutThread.join();
			stderrThread.join();

			vars = stdoutThread.getStreamOutput();
			
		} catch (IOException ioe) {
			ioe.printStackTrace();
			// IOE can only happen in Runtime.exec(), so stdoutThread and
			// stderrThread are always null if we get here
			//stdoutThread.interrupt();
			//stderrThread.interrupt();
		} catch (InterruptedException ie) {
			ie.printStackTrace();
			if (stdoutThread!=null) {
				stdoutThread.interrupt();
			}
			if (stderrThread!=null) {
				stderrThread.interrupt();
			}
		} finally {
			if (p!=null) {
				p.destroy();
			}
		}

		// Parse the stdout for name/value pairs.
		if (vars!=null) {
			BufferedReader r = new BufferedReader(new StringReader(vars));
			String line = null;
			try {
				while ((line=r.readLine())!=null) {
					int split = line.indexOf('=');
					if (split>-1) { // Should always be true
						String name = line.substring(0, split);
						String value = line.substring(split+1);
						//System.out.println("Adding var: " + name + " => " + value);
						env.put(name, value);
					}
				}
				r.close();
			} catch (IOException ioe) {
				ioe.printStackTrace(); // Never happens
			}
		}

	}


	/**
	 * Returns any extra environment variables defined for this process to run
	 * with.
	 *
	 * @return The environment variables.
	 * @see #getAppendEnvironmentVars()
	 * @see #setEnvironmentVars(Map, boolean)
	 */
	public Map getEnvironmentVars() {
		Map temp = new HashMap();
		if (envVars!=null) {
			temp.putAll(envVars);
		}
		return temp;
	}


	/**
	 * Returns the last error thrown when trying to run a process, or
	 * null if the last process ran successfully.
	 *
	 * @return The error that the last-run process ended with, if any.
	 */
	public Throwable getLastError() {
		return lastError;
	}


	/**
	 * Returns the return code of the last process ran.
	 *
	 * @return The return code of the last process ran.
	 */
	public int getReturnCode() {
		return rc;
	}


	/**
	 * Returns the stderr of the process last ran.
	 *
	 * @return The stderr of the last process ran.
	 */
	public String getStderr() {
		return stderr;
	}


	/**
	 * Returns the stdout of the process last ran.
	 *
	 * @return The stdout of the last process ran.
	 */
	public String getStdout() {
		return stdout;
	}


	/**
	 * Runs the current external process.
	 * 
	 * @throws IOException If an I/O error occurs while running the process.
	 * @see #getStdout()
	 * @see #getStderr()
	 */
	public void run() {

		clearLastOutput(); // In case we throw an exception, clear output.

		Process proc = null;
		StreamReaderThread stdoutThread = null;
		StreamReaderThread stderrThread = null;

		try {

			String[] envp = createEnvVarArray();
			proc = Runtime.getRuntime().exec(commandLine, envp, dir);

			// Create threads to read the stdout and stderr of the external
			// process.  If we do not do it this way, the process may
			// deadlock.
			InputStream errStream = proc.getErrorStream();
			InputStream outStream = proc.getInputStream();
			stdoutThread = new StreamReaderThread(proc, outStream,
													outputListener, true);
			stderrThread = new StreamReaderThread(proc, errStream,
													outputListener, false);
			stdoutThread.start();
			stderrThread.start();

			rc = proc.waitFor();
			proc = null;

			// Save the stdout and stderr. Don't interrupt reader threads;
			// just wait for them to terminate normally.
			//stdoutThread.interrupt();
			//stderrThread.interrupt();
			stdoutThread.join();
			stderrThread.join();
			stdout = stdoutThread.getStreamOutput();
			stderr = stderrThread.getStreamOutput();

		} catch (IOException ioe) {
			ioe.printStackTrace();
			// IOE can only happen in Runtime.exec(), so stdoutThread and
			// stderrThread are always null if we get here
			//stdoutThread.interrupt();
			//stderrThread.interrupt();
			lastError = ioe;
			// TODO: ???
		} catch (InterruptedException ie) {
			//ie.printStackTrace();
			if (stdoutThread!=null) {
				stdoutThread.interrupt();
			}
			if (stderrThread!=null) {
				stderrThread.interrupt();
			}
			lastError = ie;
			// TODO: ???
		} finally {
			if (proc!=null) {
				proc.destroy();
			}
		}

		if (outputListener!=null) {
			outputListener.processCompleted(proc, rc, lastError);
		}

	}


	/**
	 * Sets the directory to run the process in.
	 *
	 * @param dir The directory.
	 * @see #getDirectory()
	 */
	public void setDirectory(File dir) {
		this.dir = dir;
	}


	/**
	 * Sets the command line of the process to run.
	 *
	 * @param commandLine The command line parameters to run.
	 * @throws IllegalArgumentException If commandLine has
	 *         length 0.
	 * @throws NullPointerException If commandLine is
	 *         null.
	 * @see #getCommandLineString()
	 */
	public void setCommandLine(String[] commandLine)
									throws IllegalArgumentException {
		int size = commandLine.length;
		if (size==0) {
			throw new IllegalArgumentException(
						"Must have at least 1 command line argument");
		}
		this.commandLine = new String[size];
		System.arraycopy(commandLine,0, this.commandLine,0, size);
		clearLastOutput(); // No output from this new command line yet.
	}


	/**
	 * Sets the environment variables to be set for this process.
	 *
	 * @param vars The environment variables.  This may be null if
	 *        none are to be set.
	 * @param append Whether this should be appended to the parent process's
	 *        environment.  If this is false, then the contents of
	 *        vars will be the only environment variables set.
	 * @see #getEnvironmentVars()
	 */
	public void setEnvironmentVars(Map vars, boolean append) {
		appendEnv = append;
		if (envVars!=null) {
			envVars.clear();
		}
		else {
			envVars = new HashMap();
		}
		envVars.putAll(vars);
	}


	/**
	 * Sets the output listener to receive notification when stdout or
	 * stderr is written to.  This listener will be used for all
	 * subsequent processes run with this ProcessRunner.
	 *
	 * @param listener The new listener.  The previous listener, if any,
	 *        will be removed.  If this is null, there
	 *        will be no output listener.
	 */
	public void setOutputListener(ProcessRunnerOutputListener listener) {
		this.outputListener = listener;
	}


	/**
	 * A thread dedicated to reading either the stdout or stderr stream of
	 * an external process.  These streams are read in a dedicated thread
	 * to ensure they are consumed appropriately to prevent deadlock.  This
	 * idea was taken from
	 * 
	 * this JavaWorld article.
	 * 
	 * @author Robert Futrell
	 */
	static class StreamReaderThread extends Thread {

		private Process p;
		private BufferedReader r;
		private StringBuffer buffer;
		private ProcessRunnerOutputListener listener;
		private boolean isStdout;

		/**
		 * Constructor.
		 * 
		 * @param p The running process.
		 * @param in The stream (stdout or stderr) to read from.
		 * @param listener A listener to send notification to as
		 *        output is read.  This can be null.
		 * @param isStdout Whether this thread is reading stdout (as
		 *        opposed to stderr).
		 */
		public StreamReaderThread(Process p, InputStream in,
							ProcessRunnerOutputListener listener,
							boolean isStdout) {
			this.p = p;
			r = new BufferedReader(new InputStreamReader(in));
			this.buffer = new StringBuffer();
			this.listener = listener;
			this.isStdout = isStdout;
		}

		/**
		 * Returns the output read from the stream.
		 * 
		 * @return The stream's output, as a String.
		 */
		public String getStreamOutput() {
			return buffer.toString();
		}

		/**
		 * Continually reads from the output stream until this thread is
		 * interrupted.
		 */
		public void run() {
			String line;
			try {
				while ((line=r.readLine())!=null) {
					buffer.append(line).append('\n');
					if (listener!=null) {
						listener.outputWritten(p, line, isStdout);
					}
				}
			} catch (IOException ioe) {
				buffer.append("IOException occurred: " + ioe.getMessage());
			}
		}

	}


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy