package hudson.plugins.xvnc;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.Label;
import hudson.model.Node;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.util.FormValidation;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
/**
* {@link BuildWrapper} that runs <tt>xvnc</tt>.
*
* @author Kohsuke Kawaguchi
*/
public class Xvnc extends BuildWrapper {
/**
* Whether or not to take a screenshot upon completion of the build.
*/
public boolean takeScreenshot;
private static final String FILENAME_SCREENSHOT = "screenshot.jpg";
@DataBoundConstructor
public Xvnc(boolean takeScreenshot) {
this.takeScreenshot = takeScreenshot;
}
@Override
public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
final PrintStream logger = listener.getLogger();
DescriptorImpl DESCRIPTOR = Hudson.getInstance().getDescriptorByType(DescriptorImpl.class);
// skip xvnc execution
if (build.getBuiltOn().getAssignedLabels().contains(Label.get("noxvnc"))
|| build.getBuiltOn().getNodeProperties().get(NodePropertyImpl.class) != null) {
return new Environment(){};
}
if (DESCRIPTOR.skipOnWindows && !launcher.isUnix()) {
return new Environment(){};
}
if (DESCRIPTOR.cleanUp) {
maybeCleanUp(launcher, listener);
}
String cmd = Util.nullify(DESCRIPTOR.xvnc);
int baseDisplayNumber = DESCRIPTOR.baseDisplayNumber;
if (cmd == null) {
cmd = "vncserver :$DISPLAY_NUMBER";
}
return doSetUp(build, launcher, logger, cmd, baseDisplayNumber, 3);
}
private Environment doSetUp(AbstractBuild build, final Launcher launcher, final PrintStream logger,
String cmd, int baseDisplayNumber, int retries) throws IOException, InterruptedException {
final int displayNumber = allocator.allocate(baseDisplayNumber);
final String actualCmd = Util.replaceMacro(cmd, Collections.singletonMap("DISPLAY_NUMBER",String.valueOf(displayNumber)));
logger.println(Messages.Xvnc_STARTING());
String[] cmds = Util.tokenize(actualCmd);
final Proc proc = launcher.launch().cmds(cmds).stdout(logger).pwd(build.getWorkspace()).start();
final String vncserverCommand;
if (cmds[0].endsWith("vncserver") && cmd.contains(":$DISPLAY_NUMBER")) {
// Command just started the server; -kill will stop it.
vncserverCommand = cmds[0];
int exit = proc.join();
if (exit != 0) {
// XXX I18N
String message = "Failed to run \'" + actualCmd + "\' (exit code " + exit + "), blacklisting display #" + displayNumber +
"; consider checking the \"Clean up before start\" option";
// Do not release it; it may be "stuck" until cleaned up by an administrator.
//allocator.free(displayNumber);
if (retries > 0) {
return doSetUp(build, launcher, logger, cmd, baseDisplayNumber, retries - 1);
} else {
throw new IOException(message);
}
}
} else {
vncserverCommand = null;
}
return new Environment() {
@Override
public void buildEnvVars(Map<String, String> env) {
env.put("DISPLAY",":"+displayNumber);
}
@Override
public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
if (takeScreenshot) {
FilePath ws = build.getWorkspace();
File artifactsDir = build.getArtifactsDir();
artifactsDir.mkdirs();
logger.println(Messages.Xvnc_TAKING_SCREENSHOT());
launcher.launch().cmds("import", "-window", "root", "-display", ":" + displayNumber, FILENAME_SCREENSHOT).
stdout(logger).pwd(ws).join();
ws.child(FILENAME_SCREENSHOT).copyTo(new FilePath(artifactsDir).child(FILENAME_SCREENSHOT));
}
logger.println(Messages.Xvnc_TERMINATING());
if (vncserverCommand != null) {
// #173: stopping the wrapper script will accomplish nothing. It has already exited, in fact.
launcher.launch().cmds(vncserverCommand, "-kill", ":" + displayNumber).stdout(logger).join();
} else {
// Assume it can be shut down by being killed.
proc.kill();
}
allocator.free(displayNumber);
return true;
}
};
}
/**
* Manages display numbers in use.
*/
private static final DisplayAllocator allocator = new DisplayAllocator();
/**
* Whether {@link #maybeCleanUp} has already been run on a given node.
*/
private static final Map<Node,Boolean> cleanedUpOn = new WeakHashMap<Node,Boolean>();
// XXX I18N
private static synchronized void maybeCleanUp(Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
Node node = Computer.currentComputer().getNode();
if (cleanedUpOn.put(node, true) != null) {
return;
}
if (!launcher.isUnix()) {
listener.error("Clean up not currently implemented for non-Unix nodes; skipping");
return;
}
PrintStream logger = listener.getLogger();
// ignore any error return codes
launcher.launch().stdout(logger).cmds("pkill", "Xvnc").join();
launcher.launch().stdout(logger).cmds("pkill", "Xrealvnc").join();
launcher.launch().stdout(logger).cmds("sh", "-c", "rm -f /tmp/.X*-lock /tmp/.X11-unix/X*").join();
}
@Extension
public static final class DescriptorImpl extends BuildWrapperDescriptor {
/**
* xvnc command line. This can include macro.
*
* If null, the default will kick in.
*/
public String xvnc;
/*
* Base X display number.
*/
public int baseDisplayNumber = 10;
/**
* If true, skip xvnc launch on all Windows slaves.
*/
public boolean skipOnWindows = true;
/**
* If true, try to clean up old processes and locks when first run.
*/
public boolean cleanUp = false;
public DescriptorImpl() {
super(Xvnc.class);
load();
}
public String getDisplayName() {
return Messages.description();
}
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
// XXX is this now the right style?
req.bindJSON(this,json);
save();
return true;
}
public boolean isApplicable(AbstractProject<?, ?> item) {
return true;
}
public String getCommandline() {
return xvnc;
}
public void setCommandline(String value) {
this.xvnc = value;
}
public FormValidation doCheckCommandline(@QueryParameter String value) {
if (Util.nullify(value) == null || value.contains("$DISPLAY_NUMBER")) {
return FormValidation.ok();
} else {
return FormValidation.warningWithMarkup(Messages.Xvnc_SHOULD_INCLUDE_DISPLAY_NUMBER());
}
}
}
}