Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
0.5.3 (Next)
0.6 (Next)
============

* Your contribution here.
* [#107](https://github.com/jenkinsci/ansicolor-plugin/pull/107): Removing startup banner - [@jglick](https://github.com/jglick).
* [#128](https://github.com/jenkinsci/ansicolor-plugin/pull/128): Restoring limited compatibility for coloration generated remotely by Pipeline builds on agents - [@jglick](https://github.com/jglick).
* [#132](https://github.com/jenkinsci/ansicolor-plugin/pull/132): Reworked implementation to add markup on display, not to the actual build log - [@jglick](https://github.com/jglick).

0.5.2 (08/17/2017)
============
Expand Down
15 changes: 13 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>

<artifactId>ansicolor</artifactId>
<version>0.5.3-SNAPSHOT</version>
<version>0.6-SNAPSHOT</version>
<packaging>hpi</packaging>

<name>AnsiColor</name>
Expand Down Expand Up @@ -46,7 +46,7 @@
</distributionManagement>

<properties>
<jenkins.version>2.121.2</jenkins.version>
<jenkins.version>2.145</jenkins.version> <!-- to pick up https://github.com/jenkinsci/jenkins/pull/3662 -->
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently I should have offered jenkinsci/jenkins#3662 up for backport. At the time it did not seem like a major fix. But in fact this patch will not work without it for freestyle builds or per-step logs (still works for Pipeline whole-build logs): you get a Class for the context and there is no way to look for ColorizedAction.

Copy link
Member

@dwnusbaum dwnusbaum Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since that fix is not in any LTS line yet, I think it would be good to release what we currently have in master for anyone using JEP-210 in recent LTS lines, and then we can release this afterwards as a complete fix for future LTS lines. EDIT: Done with the recently released 0.5.3.

<java.level>8</java.level>
</properties>

Expand All @@ -57,6 +57,12 @@
<version>2.15</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.30</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
Expand Down Expand Up @@ -89,6 +95,11 @@
<artifactId>structs</artifactId>
<version>1.17</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>2.2.6</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.console.ConsoleLogFilter;
import hudson.model.TaskListener;
import hudson.model.AbstractProject;
import hudson.model.Run;
Expand Down Expand Up @@ -80,6 +79,7 @@ public DescriptorImpl getDescriptor() {
@Override
public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener,
EnvVars initialEnvironment) throws IOException, InterruptedException {
build.replaceAction(new ColorizedAction(colorMapName));
}

/**
Expand Down Expand Up @@ -188,8 +188,4 @@ public boolean isApplicable(AbstractProject<?, ?> item) {
}
}

@Override
public ConsoleLogFilter createLoggerDecorator(Run<?, ?> build) {
return new AnsiColorConsoleLogFilter(getDescriptor().getColorMap(colorMapName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

/**
* {@link ConsoleLogFilter} that adds a {@link SimpleHtmlNote} to each line.
* @deprecated Only here for serial form compatibility.
*/
@Deprecated
public final class AnsiColorConsoleLogFilter extends ConsoleLogFilter implements Serializable {

private static final long serialVersionUID = 1L;
Expand Down
60 changes: 28 additions & 32 deletions src/main/java/hudson/plugins/ansicolor/AnsiColorStep.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
package hudson.plugins.ansicolor;

import hudson.Extension;
import hudson.console.ConsoleLogFilter;
import hudson.plugins.ansicolor.AnsiColorBuildWrapper.DescriptorImpl;
import hudson.util.ListBoxModel;

import java.io.IOException;
import java.util.Collections;

import javax.annotation.Nonnull;

import jenkins.model.Jenkins;

import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.kohsuke.stapler.DataBoundConstructor;

import com.google.inject.Inject;
import hudson.model.Run;
import java.util.Set;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;

/**
* Custom pipeline step that can be used without a node and build wrapper.
*/
public class AnsiColorStep extends AbstractStepImpl {
public class AnsiColorStep extends Step {

private final String colorMapName;

Expand All @@ -50,55 +48,47 @@ private static DescriptorImpl getWrapperDescriptor() {
return Jenkins.getActiveInstance().getDescriptorByType(DescriptorImpl.class);
}

@Override
public StepExecution start(StepContext context) throws Exception {
return new ExecutionImpl(context, colorMapName);
}

/**
* Execution for {@link AnsiColorStep}.
*/
public static class ExecutionImpl extends AbstractStepExecutionImpl {
private static class ExecutionImpl extends AbstractStepExecutionImpl {

private static final long serialVersionUID = 1L;

@Inject(optional = true)
private transient AnsiColorStep step;
private final String colorMapName;

ExecutionImpl(StepContext context, String colorMapName) {
super(context);
this.colorMapName = colorMapName;
}

/**
* {@inheritDoc}
*/
@Override
public boolean start() throws Exception {
StepContext context = getContext();
context.get(Run.class).replaceAction(new ColorizedAction(colorMapName));
EnvironmentExpander currentEnvironment = context.get(EnvironmentExpander.class);
EnvironmentExpander terminalEnvironment = EnvironmentExpander.constant(Collections.singletonMap("TERM", step.getColorMapName()));
context.newBodyInvoker().withContext(createConsoleLogFilter(context))
EnvironmentExpander terminalEnvironment = EnvironmentExpander.constant(Collections.singletonMap("TERM", colorMapName));
context.newBodyInvoker()
.withContext(EnvironmentExpander.merge(currentEnvironment, terminalEnvironment))
.withCallback(BodyExecutionCallback.wrap(context)).start();
return false;
}

private ConsoleLogFilter createConsoleLogFilter(StepContext context)
throws IOException, InterruptedException {
ConsoleLogFilter original = context.get(ConsoleLogFilter.class);
ConsoleLogFilter subsequent = new AnsiColorConsoleLogFilter(step.getColorMap());
return BodyInvoker.mergeConsoleLogFilters(original, subsequent);
}

/**
* {@inheritDoc}
*/
@Override
public void stop(@Nonnull Throwable cause) throws Exception {
getContext().onFailure(cause);
}
}

/**
* Descriptor for {@link AnsiColorStep}.
*/
@Extension(optional = true)
public static class StepDescriptorImpl extends AbstractStepDescriptorImpl {

public StepDescriptorImpl() {
super(ExecutionImpl.class);
}
public static class StepDescriptorImpl extends StepDescriptor {

@Override
public String getDisplayName() {
Expand All @@ -124,5 +114,11 @@ public boolean takesImplicitBlockArgument() {
public ListBoxModel doFillColorMapNameItems() {
return getWrapperDescriptor().doFillColorMapNameItems();
}

@Override
public Set<? extends Class<?>> getRequiredContext() {
return Collections.singleton(Run.class);
}

}
}
135 changes: 135 additions & 0 deletions src/main/java/hudson/plugins/ansicolor/ColorConsoleAnnotator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* The MIT License
*
* Copyright 2018 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 hudson.plugins.ansicolor;

import hudson.Extension;
import hudson.MarkupText;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.model.Queue;
import hudson.model.Run;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.io.output.CountingOutputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringEscapeUtils;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.FlowNode;

/**
* Applies ANSI coloration to log files where requested.
*/
final class ColorConsoleAnnotator extends ConsoleAnnotator<Object> {

private static final Logger LOGGER = Logger.getLogger(ColorConsoleAnnotator.class.getName());

private static final long serialVersionUID = 1;

private final String colorMapName;
private final String charset;

ColorConsoleAnnotator(String colorMapName, String charset) {
this.colorMapName = colorMapName;
this.charset = charset;
LOGGER.fine("creating annotator with colorMapName=" + colorMapName + " charset=" + charset);
}

@Override
public ConsoleAnnotator<Object> annotate(Object context, MarkupText text) {
String s = text.getText();
if (s.indexOf('\u001B') != -1) {
AnsiColorMap colorMap = Jenkins.get().getDescriptorByType(AnsiColorBuildWrapper.DescriptorImpl.class).getColorMap(colorMapName);
CountingOutputStream outgoing = new CountingOutputStream(new NullOutputStream());
class EmitterImpl implements AnsiAttributeElement.Emitter {
CountingOutputStream incoming;
int adjustment;
int lastPoint = -1; // multiple HTML tags may be emitted for one control sequence
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The triumph of test-driven development over comprehensibility.

@Override
public void emitHtml(String html) {
LOGGER.finest("emitting " + html + " @" + incoming.getCount());
text.addMarkup(incoming.getCount(), html);
if (incoming.getCount() != lastPoint) {
lastPoint = incoming.getCount();
int hide = incoming.getCount() - outgoing.getCount() - adjustment;
LOGGER.finest("hiding " + hide + " @" + (outgoing.getCount() + adjustment));
text.addMarkup(outgoing.getCount() + adjustment, outgoing.getCount() + adjustment + hide, "<span style=\"display: none\">", "</span>");
adjustment += hide;
}
}
}
EmitterImpl emitter = new EmitterImpl();
CountingOutputStream incoming = new CountingOutputStream(new AnsiHtmlOutputStream(outgoing, colorMap, emitter));
emitter.incoming = incoming;
try {
byte[] data = s.getBytes(charset);
for (int i = 0; i < data.length; i++) {
// Do not use write(byte[]) as offsets in incoming would not be accurate.
incoming.write(data[i]);
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
}
LOGGER.finer(() -> "\"" + StringEscapeUtils.escapeJava(s) + "\"\"" + StringEscapeUtils.escapeJava(text.toString(true)) + "\"");
}
return this;
}

@Extension
public static final class Factory extends ConsoleAnnotatorFactory<Object> {

@Override
public ConsoleAnnotator<Object> newInstance(Object context) {
LOGGER.fine("context=" + context);
if (context instanceof Run) {
ColorizedAction action = ((Run) context).getAction(ColorizedAction.class);
if (action != null) {
return new ColorConsoleAnnotator(action.colorMapName, ((Run) context).getCharset().name());
}
} else if (Jenkins.get().getPlugin("workflow-api") != null && context instanceof FlowNode) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not try to verify behavior when the plugin is not in fact installed. Generally speaking, optional deps are dangerous. I would rather just make it a hard dep.

Copy link
Member

@dwnusbaum dwnusbaum Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that making it a normal dependency would be safer, but I did some testing with the plugins not installed and things seem to work fine. New freestyle builds are still colored correctly, and executing Jenkins.get().getPlugin("workflow-api") in the script console returns null, so the short-circuiting behavior here looks fine to me.

FlowNode node = (FlowNode) context;
FlowExecutionOwner owner = node.getExecution().getOwner();
if (owner != null) {
Queue.Executable exec = null;
try {
exec = owner.getExecutable();
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
}
if (exec instanceof Run) {
ColorizedAction action = ((Run) exec).getAction(ColorizedAction.class);
if (action != null) {
return new ColorConsoleAnnotator(action.colorMapName, /* JEp-206 */ "UTF-8");
}
}
}
}
return null;
}

}

}
41 changes: 41 additions & 0 deletions src/main/java/hudson/plugins/ansicolor/ColorizedAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* The MIT License
*
* Copyright 2018 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 hudson.plugins.ansicolor;

import hudson.model.InvisibleAction;

/**
* Marker for the fact that a build used colorization.
* Note that the specific log span(s) are ignored.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternately, could go the route of jenkinsci/timestamper-plugin#25 and just have a global option to turn on ANSI coloring (with a specified colormap), and deprecate the step & build wrapper altogether. This would be more in line with how Blue Ocean works: it just assumes that if it sees these escape sequences, it should render them.

*/
final class ColorizedAction extends InvisibleAction {

final String colorMapName;

ColorizedAction(String colorMapName) {
this.colorMapName = colorMapName;
}

}
Loading