package de.skuzzle.polly.process;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>This class handles the system dependent task of creating and interacting with
* processes. It currently does not support MAC OS.</p>
*
* <p>A concrete instance can be obtained using one of the static factory methods
* {@link #getOsInstance(boolean)} or {@link #getOsInstance(boolean, List)}.</p>
*
* <h2>StreamHandlers</h2>
* When a process is created, it is important to empty its standard out and standard
* error streams, as otherwise deadlocks may occur (as by the documentation of
* {@link Process}). {@link StreamHandler StreamHandlers} are threads that asynchronously
* read data from the created processes to ensure that all buffers are emptied regularly.
* A StreamHandler instance can only be attached to a process before it is executed using
* {@link #setInputHandler(StreamHandler)} and {@link #setErrorHandler(StreamHandler)}.
* If no StreamHandler is set, {@link SilentStreamHandler SilentStreamHandlers} are used
* by default.
*
* <h2>ProcessWatchers</h2>
* A ProcessWatcher is, like a StreamHandler, a separate thread which is started along
* with executing a process. It will then be notified when the created process is
* shutdown (or optionally a timeout expired).
*
* @author Simon Taddiken
*/
public abstract class ProcessExecutor {
/** Determines whether we are running UNIX */
public final static boolean IS_UNIX = ProcessExecutor.isUnix();
/** Determines whether we are running on MAC OS */
public final static boolean IS_MAC = ProcessExecutor.isMac();
/** Determines whether we are running Windows */
public final static boolean IS_WINDOWS = ProcessExecutor.isWindows();
private static boolean isWindows() {
String os = System.getProperty("os.name").toLowerCase();
return (os.indexOf("win") >= 0);
}
private static boolean isMac() {
String os = System.getProperty("os.name").toLowerCase();
return (os.indexOf("mac") >= 0);
}
private static boolean isUnix() {
String os = System.getProperty("os.name").toLowerCase();
return (os.indexOf("nix") >=0 || os.indexOf("nux") >=0);
}
/**
* Creates a proper {@link ProcessExecutor} for the current OS. MAC OS is currently
* not supported.
*
* @param runInConsole This flag is system dependent. If set to false, the
* ProcessExecutor will create the new process running in background. If set
* to true on Windows machines, the ProcessExecutor opens a new Console
* window running the process. On Unix machines the executor creates a new
* shell process which then executes the command.
* @param commands List of commandline arguments for the created ProcessExecutor.
* @return A new ProcessExecutor instance.
*/
public static ProcessExecutor getOsInstance(boolean runInConsole,
List<String> commands) {
if (IS_WINDOWS) {
return new WindowsProcessExecutor(runInConsole, commands);
} else if (IS_UNIX) {
return new UnixProcessExecutor(runInConsole, commands);
} else {
throw new UnsupportedOperationException("os not supported");
}
}
/**
* Creates a proper {@link ProcessExecutor} for the current OS. MAC OS is currently
* not supported.
*
* @param runInConsole This flag is system dependent. If set to false, the
* ProcessExecutor will create the new process running in background. If set
* to true on Windows machines, the ProcessExecutor opens a new Console
* window running the process. On Unix machines the executor creates a new
* shell process which then executes the command.
* @return A new ProcessExecutor instance.
*/
public static ProcessExecutor getOsInstance(boolean runInConsole) {
return ProcessExecutor.getOsInstance(runInConsole, new LinkedList<String>());
}
/**
* ProcessExecutor implementation for Windows machines.
*
* @author Simon Taddiken
*/
protected static class WindowsProcessExecutor extends ProcessExecutor {
public WindowsProcessExecutor(boolean runInConsole, List<String> commands) {
super(runInConsole);
if (!IS_WINDOWS) {
throw new IllegalStateException("invalid os");
}
if (runInConsole) {
// HACK: using the 'start' command causes a new window to open but also
// creates another process we can not watch. We can only watch the
// cmd process. As this process terminates when the other process
// terminates we are at least able to watch for termination.
// TODO: Find a way to redirect the output of the process created
// by 'start' to the 'cmd' process.
this.commands.add("cmd");
this.commands.add("/c");
this.commands.add("start");
this.commands.add("\"cmd\""); // window title
}
this.commands.addAll(commands);
}
@Override
protected Process doStart(ProcessBuilder pb) throws IOException {
pb.command().addAll(this.commands);
return pb.start();
}
@Override
protected String escapeCommand(String command) {
if (command.contains(" ")) {
command = "\"" + command + "\"";
}
return command;
}
}
/**
* ProcessExecutor implementation for UNIX machines.
*
* @author Simon Taddiken
*/
protected static class UnixProcessExecutor extends ProcessExecutor {
private final static List<File> SHELLS = new ArrayList<File>();
private final static List<String> SHELL_ARGS = new ArrayList<String>();
static {
SHELLS.add((new File("/bin/bash").getAbsoluteFile()));
SHELL_ARGS.add("-lic");
SHELLS.add((new File("/bin/sh").getAbsoluteFile()));
SHELL_ARGS.add("-c");
}
private List<String> processCommands;
public UnixProcessExecutor(boolean runInConsole, List<String> commands) {
super(runInConsole);
if (!IS_UNIX && !IS_MAC) {
throw new UnsupportedOperationException("invalid os");
}
this.processCommands = new LinkedList<String>();
if (runInConsole) {
boolean shellFound = false;
for (int i = 0; i < SHELLS.size(); ++i) {
if (SHELLS.get(i).exists()) {
this.processCommands.add(SHELLS.get(i).getAbsolutePath());
this.addCommandsFromString(SHELL_ARGS.get(i), this.processCommands);
shellFound = true;
break;
}
}
if (!shellFound) {
throw new UnsupportedOperationException(
"running in console not supported: no shell found");
}
}
this.commands.addAll(commands);
}
@Override
protected Process doStart(ProcessBuilder pb) throws IOException {
if (this.runInConsole) {
pb.command().addAll(this.processCommands);
StringBuilder cmd = new StringBuilder();
for (String command : this.commands) {
cmd.append(command);
cmd.append(" ");
}
pb.command().add("\"" + cmd.toString().trim() + "\"");
} else {
pb.command().addAll(this.commands);
}
return pb.start();
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
if (this.runInConsole) {
for (String cmd : this.processCommands) {
b.append(cmd);
b.append(" ");
}
b.append("\"");
b.append(super.toString());
b.append("\"");
} else {
b.append(super.toString());
}
return b.toString();
}
}
protected List<String> commands;
private boolean valid;
protected boolean runInConsole;
protected File executeIn;
private StreamHandler input;
private StreamHandler error;
private ProcessWatcher watcher;
private final static Pattern PATTERN = Pattern.compile("[^\\s\"]+|(\"[^\"]+\")");
/**
*
* @param runInConsole This flag is system dependent. If set to false, the
* ProcessExecutor will create the new process running in background. If set
* to true on Windows machines, the ProcessExecutor opens a new Console
* window running the process. On Unix machines the executor creates a new
* shell process which then executes the command.
*/
public ProcessExecutor(boolean runInConsole) {
this.runInConsole = runInConsole;
this.commands = new LinkedList<String>();
this.valid = true;
this.executeIn = new File(".");
this.input = new SilentStreamHandler("SILENT_INPUT_HANDLER");
this.error = new SilentStreamHandler("SILENT_ERROR_HANDLER");
}
/**
* Adds a single commandline argument to this executor.
*
* @param cmd The command to add.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor addCommand(String cmd) {
this.commands.add(cmd);
return this;
}
/**
* Adds a list of commandline arguments to this executor.
*
* @param cmds The commands to add.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor addCommand(List<String> cmds) {
for (String cmd : cmds) {
this.addCommand(cmd);
}
return this;
}
/**
* Parses the given string like a commandline tool would do. Parts wrapped in
* quotes (") will form a single argument, otherwise, the string is splitted at
* whitespaces.
*
* @param commandLine The string to parse.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor addCommandsFromString(String commandLine) {
this.addCommandsFromString(commandLine, this.commands);
return this;
}
/**
* Sets the working directory for the process to create.
*
* @param executeIn The working directory.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor setExecuteIn(File executeIn) {
this.executeIn = executeIn;
return this;
}
/**
* Gets the working directory for the process to create. By default, this will be
* the directory from which the current JVM is run.
*
* @return The working directory.
*/
public File getExecuteIn() {
return this.executeIn;
}
/**
* Sets the {@link StreamHandler} that will read all the data from the created
* process's standard output.
*
* @param input Standard out StreamHandler.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor setInputHandler(StreamHandler input) {
this.input = input;
return this;
}
/**
* Gets the {@link StreamHandler} that will read all the data from the created
* process's standard output.
* @return The StreamHandler.
*/
public StreamHandler getInputHandler() {
return this.input;
}
/**
* Gets the {@link StreamHandler} that will read all the data from the created
* process's standard error output.
* @return The StreamHandler.
*/
public StreamHandler getErrorHandler() {
return this.error;
}
/**
* Sets the {@link StreamHandler} that will read all the data from the created
* process's standard error output.
*
* @param error Standard out StreamHandler.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor setErrorHandler(StreamHandler error) {
this.error = error;
return this;
}
/**
* Sets a {@link ProcessWatcher} which will be notified when the create process is
* shutdown.
*
* @param watcher The ProcessWatcher to set.
* @return This instance to enable setter chaining.
*/
public ProcessExecutor setProcessWatcher(ProcessWatcher watcher) {
this.watcher = watcher;
return this;
}
protected void addCommandsFromString(String commandLine, List<String> into) {
Matcher m = PATTERN.matcher(commandLine);
while (m.find()) {
String substr = commandLine.substring(m.start(), m.end());
if (substr.startsWith("\"")) {
if (!substr.endsWith("\"")) {
throw new IllegalArgumentException("commandline format fail: " +
substr);
}
substr = substr.substring(1, substr.length() - 1);
}
into.add(substr);
}
}
/**
* <p>Starts the process using the parameters supplied by this class' setter methods.
* If no custom {@link StreamHandler StreamHandlers} was set,
* {@link SilentStreamHandler} will be used by default. By default, no
* {@link ProcessWatcher} will be attached if not set by
* {@link #setProcessWatcher(ProcessWatcher)}.</p>
*
* <p>Note: this method can only be invoked once on this instance.</p>
* @return A {@link ProcessWrapper} representing the created process.
* @throws IOException If creation of the process fails.
*/
public ProcessWrapper start() throws IOException {
if (!this.valid) {
throw new IllegalStateException("already started");
}
try {
ProcessBuilder pb = new ProcessBuilder(new LinkedList<String>());
pb.directory(this.executeIn);
Process proc = this.doStart(pb);
ProcessWrapper pw = new ProcessWrapper(proc, this.input, this.error);
this.input.setStream(proc.getInputStream());
this.error.setStream(proc.getErrorStream());
this.input.start();
this.error.start();
if (this.watcher != null) {
this.watcher.setProc(pw);
this.watcher.start();
}
return pw;
} finally {
this.valid = false;
}
}
/**
* System dependent method to start a process.
*
* @param pb The {@link ProcessBuilder} that will be used to create the process.
* @return The created process.
* @throws IOException If creation of the process fails.
*/
protected abstract Process doStart(ProcessBuilder pb) throws IOException;
/**
* System dependent method to quote a commandline argument when it contains
* whitespaces.
*
* @param command The argument to escape.
* @return The escaped argument.
*/
protected String escapeCommand(String command) {
return command;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
for (String cmd : this.commands) {
if (cmd.indexOf(" ") != -1) {
b.append("\"");
b.append(cmd);
b.append("\"");
} else {
b.append(cmd);
}
b.append(" ");
}
return b.toString().trim();
}
}