Skip to content

Commit

Permalink
Reimplemented HTML display of Pipeline console to include some featur…
Browse files Browse the repository at this point in the history
…es from #21.
  • Loading branch information
jglick committed Oct 10, 2016
1 parent e4968ef commit e6a58e8
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 136 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
</pluginRepositories>
<properties>
<jenkins.version>1.642.3</jenkins.version>
<jenkins-test-harness.version>2.16</jenkins-test-harness.version>
<jenkins-test-harness.version>2.17</jenkins-test-harness.version>
</properties>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Main;
import hudson.XmlFile;
import hudson.console.AnnotatedLargeText;
import hudson.console.ConsoleNote;
Expand Down Expand Up @@ -96,9 +95,9 @@
import org.jenkinsci.plugins.workflow.flow.StashManager;
import org.jenkinsci.plugins.workflow.graph.FlowEndNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.console.NewNodeConsoleNote;
import org.jenkinsci.plugins.workflow.job.console.PipelineLargeText;
import org.jenkinsci.plugins.workflow.job.console.PipelineLogFile;
import org.jenkinsci.plugins.workflow.job.console.WorkflowRunConsoleNote;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
Expand Down Expand Up @@ -704,7 +703,7 @@ private final class GraphL implements GraphListener {
*/
private final class NodePrintListener implements GraphListener.Synchronous {
@Override public void onNewHead(FlowNode node) {
WorkflowRunConsoleNote.print(node.getDisplayFunctionName(), listener);
NewNodeConsoleNote.print(node, listener);
}
}

Expand Down Expand Up @@ -756,9 +755,7 @@ private final class NodePrintListener implements GraphListener.Synchronous {
}

@Override public File getLogFile() {
if (!Main.isUnitTest) { // TODO at least until https://github.com/jenkinsci/jenkins-test-harness/pull/38
LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, new UnsupportedOperationException());
}
LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, new UnsupportedOperationException());

This comment has been minimized.

Copy link
@MarkEWaite

MarkEWaite Nov 6, 2018

Contributor

In JENKINS-54128 there is a question asking what should be used in Pipeline jobs to read the log file if not getLogFile.

Any recommendations?

This comment has been minimized.

Copy link
@dwnusbaum

dwnusbaum Nov 6, 2018

Member

I commented on the ticket to give some more context.

