Skip to content

Commit fbcd60f

Browse files
committed
[JENKINS-45693] Defined TaskListenerDecorator.
1 parent c9c8e89 commit fbcd60f

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-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: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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.List;
43+
import java.util.Objects;
44+
import java.util.logging.Level;
45+
import java.util.logging.Logger;
46+
import java.util.stream.Collectors;
47+
import java.util.stream.Stream;
48+
import javax.annotation.CheckForNull;
49+
import javax.annotation.Nonnull;
50+
import jenkins.util.BuildListenerAdapter;
51+
import jenkins.util.JenkinsJVM;
52+
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
53+
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
54+
import org.jenkinsci.plugins.workflow.steps.StepContext;
55+
import org.kohsuke.accmod.Restricted;
56+
import org.kohsuke.accmod.restrictions.Beta;
57+
58+
/**
59+
* A way of decorating output from a {@link TaskListener}.
60+
* Similar to {@link ConsoleLogFilter} but better matched to Pipeline logging.
61+
* <p>May be passed to a {@link BodyInvoker} in lieu of {@link BodyInvoker#mergeConsoleLogFilters},
62+
* using {@link #merge} to pick up any earlier decorator in {@link StepContext#get}.
63+
* <p>Expected to be serializable either locally or over Remoting,
64+
* so an implementation of {@link #decorate} cannot assume that {@link JenkinsJVM#isJenkinsJVM}.
65+
* Any master-side configuration should thus be saved into instance fields when the decorator is constructed.
66+
* @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-45693">JENKINS-45693</a>
67+
*/
68+
@Restricted(Beta.class)
69+
public abstract class TaskListenerDecorator implements /* TODO Remotable */ Serializable {
70+
71+
private static final long serialVersionUID = 1;
72+
73+
private static final Logger LOGGER = Logger.getLogger(TaskListenerDecorator.class.getName());
74+
75+
/**
76+
* Apply modifications to a build log.
77+
* Typical implementations use {@link LineTransformationOutputStream}.
78+
* @param logger a base logger
79+
* @return a possibly patched result
80+
*/
81+
public abstract @Nonnull OutputStream decorate(@Nonnull OutputStream logger) throws IOException, InterruptedException;
82+
83+
/**
84+
* Merges two decorators.
85+
* @param original the original decorator, if any
86+
* @param subsequent an overriding decorator, if any
87+
* @return null, or {@code original} or {@code subsequent}, or a merged result applying one then the other
88+
*/
89+
public static @CheckForNull TaskListenerDecorator merge(@CheckForNull TaskListenerDecorator original, @CheckForNull TaskListenerDecorator subsequent) {
90+
if (original == null) {
91+
if (subsequent == null) {
92+
return null;
93+
} else {
94+
return subsequent;
95+
}
96+
} else {
97+
if (subsequent == null) {
98+
return original;
99+
} else {
100+
return new MergedTaskListenerDecorator(original, subsequent);
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Tries to translate a similar core interface into the new API.
107+
* <p>The filter may implement either {@link ConsoleLogFilter#decorateLogger(AbstractBuild, OutputStream)} and/or {@link ConsoleLogFilter#decorateLogger(Run, OutputStream)},
108+
* but only {@link ConsoleLogFilter#decorateLogger(AbstractBuild, OutputStream)} will be called, and with a null {@code build} parameter.
109+
* <p>The filter must be {@link Serializable}, and furthermore must not assume that {@link JenkinsJVM#isJenkinsJVM}:
110+
* the same constraints as for {@link TaskListenerDecorator} generally.
111+
* @param filter a filter, or null
112+
* @return an adapter, or null if it is null or (after issuing a warning) not {@link Serializable}
113+
* @see <a href="https://github.com/jenkinsci/jep/blob/master/jep/210/README.adoc#backwards-compatibility">JEP-210: Backwards Compatibility</a>
114+
*/
115+
public static @CheckForNull TaskListenerDecorator fromConsoleLogFilter(@CheckForNull ConsoleLogFilter filter) {
116+
if (filter == null) {
117+
return null;
118+
} else if (filter instanceof Serializable) {
119+
return new ConsoleLogFilterAdapter(filter);
120+
} else {
121+
LOGGER.log(Level.WARNING, "{0} must implement Serializable to be used with Pipeline", filter.getClass());
122+
return null;
123+
}
124+
}
125+
126+
/**
127+
* Allows a decorator to be applied to any build.
128+
* @see #apply
129+
*/
130+
public interface Factory extends ExtensionPoint {
131+
132+
/**
133+
* Supplies a decorator applicable to one build.
134+
* @param owner a build
135+
* @return a decorator, optionally
136+
*/
137+
@CheckForNull TaskListenerDecorator of(@Nonnull FlowExecutionOwner owner);
138+
139+
}
140+
141+
/**
142+
* Wraps a logger in a supplied decorator as well as any available from {@link Factory}s.
143+
* <p>Does <em>not</em> apply {@link ConsoleLogFilter#all} even via {@link #fromConsoleLogFilter},
144+
* since there is no mechanical way to tell if implementations actually satisfy the constraints.
145+
* Anyway these singletons could not determine which build they are being applied to if remoted.
146+
* @param listener the main logger
147+
* @param owner a build
148+
* @param mainDecorator an additional contextual decorator to apply, if any
149+
* @return a possibly wrapped {@code listener}
150+
*/
151+
public static BuildListener apply(@Nonnull TaskListener listener, @Nonnull FlowExecutionOwner owner, @CheckForNull TaskListenerDecorator mainDecorator) {
152+
JenkinsJVM.checkJenkinsJVM();
153+
List<TaskListenerDecorator> decorators = Stream.concat(
154+
ExtensionList.lookup(TaskListenerDecorator.Factory.class).stream().map(f -> f.of(owner)),
155+
Stream.of(mainDecorator)).
156+
filter(Objects::nonNull).
157+
collect(Collectors.toCollection(ArrayList::new));
158+
if (decorators.isEmpty()) {
159+
return BuildListenerAdapter.wrap(listener);
160+
} else {
161+
return new DecoratedTaskListener(listener, decorators);
162+
}
163+
}
164+
165+
private static class MergedTaskListenerDecorator extends TaskListenerDecorator {
166+
167+
private static final long serialVersionUID = 1;
168+
169+
private final @Nonnull TaskListenerDecorator original;
170+
private final @Nonnull TaskListenerDecorator subsequent;
171+
172+
MergedTaskListenerDecorator(TaskListenerDecorator original, TaskListenerDecorator subsequent) {
173+
this.original = original;
174+
this.subsequent = subsequent;
175+
}
176+
177+
@Override public OutputStream decorate(OutputStream logger) throws IOException, InterruptedException {
178+
return subsequent.decorate(original.decorate(logger));
179+
}
180+
181+
}
182+
183+
private static class ConsoleLogFilterAdapter extends TaskListenerDecorator {
184+
185+
private static final long serialVersionUID = 1;
186+
187+
@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Explicitly checking for serializability.")
188+
private final @Nonnull ConsoleLogFilter filter;
189+
190+
ConsoleLogFilterAdapter(ConsoleLogFilter filter) {
191+
assert filter instanceof Serializable;
192+
this.filter = filter;
193+
}
194+
195+
@SuppressWarnings("deprecation") // the compatibility code in ConsoleLogFilter fails to delegate to the old overload when given a null argument
196+
@Override public OutputStream decorate(OutputStream logger) throws IOException, InterruptedException {
197+
return filter.decorateLogger((AbstractBuild) null, logger);
198+
}
199+
200+
}
201+
202+
private static final class DecoratedTaskListener implements BuildListener {
203+
204+
private static final long serialVersionUID = 1;
205+
206+
/**
207+
* The listener we are delegating to, which was expected to be remotable.
208+
* Note that we ignore all of its methods other than {@link TaskListener#getLogger}.
209+
*/
210+
private final @Nonnull TaskListener delegate;
211+
212+
/**
213+
* A (nonempty) list of decorators we delegate to.
214+
* They are applied in order, so the last one “wins”.
215+
*/
216+
private final @Nonnull List<TaskListenerDecorator> decorators;
217+
218+
private transient PrintStream logger;
219+
220+
DecoratedTaskListener(TaskListener delegate, List<TaskListenerDecorator> decorators) {
221+
this.delegate = delegate;
222+
assert !decorators.isEmpty();
223+
assert !decorators.contains(null);
224+
this.decorators = decorators;
225+
}
226+
227+
@Override public PrintStream getLogger() {
228+
if (logger == null) {
229+
OutputStream base = delegate.getLogger();
230+
for (TaskListenerDecorator decorator : decorators) {
231+
try {
232+
base = decorator.decorate(base);
233+
} catch (Exception x) {
234+
LOGGER.log(Level.WARNING, null, x);
235+
}
236+
}
237+
try {
238+
logger = new PrintStream(base, false, "UTF-8");
239+
} catch (UnsupportedEncodingException x) {
240+
throw new AssertionError(x);
241+
}
242+
}
243+
return logger;
244+
}
245+
246+
}
247+
248+
}

0 commit comments

Comments
 (0)