/******************************************************************************* * * Copyright (c) 2008, 2010 Thomas Holland (thomas@innot.de) and others * * This program and the accompanying materials are made * available under the terms of the GNU Public License v3 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * Thomas Holland - initial API and implementation * * $Id: ExternalCommandLauncher.java 851 2010-08-07 19:37:00Z innot $ * *******************************************************************************/ package io.sloeber.core.tools; 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.Arrays; import java.util.List; import org.eclipse.core.runtime.Assert; 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 io.sloeber.core.common.Common; import io.sloeber.core.common.Const; /** * 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 */ protected final Object fRunLock; private final ProcessBuilder fProcessBuilder; private List<String> fStdOut; private List<String> fStdErr; 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 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, List<String> log, MessageConsoleStream consolestream) { this.fReader = new BufferedReader(new InputStreamReader(instream)); this.fLog = log; this.fConsoleOutput = consolestream; } /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @Override public void run() { try { for (;;) { // Processes a new process output line. // If a Listener has been registered, call it String line = this.fReader.readLine(); if (line != null) { // Add the line to the total output this.fLog.add(line); // And print to the console (if active) if (this.fConsoleOutput != null) { this.fConsoleOutput.print(line + '\n'); } } else { break; } } } catch (IOException e) { // This is unlikely to happen, but log it nevertheless IStatus status = new Status(IStatus.ERROR, Const.CORE_PLUGIN_ID, Messages.command_io, e); Common.log(status); } finally { try { this.fReader.close(); } catch (IOException e) { // can't do anything } } synchronized (ExternalCommandLauncher.this.fRunLock) { // Notify the caller that this thread is finished ExternalCommandLauncher.this.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 * all arguments */ public ExternalCommandLauncher(String command) { Assert.isNotNull(command); this.fRunLock = this; String[] commandParts = command.split(" +(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); //$NON-NLS-1$ for (int curCommand = 0; curCommand < commandParts.length; curCommand++) { if (commandParts[curCommand].startsWith("\"") && commandParts[curCommand].endsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$ commandParts[curCommand] = commandParts[curCommand].substring(1, commandParts[curCommand].length() - 1); } } this.fProcessBuilder = new ProcessBuilder(Arrays.asList(commandParts)); } public ExternalCommandLauncher(List<String> command) { Assert.isNotNull(command); this.fRunLock = this; this.fProcessBuilder = new ProcessBuilder(command); } /** * 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. */ @SuppressWarnings("resource") public int launch(IProgressMonitor inMonitor) throws IOException { IProgressMonitor monitor = inMonitor; if (monitor == null) { monitor = new NullProgressMonitor(); } 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 (this.fConsole != null) { // Limit the size of the console this.fConsole.setWaterMarks(8192, 16384); // and get the output streams defaultConsoleStream = this.fConsole.newMessageStream(); stdoutConsoleStream = this.fConsole.newMessageStream(); stderrConsoleStream = this.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() { @Override 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(Messages.command_launching); List<String> commandAndOptions = this.fProcessBuilder.command(); for (String str : commandAndOptions) { defaultConsoleStream.print(str + Const.SPACE); } defaultConsoleStream.println(); defaultConsoleStream.println(Messages.command_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 = this.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(Messages.command_launching + this.fProcessBuilder.command().get(0), 100); this.fStdOut = new ArrayList<>(); this.fStdErr = new ArrayList<>(); this.fProcessBuilder.directory(Common.getWorkspaceRoot().toPath().toFile()); process = this.fProcessBuilder.start(); Thread stdoutRunner = new Thread( new LogStreamRunner(process.getInputStream(), this.fStdOut, stdoutConsoleStream)); Thread stderrRunner = new Thread( new LogStreamRunner(process.getErrorStream(), this.fStdErr, stderrConsoleStream)); synchronized (this.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()) { this.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 + Messages.command_aborted); } return -1; } } } // external process finished normally monitor.worked(95); if (defaultConsoleStream != null) { defaultConsoleStream.println(commandname + Messages.command_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 + Messages.command_interupted); } 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 this.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 this.fStdErr; } /** * 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) { this.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) { this.fConsole = console; } }