[MSHARED-618] CommandLineCallable does not always call the 'runAfterProcessTermination' runnable.
[MSHARED-620] CommandLineCallable should defer starting threads until called.
[MSHARED-621] CommandLineCallable should calculate process timeouts using 'System.nanoTime' instead of 'System.currentTimeMillis'.
[MSHARED-622] CommandLineCallable silently ignores exceptions thrown from the stdin processor (StreemFeeder).
git-svn-id: https://svn.apache.org/repos/asf/maven/shared/trunk@1784432 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/maven/shared/utils/cli/CommandLineUtils.java b/src/main/java/org/apache/maven/shared/utils/cli/CommandLineUtils.java
index 6e1bc81..4beea13 100644
--- a/src/main/java/org/apache/maven/shared/utils/cli/CommandLineUtils.java
+++ b/src/main/java/org/apache/maven/shared/utils/cli/CommandLineUtils.java
@@ -42,16 +42,21 @@
{
/**
- *
+ * A {@code StreamConsumer} providing consumed lines as a {@code String}.
+ *
+ * @see #getOutput()
*/
public static class StringStreamConsumer
implements StreamConsumer
{
+
private final StringBuffer string = new StringBuffer();
- private static final String LS = System.getProperty( "line.separator" );
+ private static final String LS = System.getProperty( "line.separator", "\n" );
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ */
@Override
public void consumeLine( String line )
{
@@ -65,27 +70,18 @@
{
return string.toString();
}
+
}
- private static class ProcessHook
- extends Thread
- {
- private final Process process;
+ /**
+ * Number of milliseconds per second.
+ */
+ private static final long MILLIS_PER_SECOND = 1000L;
- private ProcessHook( Process process )
- {
- super( "CommandlineUtils process shutdown hook" );
- this.process = process;
- this.setContextClassLoader( null );
- }
-
- /** {@inheritDoc} */
- public void run()
- {
- process.destroy();
- }
- }
-
+ /**
+ * Number of nanoseconds per second.
+ */
+ private static final long NANOS_PER_SECOND = 1000000000L;
/**
* @param cl The command line {@link Commandline}
@@ -248,32 +244,48 @@
final Process p = cl.execute();
- final StreamFeeder inputFeeder = systemIn != null ? new StreamFeeder( systemIn, p.getOutputStream() ) : null;
-
- final StreamPumper outputPumper = new StreamPumper( p.getInputStream(), systemOut );
-
- final StreamPumper errorPumper = new StreamPumper( p.getErrorStream(), systemErr );
-
- if ( inputFeeder != null )
+ final Thread processHook = new Thread()
{
- inputFeeder.start();
- }
- outputPumper.start();
+ {
+ this.setName( "CommandLineUtils process shutdown hook" );
+ this.setContextClassLoader( null );
+ }
- errorPumper.start();
+ @Override
+ public void run()
+ {
+ p.destroy();
+ }
- final ProcessHook processHook = new ProcessHook( p );
+ };
ShutdownHookUtils.addShutDownHook( processHook );
return new CommandLineCallable()
{
+
+ @Override
public Integer call()
throws CommandLineException
{
+ StreamFeeder inputFeeder = null;
+ StreamPumper outputPumper = null;
+ StreamPumper errorPumper = null;
try
{
+ if ( systemIn != null )
+ {
+ inputFeeder = new StreamFeeder( systemIn, p.getOutputStream() );
+ inputFeeder.start();
+ }
+
+ outputPumper = new StreamPumper( p.getInputStream(), systemOut );
+ outputPumper.start();
+
+ errorPumper = new StreamPumper( p.getErrorStream(), systemErr );
+ errorPumper.start();
+
int returnValue;
if ( timeoutInSeconds <= 0 )
{
@@ -281,85 +293,125 @@
}
else
{
- long now = System.currentTimeMillis();
- long timeoutInMillis = 1000L * timeoutInSeconds;
- long finish = now + timeoutInMillis;
- while ( isAlive( p ) && ( System.currentTimeMillis() < finish ) )
+ final long now = System.nanoTime();
+ final long timeout = now + NANOS_PER_SECOND * timeoutInSeconds;
+ while ( isAlive( p ) && ( System.nanoTime() < timeout ) )
{
- Thread.sleep( 10 );
+ // The timeout is specified in seconds. Therefore we must not sleep longer than one second
+ // but we should sleep as long as possible to reduce the number of iterations performed.
+ Thread.sleep( MILLIS_PER_SECOND - 1L );
}
+
if ( isAlive( p ) )
{
- throw new InterruptedException(
- "Process timeout out after " + timeoutInSeconds + " seconds" );
+ throw new InterruptedException( String.format( "Process timed out after %d seconds.",
+ timeoutInSeconds ) );
+
}
returnValue = p.exitValue();
}
- if ( runAfterProcessTermination != null )
+// TODO Find out if waitUntilDone needs to be called using a try-finally construct. The method may throw an
+// InterruptedException so that calls to waitUntilDone may be skipped.
+// try
+// {
+// if ( inputFeeder != null )
+// {
+// inputFeeder.waitUntilDone();
+// }
+// }
+// finally
+// {
+// try
+// {
+// outputPumper.waitUntilDone();
+// }
+// finally
+// {
+// errorPumper.waitUntilDone();
+// }
+// }
+ if ( inputFeeder != null )
{
- runAfterProcessTermination.run();
+ inputFeeder.waitUntilDone();
}
- waitForAllPumpers( inputFeeder, outputPumper, errorPumper );
+ outputPumper.waitUntilDone();
+ errorPumper.waitUntilDone();
+
+ if ( inputFeeder != null )
+ {
+ inputFeeder.close();
+
+ if ( inputFeeder.getException() != null )
+ {
+ throw new CommandLineException( "Failure processing stdin.", inputFeeder.getException() );
+ }
+ }
if ( outputPumper.getException() != null )
{
- throw new CommandLineException( "Error inside systemOut parser", outputPumper.getException() );
+ throw new CommandLineException( "Failure processing stdout.", outputPumper.getException() );
}
if ( errorPumper.getException() != null )
{
- throw new CommandLineException( "Error inside systemErr parser", errorPumper.getException() );
+ throw new CommandLineException( "Failure processing stderr.", errorPumper.getException() );
}
return returnValue;
}
catch ( InterruptedException ex )
{
+ throw new CommandLineTimeOutException( "Error while executing external command, process killed.",
+ ex );
+
+ }
+ finally
+ {
if ( inputFeeder != null )
{
inputFeeder.disable();
}
-
- outputPumper.disable();
- errorPumper.disable();
- throw new CommandLineTimeOutException( "Error while executing external command, process killed.",
- ex );
- }
- finally
- {
- ShutdownHookUtils.removeShutdownHook( processHook );
-
- processHook.run();
-
- if ( inputFeeder != null )
+ if ( outputPumper != null )
{
- inputFeeder.close();
+ outputPumper.disable();
+ }
+ if ( errorPumper != null )
+ {
+ errorPumper.disable();
}
- outputPumper.close();
+ try
+ {
+ if ( runAfterProcessTermination != null )
+ {
+ runAfterProcessTermination.run();
+ }
+ }
+ finally
+ {
+ ShutdownHookUtils.removeShutdownHook( processHook );
- errorPumper.close();
+ try
+ {
+ processHook.run();
+ }
+ finally
+ {
+ if ( inputFeeder != null )
+ {
+ inputFeeder.close();
+ }
+ }
+ }
}
}
+
};
}
- private static void waitForAllPumpers( @Nullable StreamFeeder inputFeeder, StreamPumper outputPumper,
- StreamPumper errorPumper )
- throws InterruptedException
- {
- if ( inputFeeder != null )
- {
- inputFeeder.waitUntilDone();
- }
-
- outputPumper.waitUntilDone();
- errorPumper.waitUntilDone();
- }
-
/**
* Gets the shell environment variables for this process. Note that the returned mapping from variable names to
* values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>