Skip to content

Commit

Permalink
Define OutputStreamTaskListener & close BufferedBuildListener
Browse files Browse the repository at this point in the history
  • Loading branch information
jglick committed Jul 13, 2023
1 parent 1679fa2 commit c57af92
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,38 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.CloseProofOutputStream;
import hudson.model.BuildListener;
import hudson.remoting.Channel;
import hudson.remoting.ChannelClosedException;
import hudson.remoting.RemoteOutputStream;
import hudson.util.StreamTaskListener;
import java.io.Closeable;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
import org.jenkinsci.remoting.SerializableOnlyOverRemoting;

/**
* Unlike {@link StreamTaskListener} this does not set {@code autoflush} on the reconstructed {@link PrintStream}.
* It also wraps on the remote side in {@link DelayBufferedOutputStream}.
*/
final class BufferedBuildListener implements BuildListener, Closeable, SerializableOnlyOverRemoting {
final class BufferedBuildListener implements BuildListener, Closeable, SerializableOnlyOverRemoting, OutputStreamTaskListener {

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

private final OutputStream out;
@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "using Replacement anyway, fields here are irrelevant")
private final PrintStream ps;

BufferedBuildListener(OutputStream out) throws IOException {
BufferedBuildListener(OutputStream out) {
this.out = out;
ps = new PrintStream(out, false, "UTF-8");
ps = new PrintStream(out, false, StandardCharsets.UTF_8);
}

@Override public OutputStream getOutputStream() {
return out;
}

@NonNull
Expand All @@ -75,8 +86,59 @@ private static final class Replacement implements SerializableOnlyOverRemoting {
this.ros = new RemoteOutputStream(new CloseProofOutputStream(cbl.out));
}

private Object readResolve() throws IOException {
return new BufferedBuildListener(new GCFlushedOutputStream(new DelayBufferedOutputStream(ros, tuning)));
private Object readResolve() {
var cos = new CloseableOutputStream(new GCFlushedOutputStream(new DelayBufferedOutputStream(ros, tuning)));

Check warning on line 90 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 90 is not covered by tests
Channel.currentOrFail().addListener(new Channel.Listener() {

Check warning on line 91 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 91 is not covered by tests
@Override public void onClosed(Channel channel, IOException cause) {
LOGGER.fine(() -> "closing " + channel.getName());

Check warning on line 93 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 93 is not covered by tests
cos.close(channel, cause);

Check warning on line 94 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 94 is not covered by tests
}

Check warning on line 95 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 95 is not covered by tests
});
return new BufferedBuildListener(cos);

Check warning on line 97 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 97 is not covered by tests
}

}

/**
* Output stream which throws {@link ChannelClosedException} when appropriate.
* Otherwise callers could continue trying to write to {@link DelayBufferedOutputStream}
* long after {@link Channel#isClosingOrClosed} without errors.
* In the case of {@code org.jenkinsci.plugins.durabletask.Handler.output},
* this is actively harmful since it would mean that writes apparently succeed
* and {@code last-location.txt} would move forward even though output was lost.
*/
private static final class CloseableOutputStream extends FilterOutputStream {

/** non-null if closed */
private Channel channel;
/** optional close cause */
private IOException cause;

CloseableOutputStream(OutputStream delegate) {
super(delegate);

Check warning on line 118 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 118 is not covered by tests
}

Check warning on line 119 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 119 is not covered by tests

void close(Channel channel, IOException cause) {
this.channel = channel;

Check warning on line 122 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 122 is not covered by tests
this.cause = cause;

Check warning on line 123 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 123 is not covered by tests
// Do not call close(): ProxyOutputStream.doClose would just throw ChannelClosedException: …: channel is already closed
}

Check warning on line 125 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 125 is not covered by tests

private void checkClosed() throws IOException {
if (channel != null) {

Check warning on line 128 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 128 is only partially covered, 2 branches are missing
throw new ChannelClosedException(channel, cause);

Check warning on line 129 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 129 is not covered by tests
}
LOGGER.finer("not closed yet");

Check warning on line 131 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 131 is not covered by tests
}

Check warning on line 132 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 132 is not covered by tests

@Override public void write(int b) throws IOException {
checkClosed();

Check warning on line 135 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 135 is not covered by tests
out.write(b);

Check warning on line 136 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 136 is not covered by tests
}

Check warning on line 137 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 137 is not covered by tests

@Override public void write(byte[] b, int off, int len) throws IOException {
checkClosed();

Check warning on line 140 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 140 is not covered by tests
out.write(b, off, len);

Check warning on line 141 in src/main/java/org/jenkinsci/plugins/workflow/log/BufferedBuildListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 141 is not covered by tests
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* The MIT License
*
* Copyright 2023 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.log;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.TaskListener;
import java.io.FilterOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.output.ClosedOutputStream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;

/**
* {@link TaskListener} which can directly return an {@link OutputStream} not wrapped in a {@link PrintStream}.
* This is important for logging since the error-swallowing behavior of {@link PrintStream} is unwanted,
* and {@link PrintStream#checkError} is useless.
* <p>{@link #getLogger} should generally be implemented in terms of {@link #getOutputStream}
* (no autoflush, using {@link StandardCharsets#UTF_8})
* but there is not a {@code default} implementation since the result should be cached in a field.
*/
@Restricted(Beta.class)
public interface OutputStreamTaskListener extends TaskListener {

/**
* Returns the {@link OutputStream} from which {@link #getLogger} was constructed.
*/
@NonNull OutputStream getOutputStream();

/**
* Tries to call {@link #getOutputStream} and otherwise falls back to reflective access to {@link PrintStream#out} when possible, at worst returning the {@link PrintStream} itself.
*/
static @NonNull OutputStream getOutputStream(@NonNull TaskListener listener) {
if (listener instanceof OutputStreamTaskListener) {

Check warning on line 61 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 61 is only partially covered, one branch is missing
return ((OutputStreamTaskListener) listener).getOutputStream();
}
PrintStream ps = listener.getLogger();

Check warning on line 64 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 64 is not covered by tests
if (ps.getClass() != PrintStream.class) {

Check warning on line 65 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 65 is only partially covered, 2 branches are missing
Logger.getLogger(OutputStreamTaskListener.class.getName()).warning(() -> "Unexpected PrintStream subclass " + ps.getClass().getName() + " which might override write(…); error handling is degraded unless OutputStreamTaskListener is used: " + listener.getClass().getName());

Check warning on line 66 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 66 is not covered by tests
return ps;

Check warning on line 67 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 67 is not covered by tests
}
if (Runtime.version().compareToIgnoreOptional(Runtime.Version.parse("17")) >= 0) {

Check warning on line 69 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 69 is only partially covered, 2 branches are missing
Logger.getLogger(OutputStreamTaskListener.class.getName()).warning(() -> "On Java 17+ error handling is degraded unless OutputStreamTaskListener is used: " + listener.getClass().getName());

Check warning on line 70 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 70 is not covered by tests
return ps;

Check warning on line 71 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 71 is not covered by tests
}
Field printStreamDelegate;
try {
printStreamDelegate = FilterOutputStream.class.getDeclaredField("out");

Check warning on line 75 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 75 is not covered by tests
} catch (NoSuchFieldException x) {

Check warning on line 76 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 76 is not covered by tests
Logger.getLogger(OutputStreamTaskListener.class.getName()).log(Level.WARNING, "PrintStream.out defined in Java Platform and protected, so should not happen.", x);

Check warning on line 77 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 77 is not covered by tests
return ps;

Check warning on line 78 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 78 is not covered by tests
}

Check warning on line 79 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 79 is not covered by tests
try {
printStreamDelegate.setAccessible(true);

Check warning on line 81 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 81 is not covered by tests
} catch (InaccessibleObjectException x) {

Check warning on line 82 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 82 is not covered by tests
Logger.getLogger(OutputStreamTaskListener.class.getName()).warning(() -> "Using --illegal-access=deny? Error handling is degraded unless OutputStreamTaskListener is used: " + listener.getClass().getName());

Check warning on line 83 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 83 is not covered by tests
return ps;

Check warning on line 84 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 84 is not covered by tests
}

Check warning on line 85 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 85 is not covered by tests
OutputStream os;
try {
os = (OutputStream) printStreamDelegate.get(ps);

Check warning on line 88 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 88 is not covered by tests
} catch (IllegalAccessException x) {

Check warning on line 89 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 89 is not covered by tests
Logger.getLogger(OutputStreamTaskListener.class.getName()).log(Level.WARNING, "Unexpected failure to access PrintStream.out", x);

Check warning on line 90 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 90 is not covered by tests
return ps;

Check warning on line 91 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 91 is not covered by tests
}

Check warning on line 92 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 92 is not covered by tests
if (os == null) {

Check warning on line 93 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 93 is only partially covered, 2 branches are missing
// like PrintStream.ensureOpen
return ClosedOutputStream.CLOSED_OUTPUT_STREAM;

Check warning on line 95 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 95 is not covered by tests
}
return os;

Check warning on line 97 in src/main/java/org/jenkinsci/plugins/workflow/log/OutputStreamTaskListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 97 is not covered by tests
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -237,7 +236,7 @@ private static OutputStream decorateAll(OutputStream base, List<TaskListenerDeco
return base;
}

private static final class DecoratedTaskListener implements BuildListener {
private static final class DecoratedTaskListener implements BuildListener, OutputStreamTaskListener {

private static final long serialVersionUID = 1;

Expand All @@ -253,6 +252,7 @@ private static final class DecoratedTaskListener implements BuildListener {
*/
private final @NonNull List<TaskListenerDecorator> decorators;

private transient OutputStream out;
private transient PrintStream logger;

DecoratedTaskListener(@NonNull TaskListener delegate, @NonNull List<TaskListenerDecorator> decorators) {
Expand All @@ -262,14 +262,18 @@ private static final class DecoratedTaskListener implements BuildListener {
this.decorators = decorators;
}

@NonNull
@Override public OutputStream getOutputStream() {
if (out == null) {

Check warning on line 267 in src/main/java/org/jenkinsci/plugins/workflow/log/TaskListenerDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 267 is only partially covered, one branch is missing
out = decorateAll(OutputStreamTaskListener.getOutputStream(delegate), decorators);
}
return out;
}

@NonNull
@Override public PrintStream getLogger() {
if (logger == null) {
try {
logger = new PrintStream(decorateAll(delegate.getLogger(), decorators), false, "UTF-8");
} catch (UnsupportedEncodingException x) {
throw new AssertionError(x);
}
logger = new PrintStream(getOutputStream(), false, StandardCharsets.UTF_8);
}
return logger;
}
Expand All @@ -280,7 +284,7 @@ private static final class DecoratedTaskListener implements BuildListener {

}

private static final class CloseableTaskListener implements BuildListener, AutoCloseable {
private static final class CloseableTaskListener implements BuildListener, AutoCloseable, OutputStreamTaskListener {

static BuildListener of(BuildListener mainDelegate, TaskListener closeDelegate) {
if (closeDelegate instanceof AutoCloseable) {
Expand All @@ -301,6 +305,12 @@ private CloseableTaskListener(@NonNull TaskListener mainDelegate, @NonNull TaskL
assert closeDelegate instanceof AutoCloseable;
}

@NonNull
@Override
public OutputStream getOutputStream() {
return OutputStreamTaskListener.getOutputStream(mainDelegate);

Check warning on line 311 in src/main/java/org/jenkinsci/plugins/workflow/log/TaskListenerDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 311 is not covered by tests
}

@NonNull
@Override public PrintStream getLogger() {
return mainDelegate.getLogger();
Expand Down

0 comments on commit c57af92

Please sign in to comment.