// Copyright (C) 2003-2009 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the CPL Common Public License version 1.0.
package fitnesse.testsystems;
import static java.util.Arrays.asList;
import static util.FileUtil.CHARENCODING;
import org.apache.commons.lang.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public class CommandRunner {
private static final Logger LOG = Logger.getLogger(CommandRunner.class.getName());
private Process process;
protected int exitCode = -1;
private String[] command;
private Map<String, String> environmentVariables;
private final int timeout;
private final ExecutionLogListener executionLogListener;
private String commandErrorMessage = "";
/**
* @param command Commands to run
* @param environmentVariables Map of environment variables
* @param executionLogListener Execution Log Listener
* @param timeout Time-out in seconds.
*/
public CommandRunner(String[] command, Map<String, String> environmentVariables, ExecutionLogListener executionLogListener, int timeout) {
if (executionLogListener == null) {
throw new IllegalArgumentException("executionLogListener may not be null");
}
this.command = command;
this.environmentVariables = environmentVariables;
this.executionLogListener = executionLogListener;
this.timeout = timeout;
}
public CommandRunner(String[] command, Map<String, String> environmentVariables, ExecutionLogListener executionLogListener) {
this(command, environmentVariables, executionLogListener, 2);
}
public void asynchronousStart() throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.environment().putAll(determineEnvironment());
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Starting process " + asList(command));
}
process = processBuilder.start();
sendCommandStartedEvent();
redirectOutputs(process, executionLogListener);
}
// Note: for pipe-based connection, this method is overridden in SlimClientBuilder
protected void redirectOutputs(Process process, final ExecutionLogListener executionLogListener) throws IOException {
InputStream stdout = process.getInputStream();
InputStream stderr = process.getErrorStream();
// Fit and SlimService
new Thread(new OutputReadingRunnable(stdout, new OutputWriter() {
@Override
public void write(String output) {
executionLogListener.stdOut(output);
}
}), "CommandRunner stdOut").start();
new Thread(new OutputReadingRunnable(stderr, new OutputWriter() {
@Override
public void write(String output) {
executionLogListener.stdErr(output);
setCommandErrorMessage(output);
}
}), "CommandRunner stdErr").start();
// Close stdin
process.getOutputStream().close();
}
protected void sendCommandStartedEvent() {
executionLogListener.commandStarted(new ExecutionLogListener.ExecutionContext() {
@Override
public String getCommand() {
return StringUtils.join(asList(command), " ");
}
@Override
public String getTestSystemName() {
// What to do with this? Need an identifier?
return "command";
}
});
}
private Map<String, String> determineEnvironment() {
if (environmentVariables == null) {
return Collections.emptyMap();
}
Map<String, String> systemVariables = new HashMap<>(System.getenv());
systemVariables.putAll(environmentVariables);
return systemVariables;
}
public void join() {
if (process != null) {
waitForDeathOf(process);
if (isDead(process)) {
exitCode = process.exitValue();
executionLogListener.exitCode(exitCode);
}
}
}
private void waitForDeathOf(Process process) {
int timeStep = 100;
int maxDelay = timeout * 1000;
try {
for (int delayed = 0; delayed < maxDelay; delayed += timeStep) {
if (isDead(process)) {
return;
}
Thread.sleep(timeStep);
}
} catch (InterruptedException e) {
LOG.log(Level.WARNING, "Wait for death of process " + process + " interrupted", e);
Thread.currentThread().interrupt();
}
LOG.warning("Could not detect death of command line test runner.");
}
private boolean isDead(Process process) {
try {
process.exitValue();
return true;
} catch (IllegalThreadStateException e) {
return false;
}
}
public boolean isDead() {
if (process !=null) return isDead(process);
//if there is or was never a process due to a remote / manual start then it is alive!
return false;
}
public void kill() {
if (process != null) {
process.destroy();
join();
}
}
public String[] getCommand() {
return command;
}
public String getOutput() {
return "";
}
public String getError() {
return "";
}
public List<Throwable> getExceptions() {
return Collections.emptyList();
}
@Deprecated
public int getExitCode() {
return exitCode;
}
// Used to catch exceptions thrown from the read and write threads.
public void exceptionOccurred(Exception e) {
executionLogListener.exceptionOccurred(e);
}
protected class OutputReadingRunnable implements Runnable {
public OutputWriter writer;
private BufferedReader reader;
public OutputReadingRunnable(InputStream input, OutputWriter writer) {
try {
reader = new BufferedReader(new InputStreamReader(input, CHARENCODING));
} catch (UnsupportedEncodingException e) {
exceptionOccurred(e);
}
this.writer = writer;
}
@Override
public void run() {
try {
String s;
while ((s = reader.readLine()) != null) {
writer.write(s);
}
} catch (Exception e) {
exceptionOccurred(e);
}
}
}
public int waitForCommandToFinish() throws InterruptedException {
return process.waitFor();
}
protected interface OutputWriter {
void write(String output);
}
protected void setCommandErrorMessage(String commandErrorMessage) {
this.commandErrorMessage = commandErrorMessage;
}
public String getCommandErrorMessage() {
return commandErrorMessage;
}
// TODO: Those should go, since the data is sent to the ExecutionListener already
public InputStream getInputStream() {
return process.getInputStream();
}
public OutputStream getOutputStream() {
return process.getOutputStream();
}
}