Package st.seaside

Source Code of st.seaside.PharoBuilder

package st.seaside;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
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.DaemonThreadFactory;
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(AbstractBuild, Launcher, BuildListener)} method
* will be invoked.
*
* @author Philippe Marschall
*/
public class PharoBuilder extends Builder {

  /**
   * Singleton instance of the global configuration descriptor.
   */
  @Extension
  public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

  private static final ScheduledExecutorService EXECUTOR;

  static {
    // create a demon thread, otherwise we prevent the VM from shutting down
    ThreadFactory threadFactory = new DaemonThreadFactory();
    EXECUTOR = Executors.newSingleThreadScheduledExecutor(threadFactory);
  }

  private final String image;
  private final String script;
  private final String logFile;

  // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"

  /**
   * Constructor invoked byJelly.
   *
   * <p>Fields in config.jelly must match the parameter names in the
   * "DataBoundConstructor".</p>
   *
   * @param image the name of the image, may or may not end with {@literal ".image"},
   *    must not be {@literal null}
   * @param script Smalltalk doit to execute between the before and after code,
   *    may be {@literal null}
   * @param logFile name of the debug log file to watch for, may be {@literal null}
   *    in this case no watching will happen
   */
  @DataBoundConstructor
  public PharoBuilder(String image, String script, String logFile) {
    this.image = stripDotImage(image);
    this.script = script;
    this.logFile = logFile;
  }

  /**
   * Returns the name of the image to build.
   *
   * <p>Needed by jelly.</p>
   *
   * @return the name of the image to build with {@code ".image"} stripped
   */
  public String getImage() {
    return this.image;
  }

  /**
   * Returns the Smalltalk script to execute, between the global before and
   * after scripts.
   *
   * <p>Needed by jelly.</p>
   *
   * @return the Smalltalk script to execute, may be empty for {@code null}
   */
  public String getScript() {
    return this.script;
  }

  /**
   * Returns the name of the debug log to watch for.
   *
   * <p>Needed by jelly.</p>
   *
   * @return the name of the debug log to watch for, may be empty for {@code null}
   */
  public String getFileToWatch() {
    return this.logFile;
  }

  private FilePath getImageFile(FilePath moduleRoot) {
    return moduleRoot.child(this.image + ".image");
  }

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

  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.logFile != null && !this.logFile.isEmpty()) {
      return moduleRoot.child(this.logFile);
    } else {
      return null;
    }
  }

  private String getContentsOfDebugLog(FilePath moduleRoot) throws IOException, InterruptedException {
    // wait a second to give the Pharo VM time to finish writing the debug log
    Thread.sleep(TimeUnit.SECONDS.toMillis(1L));
    FilePath debugLog = this.getDebugLog(moduleRoot);
    if (debugLog != null && debugLog.exists()) {
      return debugLog.readToString();
    } else {
      return null;
    }
  }

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

  private FilePath getStartUpScript(FilePath moduleRoot) throws IOException, InterruptedException {
    FilePath script = moduleRoot.child(this.image + ".st");
    // REVIEW not sure what the correct encoding would be
    script.write(this.getStartUpCode(), Charset.defaultCharset().name());
    return script;
  }

  private String getStartUpCode() {
    String code = "";
    String beforeCode = getDescriptor().getBeforeCode();
    if (beforeCode != null && !beforeCode.isEmpty()) {
      code += beforeCode + "\n!\n";
    }
    String buildScript = this.getScript();
    if (buildScript != null && !buildScript.isEmpty()) {
      code += buildScript + "\n!\n";
    }
    String afterCode = getDescriptor().getAfterCode();
    if (afterCode != null && !afterCode.isEmpty()) {
      code += afterCode + "\n!\n";
    }
    return code;
  }

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

    FilePath moduleRoot = build.getModuleRoot();
    this.deleteDebugLog(moduleRoot);

    ArgumentListBuilder args = new ArgumentListBuilder();
    args.add(getDescriptor().getVm());
    addVmParametersTo(args);
    args.add(this.getImageFile(moduleRoot));
    args.add(this.getStartUpScript(moduleRoot));

   return this.startVm(build, launcher, listener, args);
  }

  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) throws InterruptedException, IOException {

    FilePath moduleRoot = build.getModuleRoot();
    ScheduledFuture<?> future = null;
    Map<String, String> env = build.getEnvironment(listener);
    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);
      }
      int r = proc.join();
      this.appendDebugLog(moduleRoot, listener);
      return r == 0;
    } catch (IOException e) {
      this.appendDebugLog(moduleRoot, listener);
      Util.displayIOException(e, listener);

      String errorMessage = Messages.pharo_imageBuildFailed();
      e.printStackTrace(listener.fatalError(errorMessage));
      return false;
    } catch (InterruptedException e) {
      this.appendDebugLog(moduleRoot, listener);
      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;

    /**
     * Default constructor, loads the defaults first and then the saved data.
     */
    public DescriptorImpl() {
      super(PharoBuilder.class);
      // hack to work around missing defaults in textarea tag
      this.beforeCode = this.defaultBeforeCode();
      this.afterCode = this.defaultAfterCode();
      load();
    }

    /**
     * Constructor, only loads the defaults and not the saved data.
     *
     * @param clazz the builder class
     */
    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 'image'.
     *
     * @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 doCheckImage(@QueryParameter String value) {
      if (value.isEmpty()) {
        return FormValidation.error(Messages.pharo_imageEmpty());
      }
      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();
    }

    /**
     * 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;
    }

    /**
     * Returns the default value to display for the VM path in the global configuration
     * of the builder.
     *
     * @return the default value for the VM path
     */
    public String defaultVm() {
      return "squeak";
    }

    /**
     * Returns the default value to display for the VM parameters in the global configuration
     * of the builder.
     *
     * @return the default value for the VM parameters
     */
    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.
   *
   * <p>To execute it repeatedly you probably want to wrap it in a {@link ScheduledFuture}
   * and  {@link ScheduledFuture#cancel(boolean)} it when it is no longer needed.</p>
   */
  static final class WatdogTask implements Runnable {

    private final FilePath toWatch;

    private final Proc proc;

    private final PrintStream logger;

    /**
     * Constructor.
     *
     * @param toWatch the file to watch for,
     *    must not be {@literal null}
     * @param proc the process to kill, {@link Proc#kill()} will be invoked on
     *   it from a different thread, must not be {@literal null}
     * @param logger the logger to use for info and error logging,
     *   must not be {@literal null}
     */
    WatdogTask(FilePath toWatch, Proc proc, PrintStream logger) {
      this.logger = logger;
      this.toWatch = toWatch;
      this.proc = proc;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run() {
      try {
        if (this.toWatch.exists()) {
          this.logger.println("[INFO] found " + this.toWatch.getRemote() + ", killing");
          this.proc.kill();
        }
      } catch (IOException e) {
        this.logger.print("[ERROR] could not watch: " + this.toWatch.getRemote() + " because " + e.getMessage());
        throw new RuntimeException(e);
      } catch (InterruptedException e) {
        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.