Skip to content

Commit 3662d2b

Browse files
authored
Merge pull request #76 from jglick/ConsoleLogFilter-JENKINS-45693
[JENKINS-45693] Defined TaskListenerDecorator
2 parents 6f39eb9 + 7aa7627 commit 3662d2b

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

src/main/java/org/jenkinsci/plugins/workflow/log/LogStorage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public interface LogStorage {
5252
* Provides an alternate way of emitting output from a build.
5353
* <p>May implement {@link AutoCloseable} to clean up at the end of a build;
5454
* it may or may not be closed during Jenkins shutdown while a build is running.
55+
* <p>The caller may wrap the result using {@link TaskListenerDecorator#apply}.
5556
* @return a (remotable) build listener; do not bother overriding anything except {@link TaskListener#getLogger}
5657
* @see FlowExecutionOwner#getListener
5758
*/
@@ -61,6 +62,7 @@ public interface LogStorage {
6162
* Provides an alternate way of emitting output from a node (such as a step).
6263
* <p>May implement {@link AutoCloseable} to clean up at the end of a node ({@link FlowNode#isActive});
6364
* it may or may not be closed during Jenkins shutdown while a build is running.
65+
* <p>The caller may wrap the result using {@link TaskListenerDecorator#apply}.
6466
* @param node a running node
6567
* @return a (remotable) task listener; do not bother overriding anything except {@link TaskListener#getLogger}
6668
* @see StepContext#get
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2018 CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.jenkinsci.plugins.workflow.log;
26+
27+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28+
import hudson.ExtensionList;
29+
import hudson.ExtensionPoint;
30+
import hudson.console.ConsoleLogFilter;
31+
import hudson.console.LineTransformationOutputStream;
32+
import hudson.model.AbstractBuild;
33+
import hudson.model.BuildListener;
34+
import hudson.model.Run;
35+
import hudson.model.TaskListener;
36+
import java.io.IOException;
37+
import java.io.OutputStream;
38+
import java.io.PrintStream;
39+
import java.io.Serializable;
40+
import java.io.UnsupportedEncodingException;
41+
import java.util.ArrayList;
42+
import java.util.Collections;
43+
import java.util.List;
44+
import java.util.Objects;
45+
import java.util.logging.Level;
46+
import java.util.logging.Logger;
47+
import java.util.stream.Collectors;
48+
import java.util.stream.Stream;
49+
import javax.annotation.CheckForNull;
50+
import javax.annotation.Nonnull;
51+
import jenkins.util.BuildListenerAdapter;
52+
import jenkins.util.JenkinsJVM;
53+
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
54+
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
55+
import org.jenkinsci.plugins.workflow.steps.StepContext;
56+
import org.kohsuke.accmod.Restricted;
57+
import org.kohsuke.accmod.restrictions.Beta;
58+
59+
/**
60+
* A way of decorating output from a {@link TaskListener}.
61+
* Similar to {@link ConsoleLogFilter} but better matched to Pipeline logging.
62+
* <p>May be passed to a {@link BodyInvoker} in lieu of {@link BodyInvoker#mergeConsoleLogFilters},
63+
* using {@link #merge} to pick up any earlier decorator in {@link StepContext#get}.
64+
* <p>Expected to be serializable either locally or over Remoting,
65+
* so an implementation of {@link #decorate} cannot assume that {@link JenkinsJVM#isJenkinsJVM}.
66+
* Any master-side configuration should thus be saved into instance fields when the decorator is constructed.
67+
* @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-45693">JENKINS-45693</a>
68+
*/
69+
@Restricted(Beta.class)
70+
public abstract class TaskListenerDecorator implements /* TODO Remotable */ Serializable {
71+
72+
private static final long serialVersionUID = 1;
73+
74+
private static final Logger LOGGER = Logger.getLogger(TaskListenerDecorator.class.getName());
75+
76+
/**
77+
* Apply modifications to a build log.
78+
* Typical implementations use {@link LineTransformationOutputStream}.
79+
* @param logger a base logger
80+
* @return a possibly patched result
81+
*/
82+
public abstract @Nonnull OutputStream decorate(@Nonnull OutputStream logger) throws IOException, InterruptedException;
83+
84+
/**
85+
* Merges two decorators.
86+
* @param original the original decorator, if any
87+
* @param subsequent an overriding decorator, if any
88+
* @return null, or {@code original} or {@code subsequent}, or a merged result applying one then the other
89+
*/
90+
public static @CheckForNull TaskListenerDecorator merge(@CheckForNull TaskListenerDecorator original, @CheckForNull TaskListenerDecorator subsequent) {
91+
if (original == null) {
92+
if (subsequent == null) {
93+
return null;
94+
} else {
95+
return subsequent;
96+
}
97+
} else {
98+
if (subsequent == null) {
99+
return original;
100+
} else {
101+
return new MergedTaskListenerDecorator(original, subsequent);
102+
}
103+
}
104+
}
105+
106+
/**
107+
* Tries to translate a similar core interface into the new API.
108+
* <p>The filter may implement either {@link ConsoleLogFilter#decorateLogger(AbstractBuild, OutputStream)} and/or {@link ConsoleLogFilter#decorateLogger(Run, OutputStream)},
109+
* but only {@link ConsoleLogFilter#decorateLogger(AbstractBuild, OutputStream)} will be called, and with a null {@code build} parameter.
110+
* <p>The filter must be {@link Serializable}, and furthermore must not assume that {@link JenkinsJVM#isJenkinsJVM}:
111+
* the same constraints as for {@link TaskListenerDecorator} generally.
112+
* @param filter a filter, or null
113+
* @return an adapter, or null if it is null or (after issuing a warning) not {@link Serializable}
114+
* @see <a href="https://github.com/jenkinsci/jep/blob/master/jep/210/README.adoc#backwards-compatibility">JEP-210: Backwards Compatibility</a>
115+
*/
116+
public static @CheckForNull TaskListenerDecorator fromConsoleLogFilter(@CheckForNull ConsoleLogFilter filter) {
117+
if (filter == null) {
118+
return null;
119+
} else if (filter instanceof Serializable) {
120+
return new ConsoleLogFilterAdapter(filter);
121+
} else {
122+
LOGGER.log(Level.WARNING, "{0} must implement Serializable to be used with Pipeline", filter.getClass());
123+
return null;
124+
}
125+
}
126+
127+
/**
128+
* Allows a decorator to be applied to any build.
129+
* @see #apply
130+
*/
131+
public interface Factory extends ExtensionPoint {
132+
133+
/**
134+
* Supplies a decorator applicable to one build.
135+
* @param owner a build
136+
* @return a decorator, optionally
137+
*/
138+
@CheckForNull TaskListenerDecorator of(@Nonnull FlowExecutionOwner owner);
139+
140+
}
141+
142+
/**
143+
* Wraps a logger in a supplied decorator as well as any available from {@link Factory}s.
144+
* <p>Does <em>not</em> apply {@link ConsoleLogFilter#all} even via {@link #fromConsoleLogFilter},
145+
* since there is no mechanical way to tell if implementations actually satisfy the constraints.
146+
* Anyway these singletons could not determine which build they are being applied to if remoted.
147+
* @param listener the main logger
148+
* @param owner a build
149+
* @param mainDecorator an additional contextual decorator to apply, if any
150+
* @return a possibly wrapped {@code listener}
151+
*/
152+
public static BuildListener apply(@Nonnull TaskListener listener, @Nonnull FlowExecutionOwner owner, @CheckForNull TaskListenerDecorator mainDecorator) {
153+
JenkinsJVM.checkJenkinsJVM();
154+
List<TaskListenerDecorator> decorators = Stream.concat(
155+
ExtensionList.lookup(TaskListenerDecorator.Factory.class).stream().map(f -> f.of(owner)),
156+
Stream.of(mainDecorator)).
157+
filter(Objects::nonNull).
158+
collect(Collectors.toCollection(ArrayList::new));
159+
if (decorators.isEmpty()) {
160+
return BuildListenerAdapter.wrap(listener);
161+
} else {
162+
Collections.reverse(decorators);
163+
return new DecoratedTaskListener(listener, decorators);
164+
}
165+
}
166+
167+
private static class MergedTaskListenerDecorator extends TaskListenerDecorator {
168+
169+
private static final long serialVersionUID = 1;
170+
171+
private final @Nonnull TaskListenerDecorator original;
172+
private final @Nonnull TaskListenerDecorator subsequent;
173+
174+
MergedTaskListenerDecorator(TaskListenerDecorator original, TaskListenerDecorator subsequent) {
175+
this.original = original;
176+
this.subsequent = subsequent;
177+
}
178+
179+
@Override public OutputStream decorate(OutputStream logger) throws IOException, InterruptedException {
180+
// TODO BodyInvoker.MergedFilter probably has these backwards
181+
return original.decorate(subsequent.decorate(logger));
182+
}
183+
184+
@Override public String toString() {
185+
return "MergedTaskListenerDecorator[" + subsequent + ", " + original + "]";
186+
}
187+
188+
}
189+
190+
private static class ConsoleLogFilterAdapter extends TaskListenerDecorator {
191+
192+
private static final long serialVersionUID = 1;
193+
194+
@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Explicitly checking for serializability.")
195+
private final @Nonnull ConsoleLogFilter filter;
196+
197+
ConsoleLogFilterAdapter(ConsoleLogFilter filter) {
198+
assert filter instanceof Serializable;
199+
this.filter = filter;
200+
}
201+
202+
@SuppressWarnings("deprecation") // the compatibility code in ConsoleLogFilter fails to delegate to the old overload when given a null argument
203+
@Override public OutputStream decorate(OutputStream logger) throws IOException, InterruptedException {
204+
return filter.decorateLogger((AbstractBuild) null, logger);
205+
}
206+
207+
@Override public String toString() {
208+
return "ConsoleLogFilter[" + filter + "]";
209+
}
210+
211+
}
212+
213+
private static final class DecoratedTaskListener implements BuildListener {
214+
215+
private static final long serialVersionUID = 1;
216+
217+
/**
218+
* The listener we are delegating to, which was expected to be remotable.
219+
* Note that we ignore all of its methods other than {@link TaskListener#getLogger}.
220+
*/
221+
private final @Nonnull TaskListener delegate;
222+
223+
/**
224+
* A (nonempty) list of decorators we delegate to.
225+
* They are applied in reverse order, so the first one has the final say in what gets printed.
226+
*/
227+
private final @Nonnull List<TaskListenerDecorator> decorators;
228+
229+
private transient PrintStream logger;
230+
231+
DecoratedTaskListener(TaskListener delegate, List<TaskListenerDecorator> decorators) {
232+
this.delegate = delegate;
233+
assert !decorators.isEmpty();
234+
assert !decorators.contains(null);
235+
this.decorators = decorators;
236+
}
237+
238+
@Override public PrintStream getLogger() {
239+
if (logger == null) {
240+
OutputStream base = delegate.getLogger();
241+
for (TaskListenerDecorator decorator : decorators) {
242+
try {
243+
base = decorator.decorate(base);
244+
} catch (Exception x) {
245+
LOGGER.log(Level.WARNING, null, x);
246+
}
247+
}
248+
try {
249+
logger = new PrintStream(base, false, "UTF-8");
250+
} catch (UnsupportedEncodingException x) {
251+
throw new AssertionError(x);
252+
}
253+
}
254+
return logger;
255+
}
256+
257+
@Override public String toString() {
258+
return "DecoratedTaskListener[" + delegate + decorators + "]";
259+
}
260+
261+
}
262+
263+
}

0 commit comments

Comments
 (0)