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 extends FlowNode> 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 extends Class>> 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