Package st.seaside

Source Code of st.seaside.PharoBuilder

package st.seaside;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import st.seaside.PharoBuilder.DescriptorImpl;


/**
* {@link Builder} for <a href="http://www.pharo-project.org/">Pharo</a>
* images.
*
* <p>
* When the user configures the project and enables this builder,
* {@link DescriptorImpl#newInstance(StaplerRequest)} is invoked
* and a new {@link PharoBuilder} is created. The created
* instance is persisted to the project configuration XML by using
* XStream, so this allows you to use instance fields (like {@link #name})
* to remember the configuration.
*
* <p>
* When a build is performed, the {@link #perform(Build, Launcher, BuildListener)} method
* will be invoked.
*
* @author Philippe Marschall
*/
public class PharoBuilder extends Builder {

  @Extension
  public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

  //TODO check if we need to creat demon threads to allow the VM to shut down
  private static final ScheduledExecutorService EXECUTOR;

  static {
    EXECUTOR = Executors.newSingleThreadScheduledExecutor();
  }

  private final String inputImage;
  private final String outputImage;
  private final String code;
  private final String fileToWatch;

  // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
  @DataBoundConstructor
  public PharoBuilder(String inputImage, String outputImage, String code, String fileToWatch) {
    this.inputImage = stripDotImage(inputImage);
    this.outputImage = stripDotImage(outputImage);
    this.code = code;
    this.fileToWatch = fileToWatch;
  }

  public String getInputImage() {
    return this.inputImage;
  }

  public String getOutputImage() {
    return this.outputImage;
  }

  public String getCode() {
    return this.code;
  }

  public String getFileToWatch() {
    return this.fileToWatch;
  }

  public FilePath getImageToBuildAgainst(FilePath moduleRoot) {
    if (this.outputImage != null) {
      return moduleRoot.child(this.outputImage + ".image");
    } else {
      return moduleRoot.child(this.inputImage + ".image");
    }
  }

  private static String stripDotImage(String s) {
    if (s.endsWith(".image")) {
      return s.substring(0, s.length() - ".image".length());
    } else {
      return s;
    }
  }

  private boolean needToCopyImage() {
    return this.outputImage != null
    && !this.outputImage.isEmpty();
  }

  private void deleteDebugLog(FilePath moduleRoot) throws IOException, InterruptedException {
    FilePath debugLog = this.getDebugLog(moduleRoot);
    if (debugLog.exists()) {
      debugLog.delete();
    }
  }

  private FilePath getDebugLog(FilePath moduleRoot) {
    if (this.fileToWatch != null && !this.fileToWatch.isEmpty()) {
      return moduleRoot.child(this.fileToWatch);
    } else {
      return null;
    }
  }

  private String getContentsOfDebugLog(FilePath moduleRoot) throws IOException {
    FilePath debugLog = this.getDebugLog(moduleRoot);
    if (debugLog != null) {
      return debugLog.readToString();
    } else {
      return null;
    }
  }

  private void appendDebugLog(FilePath moduleRoot, BuildListener listener) throws IOException {
    String contents = this.getContentsOfDebugLog(moduleRoot);
    if (contents != null) {
      listener.fatalError(contents);
    }
  }

  private boolean copyImage(BuildListener listener, FilePath folder) throws IOException, InterruptedException {
    FilePath source = folder.child(this.inputImage + ".image");
    if (!source.exists()) {
      listener.fatalError(Messages.pharo_inputImageDoesNotExist());
      return false;
    }
    FilePath target = folder.child(this.outputImage + ".image");

    listener.getLogger().printf("copying %s to %s%n", source.getRemote(), target.getRemote());
    source.copyTo(target);
    return true;
  }

  private boolean copyChanges(BuildListener listener, FilePath folder) throws IOException, InterruptedException {
    FilePath source = folder.child(this.inputImage + ".changes");
    if (!source.exists()) {
      listener.fatalError(Messages.pharo_inputChangesDoesNotExist());
      return false;
    }

    FilePath target = folder.child(this.outputImage + ".changes");
    listener.getLogger().printf("copying %s to %s%n", source.getRemote(), target.getRemote());
    source.copyTo(target);
    return true;
  }

