/*******************************************************************************
* Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Thomas Holland - initial API and implementation
*******************************************************************************/
package de.innot.avreclipse.core.toolinfo;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.MessageConsole;
import org.eclipse.ui.console.MessageConsoleStream;
import de.innot.avreclipse.AVRPlugin;
import de.innot.avreclipse.core.toolinfo.ICommandOutputListener.StreamSource;
/**
* Launch external programs.
* <p>
* This is a wrapper around the <code>java.lang.ProcessBuilder</code> to launch external programs
* and fetch their results.
* </p>
* <p>
* The results of the program run are stored in two <code>List<String></code> arrays, one
* for the stdout and one for stderr. Receivers can also register a {@link ICommandOutputListener}
* to get the output line by line while it is generated, for example to update the user interface.
* </p>
* <p>
* Optionally an <code>IProgressMonitor</code> can be passed to the launch method to cancel
* running commands
* </p>
*
* @author Thomas Holland
* @since 2.2
*
*/
public class ExternalCommandLauncher {
/** Lock for internal synchronization */
private final Object fRunLock;
private final ProcessBuilder fProcessBuilder;
private List<String> fStdOut;
private List<String> fStdErr;
/** The listener to be informed about each new line of output */
private ICommandOutputListener fLogEventListener = null;
private MessageConsole fConsole = null;
private final static int COLOR_STDOUT = SWT.COLOR_DARK_GREEN;
private final static int COLOR_STDERR = SWT.COLOR_DARK_RED;
/**
* A runnable class that will read a Stream until EOF, storing each line in a List and also
* calling a listener for each line.
*/
private class LogStreamRunner implements Runnable {
private final BufferedReader fReader;
private final List<String> fLog;
private final StreamSource fSource;
private MessageConsoleStream fConsoleOutput = null;
/**
* Construct a Streamrunner that will read the given InputStream and log all lines in the
* given List.
* <p>
* If a valid <code>OutputStream</code> is set, everything read by this
* <code>LogStreamRunner</code> is also written to it.
*
* @param instream
* <code>InputStream</code> to read
* @param log
* <code>List<String></code> where all lines of the instream are stored
* @param consolestream
* <code>OutputStream</code> for secondary console output, or <code>null</code>
* for no console output.
*/
public LogStreamRunner(InputStream instream, StreamSource source, List<String> log,
MessageConsoleStream consolestream) {
fReader = new BufferedReader(new InputStreamReader(instream));
fSource = source;
fLog = log;
fConsoleOutput = consolestream;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
for (;;) {
// Processes a new process output line.
// If a Listener has been registered, call it
String line = fReader.readLine();
if (line != null) {
synchronized (ExternalCommandLauncher.this) {
if (fLogEventListener != null) {
fLogEventListener.handleLine(line, fSource);
}
}
// Add the line to the total output
fLog.add(line);
// And print to the console (if active)
if (fConsoleOutput != null) {
fConsoleOutput.print(line + "\n");
}
} else {
break;
}
}
} catch (IOException e) {
// This is unlikely to happen, but log it nevertheless
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID,
"I/O Error reading output", e);
AVRPlugin.getDefault().log(status);
} finally {
try {
fReader.close();
} catch (IOException e) {
// can't do anything
}
}
synchronized (fRunLock) {
// Notify the caller that this thread is finished
fRunLock.notifyAll();
}
}
}
/**
* Creates a new ExternalCommandLauncher for the given command and a list of arguments.
*
* @param command
* <code>String</code> with the command
* @param arguments
* <code>List<String></code> with all arguments or <code>null</code> if no
* arguments
*/
public ExternalCommandLauncher(String command, List<String> arguments) {
this(command, arguments, null);
}
/**
* Creates a new ExternalCommandLauncher for the given command and a list of arguments and a
* working directory.
*
* @param command
* <code>String</code> with the command
* @param arguments
* <code>List<String></code> with all arguments or <code>null</code> if no
* arguments
* @param cwd
* <code>IPath</code> with a current working directory or <code>null</code> to
* use the default working directory (usually the one defined with the system
* property <code>user.dir</code).
*/
public ExternalCommandLauncher(String command, List<String> arguments, IPath cwd) {
Assert.isNotNull(command);
fRunLock = this;
// make a new list suitable for ProcessBuilder, where
// the command is the first entry and all other
// arguments follow
List<String> commandlist = new ArrayList<String>();
commandlist.add(command);
if (arguments != null) {
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
// If on Windows, go through all arguments and fix the quoting.
// See the comments to winQuote() below on why we need this.
for (int i = 0; i < arguments.size(); i++) {
arguments.set(i, winQuote(arguments.get(i)));
}
}
commandlist.addAll(arguments);
}
fProcessBuilder = new ProcessBuilder(commandlist);
if (cwd != null) {
fProcessBuilder.directory(cwd.toFile());
}
}
/**
* Launch the external program.
* <p>
* This method blocks until the external program has finished.
* <p>
* The output from <code>stdout</code> can be retrieved with {@link #getStdOut()}, the output
* from <code>stderr</code> likewise with {@link #getStdErr()}.
* </p>
*
* @see java.lang.Process
* @see java.lang.ProcessBuilder#start()
*
* @return Result code of the external program. Usually <code>0</code> means successful.
* @throws IOException
* An Exception from the underlying Process.
*/
public int launch() throws IOException {
return launch(new NullProgressMonitor());
}
/**
* Launch the external program with a ProgressMonitor.
* <p>
* This method blocks until the external program has finished or the ProgressMonitor is
* canceled.
* <p>
* The output from <code>stdout</code> can be retrieved with {@link #getStdOut()}, the output
* from <code>stderr</code> likewise with {@link #getStdErr()}.
* </p>
*
* @see java.lang.Process
* @see java.lang.ProcessBuilder#start()
*
* @param monitor
* A <code>IProgressMonitor</code> to cancel the running external program
* @return Result code of the external program. Usually <code>0</code> means successful. A
* canceled program will return <code>-1</code>
* @throws IOException
* An Exception from the underlying Process.
*/
public int launch(IProgressMonitor monitor) throws IOException {
Process process = null;
final MessageConsoleStream defaultConsoleStream;
final MessageConsoleStream stdoutConsoleStream;
final MessageConsoleStream stderrConsoleStream;
// Init the console output if a console has been set
// This will set the low / high water marks,
// get three MessageStreams for the console
// (default in black, stdout in dark green, stderr in dark red)
// and print a small header (incl. command name and all args)
if (fConsole != null) {
// Limit the size of the console
fConsole.setWaterMarks(8192, 16384);
// and get the output streams
defaultConsoleStream = fConsole.newMessageStream();
stdoutConsoleStream = fConsole.newMessageStream();
stderrConsoleStream = fConsole.newMessageStream();
// Set colors for the streams. This needs to be done in the UI
// thread (in which we may not be)
Display display = PlatformUI.getWorkbench().getDisplay();
if (display != null && !display.isDisposed()) {
display.syncExec(new Runnable() {
public void run() {
stdoutConsoleStream.setColor(PlatformUI.getWorkbench().getDisplay()
.getSystemColor(COLOR_STDOUT));
stderrConsoleStream.setColor(PlatformUI.getWorkbench().getDisplay()
.getSystemColor(COLOR_STDERR));
}
});
}
// Now print the Command line before any output is written to
// the console.
defaultConsoleStream.println();
defaultConsoleStream.println();
defaultConsoleStream.print("Launching ");
List<String> commandAndOptions = fProcessBuilder.command();
for (String str : commandAndOptions) {
defaultConsoleStream.print(str + " ");
}
defaultConsoleStream.println();
defaultConsoleStream.println("Output:");
} else {
// No console output requested, set all streams to null
defaultConsoleStream = null;
stdoutConsoleStream = null;
stderrConsoleStream = null;
}
// Get the name of the command (without the path)
// This is used upon exit to print a nice exit message
String command = fProcessBuilder.command().get(0);
String commandname = command.substring(command.lastIndexOf(File.separatorChar) + 1);
// After the setup we can now start the command
try {
monitor.beginTask("Launching " + fProcessBuilder.command().get(0), 100);
fStdOut = new ArrayList<String>();
fStdErr = new ArrayList<String>();
process = fProcessBuilder.start();
Thread stdoutRunner = new Thread(new LogStreamRunner(process.getInputStream(),
StreamSource.STDOUT, fStdOut, stdoutConsoleStream));
Thread stderrRunner = new Thread(new LogStreamRunner(process.getErrorStream(),
StreamSource.STDERR, fStdErr, stderrConsoleStream));
synchronized (fRunLock) {
// Wait either for the logrunners to terminate or the user to
// cancel the job.
// The monitor is polled 10 times / sec.
stdoutRunner.start();
stderrRunner.start();
monitor.worked(5);
while (stdoutRunner.isAlive() || stderrRunner.isAlive()) {
fRunLock.wait(100);
if (monitor.isCanceled() == true) {
process.destroy();
process.waitFor();
if (defaultConsoleStream != null) {
// Write an Abort Message to the console (if active)
defaultConsoleStream.println(commandname + " execution aborted");
}
return -1;
}
}
}
// external process finished normally
monitor.worked(95);
if (defaultConsoleStream != null) {
defaultConsoleStream.println(commandname + " finished");
}
} catch (InterruptedException e) {
// This thread was interrupted from outside
// consider this to be a failure of the external programm
if (defaultConsoleStream != null) {
// Write an Abort Message to the console (if active)
defaultConsoleStream.println(commandname + " execution interrupted");
}
return -1;
} finally {
monitor.done();
if (defaultConsoleStream != null)
defaultConsoleStream.close();
if (stdoutConsoleStream != null)
stdoutConsoleStream.close();
if (stderrConsoleStream != null)
stderrConsoleStream.close();
}
// if we make it to here, the process has run without any Exceptions
// Wait for the process to finish and then get the return value.
try {
process.waitFor();
return process.exitValue();
} catch (InterruptedException e) {
// If the process was interrupted by an external source we won't do anything but return
// an error value. (the return value is unused anyway throughout the plugin.
return -1;
}
}
/**
* Returns the <code>stdout</code> output from the last external Program launch.
*
* @return <code>List<String></code> with all lines or <code>null</code> if the
* external program has never been launched
*/
public List<String> getStdOut() {
return fStdOut;
}
/**
* Returns the <code>stderr</code> output from the last external Program launch.
*
* @return <code>List<String></code> with all lines or <code>null</code> if the
* external program has never been launched
*/
public List<String> getStdErr() {
return fStdErr;
}
/**
* Sets a listener that will receive all lines from the external program output as they are
* read.
* <p>
* The listener can be used to update the user interface according to the current output.
* </p>
*
* @param listener
* Object implementing the {@link ICommandOutputListener} Interface.
*/
public synchronized void setCommandOutputListener(ICommandOutputListener listener) {
Assert.isNotNull(listener);
fLogEventListener = listener;
}
/**
* Removes the current command output listener.
* <p>
* While it is safe to call this while an external program is running, it is probably better to
* just ignore superfluous output.
* </p>
*/
public synchronized void removeCommandOutputListener() {
fLogEventListener = null;
}
/**
* Redirects the <code>stderr</code> output to <code>stdout</code>.
* <p>
* Use this either when not sure which stream an external program writes its output to (some
* programs, like avr-size.exe write their help output to stderr), or when you like any error
* messages inserted into the normal output stream for analysis
* </p>
* <p>
* Note: The redirection takes place at system level, so a command output listener will only
* receive the mixed output.
* </p>
*
* @see ProcessBuilder#redirectErrorStream(boolean)
*
* @param redirect
* <code>true</code> to redirect <code>stderr</code> to <code>stdout</code>
*/
public void redirectErrorStream(boolean redirect) {
fProcessBuilder.redirectErrorStream(redirect);
}
/**
* Sets a Console where all output of the external command will go.
* <p>
* This is mostly for debugging. The output to the console is in addition to the normal logging
* of this class.
* </p>
* <p>
* This method must be called before the {@link #launch()} method. Once the external command has
* been launched, calling this method will not have any effect.
* </p>
*
* @param console
* <code>MessageConsole</code> or <code>null</code> to disable console output.
*/
public void setConsole(MessageConsole console) {
fConsole = console;
}
/**
* Correctly enclose in quotes for Windows.
* <p>
* The Windows implementation of Java has problems, if an argument contains at least one space
* and contains quotes. With a space ProcessBuilder will automatically enclose the whole
* argument in quotes (good), but quotes in the argument are not escaped (bad).
* </p>
* <p>
* This method is a workaround that will enclose the given String with quotes if required,
* escaping all quotes with the string (again as required).
* </p>
*
* @see ProcessImpl
* @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6468220">Sun Java Bug
* 6468220</a>
*
* @param s
* Source <code>String</code>
* @return String enclosed in quotes and with inner quotes escaped.
*/
private String winQuote(String s) {
if (!needsQuoting(s))
return s;
String qs = s.replaceAll("([\\\\]*)\"", "$1$1\\\\\"");
qs = qs.replaceAll("([\\\\]*)\\z", "$1$1");
return "\"" + qs + "\"";
}
/**
* Test if a String argument needs to be encapsuled in quotes.
*
* @param s
* @return <code>true</code> if the given String contains characters which would required
* quoting the string.
*/
private boolean needsQuoting(String s) {
int len = s.length();
if (len == 0) // empty string have to be quoted as well
return true;
for (int i = 0; i < len; i++) {
switch (s.charAt(i)) {
case ' ':
case '\t':
case '\\':
case '"':
return true;
}
}
return false;
}
}