diff --git a/pom.xml b/pom.xml index 1c04af2a..207b23f4 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ org.jenkins-ci.plugins plugin - 3.19 + 3.24 org.jenkins-ci.plugins.workflow @@ -64,10 +64,11 @@ 2.26 -SNAPSHOT - 2.73.3 + 2.121.1 8 false - 2.20 + true + 2.21-rc632.93b9b13c8128 2.2.6 3.2.0 2.33 @@ -81,7 +82,7 @@ org.jenkins-ci.plugins.workflow workflow-api - 2.27 + 2.30-rc826.999cef3e08fd org.jenkins-ci.plugins.workflow @@ -120,6 +121,12 @@ tests test + + org.jenkins-ci.plugins + pipeline-stage-step + 2.2 + test + org.jenkins-ci.plugins.workflow workflow-scm-step @@ -210,6 +217,13 @@ false + + org.jenkins-ci.tools + maven-hpi-plugin + + 2.26 + + diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java index 7e1eed3a..591274d2 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java @@ -24,9 +24,6 @@ package org.jenkinsci.plugins.workflow.job; -import com.google.common.base.Optional; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableSortedSet; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; @@ -40,8 +37,9 @@ import hudson.Functions; import hudson.XmlFile; import hudson.console.AnnotatedLargeText; -import hudson.console.LineTransformationOutputStream; +import hudson.console.ConsoleNote; import hudson.console.ModelHyperlinkNote; +import hudson.model.BuildListener; import hudson.model.Executor; import hudson.model.Item; import hudson.model.ParameterValue; @@ -60,39 +58,34 @@ import hudson.scm.SCMRevisionState; import hudson.security.ACL; import hudson.slaves.NodeProperty; -import hudson.util.DaemonThreadFactory; import hudson.util.Iterators; -import hudson.util.NamingThreadFactory; import hudson.util.NullStream; import hudson.util.PersistedList; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; 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.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import javax.annotation.concurrent.GuardedBy; import jenkins.model.CauseOfInterruption; import jenkins.model.Jenkins; import jenkins.model.lazy.BuildReference; @@ -103,8 +96,6 @@ import jenkins.util.Timer; import org.acegisecurity.Authentication; import org.jenkinsci.plugins.workflow.FilePathUtils; -import org.jenkinsci.plugins.workflow.actions.LogAction; -import org.jenkinsci.plugins.workflow.actions.ThreadNameAction; import org.jenkinsci.plugins.workflow.actions.TimingAction; import org.jenkinsci.plugins.workflow.flow.BlockableResume; import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider; @@ -117,12 +108,10 @@ import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.flow.GraphListener; import org.jenkinsci.plugins.workflow.flow.StashManager; -import org.jenkinsci.plugins.workflow.graph.BlockEndNode; -import org.jenkinsci.plugins.workflow.graph.BlockStartNode; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; -import org.jenkinsci.plugins.workflow.graph.FlowStartNode; -import org.jenkinsci.plugins.workflow.job.console.WorkflowConsoleLogger; +import org.jenkinsci.plugins.workflow.job.console.NewNodeConsoleNote; +import org.jenkinsci.plugins.workflow.log.LogStorage; import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -166,7 +155,7 @@ public String url() { return WorkflowRun.this; } }; - private transient StreamBuildListener listener; + private transient BuildListener listener; private transient boolean allowTerm; @@ -197,9 +186,6 @@ public String url() { /** Protects the access to logsToCopy, completed, and branchNameCache that are used in the logCopy process */ private transient Object logCopyGuard = new Object(); - /** map from node IDs to log positions from which we should copy text */ - Map logsToCopy; // Exposed for testing - /** JENKINS-26761: supposed to always be set but sometimes is not. Access only through {@link #checkouts(TaskListener)}. */ private @CheckForNull List checkouts; // TODO could use a WeakReference to reduce memory, but that complicates how we add to it incrementally; perhaps keep a List>> @@ -212,7 +198,7 @@ public String url() { * Note: to avoid deadlocks, when nesting locks we ALWAYS need to lock on the logCopyGuard first, THEN the WorkflowRun. * Synchronizing this helps ensure that fields are not mutated during a {@link #save()} operation, since that locks on the Run. */ - private synchronized Object getLogCopyGuard() { + private synchronized Object getLogCopyGuard() { // TODO no longer used for log copying, so rename if (logCopyGuard == null) { logCopyGuard = new Object(); } @@ -223,15 +209,16 @@ private synchronized Object getLogCopyGuard() { static final StreamBuildListener NULL_LISTENER = new StreamBuildListener(new NullStream()); /** Used internally to ensure listener has been initialized correctly. */ - StreamBuildListener getListener() { - // Un-synchronized to prevent deadlocks (combination of run and logCopyGuard) until the log-handling rewrite removes the log copying + BuildListener getListener() { + // Un-synchronized to prevent deadlocks (combination of run and logCopyGuard) // Note that in portions where multithreaded access is possible we are already synchronizing on logCopyGuard if (listener == null) { try { - OutputStream logger = new FileOutputStream(getLogFile(), true); - listener = new StreamBuildListener(logger, getCharset()); - } catch (FileNotFoundException fnf) { - LOGGER.log(Level.WARNING, "Error trying to open build log file for writing, output will be lost: "+getLogFile(), fnf); + // TODO to better handle in-VM restart (e.g. in JenkinsRule), move CpsFlowExecution.suspendAll logic into a FlowExecution.notifyShutdown override, then make FlowExecutionOwner.notifyShutdown also overridable, which for WorkflowRun.Owner should listener.close() as needed + // TODO JENKINS-30777 decorate with ConsoleLogFilter.all() + listener = LogStorage.of(asFlowExecutionOwner()).overallListener(); + } catch (IOException | InterruptedException x) { + LOGGER.log(Level.WARNING, "Error trying to open build log file for writing, output will be lost: " + getLogFile(), x); return NULL_LISTENER; } } @@ -282,7 +269,7 @@ public WorkflowRun(WorkflowJob job, File dir) throws IOException { try { onStartBuilding(); charset = "UTF-8"; // cannot override getCharset, and various Run methods do not call it anyway - StreamBuildListener myListener = getListener(); + BuildListener myListener = getListener(); myListener.started(getCauses()); Authentication auth = Jenkins.getAuthentication(); if (!auth.equals(ACL.SYSTEM)) { @@ -307,7 +294,7 @@ public WorkflowRun(WorkflowJob job, File dir) throws IOException { boolean blockResume = getParent().isResumeBlocked(); ((BlockableResume) newExecution).setResumeBlocked(blockResume); if (blockResume) { - listener.getLogger().println("Resume disabled by user, switching to high-performance, low-durability mode."); + myListener.getLogger().println("Resume disabled by user, switching to high-performance, low-durability mode."); loggedHintOverride = true; } } @@ -318,8 +305,8 @@ public WorkflowRun(WorkflowJob job, File dir) throws IOException { synchronized (getLogCopyGuard()) { // Technically safe but it makes FindBugs happy FlowExecutionList.get().register(owner); newExecution.addListener(new GraphL()); + newExecution.addListener(new NodePrintListener()); completed = Boolean.FALSE; - logsToCopy = new ConcurrentSkipListMap<>(); executionLoaded = true; execution = newExecution; } @@ -349,15 +336,8 @@ public WorkflowRun(WorkflowJob job, File dir) throws IOException { throw sleep(); } - private static ScheduledExecutorService copyLogsExecutorService; - private static synchronized ScheduledExecutorService copyLogsExecutorService() { - if (copyLogsExecutorService == null) { - copyLogsExecutorService = new /*ErrorLogging*/ScheduledThreadPoolExecutor(5, new NamingThreadFactory(new DaemonThreadFactory(), "WorkflowRun.copyLogs")); - } - return copyLogsExecutorService; - } private AsynchronousExecution sleep() { - final AsynchronousExecution asynchronousExecution = new AsynchronousExecution() { + return new AsynchronousExecution() { @Override public void interrupt(boolean forShutdown) { if (forShutdown) { return; @@ -392,34 +372,6 @@ private AsynchronousExecution sleep() { return blocksRestart(); } }; - final AtomicReference> copyLogsTask = new AtomicReference<>(); - copyLogsTask.set(copyLogsExecutorService().scheduleAtFixedRate(new Runnable() { - @Override public void run() { - synchronized (getLogCopyGuard()) { - if (completed == null) { - // Loading run, give it a moment. - return; - } - if (completed) { - asynchronousExecution.completed(null); - copyLogsTask.get().cancel(false); - return; - } - Jenkins jenkins = Jenkins.getInstance(); - 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: - getListener().closeQuietly(); - listener = NULL_LISTENER; - return; - } - try (WithThreadName naming = new WithThreadName(" (" + WorkflowRun.this + ")")) { - copyLogs(); - } - } - } - }, 1, 1, TimeUnit.SECONDS)); - return asynchronousExecution; } private void printLater(final StopState state, final String message) { @@ -540,173 +492,6 @@ public boolean hasAllowKill() { return isBuilding() && allowKill; } - @GuardedBy("logCopyGuard") - private void copyLogs() { - if (logsToCopy == null) { // finished - return; - } - if (logsToCopy instanceof LinkedHashMap) { // upgrade while build is running - logsToCopy = new ConcurrentSkipListMap<>(logsToCopy); - } - boolean modified = false; - FlowExecution exec = getExecution(); - - // Early-exit if build was hard-killed -- state will be so broken that we can't actually load nodes to copy logs - if (exec == null) { - logsToCopy.clear(); - saveWithoutFailing(); - return; - } - - for (Map.Entry entry : logsToCopy.entrySet()) { - String id = entry.getKey(); - FlowNode node; - try { - node = exec.getNode(id); - } catch (IOException x) { - LOGGER.log(Level.WARNING, null, x); - logsToCopy.remove(id); - modified = true; - continue; - } - if (node == null) { - LOGGER.log(Level.WARNING, "no such node {0}", id); - logsToCopy.remove(id); - modified = true; - continue; - } - LogAction la = node.getAction(LogAction.class); - if (la != null) { - AnnotatedLargeText logText = la.getLogText(); - try { - long old = entry.getValue(); - OutputStream logger; - - String prefix = getBranchName(node); - if (prefix != null) { - logger = new LogLinePrefixOutputFilter(getListener().getLogger(), "[" + prefix + "] "); - } else { - logger = getListener().getLogger(); - } - - try { - long revised = writeRawLogTo(logText, old, logger); - if (revised != old) { - logsToCopy.put(id, revised); - modified = true; - } - if (logText.isComplete()) { - writeRawLogTo(logText, revised, logger); // defend against race condition? - assert !node.isRunning() : "LargeText.complete yet " + node + " claims to still be running"; - logsToCopy.remove(id); - modified = true; - } - } finally { - if (prefix != null) { - ((LogLinePrefixOutputFilter)logger).forceEol(); - } - } - } catch (IOException x) { - LOGGER.log(Level.WARNING, null, x); - logsToCopy.remove(id); - modified = true; - } - } else if (!node.isRunning()) { - logsToCopy.remove(id); - modified = true; - } - } - if (modified && exec.getDurabilityHint().isPersistWithEveryStep()) { - saveWithoutFailing(); - } - } - private long writeRawLogTo(AnnotatedLargeText text, long start, OutputStream out) throws IOException { - long len = text.length(); - if (start > len) { - LOGGER.log(Level.WARNING, "JENKINS-37664: attempt to copy logs in {0} @{1} past end @{2}", new Object[] {this, start, len}); - return len; - } else { - return text.writeRawLogTo(start, out); - } - } - - @GuardedBy("logCopyGuard") - private volatile transient Cache> branchNameCache; // TODO Consider making this a top-level FlowNode API - - private Cache> getBranchNameCache() { - if (branchNameCache == null) { - synchronized (getLogCopyGuard()) { // Double-checked locking safe rendered safe by volatile field - if (branchNameCache == null) { - branchNameCache = CacheBuilder.newBuilder().weakKeys().build(); - } - } - } - return branchNameCache; - } - - private @CheckForNull String getBranchName(FlowNode node) { - Cache> cache = getBranchNameCache(); - - Optional output = cache.getIfPresent(node); - if (output != null) { - return output.orNull(); - } - - // We must explicitly check for the current node being the start/end of a parallel branch - if (node instanceof BlockEndNode) { - output = Optional.fromNullable(getBranchName(((BlockEndNode) node).getStartNode())); - cache.put(node, output); - return output.orNull(); - } else if (node instanceof BlockStartNode) { // And of course this node might be the start of a parallel branch - ThreadNameAction threadNameAction = node.getPersistentAction(ThreadNameAction.class); - if (threadNameAction != null) { - String name = threadNameAction.getThreadName(); - cache.put(node, Optional.of(name)); - return name; - } - } - - // Check parent which will USUALLY result in a cache hit, but improve performance and avoid a stack overflow by not doing recursion - List parents = node.getParents(); - if (!parents.isEmpty()) { - FlowNode parent = parents.get(0); - output = cache.getIfPresent(parent); - if (output != null) { - cache.put(node, output); - return output.orNull(); - } - } - - // Fall back to looking for an enclosing parallel branch... but using more efficient APIs and avoiding stack overflows - output = Optional.absent(); - for (BlockStartNode myNode : node.iterateEnclosingBlocks()) { - ThreadNameAction threadNameAction = myNode.getPersistentAction(ThreadNameAction.class); - if (threadNameAction != null) { - output = Optional.of(threadNameAction.getThreadName()); - break; - } - } - cache.put(node, output); - return output.orNull(); - } - - private static final class LogLinePrefixOutputFilter extends LineTransformationOutputStream { - - private final PrintStream logger; - private final String prefix; - - protected LogLinePrefixOutputFilter(PrintStream logger, String prefix) { - this.logger = logger; - this.prefix = prefix; - } - - @Override - protected void eol(byte[] b, int len) throws IOException { - logger.append(prefix); - logger.write(b, 0, len); - } - } - private static final Map LOADING_RUNS = new HashMap<>(); private String key() { @@ -796,8 +581,6 @@ private void finish(@Nonnull Result r, @CheckForNull Throwable t) { completed = Boolean.TRUE; duration = Math.max(0, System.currentTimeMillis() - getStartTimeInMillis()); } - logsToCopy = null; - branchNameCache = null; try { LOGGER.log(Level.INFO, "{0} completed: {1}", new Object[]{toString(), getResult()}); if (nullListener) { @@ -813,10 +596,16 @@ private void finish(@Nonnull Result r, @CheckForNull Throwable t) { Functions.printStackTrace(t, getListener().getLogger()); } getListener().finished(getResult()); - getListener().closeQuietly(); + if (listener instanceof AutoCloseable) { + try { + ((AutoCloseable) listener).close(); + } catch (Exception x) { + LOGGER.log(Level.WARNING, "could not close build log for " + this, x); + } + } + listener = null; } - logsToCopy = null; - saveWithoutFailing(); + saveWithoutFailing(); // TODO useless if we are inside a BulkChange Timer.get().submit(() -> { try { getParent().logRotate(); @@ -891,6 +680,7 @@ private void finish(@Nonnull Result r, @CheckForNull Throwable t) { // need the build to be loaded and can result in loading loops otherwise. fetchedExecution.removeListener(finishListener); fetchedExecution.addListener(new GraphL()); + fetchedExecution.addListener(new NodePrintListener()); } } SettableFuture settablePromise = getSettableExecutionPromise(); @@ -939,7 +729,7 @@ private SettableFuture getSettableExecutionPromise() { return execOut; } - @Override public FlowExecutionOwner asFlowExecutionOwner() { + @Override public @Nonnull FlowExecutionOwner asFlowExecutionOwner() { return new Owner(this); } @@ -1200,21 +990,10 @@ private final class FailOnLoadListener implements GraphListener { private final class GraphL implements GraphListener { @Override public void onNewHead(FlowNode node) { - synchronized (getLogCopyGuard()) { - copyLogs(); - if (logsToCopy == null) { - // Only happens when a FINISHED build loses FlowNodeStorage and we have to create placeholder nodes - // after the build is nominally completed. - logsToCopy = new HashMap(3); - } - logsToCopy.put(node.getId(), 0L); - } - if (node.getPersistentAction(TimingAction.class) == null) { node.addAction(new TimingAction()); } - logNodeMessage(node); FlowExecution exec = getExecution(); if (node instanceof FlowEndNode) { finish(((FlowEndNode) node).getResult(), exec != null ? exec.getCauseOfFailure() : null); @@ -1226,19 +1005,73 @@ private final class GraphL implements GraphListener { } } - private void logNodeMessage(FlowNode node) { - WorkflowConsoleLogger wfLogger = new WorkflowConsoleLogger(getListener()); - String prefix = getBranchName(node); - if (prefix != null) { - wfLogger.log(String.format("[%s] %s", prefix, node.getDisplayFunctionName())); - } else { - wfLogger.log(node.getDisplayFunctionName()); + /** + * Prints nodes as they appear (including block start and end nodes). + */ + private final class NodePrintListener implements GraphListener.Synchronous { + @Override public void onNewHead(FlowNode node) { + NewNodeConsoleNote.print(node, getListener()); + } + } + + @SuppressWarnings("rawtypes") + @Override public AnnotatedLargeText getLogText() { + return LogStorage.of(asFlowExecutionOwner()).overallLog(this, !isLogUpdated()); + } + + // TODO log-related overrides pending JEP-207: + + @Override public InputStream getLogInputStream() throws IOException { + // Inefficient but probably rarely used anyway. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + getLogText().writeRawLogTo(0, baos); + return new ByteArrayInputStream(baos.toByteArray()); + } + + @Override public Reader getLogReader() throws IOException { + return getLogText().readAll(); + } + + @SuppressWarnings("deprecation") + @Override public String getLog() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + getLogText().writeRawLogTo(0, baos); + return baos.toString("UTF-8"); + } + + @Override public List getLog(int maxLines) throws IOException { + int lineCount = 0; + List logLines = new LinkedList<>(); + if (maxLines == 0) { + return logLines; + } + try (BufferedReader reader = new BufferedReader(getLogReader())) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + logLines.add(line); + ++lineCount; + if (lineCount > maxLines) { + logLines.remove(0); + } + } + } + if (lineCount > maxLines) { + logLines.set(0, "[...truncated " + (lineCount - (maxLines - 1)) + " lines...]"); + } + return ConsoleNote.removeNotes(logLines); + } + + @Override public File getLogFile() { + LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, new UnsupportedOperationException()); + try { + File f = File.createTempFile("deprecated", ".log", getRootDir()); + f.deleteOnExit(); + try (OutputStream os = new FileOutputStream(f)) { + getLogText().writeRawLogTo(0, os); + } + return f; + } catch (IOException x) { + throw new RuntimeException(x); } - // Flushing to keep logs printed in order as much as possible. The copyLogs method uses - // LargeText and possibly LogLinePrefixOutputFilter. Both of these buffer and flush, causing strange - // out of sequence writes to the underlying log stream (and => things being printed out of sequence) - // if we don't flush the logger here. - wfLogger.getLogger().flush(); } static void alias() { @@ -1288,9 +1121,23 @@ public void save() throws IOException { isAtomic = hint.isAtomicWrite(); } - synchronized (this) { - PipelineIOUtils.writeByXStream(this, loc, XSTREAM2, isAtomic); - SaveableListener.fireOnChange(this, file); + boolean completeAsynchronousExecution = false; + try { + synchronized (this) { + completeAsynchronousExecution = Boolean.TRUE.equals(completed); + PipelineIOUtils.writeByXStream(this, loc, XSTREAM2, isAtomic); + SaveableListener.fireOnChange(this, file); + } + } finally { + if (completeAsynchronousExecution) { + Executor executor = getExecutor(); + if (executor != null) { + AsynchronousExecution asynchronousExecution = executor.getAsynchronousExecution(); + if (asynchronousExecution != null) { + asynchronousExecution.completed(null); + } + } + } } } } diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java new file mode 100644 index 00000000..c5513c1a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java @@ -0,0 +1,134 @@ +/* + * The MIT License + * + * Copyright (c) 2015, 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.console; + +import hudson.Extension; +import hudson.MarkupText; +import hudson.Util; +import hudson.console.ConsoleAnnotationDescriptor; +import hudson.console.ConsoleAnnotator; +import hudson.console.ConsoleNote; +import hudson.model.TaskListener; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.workflow.actions.LabelAction; +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.graph.BlockEndNode; +import org.jenkinsci.plugins.workflow.graph.BlockStartNode; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.log.LogStorage; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Console line with note printed when a new {@link FlowNode} is added to the graph. + * Defines the {@code pipeline-new-node} CSS class and several attributes which may be used to control subsequent behavior: + *
    + *
  • {@code nodeId} for {@link FlowNode#getId} + *
  • {@code startId} {@link FlowNode#getId} for {@link BlockStartNode}, else {@link BlockEndNode#getStartNode}, else absent + *
  • {@code enclosingId} the immediately enclosing {@link BlockStartNode}, if any + *
  • {@code label} for {@link LabelAction} if present + *
+ * @see LogStorage#startStep + */ +@Restricted(NoExternalUse.class) +public class NewNodeConsoleNote extends ConsoleNote { + + private static final Logger LOGGER = Logger.getLogger(NewNodeConsoleNote.class.getName()); + + /** + * Prefix used in metadata lines. + */ + private static final String CONSOLE_NOTE_PREFIX = "[Pipeline] "; + + public static void print(FlowNode node, TaskListener listener) { + PrintStream logger = listener.getLogger(); + synchronized (logger) { + try { + listener.annotate(new NewNodeConsoleNote(node)); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } + logger.println(CONSOLE_NOTE_PREFIX + node.getDisplayFunctionName()); // note that StepAtomNode will never have a LabelAction at this point + } + } + + private final @Nonnull String id; + private final @CheckForNull String enclosing; + private final @CheckForNull String start; + + private NewNodeConsoleNote(FlowNode node) { + id = node.getId(); + if (node instanceof BlockEndNode) { + enclosing = null; + start = ((BlockEndNode) node).getStartNode().getId(); + } else { + Iterator it = node.iterateEnclosingBlocks().iterator(); + enclosing = it.hasNext() ? it.next().getId() : null; + start = node instanceof BlockStartNode ? node.getId() : null; + } + } + + @Override + public ConsoleAnnotator annotate(WorkflowRun context, MarkupText text, int charPos) { + StringBuilder startTag = new StringBuilder(""); + text.addMarkup(0, text.length(), startTag.toString(), ""); + return null; + } + + private static final long serialVersionUID = 1L; + + @Extension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {} + +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowConsoleLogger.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowConsoleLogger.java deleted file mode 100644 index 957f70d3..00000000 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowConsoleLogger.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2015, 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.console; - -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.Charset; - -import hudson.model.BuildListener; - -/** - * Console logger used to write workflow related metadata in console text. - * - * It wraps the regular {@link BuildListener} to add a filter that annotates any text line sent to the console through - * this logger. It also adds a prefix to the line ([Pipeline]). - * - * Annotated lines will be rendered in a lighter color so they do not interefere with the important part of the log. - */ -public class WorkflowConsoleLogger { - - private final BuildListener listener; - private final WorkflowMetadataConsoleFilter annotator; - - public WorkflowConsoleLogger(BuildListener listener) { - this.listener = listener; - this.annotator = new WorkflowMetadataConsoleFilter(listener.getLogger()); - } - - /** - * Provides access to the wrapped listener logger. - * @return the logger print stream - */ - public PrintStream getLogger() { - return listener.getLogger(); - } - - /** - * Sends an annotated log message to the console after adding the prefix [Workflow]. - * @param message the message to wrap and annotate. - */ - public void log(String message) { - logAnnot(WorkflowRunConsoleNote.CONSOLE_NOTE_PREFIX, message); - } - - private void logAnnot(String prefix, String message) { - byte[] msg = String.format("%s%s%n", prefix, message).getBytes(Charset.defaultCharset()); - try { - annotator.eol(msg, msg.length); - } catch (IOException e) { - listener.getLogger().println("Problem with writing into console log: " + e.getMessage()); - } - } -} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowMetadataConsoleFilter.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowMetadataConsoleFilter.java deleted file mode 100644 index f95558d2..00000000 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowMetadataConsoleFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2015, 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.console; - -import java.io.IOException; -import java.io.OutputStream; - -import hudson.console.LineTransformationOutputStream; - -/** - * It transforms workflow metadata log messages through {@link WorkflowRunConsoleNote}. - */ -public class WorkflowMetadataConsoleFilter extends LineTransformationOutputStream { - - private final OutputStream out; - - public WorkflowMetadataConsoleFilter(OutputStream out) { - this.out = out; - } - - @Override - protected void eol(byte[] b, int len) throws IOException { - new WorkflowRunConsoleNote().encodeTo(out); - out.write(b, 0, len); - } - - @Override - public void close() throws IOException { - super.close(); - out.close(); - } - -} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java index de4480ba..d43fdf78 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java @@ -32,9 +32,10 @@ import hudson.model.Run; /** - * Console note for Workflow metadata specific messages. - * See {@link WorkflowConsoleLogger} for more information. + * @deprecated No longer used, but retained for serial-form compatibility of old build logs. + * @see NewNodeConsoleNote */ +@Deprecated public class WorkflowRunConsoleNote extends ConsoleNote> { /** diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly b/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly index efdf467e..ed13c55a 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly @@ -31,7 +31,17 @@ - + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js new file mode 100644 index 00000000..5eb44006 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js @@ -0,0 +1,94 @@ +Behaviour.specify("span.pipeline-new-node", 'NewNodeConsoleNote', 0, function(e) { + if (e.processedNewNodeConsoleNote) { + return + } + e.processedNewNodeConsoleNote = true + var label = e.getAttribute('label') + if (label != null) { + var html = e.innerHTML + var suffix = ' (' + label.escapeHTML() + ')'; + if (!html.includes(suffix)) { + e.innerHTML = e.innerHTML.replace(/.+/, '$&' + suffix) // insert before EOL + } + } + var startId = e.getAttribute('startId') + if (startId == null || startId == e.getAttribute('nodeId')) { + e.innerHTML = e.innerHTML.replace(/.+/, '$& (hide)') + // TODO automatically hide second and subsequent branches: namely, in case a node has the same parent as an earlier one + } +}); + +function showHidePipelineSection(link) { + var span = link.parentNode.parentNode + var id = span.getAttribute('nodeId') + var display + if (link.textContent === 'hide') { + display = 'none' + link.textContent = 'show' + link.parentNode.className = '' + } else { + display = 'inline' + link.textContent = 'hide' + link.parentNode.className = 'pipeline-show-hide' + } + var showHide = function(id, display) { + var sect = '.pipeline-node-' + id + var ss = document.styleSheets[0] + for (var i = 0; i < ss.rules.length; i++) { + if (ss.rules[i].selectorText === sect) { + ss.rules[i].style.display = display + return + } + } + ss.insertRule(sect + ' {display: ' + display + '}', ss.rules.length) + } + showHide(id, display) + if (span.getAttribute('startId') != null) { + // For a block node, look up other pipeline-new-node elements parented to this (transitively) and mask them and their text too. + var nodes = $$('.pipeline-new-node') + var ids = [] + var starts = new Map() + var enclosings = new Map() // id → enclosingId + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i] + var oid = node.getAttribute('nodeId') + ids.push(oid) + starts.set(oid, node.getAttribute('startId')) + enclosings.set(oid, node.getAttribute('enclosingId')) + } + var encloses = function(enclosing, enclosed, starts, enclosings) { + var id = enclosed + var start = starts.get(id) + if (start != null && start != id) { + id = start // block end node + } + while (true) { + if (id == enclosing) { + return true // found it + } + id = enclosings.get(id) + if (id == null) { + return false // hit flow start node + } + } + } + for (var i = 0; i < ids.length; i++) { + var oid = ids[i] + if (oid != id && encloses(id, oid, starts, enclosings)) { + showHide(oid, display) + var header = $$('.pipeline-new-node[nodeId=' + oid + ']') + if (header.length > 0) { + header[0].style.display = display + } + if (display == 'inline') { + // Mark all children as shown. TODO would be nicer to leave them collapsed if they were before, but this gets complicated. + var link = $$('.pipeline-new-node[nodeId=' + oid + '] span a') + if (link.length > 0) { + link[0].textContent = 'hide' + link[0].parentNode.className = 'pipeline-show-hide' + } + } + } + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css new file mode 100644 index 00000000..ed5f78ab --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css @@ -0,0 +1,9 @@ +span.pipeline-new-node { + color: #9A9999 +} +span.pipeline-show-hide { + visibility: hidden +} +span:hover .pipeline-show-hide { + visibility: visible +} diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/CpsPersistenceTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/CpsPersistenceTest.java index b858f94a..e963f7ec 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/job/CpsPersistenceTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/CpsPersistenceTest.java @@ -102,6 +102,7 @@ static void assertCompletedCleanly(WorkflowRun run) throws Exception { Assert.assertTrue(cpsExec.getCurrentHeads().get(0) instanceof FlowEndNode); Stack starts = getCpsBlockStartNodes(cpsExec); Assert.assertTrue(starts == null || starts.isEmpty()); + Thread.sleep(1000); // TODO seems to be flaky Assert.assertFalse(cpsExec.blocksRestart()); } else { System.out.println("WARNING: no FlowExecutionForBuild"); diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunRestartTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunRestartTest.java index 533adefa..a8a3ea72 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunRestartTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunRestartTest.java @@ -81,7 +81,6 @@ public class WorkflowRunRestartTest { SemaphoreStep.success("wait/1", null); r.assertBuildStatusSuccess(r.waitForCompletion(b)); assertTrue(b.completed); - assertNull(b.logsToCopy); }); } @@ -106,7 +105,6 @@ public class WorkflowRunRestartTest { FlowExecution fe = b.getExecution(); assertTrue(b.executionLoaded); assertNotNull(fe.getOwner()); - assertNull(b.logsToCopy); }); } @@ -181,7 +179,6 @@ public class WorkflowRunRestartTest { WorkflowRun b = p.getBuildByNumber(1); assertFalse(b.isBuilding()); assertFalse(b.executionLoaded); - assertNull(b.logsToCopy); }); } @@ -212,7 +209,6 @@ public class WorkflowRunRestartTest { r.waitForMessage("Hard kill!", b); r.waitForCompletion(b); r.assertBuildStatus(Result.ABORTED, b); - assertNull(b.logsToCopy); assertTrue(b.completed); }); } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java index 7a23f8fb..34d94291 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java @@ -34,6 +34,7 @@ import hudson.security.Permission; import java.io.File; import java.io.IOException; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -54,13 +55,17 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; +import org.jenkinsci.plugins.workflow.actions.LogAction; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution; import org.jenkinsci.plugins.workflow.cps.nodes.StepNode; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.junit.ClassRule; import org.junit.Rule; @@ -265,6 +270,39 @@ public void failedToStartRun() throws Exception { assertTrue(b.completed); } + @Issue("JENKINS-38381") + @LocalData + @Test public void stepRunningAcrossUpgrade() throws Exception { + /* Setup @ 3510070cfc6ef666804258e1aad6f29fdf6e864c: + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("echo 'before'; sleep 60; echo 'after'", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + r.waitForMessage("Sleeping for ", b); + try (OutputStream os = new FileOutputStream("src/test/resources/" + WorkflowRunTest.class.getName().replace('.', '/') + "/stepRunningAcrossUpgrade.zip")) { + r.jenkins.getRootPath().zip(os, new DirScanner.Glob("jobs/,org.jenkinsci.plugins.workflow.flow.FlowExecutionList.xml", "**" + "/lastStable,**" + "/lastSuccessful")); + } + */ + WorkflowJob p = r.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getBuildByNumber(1); + r.waitForCompletion(b); + r.assertLogContains("before", b); + r.assertLogContains("Sleeping for ", b); + r.assertLogContains("No need to sleep any longer", b); + r.assertLogContains("after", b); + List echoNodes = new DepthFirstScanner().filteredNodes(b.getExecution(), new NodeStepTypePredicate("echo")); + assertEquals(2, echoNodes.size()); + assertThat(stepLog(echoNodes.get(0)), containsString("after")); + assertThat(stepLog(echoNodes.get(1)), containsString("before")); + List sleepNodes = new DepthFirstScanner().filteredNodes(b.getExecution(), new NodeStepTypePredicate("sleep")); + assertEquals(1, sleepNodes.size()); + assertThat(stepLog(sleepNodes.get(0)), allOf(containsString("Sleeping for "), containsString("No need to sleep any longer"))); + } + private static String stepLog(FlowNode node) throws Exception { + StringWriter w = new StringWriter(); + node.getAction(LogAction.class).getLogText().writeLogTo(0, w); + return w.toString(); + } + @Issue("JENKINS-29571") @Test public void buildRecordAfterRename() throws Exception { { @@ -345,33 +383,6 @@ public void failedToStartRun() throws Exception { assertEquals(log, 1, StringUtils.countMatches(log, jenkins.model.Messages.CauseOfInterruption_ShortDescription("dev"))); } - @Test - @Issue({"JENKINS-26122", "JENKINS-28222"}) - public void parallelBranchLabels() throws Exception { - WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); - p.setDefinition(new CpsFlowDefinition( - "parallel a: {\n" + - " echo 'a-outside-1'\n" + - " withEnv(['A=1']) {echo 'a-inside-1'}\n" + - " echo 'a-outside-2'\n" + - " withEnv(['A=1']) {echo 'a-inside-2'}\n" + - "}, b: {\n" + - " echo 'b-outside-1'\n" + - " withEnv(['B=1']) {echo 'b-inside-1'}\n" + - " echo 'b-outside-2'\n" + - " withEnv(['B=1']) {echo 'b-inside-2'}\n" + - "}", true)); - WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); - r.assertLogContains("[a] a-outside-1", b); - r.assertLogContains("[b] b-outside-1", b); - r.assertLogContains("[a] a-inside-1", b); - r.assertLogContains("[b] b-inside-1", b); - r.assertLogContains("[a] a-outside-2", b); - r.assertLogContains("[b] b-outside-2", b); - r.assertLogContains("[a] a-inside-2", b); - r.assertLogContains("[b] b-inside-2", b); - } - @Test @Issue("JENKINS-43396") public void globalNodePropertiesInEnv() throws Exception { diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/console/DefaultLogStorageTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/console/DefaultLogStorageTest.java new file mode 100644 index 00000000..9881fddd --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/console/DefaultLogStorageTest.java @@ -0,0 +1,215 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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.console; + +import org.jenkinsci.plugins.workflow.job.*; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import hudson.Functions; +import hudson.console.ModelHyperlinkNote; +import hudson.model.Cause; +import hudson.model.CauseAction; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.ACLContext; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.StringWriter; +import java.util.List; +import java.util.Set; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import static org.hamcrest.Matchers.*; +import org.jenkinsci.plugins.workflow.actions.LogAction; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils; +import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; + +@Issue("JENKINS-38381") +public class DefaultLogStorageTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + + @Test public void consoleNotes() throws Exception { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("hyperlink()", true)); + User alice = User.getById("alice", true); + Cause cause; + try (ACLContext context = ACL.as(alice)) { + cause = new Cause.UserIdCause(); + } + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0, new CauseAction(cause))); + HtmlPage page = r.createWebClient().goTo(b.getUrl() + "console"); + assertLogContains(page, hudson.model.Messages.Cause_UserIdCause_ShortDescription(alice.getDisplayName()), alice.getUrl()); + assertLogContains(page, "Running inside " + b.getDisplayName(), b.getUrl()); + assertThat(page.getWebResponse().getContentAsString().replace("\r\n", "\n"), + containsString("[Pipeline] hyperlink\nRunning inside { + Execution(StepContext context) { + super(context); + } + @Override protected Void run() throws Exception { + getContext().get(TaskListener.class).getLogger().println("Running inside " + ModelHyperlinkNote.encodeTo(getContext().get(Run.class))); + return null; + } + } + @TestExtension("consoleNotes") public static class DescriptorImpl extends StepDescriptor { + @Override public String getFunctionName() { + return "hyperlink"; + } + @Override public Set> getRequiredContext() { + return ImmutableSet.of(TaskListener.class, Run.class); + } + } + } + + @Test public void performance() throws Exception { + assumeFalse(Functions.isWindows()); // needs newline fixes; not bothering for now + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("@NonCPS def giant() {(0..999999).join('\\n')}; echo giant(); sleep 0", true)); + long start = System.nanoTime(); + WorkflowRun b = r.buildAndAssertSuccess(p); + System.out.printf("Took %dms to run the build%n", (System.nanoTime() - start) / 1000 / 1000); + // Whole-build HTML output: + StringWriter sw = new StringWriter(); + start = System.nanoTime(); + b.getLogText().writeHtmlTo(0, sw); + System.out.printf("Took %dms to write HTML of whole build%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(sw.toString(), containsString("\n456788\n456789\n456790\n")); + assertThat(sw.toString(), containsString("\n999999\n")); + // Length check (cf. Run/console.jelly, WorkflowRun/sidepanel.jelly): + start = System.nanoTime(); + long length = b.getLogText().length(); + System.out.printf("Took %dms to compute length of whole build%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(length, greaterThan(200000L)); + // Truncated (cf. Run/console.jelly): + long offset = length - 150 * 1024; + sw = new StringWriter(); + start = System.nanoTime(); + b.getLogText().writeHtmlTo(offset, sw); + System.out.printf("Took %dms to write truncated HTML of whole build%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(sw.toString(), not(containsString("\n456789\n"))); + assertThat(sw.toString(), containsString("\n999923\n")); + /* Whether or not this echo step is annotated in the truncated log is not really important: + assertThat(sw.toString(), containsString("\n999999\n")); + */ + // Plain text: + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + start = System.nanoTime(); + IOUtils.copy(b.getLogInputStream(), baos); + System.out.printf("Took %dms to write plain text of whole build%n", (System.nanoTime() - start) / 1000 / 1000); + // Raw: + assertThat(baos.toString(), containsString("\n456789\n")); + String rawLog = FileUtils.readFileToString(new File(b.getRootDir(), "log")); + assertThat(rawLog, containsString("0\n")); + assertThat(rawLog, containsString("\n999999\n")); + assertThat(rawLog, containsString("sleep any longer")); + // Per node: + FlowNode echo = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("echo")); + LogAction la = echo.getAction(LogAction.class); + assertNotNull(la); + baos = new ByteArrayOutputStream(); + la.getLogText().writeRawLogTo(0, baos); + assertThat(baos.toString(), not(containsString("Pipeline"))); + // Whole-build: + sw = new StringWriter(); + start = System.nanoTime(); + la.getLogText().writeHtmlTo(0, sw); + System.out.printf("Took %dms to write HTML of one long node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(sw.toString(), containsString("\n456789\n")); + // Length check (cf. AnnotatedLogAction/index.jelly): + start = System.nanoTime(); + length = la.getLogText().length(); + System.out.printf("Took %dms to compute length of one long node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(length, greaterThan(200000L)); + // Truncated (cf. AnnotatedLogAction/index.jelly): + sw = new StringWriter(); + offset = length - 150 * 1024; + start = System.nanoTime(); + la.getLogText().writeHtmlTo(offset, sw); + System.out.printf("Took %dms to write truncated HTML of one long node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(sw.toString(), not(containsString("\n456789\n"))); + assertThat(sw.toString(), containsString("\n999923\n")); + // Raw (currently not exposed in UI but could be): + baos = new ByteArrayOutputStream(); + start = System.nanoTime(); + la.getLogText().writeRawLogTo(0, baos); + System.out.printf("Took %dms to write plain text of one long node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(baos.toString(), containsString("\n456789\n")); + // Node with litte text: + FlowNode sleep = new DepthFirstScanner().findFirstMatch(b.getExecution(), new NodeStepTypePredicate("sleep")); + la = sleep.getAction(LogAction.class); + assertNotNull(la); + sw = new StringWriter(); + start = System.nanoTime(); + la.getLogText().writeHtmlTo(0, sw); + System.out.printf("Took %dms to write HTML of one short node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(sw.toString(), containsString("No need to sleep any longer")); + // Length check + start = System.nanoTime(); + length = la.getLogText().length(); + System.out.printf("Took %dms to compute length of one short node%n", (System.nanoTime() - start) / 1000 / 1000); + assertThat(length, lessThan(50L)); + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNoteTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNoteTest.java new file mode 100644 index 00000000..8b29790b --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNoteTest.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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.console; + +import static org.hamcrest.Matchers.containsString; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import static org.junit.Assert.assertThat; +import org.junit.Test; +import org.junit.ClassRule; +import org.junit.Rule; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsRule; + +public class NewNodeConsoleNoteTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Test public void labels() throws Exception { + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("parallel first: {}, second: {stage('details') {}}; stage 'not \"blocky\"'", true)); + WorkflowRun b = r.buildAndAssertSuccess(p); + String html = r.createWebClient().goTo(b.getUrl() + "console").getWebResponse().getContentAsString(); + assertThat(html, containsString("[Pipeline] parallel")); + assertThat(html, containsString("[Pipeline] {")); + assertThat(html, containsString("[Pipeline] {")); + assertThat(html, containsString("[Pipeline] {")); + assertThat(html, containsString("[Pipeline] // parallel")); + assertThat(html, containsString("[Pipeline] stage")); + } + +} diff --git a/src/test/resources/org/jenkinsci/plugins/workflow/job/WorkflowRunTest/stepRunningAcrossUpgrade.zip b/src/test/resources/org/jenkinsci/plugins/workflow/job/WorkflowRunTest/stepRunningAcrossUpgrade.zip new file mode 100644 index 00000000..a5bab558 Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/workflow/job/WorkflowRunTest/stepRunningAcrossUpgrade.zip differ