  private File getStartUpScript() throws IOException {
    File file = File.createTempFile("pharo_build", ".st");
    FileOutputStream stream = new FileOutputStream(file);
    try {
      OutputStreamWriter writer = new OutputStreamWriter(stream);
      String beforeCode = getDescriptor().getBeforeCode();
      if (beforeCode != null && !beforeCode.isEmpty()) {
        writer.write(beforeCode + "\n!\n");
      }
      writer.write(this.getCode() + "\n!\n");
      String afterCode = getDescriptor().getAfterCode();
      if (afterCode != null && !afterCode.isEmpty()) {
        writer.write(afterCode + "\n!\n");
      }
      writer.close();
    } finally {
      stream.close();
    }
    return file;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
      throws InterruptedException, IOException {

    FilePath moduleRoot = build.getModuleRoot();
    this.deleteDebugLog(moduleRoot);
    if (this.needToCopyImage()
        && (!copyImage(listener, moduleRoot) || !copyChanges(listener, moduleRoot))) {
      return false;
    }

    ArgumentListBuilder args = new ArgumentListBuilder();
    args.add(getDescriptor().getVm());
    addVmParametersTo(args);
    args.add(getImageToBuildAgainst(moduleRoot));

    Map<String, String> env = build.getEnvironment(listener);
    File script = this.getStartUpScript();
    try {
      args.add(script);
      return this.startVm(build, launcher, listener, args, env);
    } finally {
      //TODO uncomment
      //if (!script.delete()) {
      //  listener.getLogger().println("could not delete temp file");
      //}
    }
  }

  private void addVmParametersTo(ArgumentListBuilder args) {
    String trimmed = Util.fixEmptyAndTrim(getDescriptor().getParameters());
    if (trimmed != null) {
      for (String each : trimmed.split(" ")) {
        if (each != null && !each.isEmpty()) {
          args.add(each);
        }
      }
    }
  }

  private boolean startVm(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener,
      ArgumentListBuilder args, Map<String, String> env) throws InterruptedException, IOException {
    FilePath moduleRoot = build.getModuleRoot();
    ScheduledFuture<?> future = null;
    try {
      Proc proc = launcher.launch().cmds(args).envs(env).stdout(listener).pwd(moduleRoot).start();
      FilePath debugLog = this.getDebugLog(moduleRoot);
      if (debugLog != null) {
        Runnable watchdog = new WatdogTask(debugLog, proc, listener.getLogger());
        future = EXECUTOR.scheduleAtFixedRate(watchdog, 500L, 500L, TimeUnit.MILLISECONDS);
      } else {
        // FIXME
        listener.getLogger().println("debug log doesn't exist");
      }
      int r = proc.join();
      // FIXME
      listener.getLogger().println("returned: " + r);
      return r == 0;
    } catch (IOException e) {
      // FIXME
      listener.getLogger().println("cought IOException");
      this.appendDebugLog(moduleRoot, listener);
      Util.displayIOException(e, listener);

      String errorMessage = Messages.pharo_imageBuildFailed();
      e.printStackTrace(listener.fatalError(errorMessage));
      return false;
    } catch (InterruptedException e) {
      // FIXME
      listener.getLogger().println("cought InterruptedException");
      this.appendDebugLog(moduleRoot, listener);
      Thread.currentThread().interrupt();
      throw new InterruptedException();
    } finally {
      if (future != null) {
        future.cancel(true);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public DescriptorImpl getDescriptor() {
    return DESCRIPTOR;
  }

  /**
   * Descriptor for {@link PharoBuilder}. Used as a singleton.
   * The class is marked as public so that it can be accessed from views.
   *
   * <p>
   * See <tt>views/hudson/plugins/pharo-build/PharoBuilder/*.jelly</tt>
   * for the actual HTML fragment for the configuration screen.
   */
  public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {

    private String vm;

    private String parameters;

    private String beforeCode;

    private String afterCode;

    public DescriptorImpl() {
      super(PharoBuilder.class);
      // hack to work around missing defaults in textarea tag
      this.beforeCode = this.defaultBeforeCode();
      this.afterCode = this.defaultAfterCode();
      load();
    }

    protected DescriptorImpl(Class<? extends PharoBuilder> clazz) {
      super(clazz);
      // hack to work around missing defaults in textarea tag
      this.beforeCode = this.defaultBeforeCode();
      this.afterCode = this.defaultAfterCode();
    }

    /**
     * Performs on-the-fly validation of the form field 'vm'.
     *
     * @param value
     *      This parameter receives the value that the user has typed.
     * @return
     *      Indicates the outcome of the validation. This is sent to the browser.
     */
    public FormValidation doCheckVm(@QueryParameter String value) {
      if (value.isEmpty()) {
        return FormValidation.error(Messages.pharo_vmPathEmpty());
      }
      return FormValidation.ok();
    }

    /**
     * Performs on-the-fly validation of the form field 'vm'.
     *
     * @param value
     *      This parameter receives the value that the user has typed.
     * @return
     *      Indicates the outcome of the validation. This is sent to the browser.
     */
    public FormValidation doCheckVM(@QueryParameter String value) {
      if (value.isEmpty()) {
        return FormValidation.error(Messages.pharo_vmPathEmpty() + "!");
      }
      return FormValidation.ok();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isApplicable(@SuppressWarnings("rawtypes") /* broken in superclass */
        Class<? extends AbstractProject> aClass) {
      // indicates that this builder can be used with all kinds of project types
      return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDisplayName() {
      return Messages.pharo_displayName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean configure(StaplerRequest request, JSONObject formData) throws FormException {
      this.vm = formData.getString("vm");
      this.parameters = formData.getString("parameters");
      this.beforeCode = formData.getString("beforeCode");
      this.afterCode = formData.getString("afterCode");
      // ^Can also use req.bindJSON(this, formData);
      //  (easier when there are many fields; need set* methods for this)
      save();
      return super.configure(request, formData);
    }

    public String getVm() {
      return this.vm;
    }

    public String getParameters() {
      return this.parameters;
    }

    public String getBeforeCode() {
      return this.beforeCode;
    }

    public String getAfterCode() {
      return this.afterCode;
    }

    public String defaultVm() {
      return "squeak";
    }
    public String defaultParamters() {
      return "-nodisplay -nosound";
    }

    public String defaultBeforeCode() {
      return "\"Preparations\"\n"
      + "MCCacheRepository instVarNamed: 'default' put: nil.";
    }

    public String defaultAfterCode() {
      return "\"Clear Author\"\n"
      + "Author reset.\n"
      + "!\n"
      + "\"Clear Monticello Caches\"\n"
      + "MCCacheRepository instVarNamed: 'default' put: nil.\n"
      + "MCFileBasedRepository flushAllCaches.\n"
      + "MCMethodDefinition shutDown.\n"
      + "MCDefinition clearInstances.\n"
      + "!\n"
      + "\"Cleanup Smalltalk\"\n"
      + "Smalltalk flushClassNameCache.\n"
      + "Smalltalk organization removeEmptyCategories.\n"
      + "Smalltalk allClassesAndTraitsDo: [ :each |\n"
      + "    each organization removeEmptyCategories; sortCategories.\n"
      + "    each class organization removeEmptyCategories; sortCategories ].\n"
      + "!\n"
      + "\"Cleanup System Memory\"\n"
      + "Smalltalk garbageCollect.\n"
      + "Symbol compactSymbolTable.\n"
      + "!\n"
      + "\"Save and Quit\"\n"
      + "WorldState addDeferredUIMessage: [\n"
      + "    SmalltalkImage current snapshot: true andQuit: true ].";
    }

  }

  /**
   * A simple task that checks for file. It it's there it kills a given process.
   */
  static final class WatdogTask implements Runnable {

    private final FilePath toWatch;

    private final Proc proc;

    private final PrintStream logger;

    WatdogTask(FilePath toWatch, Proc proc, PrintStream logger) {
      this.logger = logger;
      this.toWatch = toWatch;
      this.proc = proc;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run() {
      try {
        //FIXME
        this.logger.println("checking: " + this.toWatch.getRemote());
        if (this.toWatch.exists()) {
          //FIXME
          this.logger.println("exists, killing");
          //FIXME kill does a join, blocking the pool
          this.proc.kill();
          this.logger.println("exists, killed");
        }
      } catch (IOException e) {
        //FIXME
        this.logger.println("Watchdog IOException");
        this.logger.print("[ERROR] could not watch: " + this.toWatch.getRemote() + " because " + e.getMessage());
        throw new RuntimeException(e);
      } catch (InterruptedException e) {
        //FIXME
        this.logger.println("Watchdog InterruptedException");
        Thread.currentThread().interrupt();
        return;
      }

    }

  }
}
TOP

Related Classes of st.seaside.PharoBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.