/*******************************************************************************
* Copyright (c) 2009 Cloudsmith Inc. 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:
* Cloudsmith Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.buckminster.util.progress;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IProgressMonitorWithBlocking;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
/**
* An ExternalProgressMonitor is a IProgressMonitorWithBlocking implementation that adapts a "local" progress monitor
* to handle progress monitoring from an external process. Subclasses of this abstract class provide concrete
* implementations for different communication protocols.
*
* <h4>Usage</h4>
* Instantiate this class with the parent (i.e. real) progress monitor that would have been used if the work
* had been performed directly in the same process.
* Then, set the command string to the values expected by {@link Runtime#exec(String[])}.
* Optionally set a prefix String (that is prefixed to messages in the communication) to enable filtering out other
* content on the same channel. This is primarily useful for running shell or bat scripts via Stdin/Stdout and where
* these scripts also produce other output that should be ignored. A suitable prefix is "___". By default, no prefix
* is used.
* Subclasses may require additional setup.
* Finally, when everything is setup, call {@link #execute()} to run and monitor the external command.
*
* Note that it is possible to cancel the external job by simply calling {@link #setCanceled(boolean)} if the
* externally executing process is using an instance of {@link ExternalProgressProxyMonitor} or is compatible with
* how the cancelation is performed. See {@link ExternalProgressProxyMonitor} for more information.
*
* See {@link StdInOutProgressMonitor} and {@link SocketProgressMonitor} for two concrete
* ExternalProgressMonitor classes.
*
* @author henrik.lindberg@cloudsmith.com
*
*/
public abstract class ExternalProgressMonitor implements IProgressMonitor, IProgressMonitorWithBlocking, ExternalProgressMonitorConstants {
private IProgressMonitor monitor;
private boolean cancelSent;
protected OutputStreamWriter out;
protected String[] cmd;
protected String prefix = DEFAULT_PREFIX;
/**
* Create an external monitor for the real monitor (that would have been used if the work was performed
* directly). The parent monitor should not have been used.
* @param parentMonitor
*/
public ExternalProgressMonitor(IProgressMonitor parentMonitor) {
monitor = parentMonitor;
cancelSent = false;
}
/**
* Sets up filtering in the communication so only lines received with a matching prefix
* will be parsed. (All other lines are ignored. Naturally, the proxy (or schell script) must be aware of the
* prefix. A suitable prefix is "___".
* @param prefixString
*/
public void setPrefix(String prefixString) {
prefix = prefixString;
}
/**
* Sets the arguments to pass to {@link Runtime#exec(String[])}. This describes what will be executed.
* @param execArgs
*/
public void setCmd(String[] execArgs) {
cmd = execArgs;
}
/**
* Reads monitor instructions from the input stream and adapts them to this
* instance IProgressMonitor API.
* @return
*/
public abstract int execute();
/**
* Reads an processes the stream from the executing process.
* @param inputStream
*/
protected void processStream(InputStream inputStream) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
String data;
while ((data = reader.readLine()) != null) {
if (isCanceled() && !cancelSent)
processCancelation();
if (data.startsWith(prefix))
data = data.substring(prefix.length());
else
continue; // filter out non matching strings
try {
// parse and act
switch (data.charAt(0)) {
case WORKED :
worked(Integer.parseInt(data.substring(1)));
break;
case INTERNAL_WORKED :
internalWorked(Double.parseDouble(data.substring(1)));
break;
case WORKED_ONE :
worked(1);
break;
case SET_TASK_NAME :
setTaskName(data.substring(1));
break;
case SUB_TASK :
subTask(data.substring(1));
break;
case CLEAR_BLOCKED :
clearBlocked();
break;
case SET_BLOCKED :
setBlocked(new Status(IStatus.OK, Activator.PLUGIN_ID, data.substring(1)));
break;
case DONE :
done();
break;
case BEGIN :
int commaPos = data.indexOf(',');
int totalWork = Integer.parseInt(data.substring(1, commaPos));
String name = data.substring(commaPos + 1);
beginTask(name, totalWork);
break;
case SET_CANCELED :
setCanceled(data.charAt(1) == 't');
break;
case CHECK_CANCEL :
default :
// text that is not part of the protocol - assume monitoring is broken
}
} catch (NumberFormatException e) {
// ignore - bad input
} catch (IndexOutOfBoundsException e) {
// ignore - bad input
}
}
} catch (IOException e) {
// ignore
} finally {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* Sinks an input stream (i.e. reads it and throws away what it reads).
* Compare to directing output to /dev/null on a UN*X box.
* The sinking is done in a separate thread which terminates when the input is closed.
*
* @param inputStream
*/
public static void sinkStream(final InputStream inputStream) {
Runnable sinker = new Runnable() {
// read stream and ignore until stream is closed
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
while (reader.readLine() != null) {
// do nothing
}
} catch (IOException e) {
// ignore
} finally {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}
};
new Thread(sinker).start();
}
public void beginTask(String name, int totalWork) {
monitor.beginTask(name, totalWork);
}
public void done() {
monitor.done();
}
public void internalWorked(double work) {
monitor.internalWorked(work);
}
public boolean isCanceled() {
return monitor.isCanceled();
}
public void setCanceled(boolean value) {
monitor.setCanceled(value);
if (isCanceled() && !cancelSent)
processCancelation();
}
/**
* Sends cancellation message to client.
*/
private void processCancelation() {
Runnable canceler = new Runnable() {
public void run() {
try {
synchronized (out) {
// System.err.print("Sending CANCEL\n");
if (!cancelSent) {
out.write(prefix + SET_CANCELED + (isCanceled() ? "t" : "f") + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
out.flush();
}
cancelSent = true;
}
} catch (IOException e) {
/* ignore - can't cancel */
}
}
};
new Thread(canceler).start();
}
public void setTaskName(String name) {
monitor.setTaskName(name);
}
public void subTask(String name) {
monitor.subTask(name);
}
public void worked(int work) {
monitor.worked(work);
}
public void clearBlocked() {
if (monitor instanceof IProgressMonitorWithBlocking)
((IProgressMonitorWithBlocking) monitor).clearBlocked();
}
public void setBlocked(IStatus reason) {
if (monitor instanceof IProgressMonitorWithBlocking)
((IProgressMonitorWithBlocking) monitor).setBlocked(reason);
}
}