package net.sourceforge.seqware.common.util.runtools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import net.sourceforge.seqware.common.module.ReturnValue;
import net.sourceforge.seqware.common.util.Log;
import net.sourceforge.seqware.common.util.iotools.BufferedReaderThread;
/**
* <p>
* RunTools class.
* </p>
*
* @author boconnor
* @version $Id: $Id
*/
public class RunTools {
/**
* <p>
* runCommand.
* </p>
*
* @param command
* a {@link java.lang.String} object.
* @return a {@link net.sourceforge.seqware.common.module.ReturnValue} object.
*/
public static ReturnValue runCommand(String command) {
return (RunTools.runCommand(splitCommandPreserveQuote(command)));
}
/**
* <p>
* runCommand.
* </p>
*
* @param command
* an array of {@link java.lang.String} objects.
* @return a {@link net.sourceforge.seqware.common.module.ReturnValue} object.
*/
public static ReturnValue runCommand(String[] command) {
return (RunTools.runCommand(null, command));
}
public static ReturnValue runCommand(Map<String, String> env, String[] command) {
return runCommand(env, command, Integer.MAX_VALUE, Integer.MAX_VALUE, null);
}
public static ReturnValue runCommand(Map<String, String> env, String[] command, int stdoutCapacity, int stderrCapacity) {
return runCommand(env, command, stdoutCapacity, stderrCapacity, null);
}
/**
* A simple command runner that captures the return value of the program. If the command was OK it should (in most cases) return 0. This
* util assumes this is the case.
*
* FIXME: Jordan, the Process object does not start the command within a shell environment, so if you're depending on environmental
* variables you'll run into problems, see http://java.sun.com/javase/6/docs/api/java/lang/Process.html
*
* @param command
* an array of {@link java.lang.String} objects.
* @param env
* a {@link java.util.Map} object.
* @param stdoutCapacity
* @param stderrCapacity
* @param permanentStoragePrefix
* store a permanent copy of output at permanentStoragePrefix.stderr and .stdout
* @return a {@link net.sourceforge.seqware.common.module.ReturnValue} object.
*/
public static ReturnValue runCommand(Map<String, String> env, String[] command, int stdoutCapacity, int stderrCapacity,
String permanentStoragePrefix) {
ReturnValue ret = new ReturnValue();
Process p = null;
// Log.debug("Env:"+env);
// Log.debug("Command:");
// for (String s: command)
// {
// Log.debug(s);
// }
// Log.debug("End Command");
try {
p = startCommand(env, command);
} catch (Exception e) {
// make sure return value is not success if we got a exception!
if (p != null) {
ret.setProcessExitStatus(p.exitValue());
} else {
Log.error("The result of the process was null - env:" + env + " cmd:" + Arrays.toString(command), e);
ret.setProcessExitStatus(ReturnValue.PROGRAMFAILED);
}
if (ret.getExitStatus() == ReturnValue.SUCCESS) {
Log.error("An exception was thrown but the return code was 0 - success", e);
ret.setExitStatus(ReturnValue.PROGRAMFAILED);
}
e.printStackTrace();
ret.setStderr(e.getMessage());
return ret;
}
ret = waitAndGetReturn(p, stdoutCapacity, stderrCapacity, permanentStoragePrefix);
return ret;
}
/**
* Wait on a process to finish, then parse information into return value
*
* @param p
* a {@link java.lang.Process} object.
* @param stdoutLineCapacity
* limit the number of lines stored or set to 0 to store everything
* @param stderrLineCapacity
* limit the number of lines stored or set to 0 to store everything
* @param permanentStoragePrefix
* @return a {@link net.sourceforge.seqware.common.module.ReturnValue} object.
*/
public static ReturnValue waitAndGetReturn(Process p, int stdoutLineCapacity, int stderrLineCapacity, String permanentStoragePrefix) {
ReturnValue ret = new ReturnValue();
// Make sure we weren't passed a null process
if (p == null) {
// FIXME: Return ReturnValue indicating it was NULL, and there was an error
}
try {
// Spawn reader threads to grab stdout and stderr
BufferedReaderThread stdOutThread = new BufferedReaderThread(p.getInputStream(), stdoutLineCapacity,
permanentStoragePrefix != null ? new File(permanentStoragePrefix + ".stdout") : null);
BufferedReaderThread stdErrThread = new BufferedReaderThread(p.getErrorStream(), stderrLineCapacity,
permanentStoragePrefix != null ? new File(permanentStoragePrefix + ".stderr") : null);
stdOutThread.start();
stdErrThread.start();
// Wait on thread and process
stdOutThread.join();
stdErrThread.join();
p.waitFor();
// When all is done, get return and output
ret.setProcessExitStatus(p.exitValue());
// save output
ret.setStdout(stdOutThread.getOutput());
ret.setStderr(stdErrThread.getOutput());
ret.setExitStatus(ReturnValue.SUCCESS);
// Check for errors
if (p.exitValue() != 0) {
Log.stdout(stdOutThread.getOutput());
Log.stderr(stdErrThread.getOutput());
Log.error("The exit value was " + p.exitValue());
ret.setExitStatus(ReturnValue.PROGRAMFAILED);
}
if (stdOutThread.getError() != null) {
ret.getStderr().concat("Reading stdout threw an exception:" + stdOutThread.getError());
ret.setExitStatus(ReturnValue.PROGRAMFAILED);
}
if (stdErrThread.getError() != null) {
ret.getStderr().concat("Reading stderr threw an exception:" + stdErrThread.getError());
ret.setExitStatus(ReturnValue.PROGRAMFAILED);
}
} catch (Exception e) {
// make sure return value is not success if we got a exception!
if (p != null) {
ret.setProcessExitStatus(p.exitValue());
} else {
Log.error("The result of the process was null", e);
ret.setProcessExitStatus(ReturnValue.PROGRAMFAILED);
}
if (ret.getExitStatus() == ReturnValue.SUCCESS) {
Log.error("An exception was thrown but the return code was 0 - success", e);
ret.setExitStatus(ReturnValue.PROGRAMFAILED);
}
e.printStackTrace();
ret.getStderr().concat(e.getMessage());
}
return ret;
}
/**
* An alternate runner that launches a process and returns it so that the caller can process it's output directly. A use case is when we
* need to output stdout or stderr to a file in real-time, or if it will be too large to buffer in RAM and return.
*
* @throws java.io.IOException
* if any.
* @param env
* a {@link java.util.Map} object.
* @param command
* an array of {@link java.lang.String} objects.
* @return a {@link java.lang.Process} object.
*/
// FIXME: This should instantiate the shell. Modules should not need to say
// FIXME: RunTools.runCommand( new String[] { "bash", "-c", cmd.toString() } );
// FIXME: Instead they should just say:
// FIXME: RunTools.runCommand( cmd.toString() ) with this function adding the bash -c logic
// FIXME: Doing this will break modules, so when we do this, also need to remove the bash -c from whatever calls us
public static Process startCommand(Map<String, String> env, String[] command) throws IOException {
ProcessBuilder pb = new ProcessBuilder(command);
Map<String, String> envMap = pb.environment();
if (env != null) {
for (String key : env.keySet()) {
envMap.put(key, env.get(key));
}
}
return pb.start();
}
/**
* <p>
* main.
* </p>
*
* @param args
* an array of {@link java.lang.String} objects.
*/
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (String token : args) {
sb.append(token).append(" ");
}
ReturnValue rv = RunTools.runCommand(sb.toString().trim());
Log.info("Process status: " + rv.getProcessExitStatus());
Log.info("My status: " + rv.getExitStatus());
Log.info("Error status: " + rv.getStderr());
Log.info("Output:\n" + rv.getStdout());
}
/**
* A little helper that splits up a command into an array but preserves any options that are quoted (either ' or ") as single entries in
* the array rather than further breaking them up.
*
* This won't work well if you have something like this:
*
* bash -c 'echo "foo 'bar' "'
*
* It won't like the single quotes inside single quotes.
*
* This method preserves the outside quotes so they will be passed to the underlying shell
*
* @param command
* @return
*/
private static String[] splitCommandPreserveQuote(String command) {
ArrayList<String> result = new ArrayList<>();
String[] tokens = command.split("\\s+");
boolean matching = false;
String quoteString = null;
StringBuffer match = new StringBuffer();
for (String t : tokens) {
if (matching && !t.endsWith(quoteString)) {
match.append(" ").append(t);
} else if (!matching && t.startsWith("\"")) {
quoteString = "\"";
matching = true;
match.append(t.substring(1));
} else if (!matching && t.startsWith("'")) {
quoteString = "'";
matching = true;
match.append(t.substring(1));
} else if (matching && t.endsWith(quoteString)) {
match.append(" ").append(t.substring(0, t.length() - 1));
matching = false;
quoteString = null;
result.add(match.toString());
match = new StringBuffer();
} else {
result.add(t);
}
}
return (result.toArray(new String[result.size()]));
}
}