package za.co.enerweb.toolbox.io;
import static za.co.enerweb.toolbox.string.StringUtils.nullString2EmptyString;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import za.co.enerweb.toolbox.io.internal.OutputWaiter;
import za.co.enerweb.toolbox.string.StringUtils;
import za.co.enerweb.toolbox.timing.Stopwatch;
// leaving the following there for future reference
// if( systemCommandMode.equals(SystemCommandMode.ONELINE )){
// cl.runCommandLine(cmd);
// }else if( systemCommandMode.equals(SystemCommandMode.BASH )){
// cl.runCommandLine("bash");
// cl.writelnToStdIn(cmd);
// cl.writelnToStdIn("exit");
// }else if( systemCommandMode.equals(SystemCommandMode.COMMAND )){
// cl.runCommandLine("command");
// cl.writelnToStdIn(cmd);
// cl.writelnToStdIn("exit");
// }
// rather do "cmd /c" or "bash -c"
/**
* Utility for running commands. Suitable defaults are used for handling output,
* the working dir etc. XXX: We can later provide another form that can pass in
* input.
*/
@Slf4j
public class CommandUtils {
private static final int MILLISECONDS_TO_WAIT_FOR_OUTPUT = 300;
protected OutputHandler stdoutHandler;
private OutputHandler stderrHandler;
protected Writer stdin;
protected Process process;
protected OutputWaiter stdoutWaiter;
private OutputWaiter stderrWaiter;
private String[] commandAndArgs;
private boolean deleteWorkDirWhenDone = false;
protected String command;
private String commandInPath = null;
private String[] args;
private File workingDir;
private boolean checkExitCode = true;
private String[] environment = new String[0];
private boolean cacheOutput = true;
private static CommandUtils simpleExecute;
public static String simpleExecute(String... commandAndArgs)
throws IOException {
return simpleExecute(null, commandAndArgs);
}
public static String simpleExecute(File workdir, String... commandAndArgs)
throws IOException {
if (simpleExecute == null) {
synchronized (CommandUtils.class) {
if (simpleExecute == null) {
simpleExecute = new CommandUtils();
}
}
}
simpleExecute.setWorkingDir(workdir);
simpleExecute.setCommandAndArgs(commandAndArgs);
simpleExecute.excecute();
return simpleExecute.getStdOutOutput();
}
public CommandUtils() {
}
public CommandUtils(final String command) {
this.command = command;
}
public CommandUtils(final String command, final String... args) {
this.command = command;
this.args = args;
}
public CommandUtils command(final String command) {
this.command = command;
commandInPath = null;
return this;
}
public void setArgs(final String... args) {
this.args = args;
}
public CommandUtils args(final String... args) {
this.args = args;
return this;
}
public void setCommandAndArgs(final String... commandAndArgs) {
command(commandAndArgs[0]);
args = new String[commandAndArgs.length - 1];
System.arraycopy(commandAndArgs, 1, args, 0, args.length);
}
public void mustCopyEnvironmentToSubprocess() {
setEnvironment(null);
}
/**
* (Resets output handlers)
*/
public void setCacheOutput(boolean cacheOutput) {
this.cacheOutput = cacheOutput;
stdoutHandler = null;
stderrHandler = null;
}
/**
* Will use string builder output handlers.
* (Resets output handlers)
*/
public void cacheOutput() {
setCacheOutput(true);
}
/**
* Just log the output.
* (Resets output handlers)
*/
public void logOutput() {
setCacheOutput(false);
}
public String getStdOutOutput() {
return getOutput(stdoutHandler);
}
public String getStdErrOutput() {
return getOutput(stderrHandler);
}
private String getOutput(final OutputHandler outputHandler) {
if (outputHandler instanceof StringBuilderOutputHandler) {
return ((StringBuilderOutputHandler) outputHandler).toString();
}
throw new IllegalStateException("The output was not cached.");
}
protected void start() throws IOException {
validateNotStarted();
validateSetup();
if (commandInPath == null) {
commandInPath = findFileInPath(workingDir, command)
.getAbsolutePath();
}
commandAndArgs = new String[args.length + 1];
commandAndArgs[0] = commandInPath;
System.arraycopy(args, 0, commandAndArgs, 1, args.length);
workingDir.mkdirs();
log.debug("Executing:\n{}\n(In {})", toString(),
workingDir.getAbsolutePath());
process = Runtime.getRuntime().exec(commandAndArgs, environment,
workingDir);
stdin = new OutputStreamWriter(new BufferedOutputStream(process
.getOutputStream()));
stdoutWaiter = new OutputWaiter(process.getInputStream(),
stdoutHandler);
stderrWaiter = new OutputWaiter(process.getErrorStream(),
stderrHandler);
stdoutWaiter.start();
stderrWaiter.start();
}
public void stop() {
if (process != null) {
process.destroy();
}
}
protected void validateStarted() {
if (process == null) {
throw new IllegalStateException("Process not started");
}
}
protected void validateNotStarted() {
if (process != null) {
throw new IllegalStateException("Process started already.");
}
}
protected void validateSetup() {
if (command == null) {
throw new IllegalStateException("No command specified");
}
// use suitable defaults if they are not provided
if (args == null) {
args = new String[0];
}
if (stdoutHandler == null) {
if (cacheOutput) {
setStdoutHandler(new StringBuilderOutputHandler());
} else {
stdoutHandler = new LogOutputHandler();
}
} else {
stdoutHandler.clear();
}
if (stderrHandler == null) {
if (cacheOutput) {
setStderrHandler(new StringBuilderOutputHandler());
} else {
stderrHandler = new LogOutputHandler();
}
} else {
stderrHandler.clear();
}
if (workingDir == null) {
workingDir = TempDir.createTempDir();
deleteWorkDirWhenDone = true;
} else {
deleteWorkDirWhenDone = false;
}
}
@Override
public String toString() {
return StringUtils.join(commandAndArgs, " ");
}
/**
* If we get interrupted, the external process will be killed.
*
* @return
* @throws IOException
*/
public int excecute() throws IOException {
start();
int exitValue = -1;
boolean interrupted = false;
try {
exitValue = process.waitFor();
} catch (InterruptedException e) {
log.info("Interrupted while running " + toString()
+ ", killing it now.", e);
process.destroy();
interrupted = true;
}
if (!interrupted) {
// wait a little for the output, if we were not interrupted.
try {
stdoutWaiter.join(MILLISECONDS_TO_WAIT_FOR_OUTPUT);
stderrWaiter.join(MILLISECONDS_TO_WAIT_FOR_OUTPUT);
} catch (InterruptedException e) {
log.debug("Interrupted while waiting for output.", e);
}
} else {
stdoutWaiter.interrupt();
stderrWaiter.interrupt();
}
if (deleteWorkDirWhenDone) {
FileUtils.deleteQuietly(workingDir);
workingDir = null;
}
process = null;
if (checkExitCode) {
checkExitCode(exitValue);
}
return exitValue;
}
private void checkExitCode(final int exitValue) {
if (exitValue != 0) {
throw new RuntimeException("Non-zero exit value " + exitValue
+ " while executing:\n" + toString()
+ "\n(with working dir: " + workingDir.getAbsolutePath() + ")");
}
}
/**
* look for the file in the working dir, the check if its absolute then look
* for it in the system $PATH TODO: look for .exe, .com, .bat, .cmd on
* windows too
*
* @param workingDir
* @param fileName
* @return
* @throws FileNotFoundException
*/
public static File findFileInPath(final File workingDir,
final String fileName) throws FileNotFoundException {
// see if its in the working dir
File ret = new File(workingDir, fileName);
if (ret.isFile()) {
return ret;
}
// see if its absolute
ret = new File(fileName);
if (ret.isFile()) {
return ret;
}
// look for it on the path
String systemPath = nullString2EmptyString(System.getenv("PATH"));
String[] pathDirs = systemPath.split(File.pathSeparator);
for (String pathDir : pathDirs) {
ret = new File(pathDir, fileName);
if (ret.isFile()) {
return ret;
}
}
throw new FileNotFoundException("The file '" + fileName
+ "' could not be found in the workingdir, it is not absolute "
+ "and it is not in the system path (" + systemPath + ").");
}
// this is useful for manually testing this on different platforms
public static void main(final String[] args) {
if (args.length < 1) {
log.error("USAGE: java za.co.enerweb.toolbox.io.CommandUtils "
+ "sping localhost -c 12");
System.exit(1);
}
CommandUtils cu = new CommandUtils();
cu.setCommandAndArgs(args);
Stopwatch sw = new Stopwatch();
try {
cu.excecute();
} catch (IOException e) {
log.error("Error while executing " + args[0], e);
} finally {
log.debug("Execution time: {}", sw.getDelta());
}
}
public void setStdoutHandler(final OutputHandler stdoutHandler) {
if (stdoutWaiter != null) {
stdoutWaiter.setOutputHandler(stdoutHandler);
}
this.stdoutHandler = stdoutHandler;
}
public void setStderrHandler(final OutputHandler stderrHandler) {
if (stderrWaiter != null) {
stderrWaiter.setOutputHandler(stderrHandler);
}
this.stderrHandler = stderrHandler;
}
public String getCommand() {
return command;
}
public void setCommand(final String command) {
this.command = command;
}
public File getWorkingDir() {
return workingDir;
}
public void setWorkingDir(final File workingDir) {
this.workingDir = workingDir;
}
public boolean isCheckExitCode() {
return checkExitCode;
}
public void setCheckExitCode(final boolean checkExitCode) {
this.checkExitCode = checkExitCode;
}
public String[] getArgs() {
return args;
}
public String[] getEnvironment() {
return environment;
}
public void setEnvironment(final String[] environment) {
this.environment = environment;
}
}