blob: 4cf00ef5560dbf016fd977e4ba8c37dad9bb3142 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.tools.ant.module.bridge.impl;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.module.bridge.AntBridge;
import org.apache.tools.ant.module.run.StandardLogger;
import org.apache.tools.ant.module.spi.AntSession;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.Redirector;
import org.openide.util.RequestProcessor;
import org.openide.windows.FoldHandle;
import org.openide.windows.IOFolding;
import org.openide.windows.OutputWriter;
/**
* Replacement for Ant's java task which directly sends I/O to the output without line buffering.
* Idea from ide/projectimport/bluej/antsrc/org/netbeans/bluej/ant/task/BlueJava.java.
* See issue #56341.
*/
public class ForkedJavaOverride extends Java {
private static final RequestProcessor PROCESSOR = new RequestProcessor(ForkedJavaOverride.class.getName(), Integer.MAX_VALUE);
public static final int LOGGER_MAX_LINE_LENGTH = Integer.getInteger("logger.max.line.length", 3000); //NOI18N
// should be consistent with java.project.JavaAntLogger.STACK_TRACE
// should be consistent with org.netbeans.modules.java.j2seembedded.project.RemoteJavaAntLogger
private static final String JIDENT = "[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*"; // NOI18N
private static final Pattern STACK_TRACE = Pattern.compile(
"(.*?((?:" + JIDENT + "[.])*)(" + JIDENT + ")[.](?:" + JIDENT + "|<init>|<clinit>)" + // NOI18N
"[(])(((?:"+JIDENT+"(?:\\."+JIDENT+")*/)?" + JIDENT + "[.]java):([0-9]+)|Unknown Source)([)].*)"); // NOI18N
public ForkedJavaOverride() {
redirector = new NbRedirector(this);
super.setFork(true);
}
@Override
public void setFork(boolean fork) {
// #47645: ignore! Does not work to be set to false.
}
private void useStandardRedirector() { // #121512, #168153
if (redirector instanceof NbRedirector) {
redirector = new Redirector(this);
}
getProject().setProperty(StandardLogger.USING_STANDARD_REDIRECTOR, "true");
}
public @Override void setInput(File input) {
useStandardRedirector();
super.setInput(input);
}
public @Override void setInputString(String inputString) {
useStandardRedirector();
super.setInputString(inputString);
}
public @Override void setOutput(File out) {
useStandardRedirector();
super.setOutput(out);
}
public @Override void setOutputproperty(String outputProp) {
useStandardRedirector();
super.setOutputproperty(outputProp);
}
public @Override void setError(File error) {
useStandardRedirector();
super.setError(error);
}
public @Override void setErrorProperty(String errorProperty) {
useStandardRedirector();
super.setErrorProperty(errorProperty);
}
private class NbRedirector extends Redirector {
private String outEncoding = System.getProperty("file.encoding"); // NOI18N
private String errEncoding = System.getProperty("file.encoding"); // NOI18N
// #158492. In Ant 1.8.0 output redirection cannot be distinguished by OutputStream subclass type
// (LogOutputStream vs OutputStreamFunneler$Funnel) as OutputStreamFunneler$Funnel is
// used in both cases
private boolean delegateOutputStream = true;
private boolean delegateErrorStream = true;
public NbRedirector(Task task) {
super(task);
}
public @Override ExecuteStreamHandler createHandler() throws BuildException {
createStreams();
return new NbOutputStreamHandler();
}
public @Override synchronized void setOutputEncoding(String outputEncoding) {
outEncoding = outputEncoding;
super.setOutputEncoding(outputEncoding);
}
public @Override synchronized void setErrorEncoding(String errorEncoding) {
errEncoding = errorEncoding;
super.setErrorEncoding(errorEncoding);
}
@Override
public void setOutput(File out) {
if (out != null) {
delegateOutputStream = false;
}
super.setOutput(out);
}
@Override
public synchronized void setOutput(File[] out) {
if (out != null && out.length > 0) {
delegateOutputStream = false;
}
super.setOutput(out);
}
@Override
public void setError(File error) {
if (error != null) {
delegateErrorStream = false;
}
super.setError(error);
}
@Override
public synchronized void setError(File[] error) {
if (error != null && error.length > 0) {
delegateOutputStream = false;
}
super.setError(error);
}
private class NbOutputStreamHandler implements ExecuteStreamHandler {
private final ExecutorService tasks;
private final FoldingHelper foldingHelper;
private Future inputTask;
private InputStream stdout, stderr;
private OutputStream stdin;
NbOutputStreamHandler() {
this.foldingHelper = new FoldingHelper();
this.tasks = Executors.newFixedThreadPool(3, (r) -> {
Thread t = new Thread(Thread.currentThread().getThreadGroup(),
r,
"I/O Thread for " + getProject().getName()); // NOI18N
t.setDaemon(true);
return t;
});
}
private void setCopier(InputStream inputStream, OutputStream os, boolean delegate, boolean err) {
if (os == null || delegate) {
tasks.submit(new TransferCopier(inputStream, AntBridge.delegateOutputStream(err)));
} else {
tasks.submit(new TransferCopier(inputStream, os));
}
}
public void start() throws IOException {
NbBuildLogger buildLogger = getProject().getBuildListeners().stream()
.filter(o -> o instanceof NbBuildLogger)
.map(o -> (NbBuildLogger)o)
.findFirst()
.orElse(null);
if (buildLogger != null) {
tasks.submit(new MarkupCopier(stdout, Project.MSG_INFO, false, outEncoding, buildLogger, foldingHelper));
tasks.submit(new MarkupCopier(stderr, Project.MSG_WARN, true, errEncoding, buildLogger, foldingHelper));
} else {
setCopier(stdout, getOutputStream(), delegateOutputStream, false);
setCopier(stderr, getErrorStream(), delegateErrorStream, true);
}
InputStream is = getInputStream();
if (is == null)
is = AntBridge.delegateInputStream();
inputTask = tasks.submit(new TransferCopier(is, stdin));
}
public void stop() {
try {
if (inputTask != null)
inputTask.cancel(true);
tasks.shutdown();
tasks.awaitTermination(3, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
} finally {
tasks.shutdownNow();
}
}
public void setProcessOutputStream(InputStream inputStream) throws IOException {
this.stdout = inputStream;
}
public void setProcessErrorStream(InputStream inputStream) throws IOException {
this.stderr = inputStream;
}
public void setProcessInputStream(OutputStream outputStream) throws IOException {
this.stdin = outputStream;
}
}
}
/**
* Simple copier that transfers all input to output.
*/
private class TransferCopier implements Runnable {
private final InputStream in;
private final OutputStream out;
public TransferCopier(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
}
@Override
public void run() {
try {
byte[] data = new byte[1024];
int len;
while ((len = in.read(data)) >= 0) {
out.write(data, 0, len);
out.flush();
}
} catch (IOException x) {
// ignore IOException: Broken pipe from FileOutputStream.writeBytes in BufferedOutputStream.flush
}
}
}
/**
* Filtering copier that marks up links, ignoring stack traces.
*/
private class MarkupCopier implements Runnable {
private final InputStream in;
private final int logLevel;
private final String encoding;
private final RequestProcessor.Task flusher;
private final ByteArrayOutputStream currentLine;
private final OutputWriter ow;
private final boolean err;
private final AntSession session;
private final FoldingHelper foldingHelper;
public MarkupCopier(InputStream in, int logLevel, boolean err, String encoding, NbBuildLogger buildLogger, FoldingHelper foldingHelper) {
this.in = in;
this.logLevel = logLevel;
this.err = err;
this.encoding = encoding;
this.foldingHelper = foldingHelper;
flusher = PROCESSOR.create(() -> maybeFlush(false));
currentLine = new ByteArrayOutputStream();
ow = err ? buildLogger.err : buildLogger.out;
session = buildLogger.thisSession;
}
private synchronized void append(byte[] data, int off, int len) {
currentLine.write(data, off, len);
if (currentLine.size() > 8192) {
flusher.run();
} else {
flusher.schedule(250);
}
}
private synchronized String appendAndTake(byte[] data, int off, int len) throws UnsupportedEncodingException {
currentLine.write(data, off, len);
String str = currentLine.toString(encoding);
currentLine.reset();
return str;
}
private synchronized String take() throws UnsupportedEncodingException {
String str = currentLine.toString(encoding);
currentLine.reset();
return str;
}
public void run() {
try {
byte[] data = new byte[1024];
int len;
try {
while ((len = in.read(data)) >= 0) {
int last = 0;
for (int i = 0; i < len; i++) {
int c = data[i] & 0xff;
// Add folds for stack traces and mark up lines
// not processed by JavaAntLogger stack trace detection
if (c == '\n') {
String str = appendAndTake(data, last, i > last && data[i - 1] == '\r' ? i - last - 1 : i - last);
synchronized (foldingHelper) {
foldingHelper.checkFolds(str, err, session);
if (str.length() >= LOGGER_MAX_LINE_LENGTH || !STACK_TRACE.matcher(str).matches())
StandardLogger.findHyperlink(str, session, null).println(session, err);
log(str, logLevel);
}
last = i + 1;
}
}
if (last < len)
append(data, last, len - last);
}
} finally {
maybeFlush(true);
}
} catch (IOException x) {
// ignore IOException: Broken pipe from FileOutputStream.writeBytes in BufferedOutputStream.flush
}
}
public void maybeFlush(boolean end) {
try {
String str = take();
synchronized (foldingHelper) {
if (!str.isEmpty()) {
ow.write(str);
log(str, logLevel);
}
if (end && err)
foldingHelper.clearHandle();
}
} catch (IOException x) {
// ignore IOException: Broken pipe from FileOutputStream.writeBytes in BufferedOutputStream.flush
}
}
}
/**
* A helper class for detecting stacktraces in the output and for creating
* folds for them. It is also used as a shared lock for {@link MarkupCopier}s of
* standard and error outputs, which should make the mixed output a bit more
* readable.
*/
public static class FoldingHelper {
private final Pattern STACK_TRACE = Pattern.compile(
"^\\s+at.*:"); //NOI18N
private final Pattern EXCEPTION = Pattern.compile(
"^(\\.?\\w)*(Exception|Error).*"); //NOI18N
private FoldHandle foldHandle = null;
boolean inStackTrace = false;
private void checkFolds(String s, boolean error, AntSession session) {
// ignore too long, expensive messages, probably coming from user, so no need for folds
boolean cheap = s.length() < LOGGER_MAX_LINE_LENGTH;
if (cheap && error && EXCEPTION.matcher(s).find()) {
clearHandle();
inStackTrace = true;
} else if (cheap && error && inStackTrace
&& STACK_TRACE.matcher(s).find()) {
if (foldHandle == null) {
foldHandle = IOFolding.startFold(session.getIO(), true);
}
} else {
inStackTrace = false;
clearHandle();
}
}
void clearHandle() {
if (foldHandle != null) {
if (!foldHandle.isFinished()) {
foldHandle.silentFinish();
}
foldHandle = null;
}
}
}
}