try {
File f = File.createTempFile("deprecated", ".log", getRootDir());
f.deleteOnExit();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleNote;
import hudson.model.Run;
import hudson.model.TaskListener;
import java.io.IOException;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction;
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:
* <ul>
* <li>{@code nodeId} for {@link FlowNode#getId}
* <li>{@code parentIds} for {@link FlowNode#getParents}
* <li>{@code startId} for {@link BlockEndNode#getStartNode} (otherwise absent)
* </ul>
* @see AnnotatedLogAction#annotateHtml
*/
@Restricted(NoExternalUse.class)
public class NewNodeConsoleNote extends ConsoleNote<Run<?, ?>> {

/**
* Prefix used in metadata lines.
*/
private static final String CONSOLE_NOTE_PREFIX = "[Pipeline] ";

public static void print(FlowNode node, TaskListener listener) {
try {
listener.annotate(new NewNodeConsoleNote(node));
} catch (IOException x) {
// never mind
}
listener.getLogger().println(CONSOLE_NOTE_PREFIX + node.getDisplayFunctionName());
}

private final @Nonnull String id;
private final @Nonnull String[] parents;
private final @CheckForNull String start;

private NewNodeConsoleNote(FlowNode node) {
id = node.getId();
List<FlowNode> parentNodes = node.getParents();
parents = new String[parentNodes.size()];
for (int i = 0; i < parentNodes.size(); i++) {
parents[i] = parentNodes.get(i).getId();
}
start = node instanceof BlockEndNode ? ((BlockEndNode) node).getStartNode().getId() : null;
}

@Override
public ConsoleAnnotator<?> annotate(Run<?, ?> context, MarkupText text, int charPos) {
StringBuilder startTag = new StringBuilder("<span class=\"pipeline-new-node\" nodeId=\"").append(id);
for (int i = 0; i < parents.length; i++) {
startTag.append(i == 0 ? "\" parentIds=\"" : " ").append(parents[i]);
}
if (start != null) {
startTag.append("\" startId=\"").append(start);
}
startTag.append("\">");
text.addMarkup(0, text.length(), startTag.toString(), "</span>");
// TODO should we also add another span around the actual displayFunctionName text, to make it easy to parse out?
return null;
}

private static final long serialVersionUID = 1L;

@Extension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,40 @@
package org.jenkinsci.plugins.workflow.job.console;

import com.google.common.base.Charsets;
import com.jcraft.jzlib.GZIPInputStream;
import com.jcraft.jzlib.GZIPOutputStream;
import com.trilead.ssh2.crypto.Base64;
import hudson.console.AnnotatedLargeText;
import hudson.console.ConsoleAnnotationOutputStream;
import hudson.console.ConsoleAnnotator;
import hudson.remoting.ClassFilter;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.TimeUnit2;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Writer;
import static java.lang.Math.abs;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import jenkins.model.Jenkins;
import jenkins.security.CryptoConfidentialKey;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.framework.io.ByteBuffer;

/**
Expand All @@ -43,25 +67,90 @@
@Restricted(NoExternalUse.class)
public class PipelineLargeText extends AnnotatedLargeText<WorkflowRun> {

private final WorkflowRun context;

public PipelineLargeText(WorkflowRun build) {
this(build, new ByteBuffer());
this(build, new HackedByteBuffer());
}

/** Records length of the raw log file, so that {@link #doProgressText} does not think we have blown past the end. */
static class HackedByteBuffer extends ByteBuffer {
long length;
@Override public long length() {
return Math.max(length, super.length());
}
}

private PipelineLargeText(WorkflowRun build, ByteBuffer buf) {
private PipelineLargeText(WorkflowRun build, HackedByteBuffer buf) {
super(buf, Charsets.UTF_8, !build.isLogUpdated(), build);
// Overriding getLogTo works to strip annotations from plain-text console output.
// It does *not* work to override writeHtmlTo:
// AbstractMarkupText.wrapBy and similar routinely put the close tag on the next line,
// since the marked-up text includes the newline.
// Thus we would be trying to strip, e.g. "<span class='red'>Some headline\n</span>¦123Regular line\n".
// TODO for simplicitly, currently just making a copy of the log into a memory buffer.
// Overriding writeLogTo would work to strip annotations from plain-text console output more efficiently,
// though it would be cumbersome to also override all the other LargeText methods, esp. doProgressText.
// (We could also override ByteBuffer to stream output after stripping, but the length would be wrong, if anyone cares.)
FlowExecutionOwner owner = build.asFlowExecutionOwner();
if (owner != null) {
try (InputStream log = owner.getLog()) {
AnnotatedLogAction.strip(log, buf);
} catch (IOException ex) {
Logger.getLogger(PipelineLargeText.class.getName()).log(Level.SEVERE, null, ex);
assert owner != null;
try (InputStream log = owner.getLog(); CountingInputStream cis = new CountingInputStream(log)) {
AnnotatedLogAction.strip(cis, buf);
buf.length = cis.getByteCount();
} catch (IOException ex) {
Logger.getLogger(PipelineLargeText.class.getName()).log(Level.SEVERE, null, ex);
}
this.context = build;
}

// It does *not* work to override writeHtmlTo to strip node annotations after ConsoleNote’s are processed:
// AbstractMarkupText.wrapBy and similar routinely put the close tag on the next line,
// since the marked-up text includes the newline.
// Thus we would be trying to parse, e.g., "123¦<span class='red'>Some headline\n</span>123¦Regular line\n"
// and it is not necessarily obvious where the boundaries of the ID are.
// Anyway AnnotatedLogAction.annotateHtml is an easier way of handling node annotations.
@Override public long writeHtmlTo(long start, Writer w) throws IOException {
ConsoleAnnotationOutputStream<WorkflowRun> caw = AnnotatedLogAction.annotateHtml(
w, createAnnotator(Stapler.getCurrentRequest()), context);
FlowExecutionOwner owner = context.asFlowExecutionOwner();
assert owner != null;
long r;
try (InputStream log = owner.getLog()) {
log.skip(start); // TODO probably want to let the implementation of PipelineLogFile implement this more efficiently
CountingInputStream cis = new CountingInputStream(log);
IOUtils.copy(cis, caw);
r = start + cis.getByteCount();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Cipher sym = PASSING_ANNOTATOR.encrypt();
try (ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos, sym)))) {
oos.writeLong(System.currentTimeMillis());
oos.writeObject(caw.getConsoleAnnotator());
}
StaplerResponse rsp = Stapler.getCurrentResponse();
if (rsp != null) {
rsp.setHeader("X-ConsoleAnnotator", new String(Base64.encode(baos.toByteArray())));
}
return r;
}

private ConsoleAnnotator<WorkflowRun> createAnnotator(StaplerRequest req) throws IOException {
try {
String base64 = req != null ? req.getHeader("X-ConsoleAnnotator") : null;
if (base64 != null) {
Cipher sym = PASSING_ANNOTATOR.decrypt();
try (ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(
new CipherInputStream(new ByteArrayInputStream(Base64.decode(base64.toCharArray())), sym)),
Jenkins.getInstance().pluginManager.uberClassLoader,
ClassFilter.DEFAULT)) {
long timestamp = ois.readLong();
if (TimeUnit2.HOURS.toMillis(1) > abs(System.currentTimeMillis() - timestamp)) {
@SuppressWarnings("unchecked") ConsoleAnnotator<WorkflowRun> annotator = (ConsoleAnnotator) ois.readObject();
return annotator;
}
}
}
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
return ConsoleAnnotator.initial(context);
}

private static final CryptoConfidentialKey PASSING_ANNOTATOR = new CryptoConfidentialKey(PipelineLargeText.class, "consoleAnnotator");

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@

package org.jenkinsci.plugins.workflow.job.console;

import org.jenkinsci.plugins.workflow.job.WorkflowRun;

import hudson.MarkupText;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleNote;
import hudson.model.Run;
import hudson.model.TaskListener;
import java.io.IOException;

/**
* Console note for Workflow metadata specific messages.
* @deprecated No longer used, but retained for serial-form compatibility of old build logs.
* @see NewNodeConsoleNote
*/
@Deprecated
public class WorkflowRunConsoleNote extends ConsoleNote<Run<?, ?>> {

/**
* Prefix used in metadata lines.
*/
private static final String CONSOLE_NOTE_PREFIX = "[Pipeline] ";
public static final String CONSOLE_NOTE_PREFIX = "[Pipeline] ";

/**
* CSS color selector.
Expand All @@ -49,20 +51,13 @@ public class WorkflowRunConsoleNote extends ConsoleNote<Run<?, ?>> {
private static final String START_NOTE = "<span style=\"color:#"+ TEXT_COLOR +"\">";
private static final String END_NOTE = "</span>";

public static void print(String message, TaskListener listener) {
try {
listener.annotate(new WorkflowRunConsoleNote());
} catch (IOException x) {
// never mind
}
listener.getLogger().println(CONSOLE_NOTE_PREFIX + message);
}

private WorkflowRunConsoleNote() {}

@Override
public ConsoleAnnotator<?> annotate(Run<?, ?> context, MarkupText text, int charPos) {
text.addMarkup(0, text.length(), START_NOTE, END_NOTE);
public ConsoleAnnotator<Run<?,?>> annotate(Run<?, ?> context, MarkupText text, int charPos) {
if (context instanceof WorkflowRun) {
if (text.getText().startsWith(CONSOLE_NOTE_PREFIX)) {
text.addMarkup(0, text.length(), START_NOTE, END_NOTE);
}
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@
<l:task icon="images/24x24/up.png" href="${rootURL}/${it.parent.url}" title="${%Back to Project}" contextMenu="false"/>
<l:task icon="images/24x24/search.png" href="${buildUrl.baseUrl}/" title="${%Status}" contextMenu="false"/>
<l:task icon="images/24x24/notepad.png" href="${buildUrl.baseUrl}/changes" title="${%Changes}"/>
<p:console-link/>
<j:choose> <!-- TODO <p:console-link/> may not currently be used as it calls getLogFile -->
<j:when test="${it.logText.length() > 200000}">
<l:task href="${buildUrl.baseUrl}/console" icon="icon-terminal icon-md" title="${%Console Output}"/>
<l:task href="${buildUrl.baseUrl}/consoleText" icon="icon-document icon-md" title="${%View as plain text}"/>
</j:when>
<j:otherwise>
<l:task icon="images/24x24/terminal.png" href="${buildUrl.baseUrl}/console" title="${%Console Output}">
<l:task href="${buildUrl.baseUrl}/consoleText" icon="icon-document icon-md" title="${%View as plain text}"/>
</l:task>
</j:otherwise>
</j:choose>
<l:task icon="images/24x24/notepad.png" href="${buildUrl.baseUrl}/configure" title="${h.hasPermission(it,it.UPDATE)?'%Edit Build Information':'%View Build Information'}"/>
<st:include page="delete.jelly"/>
<st:include page="actions.jelly"/>
Expand Down
Loading

0 comments on commit e6a58e8

Please sign in to comment.