@@ -53,6 +53,10 @@ Fixed bugs: | |||||
suggesting the fix. | suggesting the fix. | ||||
Bugzilla Report 19516 | Bugzilla Report 19516 | ||||
* Fixed an issue where the content redirected from output/error | |||||
streams of a process, could end up being truncated. | |||||
Bugzilla Report 58833, 58451 | |||||
Other changes: | Other changes: | ||||
-------------- | -------------- | ||||
@@ -0,0 +1,122 @@ | |||||
<?xml version="1.0"?> | |||||
<!-- | |||||
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. | |||||
--> | |||||
<project name="exec-redirector-test" basedir="."> | |||||
<target name="setUp"> | |||||
<!-- This "output" property is set on the project in the Java test case (ExecStreamRedirectorTest) --> | |||||
<mkdir dir="${output}"/> | |||||
<condition property="dir.listing.command" value="ls" else="cmd.exe"> | |||||
<os family="unix"/> | |||||
</condition> | |||||
<condition property="dir.to.ls" value="/usr/bin" else="${user.dir}"> | |||||
<os family="unix"/> | |||||
</condition> | |||||
<condition property="dir.listing.command.arg" value="-l" else="dir"> | |||||
<os family="unix"/> | |||||
</condition> | |||||
<property name="dir.to.ls" value="/usr/bin"/> | |||||
</target> | |||||
<target name="list-dir"> | |||||
<!-- Just do listing of the same directory and redirect the output to different files --> | |||||
<parallel> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls1.txt" error="${output}/ls1.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls2.txt" error="${output}/ls2.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls3.txt" error="${output}/ls3.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls4.txt" error="${output}/ls4.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls5.txt" error="${output}/ls5.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls6.txt" error="${output}/ls6.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls7.txt" error="${output}/ls7.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls8.txt" error="${output}/ls8.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls9.txt" error="${output}/ls9.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls10.txt" error="${output}/ls10.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls11.txt" error="${output}/ls11.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls12.txt" error="${output}/ls12.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls13.txt" error="${output}/ls13.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls14.txt" error="${output}/ls14.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls15.txt" error="${output}/ls15.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
<exec executable="${dir.listing.command}" dir="${basedir}" failonerror="true" logerror="true"> | |||||
<redirector output="${output}/ls16.txt" error="${output}/ls16.err" alwayslog="true"/> | |||||
<arg value="${dir.listing.command.arg}"/> | |||||
<arg value="${dir.to.ls}"/> | |||||
</exec> | |||||
</parallel> | |||||
</target> | |||||
</project> |
@@ -21,6 +21,7 @@ package org.apache.tools.ant.taskdefs; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.util.concurrent.TimeUnit; | |||||
import org.apache.tools.ant.util.FileUtils; | import org.apache.tools.ant.util.FileUtils; | ||||
@@ -183,12 +184,21 @@ public class PumpStreamHandler implements ExecuteStreamHandler { | |||||
if (!t.isAlive()) { | if (!t.isAlive()) { | ||||
return; | return; | ||||
} | } | ||||
StreamPumper.PostStopHandle postStopHandle = null; | |||||
if (s != null && !s.isFinished()) { | if (s != null && !s.isFinished()) { | ||||
s.stop(); | |||||
postStopHandle = s.stop(); | |||||
} | |||||
if (postStopHandle != null && postStopHandle.isInPostStopTasks()) { | |||||
// the stream pumper is in post stop tasks (like flushing output), which | |||||
// indicates that the stream pumper has respected the stop request and | |||||
// is cleaning up before finishing. Give it some time to finish this | |||||
// post stop activity, before trying to force interrupt the underlying thread | |||||
// of the stream pumper | |||||
postStopHandle.awaitPostStopCompletion(2, TimeUnit.SECONDS); | |||||
} | } | ||||
t.join(JOIN_TIMEOUT); | |||||
while ((s == null || !s.isFinished()) && t.isAlive()) { | while ((s == null || !s.isFinished()) && t.isAlive()) { | ||||
// we waited for the thread/stream pumper to finish, but it hasn't yet. | |||||
// so we interrupt it | |||||
t.interrupt(); | t.interrupt(); | ||||
t.join(JOIN_TIMEOUT); | t.join(JOIN_TIMEOUT); | ||||
} | } | ||||
@@ -17,11 +17,13 @@ | |||||
*/ | */ | ||||
package org.apache.tools.ant.taskdefs; | package org.apache.tools.ant.taskdefs; | ||||
import org.apache.tools.ant.util.FileUtils; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import org.apache.tools.ant.util.FileUtils; | |||||
import java.util.concurrent.CountDownLatch; | |||||
import java.util.concurrent.TimeUnit; | |||||
/** | /** | ||||
* Copies all data from an input stream to an output stream. | * Copies all data from an input stream to an output stream. | ||||
@@ -34,7 +36,7 @@ public class StreamPumper implements Runnable { | |||||
private final InputStream is; | private final InputStream is; | ||||
private final OutputStream os; | private final OutputStream os; | ||||
private volatile boolean finish; | |||||
private volatile boolean askedToStop; | |||||
private volatile boolean finished; | private volatile boolean finished; | ||||
private final boolean closeWhenExhausted; | private final boolean closeWhenExhausted; | ||||
private boolean autoflush = false; | private boolean autoflush = false; | ||||
@@ -42,6 +44,7 @@ public class StreamPumper implements Runnable { | |||||
private int bufferSize = SMALL_BUFFER_SIZE; | private int bufferSize = SMALL_BUFFER_SIZE; | ||||
private boolean started = false; | private boolean started = false; | ||||
private final boolean useAvailable; | private final boolean useAvailable; | ||||
private PostStopHandle postStopHandle; | |||||
/** | /** | ||||
* Create a new StreamPumper. | * Create a new StreamPumper. | ||||
@@ -57,7 +60,6 @@ public class StreamPumper implements Runnable { | |||||
/** | /** | ||||
* Create a new StreamPumper. | * Create a new StreamPumper. | ||||
* | |||||
* <p><b>Note:</b> If you set useAvailable to true, you must | * <p><b>Note:</b> If you set useAvailable to true, you must | ||||
* explicitly invoke {@link #stop stop} or interrupt the | * explicitly invoke {@link #stop stop} or interrupt the | ||||
* corresponding Thread when you are done or the run method will | * corresponding Thread when you are done or the run method will | ||||
@@ -122,39 +124,27 @@ public class StreamPumper implements Runnable { | |||||
try { | try { | ||||
int length; | int length; | ||||
while (true) { | |||||
while (!this.askedToStop && !Thread.interrupted()) { | |||||
waitForInput(is); | waitForInput(is); | ||||
if (finish || Thread.interrupted()) { | |||||
if (askedToStop || Thread.interrupted()) { | |||||
break; | break; | ||||
} | } | ||||
length = is.read(buf); | length = is.read(buf); | ||||
if (length <= 0 || Thread.interrupted()) { | |||||
if (length < 0) { | |||||
// EOF | |||||
break; | break; | ||||
} | } | ||||
os.write(buf, 0, length); | |||||
if (autoflush) { | |||||
os.flush(); | |||||
} | |||||
if (finish) { //NOSONAR | |||||
break; | |||||
} | |||||
} | |||||
// On completion, drain any available data (which might be the first data available for quick executions) | |||||
if (finish) { | |||||
while ((length = is.available()) > 0) { | |||||
if (Thread.interrupted()) { | |||||
break; | |||||
} | |||||
length = is.read(buf, 0, Math.min(length, buf.length)); | |||||
if (length <= 0) { | |||||
break; | |||||
} | |||||
if (length > 0) { | |||||
// we did read something, so write it out | |||||
os.write(buf, 0, length); | os.write(buf, 0, length); | ||||
if (autoflush) { | |||||
os.flush(); | |||||
} | |||||
} | } | ||||
} | } | ||||
os.flush(); | |||||
this.doPostStop(); | |||||
} catch (InterruptedException ie) { | } catch (InterruptedException ie) { | ||||
// likely PumpStreamHandler trying to stop us | // likely PumpStreamHandler trying to stop us | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
@@ -166,7 +156,7 @@ public class StreamPumper implements Runnable { | |||||
FileUtils.close(os); | FileUtils.close(os); | ||||
} | } | ||||
finished = true; | finished = true; | ||||
finish = false; | |||||
askedToStop = false; | |||||
synchronized (this) { | synchronized (this) { | ||||
notifyAll(); | notifyAll(); | ||||
} | } | ||||
@@ -206,6 +196,7 @@ public class StreamPumper implements Runnable { | |||||
/** | /** | ||||
* Get the size in bytes of the read buffer. | * Get the size in bytes of the read buffer. | ||||
* | |||||
* @return the int size of the read buffer. | * @return the int size of the read buffer. | ||||
*/ | */ | ||||
public synchronized int getBufferSize() { | public synchronized int getBufferSize() { | ||||
@@ -225,19 +216,26 @@ public class StreamPumper implements Runnable { | |||||
* Note that it may continue to block on the input stream | * Note that it may continue to block on the input stream | ||||
* but it will really stop the thread as soon as it gets EOF | * but it will really stop the thread as soon as it gets EOF | ||||
* or any byte, and it will be marked as finished. | * or any byte, and it will be marked as finished. | ||||
* @return Returns a {@link PostStopHandle} for the callers to | |||||
* know if the status of post-stop activities, that happen, before this | |||||
* {@link StreamPumper} is actually finished | |||||
* @since Ant 1.6.3 | * @since Ant 1.6.3 | ||||
* @since Ant 10.2.0 this method returns a {@link PostStopHandle} | |||||
*/ | */ | ||||
/*package*/ synchronized void stop() { | |||||
finish = true; | |||||
/*package*/ | |||||
synchronized PostStopHandle stop() { | |||||
askedToStop = true; | |||||
postStopHandle = new PostStopHandle(); | |||||
notifyAll(); | notifyAll(); | ||||
return postStopHandle; | |||||
} | } | ||||
private static final long POLL_INTERVAL = 100; | private static final long POLL_INTERVAL = 100; | ||||
private void waitForInput(InputStream is) | private void waitForInput(InputStream is) | ||||
throws IOException, InterruptedException { | |||||
throws IOException, InterruptedException { | |||||
if (useAvailable) { | if (useAvailable) { | ||||
while (!finish && is.available() == 0) { | |||||
while (!askedToStop && is.available() == 0) { | |||||
if (Thread.interrupted()) { | if (Thread.interrupted()) { | ||||
throw new InterruptedException(); | throw new InterruptedException(); | ||||
} | } | ||||
@@ -249,4 +247,66 @@ public class StreamPumper implements Runnable { | |||||
} | } | ||||
} | } | ||||
private void doPostStop() throws IOException { | |||||
try { | |||||
final byte[] buf = new byte[bufferSize]; | |||||
int length; | |||||
// We were asked to stop, the contract allows us to do any non-blocking | |||||
// final bits of reads, before actually finishing. So we try and drain any (non-blocking) available | |||||
// data. We *don't* check the thread interrupt status, anymore, once we start draining this non-blocking | |||||
// available data, to allow us to cleanly write out any available data. | |||||
if (askedToStop) { | |||||
int bytesReadableWithoutBlocking; | |||||
while ((bytesReadableWithoutBlocking = is.available()) > 0) { | |||||
length = is.read(buf, 0, Math.min(bytesReadableWithoutBlocking, buf.length)); | |||||
if (length <= 0) { | |||||
break; | |||||
} | |||||
os.write(buf, 0, length); | |||||
} | |||||
} | |||||
// this can potentially be blocking, but that's OK since our post stop activity is allowed to | |||||
// cleanup/flush any data and the PostStopHandle let's the caller control over how long they want | |||||
// this to go, before actually interrupting the thread | |||||
os.flush(); | |||||
} finally { | |||||
if (this.postStopHandle != null) { | |||||
this.postStopHandle.latch.countDown(); | |||||
this.postStopHandle.inPostStopTasks = false; | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* A handle that can be used after {@link #stop()} has been invoked to check if the | |||||
* {@link StreamPumper} is in the process of do some post-stop tasks (like flushing | |||||
* of streams), before finishing. | |||||
*/ | |||||
final class PostStopHandle { | |||||
private boolean inPostStopTasks = true; | |||||
private final CountDownLatch latch = new CountDownLatch(1); | |||||
/** | |||||
* Returns true if the {@link StreamPumper} is doing post-stop tasks (like flushing of streams). | |||||
* Else returns false. | |||||
* @return | |||||
*/ | |||||
boolean isInPostStopTasks() { | |||||
return inPostStopTasks; | |||||
} | |||||
/** | |||||
* Waits for a maximum of {@code timeout} time for the post-stop activities to complete. | |||||
* | |||||
* @param timeout The maximum amount of time to wait for the post-stop activities to complete | |||||
* @param timeUnit The unit of {@code timeout} | |||||
* @return Returns true if the post-stop activities completed within the specified {@code timeout}. | |||||
* Else returns false | |||||
* @throws InterruptedException If the current thread was interrupted while waiting | |||||
*/ | |||||
boolean awaitPostStopCompletion(final long timeout, final TimeUnit timeUnit) throws InterruptedException { | |||||
return this.latch.await(timeout, timeUnit); | |||||
} | |||||
} | |||||
} | } |
@@ -0,0 +1,91 @@ | |||||
package org.apache.tools.ant.taskdefs; | |||||
import org.apache.tools.ant.Project; | |||||
import org.apache.tools.ant.ProjectHelper; | |||||
import org.junit.Before; | |||||
import org.junit.Test; | |||||
import java.io.ByteArrayOutputStream; | |||||
import java.io.File; | |||||
import java.io.FileInputStream; | |||||
import java.io.IOException; | |||||
import java.util.Arrays; | |||||
import static org.junit.Assert.assertFalse; | |||||
import static org.junit.Assert.assertNotNull; | |||||
import static org.junit.Assert.assertTrue; | |||||
/** | |||||
* Tests the {@code exec} task which uses a {@code redirector} to redirect its output and error streams | |||||
*/ | |||||
public class ExecStreamRedirectorTest { | |||||
private Project project; | |||||
@Before | |||||
public void setUp() throws Exception { | |||||
project = new Project(); | |||||
project.init(); | |||||
final File antFile = new File(System.getProperty("root"), "src/etc/testcases/taskdefs/exec/exec-with-redirector.xml"); | |||||
project.setUserProperty("ant.file", antFile.getAbsolutePath()); | |||||
final File outputDir = this.createTmpDir(); | |||||
project.setUserProperty("output", outputDir.toString()); | |||||
ProjectHelper.configureProject(project, antFile); | |||||
project.executeTarget("setUp"); | |||||
} | |||||
/** | |||||
* Tests that the redirected streams of the exec'ed process aren't truncated. | |||||
* | |||||
* @throws Exception | |||||
* @see <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=58451">bz-58451</a> and | |||||
* <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=58833">bz-58833</a> for more details | |||||
*/ | |||||
@Test | |||||
public void testRedirection() throws Exception { | |||||
final String dirToList = project.getProperty("dir.to.ls"); | |||||
assertNotNull("Directory to list isn't available", dirToList); | |||||
assertTrue(dirToList + " is not a directory", new File(dirToList).isDirectory()); | |||||
project.executeTarget("list-dir"); | |||||
// verify the redirected output | |||||
final String outputDirPath = project.getProperty("output"); | |||||
byte[] dirListingOutput = null; | |||||
for (int i = 1; i <= 16; i++) { | |||||
final File redirectedOutputFile = new File(outputDirPath, "ls" + i + ".txt"); | |||||
assertTrue(redirectedOutputFile + " is missing or not a regular file", redirectedOutputFile.isFile()); | |||||
final byte[] redirectedOutput = readAllBytes(redirectedOutputFile); | |||||
assertNotNull("No content was redirected to " + redirectedOutputFile, redirectedOutput); | |||||
assertFalse("Content in redirected file " + redirectedOutputFile + " was empty", redirectedOutput.length == 0); | |||||
if (dirListingOutput != null) { | |||||
// compare the directory listing that was redirected to these files. all files should have the same content | |||||
assertTrue("Redirected output in file " + redirectedOutputFile + | |||||
" doesn't match content in other redirected output file(s)", Arrays.equals(dirListingOutput, redirectedOutput)); | |||||
} | |||||
dirListingOutput = redirectedOutput; | |||||
} | |||||
} | |||||
private File createTmpDir() { | |||||
final File tmpDir = new File(System.getProperty("java.io.tmpdir"), String.valueOf("temp-" + System.nanoTime())); | |||||
tmpDir.mkdir(); | |||||
tmpDir.deleteOnExit(); | |||||
return tmpDir; | |||||
} | |||||
private static byte[] readAllBytes(final File file) throws IOException { | |||||
final FileInputStream fis = new FileInputStream(file); | |||||
final ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |||||
try { | |||||
final byte[] dataChunk = new byte[1024]; | |||||
int numRead = -1; | |||||
while ((numRead = fis.read(dataChunk)) > 0) { | |||||
bos.write(dataChunk, 0, numRead); | |||||
} | |||||
} finally { | |||||
fis.close(); | |||||
} | |||||
return bos.toByteArray(); | |||||
} | |||||
} |