-
-
Notifications
You must be signed in to change notification settings - Fork 86
Allow escape sequences to span multiple lines and support default fg/bg colors #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
e6b8ff5
cfa7144
a95ea81
2c19b61
a2a74eb
7d8b58d
c1d7aab
50893ab
ce424f3
be7df32
31bb937
200bf2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,8 +31,11 @@ | |
| import hudson.model.Queue; | ||
| import hudson.model.Run; | ||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
| import javax.annotation.Nonnull; | ||
| import jenkins.model.Jenkins; | ||
| import org.apache.commons.io.output.CountingOutputStream; | ||
| import org.apache.commons.io.output.NullOutputStream; | ||
|
|
@@ -51,59 +54,102 @@ final class ColorConsoleAnnotator extends ConsoleAnnotator<Object> { | |
|
|
||
| private final String colorMapName; | ||
| private final String charset; | ||
| private final @Nonnull List<AnsiAttributeElement> openTags; | ||
|
|
||
| ColorConsoleAnnotator(String colorMapName, String charset) { | ||
| ColorConsoleAnnotator(String colorMapName, String charset, List<AnsiAttributeElement> openTags) { | ||
| this.colorMapName = colorMapName; | ||
| this.charset = charset; | ||
| LOGGER.fine("creating annotator with colorMapName=" + colorMapName + " charset=" + charset); | ||
| this.openTags = openTags; | ||
| LOGGER.log(Level.FINE, "creating annotator with colorMapName={0} charset={1} openTags={2}", new Object[] { colorMapName, charset, openTags }); | ||
| } | ||
|
|
||
| ColorConsoleAnnotator(String colorMapName, String charset) { | ||
| this(colorMapName, charset, Collections.emptyList()); | ||
| } | ||
|
|
||
| @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); | ||
| List<AnsiAttributeElement> nextOpenTags = openTags; | ||
| // TODO: As a performance improvement, we could create a branch where `s.indexOf('\u001B') == -1` but other | ||
| // conditions are true that surrounds the text in the appropriate tags without going through AnsiHtmlOutputStream. | ||
| AnsiColorMap colorMap = Jenkins.get().getDescriptorByType(AnsiColorBuildWrapper.DescriptorImpl.class).getColorMap(colorMapName); | ||
| if (s.indexOf('\u001B') != -1 || !openTags.isEmpty() || colorMap.getDefaultBackground() != null || colorMap.getDefaultForeground() != null) { | ||
| 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 | ||
| @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; | ||
| int inCount = incoming.getCount(); | ||
| // All ANSI escapes sequences contain at least 2 bytes on modern platforms, so any HTML emitted | ||
| // directly after the first byte is received is due to the initialization process of the stream and | ||
| // belongs at position 0 (i.e. default background/foreground colors). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I need more caffeine to understand this. :-)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this code was and still is quite confusing to me. Thankfully, the |
||
| if (inCount == 1) { | ||
| inCount = 0; | ||
| } | ||
| LOGGER.log(Level.FINEST, "emitting {0} @{1}", new Object[] { html, inCount }); | ||
| text.addMarkup(inCount, html); | ||
| if (inCount != lastPoint) { | ||
| lastPoint = inCount; | ||
| int hide = inCount - outgoing.getCount() - adjustment; | ||
| // If openTags is not empty, but there are no escape sequences directly on this line, or if we | ||
| // are emitting closing tags when closing the stream, there is nothing to hide. | ||
| if (hide != 0) { | ||
| LOGGER.log(Level.FINEST, "hiding {0} @{1}", new Object[] { hide, outgoing.getCount() + adjustment }); | ||
| text.addMarkup(outgoing.getCount() + adjustment, outgoing.getCount() + adjustment + hide, "<span style=\"display: none\">", "</span>"); | ||
| adjustment += hide; | ||
| } | ||
| } | ||
| } | ||
| public void emitHtmlDirect(String html) { | ||
| text.addMarkup(incoming.getCount(), html); | ||
| } | ||
| } | ||
| EmitterImpl emitter = new EmitterImpl(); | ||
| CountingOutputStream incoming = new CountingOutputStream(new AnsiHtmlOutputStream(outgoing, colorMap, emitter)); | ||
| emitter.incoming = incoming; | ||
| try { | ||
| // We need to reopen tags that were still open at the end of the previous line so the stream's state is | ||
| // correct in case those tags are closed in the middle of this line. | ||
| try (AnsiHtmlOutputStream ansiOs = new AnsiHtmlOutputStream(outgoing, colorMap, emitter, openTags); | ||
| CountingOutputStream incoming = new CountingOutputStream(ansiOs)) { | ||
| emitter.incoming = incoming; | ||
| 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]); | ||
| } | ||
| nextOpenTags = ansiOs.getOpenTags(); | ||
| if (colorMap.getDefaultBackground() != null || colorMap.getDefaultForeground() != null) { | ||
| // The default color scheme will be opened automatically at the beginning of the stream on the next | ||
| // line, so we don't want to duplicate it. | ||
| nextOpenTags.remove(0); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I tried switching to |
||
| } | ||
| // Tags open at the end of the line are closed when the stream is closed by the try-with-resources block. | ||
| } catch (IOException x) { | ||
| LOGGER.log(Level.WARNING, null, x); | ||
| } | ||
| LOGGER.finer(() -> "\"" + StringEscapeUtils.escapeJava(s) + "\" → \"" + StringEscapeUtils.escapeJava(text.toString(true)) + "\""); | ||
| } | ||
| return this; | ||
| return openTags == nextOpenTags | ||
| ? this | ||
| : new ColorConsoleAnnotator(colorMapName, charset, nextOpenTags); | ||
|
||
| } | ||
|
|
||
| private Object readResolve() { | ||
dwnusbaum marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Compatibility for instances serialized before the openTags field was added. | ||
| if (openTags == null) { | ||
| return new ColorConsoleAnnotator(colorMapName, charset); | ||
| } else { | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| @Extension | ||
| public static final class Factory extends ConsoleAnnotatorFactory<Object> { | ||
|
|
||
| @Override | ||
| public ConsoleAnnotator<Object> newInstance(Object context) { | ||
| LOGGER.fine("context=" + context); | ||
| LOGGER.log(Level.FINE, "context={0}", context); | ||
| if (context instanceof Run) { | ||
| ColorizedAction action = ((Run) context).getAction(ColorizedAction.class); | ||
| if (action != null) { | ||
|
|
@@ -122,7 +168,7 @@ public ConsoleAnnotator<Object> newInstance(Object context) { | |
| if (exec instanceof Run) { | ||
| ColorizedAction action = ((Run) exec).getAction(ColorizedAction.class); | ||
| if (action != null) { | ||
| return new ColorConsoleAnnotator(action.colorMapName, /* JEp-206 */ "UTF-8"); | ||
| return new ColorConsoleAnnotator(action.colorMapName, /* JEP-206 */ "UTF-8"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,61 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen | |
| }); | ||
| } | ||
|
|
||
| @Test | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should create a JIRA issue for these and add |
||
| public void testMultilineEscapeSequence() throws Exception { | ||
| story.then(r -> { | ||
| FreeStyleProject p = r.createFreeStyleProject(); | ||
| p.getBuildWrappersList().add(new AnsiColorBuildWrapper(null)); | ||
| p.getBuildersList().add(new TestBuilder() { | ||
| @Override | ||
| public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { | ||
| listener.getLogger().println("\u001B[1;34mThis text should be bold and blue"); | ||
| listener.getLogger().println("Still bold and blue"); | ||
| listener.getLogger().println("\u001B[mThis text should be normal"); | ||
| return true; | ||
| } | ||
| }); | ||
| FreeStyleBuild b = r.buildAndAssertSuccess(p); | ||
| StringWriter writer = new StringWriter(); | ||
| b.getLogText().writeHtmlTo(0L, writer); | ||
| String html = writer.toString(); | ||
| System.out.print(html); | ||
| assertThat(html.replaceAll("<span style=\"display: none\">.+?</span>", ""), | ||
| allOf( | ||
| containsString("<b><span style=\"color: #1E90FF;\">This text should be bold and blue\n</span></b>"), | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The literal |
||
| containsString("<b><span style=\"color: #1E90FF;\">Still bold and blue\n</span></b>"), | ||
| not(containsString("\u001B[m")))); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDefaultForegroundBackground() throws Exception { | ||
| story.then(r -> { | ||
| FreeStyleProject p = r.createFreeStyleProject(); | ||
| // The VGA ColorMap sets default foreground and background colors. | ||
| p.getBuildWrappersList().add(new AnsiColorBuildWrapper("vga")); | ||
| p.getBuildersList().add(new TestBuilder() { | ||
| @Override | ||
| public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { | ||
| listener.getLogger().println("White on black"); | ||
| listener.getLogger().println("\u001B[1;34mBold and blue on black"); | ||
| listener.getLogger().println("Still bold and blue on black\u001B[mBack to white on black"); | ||
| return true; | ||
| } | ||
| }); | ||
| FreeStyleBuild b = r.buildAndAssertSuccess(p); | ||
| StringWriter writer = new StringWriter(); | ||
| b.getLogText().writeHtmlTo(0L, writer); | ||
| String html = writer.toString(); | ||
| System.out.print(html); | ||
| assertThat(html.replaceAll("<span style=\"display: none\">.+?</span>", ""), | ||
| allOf( | ||
| containsString("<div style=\"background-color: #000000;color: #AAAAAA;\">White on black\n</div>"), | ||
| containsString("<div style=\"background-color: #000000;color: #AAAAAA;\"><b><span style=\"color: #0000AA;\">Bold and blue on black\n</span></b></div>"), | ||
| containsString("<div style=\"background-color: #000000;color: #AAAAAA;\"><b><span style=\"color: #0000AA;\">Still bold and blue on black</span></b>Back to white on black\n</div>"))); | ||
| }); | ||
| } | ||
|
|
||
| @Issue("JENKINS-54133") | ||
| @Test | ||
| public void testWorkflowWrap() throws Exception { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary—this case is likely quite rare.