Java tutorial
/* * The MIT License * * Copyright (c) 2013-2014, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.jenkinsci.plugins.workflow.job; import org.jenkinsci.plugins.workflow.flow.FlowExecutionList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.XmlFile; import hudson.console.AnnotatedLargeText; import hudson.console.PlainTextConsoleOutputStream; import hudson.model.Computer; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.Queue; import hudson.model.Result; import hudson.model.Run; import hudson.model.StreamBuildListener; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; import hudson.model.listeners.SCMListener; import hudson.scm.ChangeLogSet; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import hudson.util.NullStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import jenkins.model.Jenkins; import jenkins.model.lazy.BuildReference; import jenkins.model.lazy.LazyBuildMixIn; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.ReaderInputStream; import org.jenkinsci.plugins.workflow.actions.LogAction; import org.jenkinsci.plugins.workflow.flow.FlowDefinition; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.flow.GraphListener; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable; import org.kohsuke.stapler.framework.io.LargeText; @SuppressWarnings("SynchronizeOnNonFinalField") public final class WorkflowRun extends Run<WorkflowJob, WorkflowRun> implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun<WorkflowJob, WorkflowRun> { private static final Logger LOGGER = Logger.getLogger(WorkflowRun.class.getName()); /** null until started */ private @Nullable FlowExecution execution; /** * {@link Future} that yields {@link #execution}, when it is fully configured and ready to be exposed. */ private transient SettableFuture<FlowExecution> executionPromise = SettableFuture.create(); private transient final LazyBuildMixIn.RunMixIn<WorkflowJob, WorkflowRun> runMixIn = new LazyBuildMixIn.RunMixIn<WorkflowJob, WorkflowRun>() { @Override protected WorkflowRun asRun() { return WorkflowRun.this; } }; private transient StreamBuildListener listener; private transient AtomicBoolean completed; /** Jenkins instance in effect when {@link #waitForCompletion} was last called. */ private transient Jenkins jenkins; /** map from node IDs to log positions from which we should copy text */ private Map<String, Long> logsToCopy; List<SCMCheckout> checkouts; // TODO could use a WeakReference to reduce memory, but that complicates how we add to it incrementally; perhaps keep a List<WeakReference<ChangeLogSet<?>>> private transient List<ChangeLogSet<? extends ChangeLogSet.Entry>> changeSets; public WorkflowRun(WorkflowJob job) throws IOException { super(job); //System.err.printf("created %s @%h%n", this, this); } public WorkflowRun(WorkflowJob job, File dir) throws IOException { super(job, dir); //System.err.printf("loaded %s @%h%n", this, this); } @Override public LazyBuildMixIn.RunMixIn<WorkflowJob, WorkflowRun> getRunMixIn() { return runMixIn; } @Override protected BuildReference<WorkflowRun> createReference() { return getRunMixIn().createReference(); } @Override protected void dropLinks() { getRunMixIn().dropLinks(); } @Override public WorkflowRun getPreviousBuild() { return getRunMixIn().getPreviousBuild(); } @Override public WorkflowRun getNextBuild() { return getRunMixIn().getNextBuild(); } /** * Exposed to URL space via Stapler. */ public FlowGraphTable getFlowGraph() { FlowGraphTable t = new FlowGraphTable(getExecution()); t.build(); return t; } @Override public void run() { // TODO how to set startTime? reflection? https://trello.com/c/Gbg8I3pl/41-run-starttime // Some code here copied from execute(RunExecution), but subsequently modified quite a bit. try { OutputStream logger = new FileOutputStream(getLogFile()); listener = new StreamBuildListener(logger, Charset.defaultCharset()); listener.started(getCauses()); RunListener.fireStarted(this, listener); updateSymlinks(listener); FlowDefinition definition = getParent().getDefinition(); if (definition == null) { listener.error("No flow definition, cannot run"); return; } Owner owner = new Owner(this); FlowExecutionList.get().register(owner); execution = definition.create(owner, getAllActions()); execution.addListener(new GraphL()); completed = new AtomicBoolean(); logsToCopy = new LinkedHashMap<String, Long>(); checkouts = new LinkedList<SCMCheckout>(); execution.start(); executionPromise.set(execution); waitForCompletion(); } catch (Exception x) { if (listener == null) { LOGGER.log(Level.WARNING, this + " failed to start", x); } else { x.printStackTrace(listener.error("failed to start build")); } setResult(Result.FAILURE); executionPromise.setException(x); } } /** * Sleeps until the run is finished, updating log messages periodically. */ void waitForCompletion() { jenkins = Jenkins.getInstance(); synchronized (completed) { while (!completed.get()) { if (jenkins == null || jenkins.isTerminating()) { LOGGER.log(Level.FINE, "shutting down, breaking waitForCompletion on {0}", this); // Stop writing content, in case a new set of objects gets loaded after in-VM restart and starts writing to the same file: listener.closeQuietly(); listener = new StreamBuildListener(new NullStream()); break; } try { completed.wait(1000); } catch (InterruptedException x) { try { execution.abort(); } catch (Exception x2) { LOGGER.log(Level.WARNING, null, x2); } } copyLogs(); } } } @GuardedBy("completed") private void copyLogs() { if (logsToCopy == null) { // finished return; } Iterator<Map.Entry<String, Long>> it = logsToCopy.entrySet().iterator(); boolean modified = false; while (it.hasNext()) { Map.Entry<String, Long> entry = it.next(); FlowNode node; try { node = execution.getNode(entry.getKey()); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); it.remove(); modified = true; continue; } if (node == null) { LOGGER.log(Level.WARNING, "no such node {0}", entry.getKey()); it.remove(); modified = true; continue; } LogAction la = node.getAction(LogAction.class); if (la != null) { AnnotatedLargeText<? extends FlowNode> logText = la.getLogText(); try { long old = entry.getValue(); long revised = writeLogTo(logText, old, listener.getLogger()); if (revised != old) { entry.setValue(revised); modified = true; } if (logText.isComplete()) { writeLogTo(logText, entry.getValue(), listener.getLogger()); // defend against race condition? assert !node.isRunning() : "LargeText.complete yet " + node + " claims to still be running"; it.remove(); modified = true; } } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); it.remove(); modified = true; } } else if (!node.isRunning()) { it.remove(); modified = true; } } if (modified) { try { save(); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); } } } /** * Equivalent to calling {@link LargeText#writeLogTo(long, OutputStream)} without the unwanted override in {@link AnnotatedLargeText} that wraps in a {@link PlainTextConsoleOutputStream}. * TODO replace with {@code AnnotatedLargeText#writeRawLogTo} in 1.577 */ private static long writeLogTo(AnnotatedLargeText<?> text, long position, OutputStream os) throws IOException { Charset c; try { Field f = LargeText.class.getDeclaredField("charset"); f.setAccessible(true); c = (Charset) f.get(text); } catch (Exception x) { LOGGER.log(Level.WARNING, null, x); return text.writeLogTo(position, os); // give up } Reader r = text.readAll(); try { InputStream is = new ReaderInputStream(r, c); is.skip(position); return position + IOUtils.copyLarge(is, os); } finally { r.close(); } } private static final Map<String, WorkflowRun> LOADING_RUNS = new HashMap<String, WorkflowRun>(); private String key() { return getParent().getFullName() + '/' + getId(); } /** Hack to allow {@link #execution} to use an {@link Owner} referring to this run, even when it has not yet been loaded. */ @Override public void reload() throws IOException { synchronized (LOADING_RUNS) { LOADING_RUNS.put(key(), this); } // super.reload() forces result to be FAILURE, so working around that new XmlFile(XSTREAM, new File(getRootDir(), "build.xml")).unmarshal(this); } @Override protected void onLoad() { super.onLoad(); if (execution != null) { execution.onLoad(); execution.addListener(new GraphL()); executionPromise.set(execution); if (!execution.isComplete()) { try { OutputStream logger = new FileOutputStream(getLogFile(), true); listener = new StreamBuildListener(logger, Charset.defaultCharset()); listener.getLogger().println("Resuming build"); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); listener = new StreamBuildListener(new NullStream()); } completed = new AtomicBoolean(); Queue.getInstance().schedule(new AfterRestartTask(this), 0); } } synchronized (LOADING_RUNS) { LOADING_RUNS.remove(key()); // or could just make the value type be WeakReference<WorkflowRun> LOADING_RUNS.notifyAll(); } } // Overridden since super version has an unwanted assertion about this.state, which we do not use. @Override public void setResult(Result r) { if (result == null || r.isWorseThan(result)) { result = r; LOGGER.log(Level.FINE, this + " in " + getRootDir() + ": result is set to " + r, LOGGER.isLoggable(Level.FINER) ? new Exception() : null); } } private void finish(Result r) { LOGGER.log(Level.INFO, "{0} completed: {1}", new Object[] { this, r }); setResult(r); // TODO set duration RunListener.fireCompleted(WorkflowRun.this, listener); Throwable t = execution.getCauseOfFailure(); if (t instanceof AbortException) { listener.error(t.getMessage()); } else if (t != null) { t.printStackTrace(listener.getLogger()); } listener.finished(getResult()); listener.closeQuietly(); logsToCopy = null; try { save(); getParent().logRotate(); } catch (Exception x) { LOGGER.log(Level.WARNING, null, x); } RunListener.fireFinalized(this); assert completed != null; synchronized (completed) { completed.set(true); completed.notifyAll(); } FlowExecutionList.get().unregister(execution.getOwner()); } /** * Gets the associated execution state. * @return non-null after the flow has started, even after finished (but may be null temporarily when about to start, or if starting failed) */ public @CheckForNull FlowExecution getExecution() { return execution; } /** * Allows the caller to block on {@link FlowExecution}, which gets created relatively quickly * after the build gets going. */ public ListenableFuture<FlowExecution> getExecutionPromise() { return executionPromise; } @Override public boolean hasntStartedYet() { return result == null && execution == null; } @Override public boolean isBuilding() { return result == null || isInProgress(); } @Override protected boolean isInProgress() { return execution != null && !execution.isComplete(); } @Override public boolean isLogUpdated() { return isBuilding(); // there is no equivalent to a post-production state for flows } @Override public EnvVars getEnvironment(TaskListener listener) throws IOException, InterruptedException { EnvVars env = super.getEnvironment(listener); ParametersAction a = getAction(ParametersAction.class); if (a != null) { for (ParameterValue v : a) { v.buildEnvironment(this, env); } } EnvVars.resolve(env); return env; } public synchronized List<ChangeLogSet<? extends ChangeLogSet.Entry>> getChangeSets() { if (changeSets == null) { changeSets = new ArrayList<ChangeLogSet<? extends ChangeLogSet.Entry>>(); for (SCMCheckout co : checkouts) { if (co.changelogFile != null && co.changelogFile.isFile()) { try { changeSets.add(co.scm.createChangeLogParser().parse(this, co.scm.getEffectiveBrowser(), co.changelogFile)); } catch (Exception x) { LOGGER.log(Level.WARNING, "could not parse " + co.changelogFile, x); } } } } return changeSets; } private void onCheckout(SCM scm, FilePath workspace, @CheckForNull File changelogFile, @CheckForNull SCMRevisionState pollingBaseline) throws Exception { if (changelogFile != null && changelogFile.isFile()) { ChangeLogSet<?> cls = scm.createChangeLogParser().parse(this, scm.getEffectiveBrowser(), changelogFile); getChangeSets().add(cls); for (SCMListener l : SCMListener.all()) { l.onChangeLogParsed(this, scm, listener, cls); } } String node = null; // TODO: switch to FilePath.toComputer in 1.571 Jenkins j = Jenkins.getInstance(); if (j != null) { for (Computer c : j.getComputers()) { if (workspace.getChannel() == c.getChannel()) { node = c.getName(); break; } } } if (node == null) { throw new IllegalStateException(); } checkouts.add(new SCMCheckout(scm, node, workspace.getRemote(), changelogFile, pollingBaseline)); } static final class SCMCheckout { final SCM scm; final String node; final String workspace; // TODO make this a String and relativize to Run.rootDir if possible final @CheckForNull File changelogFile; final @CheckForNull SCMRevisionState pollingBaseline; SCMCheckout(SCM scm, String node, String workspace, File changelogFile, SCMRevisionState pollingBaseline) { this.scm = scm; this.node = node; this.workspace = workspace; this.changelogFile = changelogFile; this.pollingBaseline = pollingBaseline; } } private static final class Owner extends FlowExecutionOwner { private final String job; private final String id; private transient @CheckForNull WorkflowRun run; Owner(WorkflowRun run) { job = run.getParent().getFullName(); id = run.getId(); } private String key() { return job + '/' + id; } private @Nonnull WorkflowRun run() throws IOException { if (run == null) { WorkflowRun candidate = LOADING_RUNS.get(key()); if (candidate != null && candidate.getParent().getFullName().equals(job) && candidate.getId().equals(id)) { run = candidate; } else { WorkflowJob j = Jenkins.getInstance().getItemByFullName(job, WorkflowJob.class); if (j == null) { throw new IOException("no such WorkflowJob " + job); } run = j._getRuns().getById(id); if (run == null) { throw new IOException("no such build " + id + " in " + job); } //System.err.printf("getById found %s @%h%n", run, run); } } return run; } @Override public FlowExecution get() throws IOException { WorkflowRun r = run(); synchronized (LOADING_RUNS) { while (r.execution == null && LOADING_RUNS.containsKey(key())) { try { LOADING_RUNS.wait(); } catch (InterruptedException x) { LOGGER.log(Level.WARNING, "failed to wait for " + r + " to be loaded", x); break; } } } FlowExecution exec = r.execution; if (exec != null) { return exec; } else { throw new IOException(r + " did not yet start"); } } @Override public File getRootDir() throws IOException { return run().getRootDir(); } @Override public Queue.Executable getExecutable() throws IOException { return run(); } @Override public PrintStream getConsole() { try { return run().listener.getLogger(); } catch (IOException e) { return System.out; // fallback } } @Override public String getUrl() throws IOException { return run().getUrl(); } @Override public String toString() { return "Owner[" + key() + ":" + run + "]"; } @Override public boolean equals(Object o) { if (!(o instanceof Owner)) { return false; } Owner that = (Owner) o; return job.equals(that.job) && id.equals(that.id); } @Override public int hashCode() { return job.hashCode() ^ id.hashCode(); } } private final class GraphL implements GraphListener { @Override public void onNewHead(FlowNode node) { synchronized (completed) { copyLogs(); logsToCopy.put(node.getId(), 0L); } listener.getLogger().println("Running: " + node.getDisplayName()); if (node instanceof FlowEndNode) { finish(((FlowEndNode) node).getResult()); } else { try { save(); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); } } } } static void alias() { Run.XSTREAM2.alias("flow-build", WorkflowRun.class); new XmlFile(null).getXStream().aliasType("flow-owner", Owner.class); // hack! but how else to set it for arbitrary Descriptors? Run.XSTREAM2.aliasType("flow-owner", Owner.class); } @Extension public static final class SCMListenerImpl extends SCMListener { @Override public void onCheckout(Run<?, ?> build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState pollingBaseline) throws Exception { if (build instanceof WorkflowRun) { ((WorkflowRun) build).onCheckout(scm, workspace, changelogFile, pollingBaseline); } } } }