| @@ -53,6 +53,10 @@ Fixed bugs: | |||
| suggesting the fix. | |||
| 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: | |||
| -------------- | |||
| @@ -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.InputStream; | |||
| import java.io.OutputStream; | |||
| import java.util.concurrent.TimeUnit; | |||
| import org.apache.tools.ant.util.FileUtils; | |||
| @@ -183,12 +184,21 @@ public class PumpStreamHandler implements ExecuteStreamHandler { | |||
| if (!t.isAlive()) { | |||
| return; | |||
| } | |||
| StreamPumper.PostStopHandle postStopHandle = null; | |||
| 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()) { | |||
| // we waited for the thread/stream pumper to finish, but it hasn't yet. | |||
| // so we interrupt it | |||
| t.interrupt(); | |||
| t.join(JOIN_TIMEOUT); | |||
| } | |||
| @@ -17,11 +17,13 @@ | |||
| */ | |||
| package org.apache.tools.ant.taskdefs; | |||
| import org.apache.tools.ant.util.FileUtils; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| 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. | |||
| @@ -34,7 +36,7 @@ public class StreamPumper implements Runnable { | |||
| private final InputStream is; | |||
| private final OutputStream os; | |||
| private volatile boolean finish; | |||
| private volatile boolean askedToStop; | |||
| private volatile boolean finished; | |||
| private final boolean closeWhenExhausted; | |||
| private boolean autoflush = false; | |||
| @@ -42,6 +44,7 @@ public class StreamPumper implements Runnable { | |||
| private int bufferSize = SMALL_BUFFER_SIZE; | |||
| private boolean started = false; | |||
| private final boolean useAvailable; | |||
| private PostStopHandle postStopHandle; | |||
| /** | |||
| * Create a new StreamPumper. | |||
| @@ -57,7 +60,6 @@ public class StreamPumper implements Runnable { | |||
| /** | |||
| * Create a new StreamPumper. | |||
| * | |||
| * <p><b>Note:</b> If you set useAvailable to true, you must | |||
| * explicitly invoke {@link #stop stop} or interrupt the | |||
| * corresponding Thread when you are done or the run method will | |||
| @@ -122,39 +124,27 @@ public class StreamPumper implements Runnable { | |||
| try { | |||
| int length; | |||
| while (true) { | |||
| while (!this.askedToStop && !Thread.interrupted()) { | |||
| waitForInput(is); | |||
| if (finish || Thread.interrupted()) { | |||
| if (askedToStop || Thread.interrupted()) { | |||
| break; | |||
| } | |||
| length = is.read(buf); | |||
| if (length <= 0 || Thread.interrupted()) { | |||
| if (length < 0) { | |||
| // EOF | |||
| 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); | |||
| if (autoflush) { | |||
| os.flush(); | |||
| } | |||
| } | |||
| } | |||
| os.flush(); | |||
| this.doPostStop(); | |||
| } catch (InterruptedException ie) { | |||
| // likely PumpStreamHandler trying to stop us | |||
| } catch (Exception e) { | |||
| @@ -166,7 +156,7 @@ public class StreamPumper implements Runnable { | |||
| FileUtils.close(os); | |||
| } | |||
| finished = true; | |||
| finish = false; | |||
| askedToStop = false; | |||
| synchronized (this) { | |||
| notifyAll(); | |||
| } | |||
| @@ -206,6 +196,7 @@ public class StreamPumper implements Runnable { | |||
| /** | |||
| * Get the size in bytes of the read buffer. | |||
| * | |||
| * @return the int size of the read buffer. | |||
| */ | |||
| public synchronized int getBufferSize() { | |||
| @@ -225,19 +216,26 @@ public class StreamPumper implements Runnable { | |||
| * Note that it may continue to block on the input stream | |||
| * but it will really stop the thread as soon as it gets EOF | |||
| * 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 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(); | |||
| return postStopHandle; | |||
| } | |||
| private static final long POLL_INTERVAL = 100; | |||
| private void waitForInput(InputStream is) | |||
| throws IOException, InterruptedException { | |||
| throws IOException, InterruptedException { | |||
| if (useAvailable) { | |||
| while (!finish && is.available() == 0) { | |||
| while (!askedToStop && is.available() == 0) { | |||
| if (Thread.interrupted()) { | |||
| 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(); | |||
| } | |||
| } | |||