package org.jenkinsci.plugins.unity3d;
import hudson.CopyOnWrite;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.tools.ToolInstallation;
import hudson.util.ArgumentListBuilder;
import hudson.util.QuotedStringTokenizer;
import java.io.IOException;
import java.io.PrintStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.unity3d.io.Pipe;
import org.jenkinsci.plugins.unity3d.io.StreamCopyThread;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
/**
* Unity3d builder
* <p>
* Features:<br/>
* <u>
* <li>supports local and remote execution</li>
* <li>pipe the editor.log into the console</li>
* </u>
* @author Jerome Lacoste
*/
public class Unity3dBuilder extends Builder {
private String unity3dName;
private String argLine;
@DataBoundConstructor
public Unity3dBuilder(String unity3dName, String argLine) {
this.unity3dName = unity3dName;
this.argLine = argLine;
}
public String getArgLine() {
return argLine;
}
private String getArgLineOrGlobalArgLine() {
if (argLine != null && argLine.trim().length() > 0) {
return argLine;
} else {
return getDescriptor().globalArgLine;
}
}
public String getUnity3dName() {
return unity3dName;
}
private static class PerformException extends Exception {
private static final long serialVersionUID = 1L;
private PerformException(String s) {
super(s);
}
}
@Override
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException {
try {
_perform(build, launcher, listener);
return true;
} catch (PerformException e) {
listener.fatalError(e.getMessage());
return false;
} catch (IOException e) {
Util.displayIOException(e, listener);
String errorMessage = Messages.Unity3d_ExecUnexpectedlyFailed();
e.printStackTrace(listener.fatalError(errorMessage));
return false;
}
}
private void _perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException, PerformException {
EnvVars env = build.getEnvironment(listener);
Unity3dInstallation ui = getAndConfigureUnity3dInstallation(listener, env);
ArgumentListBuilder args = prepareCommandlineArguments(build, launcher, ui, env);
String customLogFile = findLogFileArgument(args);
Pipe pipe = Pipe.createRemoteToLocal(launcher);
PrintStream ca = listener.getLogger();
ca.println("Piping unity Editor.log from " + ui.getEditorLogPath(launcher, customLogFile));
Future<Long> futureReadBytes = ui.pipeEditorLog(launcher, customLogFile, pipe.getOut());
// Unity3dConsoleAnnotator ca = new Unity3dConsoleAnnotator(listener.getLogger(), build.getCharset());
StreamCopyThread copierThread = new StreamCopyThread("Pipe editor.log to output thread.", pipe.getIn(), ca);
try {
copierThread.start();
int r = launcher.launch().cmds(args).envs(env).stdout(ca).pwd(build.getWorkspace()).join();
// r == 11 means executeMethod could not be found ?
if (r != 0) {
throw new PerformException(Messages.Unity3d_UnityExecFailed(r));
}
} finally {
// give a bit of time for the piping to complete. Not really
// sure why it's not properly flushed otherwise
Thread.sleep(1000);
if (!futureReadBytes.isDone()) {
// NOTE According to the API, cancel() should cause future calls to get() to fail with an exception
// Jenkins implementation doesn't seem to record it right now and just interrupts the remote task
// but we won't use the value, in case that behavior changes, even for debugging / informative purposes
// we still call cancel to stop the task.
futureReadBytes.cancel(true);
// listener.getLogger().print("Read " + futureReadBytes.get() + " bytes from Editor.log");
}
try {
copierThread.join();
if (copierThread.getFailure() != null) {
ca.println("Failure on remote ");
copierThread.getFailure().printStackTrace(ca);
}
}
finally {
//ca.forceEol();
}
}
}
/** Find the -logFile argument from the built arg line **/
private String findLogFileArgument(ArgumentListBuilder args) {
String customLogFile = null;
List<String> a = args.toList();
for (int i = 0; i < a.size() - 1; i++) {
if (a.get(i).equals("-logFile")) {
customLogFile = a.get(i+1);
}
}
return customLogFile;
}
private ArgumentListBuilder prepareCommandlineArguments(AbstractBuild<?,?> build, Launcher launcher, Unity3dInstallation ui, EnvVars vars) throws IOException, InterruptedException, PerformException {
String exe = ui.getExecutable(launcher);
if (exe==null) {
throw new PerformException(Messages.Unity3d_ExecutableNotFound(ui.getName()));
}
FilePath moduleRoot = build.getModuleRoot();
String moduleRootRemote = moduleRoot.getRemote();
Map<String,String> buildParameters = build.getBuildVariables();
return createCommandlineArgs(exe, moduleRootRemote, vars, buildParameters);
}
private Unity3dInstallation getAndConfigureUnity3dInstallation(BuildListener listener, EnvVars env) throws PerformException, IOException, InterruptedException {
Unity3dInstallation ui = getUnity3dInstallation();
if(ui==null) {
throw new PerformException(Messages.Unity3d_NoUnity3dInstallation());
}
ui = ui.forNode(Computer.currentComputer().getNode(), listener);
ui = ui.forEnvironment(env);
return ui;
}
ArgumentListBuilder createCommandlineArgs(String exe, String moduleRootRemote, EnvVars vars, Map<String,String> buildVariables) {
ArgumentListBuilder args = new ArgumentListBuilder();
args.add(exe);
String theArgLine = getArgLineOrGlobalArgLine();
if (!theArgLine.contains("-projectPath")) {
args.add("-projectPath", moduleRootRemote);
}
String finalArgLine = Util.replaceMacro(theArgLine, buildVariables);
finalArgLine = Util.replaceMacro(finalArgLine, vars);
args.add(QuotedStringTokenizer.tokenize(finalArgLine));
return args;
}
/**
* @return the Unity3d to invoke,
* or null to invoke the default one.
*/
private Unity3dInstallation getUnity3dInstallation() {
for( Unity3dInstallation i : getDescriptor().getInstallations() ) {
if(unity3dName!=null && unity3dName.equals(i.getName()))
return i;
}
return null;
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
private String globalArgLine;
@CopyOnWrite
private volatile Unity3dInstallation[] installations = new Unity3dInstallation[0];
public DescriptorImpl() {
load();
}
public Unity3dInstallation.DescriptorImpl getToolDescriptor() {
return ToolInstallation.all().get(Unity3dInstallation.DescriptorImpl.class);
}
public Unity3dInstallation[] getInstallations() {
return installations;
}
public void setInstallations(Unity3dInstallation... antInstallations) {
this.installations = antInstallations;
save();
}
public String getGlobalArgLine() {
return globalArgLine;
}
public void setGlobalArgLine(String globalArgLine) {
System.out.println("HeLLO: " + globalArgLine);
this.globalArgLine = globalArgLine;
save();
}
@Override
public boolean configure( StaplerRequest req, JSONObject o ) {
globalArgLine = Util.fixEmptyAndTrim(o.getString("globalArgLine"));
save();
return true;
}
@SuppressWarnings("rawtypes")
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
public String getDisplayName() {
return "Invoke Unity3d Editor";
}
}
}