package hudson.plugins.fitnesse;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Launcher.ProcStarter;
import hudson.model.AbstractBuild;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
/**
*
* @author Tim Bacon
*/
public class FitnesseExecutor {
private static final int SLEEP_MILLIS = 1000;
private static final int STARTUP_TIMEOUT_MILLIS = 10*1000;
private static final int ADDITIONAL_TIMEOUT_MILLIS = 20*1000;
private final FitnesseBuilder builder;
public FitnesseExecutor(FitnesseBuilder builder) {
this.builder = builder;
}
public boolean execute(AbstractBuild<?, ?> build, Launcher launcher, PrintStream logger, EnvVars environment)
throws InterruptedException {
Proc fitnesseProc = null;
StdConsole console = new StdConsole();
build.addAction(getFitnesseBuildAction());
try {
if (builder.getFitnesseStart()) {
fitnesseProc = startFitnesse(build, launcher, environment, logger, console);
if (!procStarted(fitnesseProc, logger, console)) {
return false;
}
console.logIncrementalOutput(logger);
}
FilePath resultsFilePath = getResultsFilePath(getWorkingDirectory(build),
builder.getFitnessePathToXmlResultsOut());
readAndWriteFitnesseResults(logger, console, getFitnessePageCmdURL(), resultsFilePath);
return true;
} catch (Throwable t) {
t.printStackTrace(logger);
if (t instanceof InterruptedException) throw (InterruptedException) t;
return false;
} finally {
killProc(logger, fitnesseProc);
console.logIncrementalOutput(logger);
}
}
private FitnesseBuildAction getFitnesseBuildAction() {
return new FitnesseBuildAction(
builder.getFitnesseStart(),
builder.getFitnesseHost(),
builder.getFitnessePort());
}
private Proc startFitnesse(AbstractBuild<?,?> build, Launcher launcher, EnvVars envVars, PrintStream logger, StdConsole console) throws IOException {
logger.println("Starting new Fitnesse instance...");
ProcStarter procStarter = launcher.launch().cmds(getJavaCmd(getWorkingDirectory(build), envVars));
procStarter.pwd(new File(getAbsolutePathToFileThatMayBeRelativeToWorkspace(getWorkingDirectory(build), builder.getFitnesseJavaWorkingDirectory())));
console.provideStdOutAndStdErrFor(procStarter);
return procStarter.start();
}
public ArrayList<String> getJavaCmd(FilePath workingDirectory, EnvVars envVars) {
String java = "java";
if (envVars.containsKey("JAVA_HOME"))
java = new File(new File(envVars.get("JAVA_HOME"), "bin"), java).getAbsolutePath();
String fitnesseJavaOpts = builder.getFitnesseJavaOpts();
String[] java_opts = ("".equals(fitnesseJavaOpts) ? new String[0] : fitnesseJavaOpts.split(" "));
String absolutePathToFitnesseJar = getAbsolutePathToFileThatMayBeRelativeToWorkspace(workingDirectory, builder.getFitnessePathToJar());
String[] jar_opts = {"-jar", absolutePathToFitnesseJar};
File fitNesseRoot = new File(getAbsolutePathToFileThatMayBeRelativeToWorkspace(workingDirectory, builder.getFitnessePathToRoot()));
String[] fitnesse_opts = {"-d", fitNesseRoot.getParent(),
"-r", fitNesseRoot.getName(),
"-p", Integer.toString(builder.getFitnessePort())};
ArrayList<String> cmd = new ArrayList<String>();
cmd.add(java);
if (java_opts.length > 0) cmd.addAll(Arrays.asList(java_opts));
cmd.addAll(Arrays.asList(jar_opts));
cmd.addAll(Arrays.asList(fitnesse_opts));
return cmd;
}
private boolean procStarted(Proc fitnesseProc, PrintStream log, StdConsole console) throws IOException, InterruptedException {
if (fitnesseProc.isAlive()) {
return fitnesseStarted(log, console, STARTUP_TIMEOUT_MILLIS);
}
return false;
}
/**
* Detect if fitnesse has started by monitoring the console.
* If fitnesse.jar is unpacking itself there will be an initial write
* to stderr followed by multiple writes to stdout, otherwise there
* should only be an initial short write to stdout (and any writes to stderr
* are probably exception messages.)
* @return true if fitnesse has started, false otherwise
*/
public boolean fitnesseStarted(PrintStream log, StdConsole console, long timeout) throws InterruptedException {
long waitedAlready = 0;
do {
Thread.sleep(SLEEP_MILLIS);
if (console.noIncrementalOutput()) {
waitedAlready += SLEEP_MILLIS;
} else {
if (console.incrementalOutputOnStdErr())
timeout += ADDITIONAL_TIMEOUT_MILLIS;
console.logIncrementalOutput(log);
}
} while (waitedAlready < timeout) ;
if (console.noOutputOnStdOut()) {
log.println("Waited " + waitedAlready + "ms for fitnesse to start.");
return false;
}
return true;
}
private void killProc(PrintStream log, Proc proc) {
if (proc != null) {
try {
proc.kill();
for (int i=0; i < 4; ++i) {
if (proc.isAlive()) Thread.sleep(SLEEP_MILLIS);
}
} catch (Exception e) {
e.printStackTrace(log);
}
}
}
private void readAndWriteFitnesseResults(final PrintStream logger, final StdConsole console,
final URL readFromURL, final FilePath writeToFilePath)
throws InterruptedException {
final RunnerWithTimeOut runnerWithTimeOut = new RunnerWithTimeOut(builder.getFitnesseHttpTimeout());
Runnable readAndWriteResults = new Runnable() {
public void run() {
try {
writeToFilePath.delete();
} catch (Exception e) {
// swallow - file may not exist
}
final byte[] bytes = getHttpBytes(logger, readFromURL, runnerWithTimeOut);
writeFitnesseResults(logger, writeToFilePath, bytes);
}
};
ResetEvent logToConsole = new ResetEvent() {
public void onReset() {
console.logIncrementalOutput(logger);
}
};
runnerWithTimeOut.run(readAndWriteResults, logToConsole);
}
public byte[] getHttpBytes(PrintStream log, URL pageCmdTarget, Resettable timeout) {
InputStream inputStream = null;
ByteArrayOutputStream bucket = new ByteArrayOutputStream();
try {
log.println("Connnecting to " + pageCmdTarget);
HttpURLConnection connection = (HttpURLConnection) pageCmdTarget.openConnection();
log.println("Connected: " + connection.getResponseCode() + "/" + connection.getResponseMessage());
inputStream = connection.getInputStream();
long recvd = 0, lastLogged = 0;
byte[] buf = new byte[4096];
int lastRead;
while ((lastRead = inputStream.read(buf)) > 0) {
bucket.write(buf, 0, lastRead);
timeout.reset();
recvd += lastRead;
if (recvd - lastLogged > 1024) {
log.println(recvd/1024 + "k...");
lastLogged = recvd;
}
}
} catch (IOException e) {
// this may be a "premature EOF" caused by e.g. incorrect content-length HTTP header
// so it may be non-fatal -- try to recover
e.printStackTrace(log);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
// swallow
}
}
}
return bucket.toByteArray();
}
public URL getFitnessePageCmdURL() throws MalformedURLException {
return new URL("http",
builder.getFitnesseHost(),
builder.getFitnessePort(),
getFitnessePageCmd());
}
public String getFitnessePageCmd() {
String targetPageExpression = builder.getFitnesseTargetPage();
if (targetPageExpression.contains("?"))
return "/" + targetPageExpression+"&format=xml";
int pos = targetPageExpression.indexOf('&');
if (pos == -1) pos = targetPageExpression.length();
return String.format("/%1$s?%2$s%3$s",
targetPageExpression.substring(0, pos),
builder.getFitnesseTargetIsSuite() ? "suite" : "test",
targetPageExpression.substring(pos)+"&format=xml");
}
private void writeFitnesseResults(PrintStream log, FilePath resultsFilePath, byte[] results) {
OutputStream resultsStream = null;
try {
resultsStream = resultsFilePath.write();
resultsStream.write(results);
log.println("Xml results saved as " + Charset.defaultCharset().displayName()
+ " to " + resultsFilePath.getRemote());
} catch (IOException e) {
e.printStackTrace(log);
} catch (InterruptedException e2) {
e2.printStackTrace(log);
} finally {
try {
if (resultsStream != null) resultsStream.close();
} catch (Exception e) {
// swallow
}
}
}
static FilePath getWorkingDirectory(AbstractBuild<?, ?> build) {
FilePath workspace = build.getWorkspace();
if (workspace != null) return workspace;
return new FilePath(build.getRootDir());
}
static FilePath getResultsFilePath(FilePath workingDirectory, String fileName) {
File fileNameFile = new File(fileName);
if (fileNameFile.getParent() != null) {
if (fileNameFile.exists() || fileNameFile.getParentFile().exists()) {
return new FilePath(fileNameFile);
}
}
return workingDirectory.child(fileName);
}
static String getAbsolutePathToFileThatMayBeRelativeToWorkspace(FilePath workingDirectory, String fileName) {
if (new File(fileName).exists()) return fileName;
return new File(workingDirectory.getRemote(), fileName).getAbsolutePath();
}
}