This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git

commit dcccd2330e5365a575a2b782854d58b5ed2f98b0
Author: Tibor Digana <[email protected]>
AuthorDate: Mon Apr 5 13:53:44 2021 +0200

    [SUREFIRE-1881] Adds additional debug log and fork connection timeout
---
 .../plugin/surefire/booterclient/ForkStarter.java  |  34 +--
 .../extensions/InvalidSessionIdException.java      |   3 +-
 .../surefire/extensions/LegacyForkChannel.java     |  40 +++-
 .../surefire/extensions/SurefireForkChannel.java   | 215 ++++++++++++++-----
 .../maven/plugin/surefire/extensions/E2ETest.java  | 237 +++++++++++++++------
 .../maven/surefire/extensions/ForkChannelTest.java |   6 +-
 .../AbstractNoninterruptibleReadableChannel.java   |  10 +-
 .../surefire/extensions/CloseableDaemonThread.java |   5 +-
 ...CloseableDaemonThread.java => Completable.java} |  23 +-
 .../maven/surefire/extensions/ForkChannel.java     |  36 ++--
 .../{CloseableDaemonThread.java => Stoppable.java} |  13 +-
 .../extensions/util/CountDownLauncher.java         |  33 ++-
 .../maven/surefire/its/jiras/Surefire1881.java     |  22 +-
 .../src/test/resources/surefire-1881/pom.xml       | 106 +++++++++
 .../src/main/java/de/scrum_master/dummy/Agent.java |  49 +++++
 .../test/java/de/scrum_master/dummy/AgentIT.java   |  23 ++
 16 files changed, 642 insertions(+), 213 deletions(-)

diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
index 200d2a8..a999069 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
+++ 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
@@ -41,11 +41,11 @@ import org.apache.maven.surefire.api.booter.Shutdown;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
 import org.apache.maven.surefire.booter.SurefireExecutionException;
-import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkChannel;
 import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.Stoppable;
 import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
 import org.apache.maven.surefire.extensions.util.CommandlineStreams;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
@@ -628,31 +628,24 @@ public class ForkStarter
         Integer result = null;
         RunResult runResult = null;
         SurefireBooterForkException booterForkException = null;
-        CloseableDaemonThread in = null;
-        CloseableDaemonThread out = null;
-        CloseableDaemonThread err = null;
+        Stoppable err = null;
         DefaultReporterFactory reporter = 
forkClient.getDefaultReporterFactory();
         currentForkClients.add( forkClient );
         CountdownCloseable countdownCloseable =
             new CountdownCloseable( eventConsumer, 
forkChannel.getCountdownCloseablePermits() );
         try ( CommandlineExecutor exec = new CommandlineExecutor( cli, 
countdownCloseable ) )
         {
+            forkChannel.tryConnectToClient();
             CommandlineStreams streams = exec.execute();
             closer.addCloseable( streams );
 
-            forkChannel.connectToClient();
-            log.debug( "Fork Channel [" + forkNumber + "] connected to the 
client." );
+            err = bindErrorStream( forkNumber, countdownCloseable, streams );
 
-            in = forkChannel.bindCommandReader( commandReader, 
streams.getStdInChannel() );
-            in.start();
+            forkChannel.bindCommandReader( commandReader, 
streams.getStdInChannel() );
 
-            out = forkChannel.bindEventHandler( eventConsumer, 
countdownCloseable, streams.getStdOutChannel() );
-            out.start();
+            forkChannel.bindEventHandler( eventConsumer, countdownCloseable, 
streams.getStdOutChannel() );
 
-            EventHandler<String> errConsumer = new NativeStdErrStreamConsumer( 
log );
-            err = new LineConsumerThread( "fork-" + forkNumber + 
"-err-thread", streams.getStdErrChannel(),
-                errConsumer, countdownCloseable );
-            err.start();
+            log.debug( "Fork Channel [" + forkNumber + "] connected to the 
client." );
 
             result = exec.awaitExit();
 
@@ -670,8 +663,7 @@ public class ForkStarter
         {
             log.error( "Closing the streams after (InterruptedException) '" + 
e.getLocalizedMessage() + "'" );
             // maybe implement it in the Future.cancel() of the extension or 
similar
-            in.disable();
-            out.disable();
+            forkChannel.disable();
             err.disable();
         }
         catch ( Exception e )
@@ -761,6 +753,16 @@ public class ForkStarter
         return runResult;
     }
 
+    private Stoppable bindErrorStream( int forkNumber, CountdownCloseable 
countdownCloseable,
+                                       CommandlineStreams streams )
+    {
+        EventHandler<String> errConsumer = new NativeStdErrStreamConsumer( log 
);
+        LineConsumerThread stdErr = new LineConsumerThread( "fork-" + 
forkNumber + "-err-thread",
+            streams.getStdErrChannel(), errConsumer, countdownCloseable );
+        stdErr.start();
+        return stdErr;
+    }
+
     private Iterable<Class<?>> getSuitesIterator()
         throws SurefireBooterForkException
     {
diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
index 2ceaf12..30664e2 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
+++ 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
@@ -25,7 +25,8 @@ import 
org.apache.maven.surefire.extensions.util.CommandlineExecutor;
 import java.io.IOException;
 
 /**
- * After the authentication has failed, {@link ForkChannel#connectToClient()} 
throws {@link InvalidSessionIdException}
+ * After the authentication has failed, {@link 
ForkChannel#tryConnectToClient()}
+ * throws {@link InvalidSessionIdException}
  * and {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter} 
should close {@link CommandlineExecutor}.
  *
  * @since 3.0.0-M5
diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
index e18f77d..b754535 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
+++ 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
@@ -41,13 +41,16 @@ import java.nio.channels.WritableByteChannel;
  */
 final class LegacyForkChannel extends ForkChannel
 {
+    private CloseableDaemonThread commandReaderBindings;
+    private CloseableDaemonThread eventHandlerBindings;
+
     LegacyForkChannel( @Nonnull ForkNodeArguments arguments )
     {
         super( arguments );
     }
 
     @Override
-    public void connectToClient()
+    public void tryConnectToClient()
     {
     }
 
@@ -64,20 +67,37 @@ final class LegacyForkChannel extends ForkChannel
     }
 
     @Override
-    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader 
commands,
-                                                    WritableByteChannel stdIn )
+    public void bindCommandReader( @Nonnull CommandReader commands, 
WritableByteChannel stdIn )
+    {
+        ForkNodeArguments args = getArguments();
+        String threadName = "commands-fork-" + args.getForkChannelId();
+        commandReaderBindings = new StreamFeeder( threadName, stdIn, commands, 
args.getConsoleLogger() );
+        commandReaderBindings.start();
+    }
+
+    @Override
+    public void bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                  @Nonnull CountdownCloseable 
countdownCloseable,
+                                  ReadableByteChannel stdOut )
     {
-        return new StreamFeeder( "std-in-fork-" + 
getArguments().getForkChannelId(), stdIn, commands,
-            getArguments().getConsoleLogger() );
+        ForkNodeArguments args = getArguments();
+        String threadName = "fork-" + args.getForkChannelId() + 
"-event-thread";
+        eventHandlerBindings = new EventConsumerThread( threadName, stdOut, 
eventHandler, countdownCloseable, args );
+        eventHandlerBindings.start();
     }
 
     @Override
-    public CloseableDaemonThread bindEventHandler( @Nonnull 
EventHandler<Event> eventHandler,
-                                                   @Nonnull CountdownCloseable 
countdownCloseable,
-                                                   ReadableByteChannel stdOut )
+    public void disable()
     {
-        return new EventConsumerThread( "fork-" + 
getArguments().getForkChannelId() + "-event-thread", stdOut,
-            eventHandler, countdownCloseable, getArguments() );
+        if ( eventHandlerBindings != null )
+        {
+            eventHandlerBindings.disable();
+        }
+
+        if ( commandReaderBindings != null )
+        {
+            commandReaderBindings.disable();
+        }
     }
 
     @Override
diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
index 640d562..e070679 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
+++ 
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
@@ -21,11 +21,12 @@ package org.apache.maven.plugin.surefire.extensions;
 
 import 
org.apache.maven.plugin.surefire.booterclient.output.NativeStdOutStreamConsumer;
 import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.CommandReader;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.util.CountDownLauncher;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.apache.maven.surefire.extensions.util.LineConsumerThread;
 
@@ -44,6 +45,7 @@ import java.nio.channels.WritableByteChannel;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 import static java.net.StandardSocketOptions.SO_KEEPALIVE;
 import static java.net.StandardSocketOptions.SO_REUSEADDR;
@@ -56,11 +58,13 @@ import static 
org.apache.maven.surefire.api.util.internal.Channels.newChannel;
 import static 
org.apache.maven.surefire.api.util.internal.Channels.newInputStream;
 import static 
org.apache.maven.surefire.api.util.internal.Channels.newOutputStream;
 import static 
org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
+import static org.apache.maven.surefire.shared.lang3.StringUtils.isBlank;
+import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
 
 /**
  * The TCP/IP server accepting only one client connection. The forked JVM 
connects to the server using the
  * {@link #getForkNodeConnectionString() connection string}.
- * The main purpose of this class is to {@link #connectToClient() conect with 
tthe client}, bind the
+ * The main purpose of this class is to {@link #tryConnectToClient() conect 
with tthe client}, bind the
  * {@link #bindCommandReader(CommandReader, WritableByteChannel) command 
reader} to the internal socket's
  * {@link java.io.InputStream}, and bind the
  * {@link #bindEventHandler(EventHandler, CountdownCloseable, 
ReadableByteChannel) event handler} writing the event
@@ -79,8 +83,13 @@ final class SurefireForkChannel extends ForkChannel
     private final String localHost;
     private final int localPort;
     private final String sessionId;
-    private volatile AsynchronousSocketChannel worker;
+    private final Bindings bindings = new Bindings( 2 );
+    private volatile Future<AsynchronousSocketChannel> session;
     private volatile LineConsumerThread out;
+    private volatile CloseableDaemonThread commandReaderBindings;
+    private volatile CloseableDaemonThread eventHandlerBindings;
+    private volatile EventBindings eventBindings;
+    private volatile CommandBindings commandBindings;
 
     SurefireForkChannel( @Nonnull ForkNodeArguments arguments ) throws 
IOException
     {
@@ -96,45 +105,108 @@ final class SurefireForkChannel extends ForkChannel
     }
 
     @Override
-    public void connectToClient() throws IOException
+    public void tryConnectToClient()
     {
-        if ( worker != null )
+        if ( session != null )
         {
             throw new IllegalStateException( "already accepted TCP client 
connection" );
         }
+        session = server.accept();
+    }
 
-        try
+    @Override
+    public String getForkNodeConnectionString()
+    {
+        return "tcp://" + localHost + ":" + localPort + ( isBlank( sessionId ) 
? "" : "?sessionId=" + sessionId );
+    }
+
+    @Override
+    public int getCountdownCloseablePermits()
+    {
+        return 3;
+    }
+
+    @Override
+    public void bindCommandReader( @Nonnull CommandReader commands, 
WritableByteChannel stdIn )
+        throws IOException, InterruptedException
+    {
+        commandBindings = new CommandBindings( commands );
+
+        bindings.countDown();
+    }
+
+    @Override
+    public void bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                  @Nonnull CountdownCloseable countdown,
+                                  ReadableByteChannel stdOut )
+        throws IOException, InterruptedException
+    {
+        ForkNodeArguments args = getArguments();
+        out = new LineConsumerThread( "fork-" + args.getForkChannelId() + 
"-out-thread", stdOut,
+            new NativeStdOutStreamConsumer( args.getConsoleLogger() ), 
countdown );
+        out.start();
+
+        eventBindings = new EventBindings( eventHandler, countdown );
+
+        bindings.countDown();
+    }
+
+    @Override
+    public void disable()
+    {
+        if ( eventHandlerBindings != null )
         {
-            worker = server.accept().get();
-            verifySessionId();
+            eventHandlerBindings.disable();
         }
-        catch ( InterruptedException e )
+
+        if ( commandReaderBindings != null )
         {
-            throw new IOException( e.getLocalizedMessage(), e );
+            commandReaderBindings.disable();
         }
-        catch ( ExecutionException e )
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        //noinspection unused,EmptyTryBlock,EmptyTryBlock
+        try ( Closeable c1 = getChannel(); Closeable c2 = server; Closeable c3 
= out )
         {
-            throw new IOException( e.getLocalizedMessage(), e.getCause() );
+            // only close all channels
+        }
+        catch ( InterruptedException e )
+        {
+            Throwable cause = e.getCause();
+            throw cause instanceof IOException ? (IOException) cause : new 
IOException( cause );
         }
     }
 
-    private void verifySessionId() throws InterruptedException, 
ExecutionException, IOException
+    private void verifySessionId() throws InterruptedException, IOException
     {
-        ByteBuffer buffer = ByteBuffer.allocate( sessionId.length() );
-        int read;
-        do
-        {
-            read = worker.read( buffer ).get();
-        } while ( read != -1 && buffer.hasRemaining() );
-        if ( read == -1 )
+        try
         {
-            throw new IOException( "Channel closed while verifying the 
client." );
+            ByteBuffer buffer = ByteBuffer.allocate( sessionId.length() );
+            int read;
+            do
+            {
+                read = getChannel().read( buffer ).get();
+            } while ( read != -1 && buffer.hasRemaining() );
+
+            if ( read == -1 )
+            {
+                throw new IOException( "Channel closed while verifying the 
client." );
+            }
+
+            ( (Buffer) buffer ).flip();
+            String clientSessionId = new String( buffer.array(), US_ASCII );
+            if ( !clientSessionId.equals( sessionId ) )
+            {
+                throw new InvalidSessionIdException( clientSessionId, 
sessionId );
+            }
         }
-        ( (Buffer) buffer ).flip();
-        String clientSessionId = new String( buffer.array(), US_ASCII );
-        if ( !clientSessionId.equals( sessionId ) )
+        catch ( ExecutionException e )
         {
-            throw new InvalidSessionIdException( clientSessionId, sessionId );
+            Throwable cause = e.getCause();
+            throw cause instanceof IOException ? (IOException) cause : new 
IOException( cause );
         }
     }
 
@@ -151,51 +223,80 @@ final class SurefireForkChannel extends ForkChannel
         }
     }
 
-    @Override
-    public String getForkNodeConnectionString()
+    private class EventBindings
     {
-        return "tcp://" + localHost + ":" + localPort + "?sessionId=" + 
sessionId;
-    }
+        private final EventHandler<Event> eventHandler;
+        private final CountdownCloseable countdown;
 
-    @Override
-    public int getCountdownCloseablePermits()
-    {
-        return 3;
+        private EventBindings( EventHandler<Event> eventHandler, 
CountdownCloseable countdown )
+        {
+            this.eventHandler = eventHandler;
+            this.countdown = countdown;
+        }
+
+        void bindEventHandler( AsynchronousSocketChannel source )
+        {
+            ForkNodeArguments args = getArguments();
+            String threadName = "fork-" + args.getForkChannelId() + 
"-event-thread";
+            ReadableByteChannel channel = newBufferedChannel( newInputStream( 
source ) );
+            eventHandlerBindings = new EventConsumerThread( threadName, 
channel, eventHandler, countdown, args );
+            eventHandlerBindings.start();
+        }
     }
 
-    @Override
-    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader 
commands,
-                                                    WritableByteChannel stdIn )
+    private class CommandBindings
     {
-        // dont use newBufferedChannel here - may cause the command is not 
sent and the JVM hangs
-        // only newChannel flushes the message
-        // newBufferedChannel does not flush
-        WritableByteChannel channel = newChannel( newOutputStream( worker ) );
-        return new StreamFeeder( "commands-fork-" + 
getArguments().getForkChannelId(), channel, commands,
-            getArguments().getConsoleLogger() );
+        private final CommandReader commands;
+
+        private CommandBindings( CommandReader commands )
+        {
+            this.commands = commands;
+        }
+
+        void bindCommandSender( AsynchronousSocketChannel source )
+        {
+            // don't use newBufferedChannel here - may cause the command is 
not sent and the JVM hangs
+            // only newChannel flushes the message
+            // newBufferedChannel does not flush
+            ForkNodeArguments args = getArguments();
+            WritableByteChannel channel = newChannel( newOutputStream( source 
) );
+            String threadName = "commands-fork-" + args.getForkChannelId();
+            commandReaderBindings = new StreamFeeder( threadName, channel, 
commands, args.getConsoleLogger() );
+            commandReaderBindings.start();
+        }
     }
 
-    @Override
-    public CloseableDaemonThread bindEventHandler( @Nonnull 
EventHandler<Event> eventHandler,
-                                                   @Nonnull CountdownCloseable 
countdownCloseable,
-                                                   ReadableByteChannel stdOut )
+    private class Bindings extends CountDownLauncher
     {
-        out = new LineConsumerThread( "fork-" + 
getArguments().getForkChannelId() + "-out-thread", stdOut,
-            new NativeStdOutStreamConsumer( getArguments().getConsoleLogger() 
), countdownCloseable );
-        out.start();
+        private Bindings( int count )
+        {
+            super( count );
+        }
 
-        ReadableByteChannel channel = newBufferedChannel( newInputStream( 
worker ) );
-        return new EventConsumerThread( "fork-" + 
getArguments().getForkChannelId() + "-event-thread", channel,
-            eventHandler, countdownCloseable, getArguments() );
+        @Override
+        protected void job() throws IOException, InterruptedException
+        {
+            AsynchronousSocketChannel channel = getChannel();
+            if ( isNotBlank( sessionId ) )
+            {
+                verifySessionId();
+            }
+            eventBindings.bindEventHandler( channel );
+            commandBindings.bindCommandSender( channel );
+        }
     }
 
-    @Override
-    public void close() throws IOException
+    private AsynchronousSocketChannel getChannel()
+        throws InterruptedException, IOException
     {
-        //noinspection unused,EmptyTryBlock,EmptyTryBlock
-        try ( Closeable c1 = worker; Closeable c2 = server; Closeable c3 = out 
)
+        try
         {
-            // only close all channels
+            return session == null ? null : session.get();
+        }
+        catch ( ExecutionException e )
+        {
+            Throwable cause = e.getCause();
+            throw cause instanceof IOException ? (IOException) cause : new 
IOException( cause );
         }
     }
 }
diff --git 
a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
 
b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
index 87bcca7..a9fde90 100644
--- 
a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
+++ 
b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
@@ -22,12 +22,14 @@ package org.apache.maven.plugin.surefire.extensions;
 import 
org.apache.maven.plugin.surefire.booterclient.output.ThreadedStreamConsumer;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.log.api.NullConsoleLogger;
-import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.booter.Command;
 import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.api.report.ConsoleOutputReceiver;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
 import 
org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
+import org.apache.maven.surefire.extensions.CommandReader;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.junit.Rule;
 import org.junit.Test;
@@ -36,21 +38,22 @@ import org.junit.rules.ExpectedException;
 import javax.annotation.Nonnull;
 import java.io.Closeable;
 import java.io.File;
+import java.io.IOException;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 /**
  * Simulates the End To End use case where Maven process and Surefire process 
communicate using the TCP/IP protocol.
@@ -68,22 +71,63 @@ public class E2ETest
     public void endToEndTest() throws Exception
     {
         ForkNodeArguments arguments = new Arguments( 
UUID.randomUUID().toString(), 1, new NullConsoleLogger() );
+
         final SurefireForkChannel server = new SurefireForkChannel( arguments 
);
+        server.tryConnectToClient();
 
         final String connection = server.getForkNodeConnectionString();
 
         final SurefireMasterProcessChannelProcessorFactory factory = new 
SurefireMasterProcessChannelProcessorFactory();
         factory.connect( connection );
-        final MasterProcessChannelEncoder encoder = factory.createEncoder( 
arguments );
-
-        System.gc();
-
-        TimeUnit.SECONDS.sleep( 3L );
+        final EventChannelEncoder encoder = (EventChannelEncoder) 
factory.createEncoder( arguments );
 
         final CountDownLatch awaitHandlerFinished = new CountDownLatch( 2 );
 
+        final AtomicLong readTime = new AtomicLong();
+
         final int totalCalls = 400_000; // 400_000; // 1_000_000; // 
10_000_000;
 
+        EventHandler<Event> h = new EventHandler<Event>()
+        {
+            private final AtomicInteger counter = new AtomicInteger();
+            private volatile long t1;
+
+            @Override
+            public void handleEvent( @Nonnull Event event )
+            {
+                try
+                {
+                    if ( counter.getAndIncrement() == 0 )
+                    {
+                        t1 = System.currentTimeMillis();
+                    }
+
+                    long t2 = System.currentTimeMillis();
+                    long spent = t2 - t1;
+
+                    if ( counter.get() == totalCalls - 64 * 1024 )
+                    {
+                        readTime.set( spent );
+                        System.out.println( spent + "ms on read" );
+                        awaitHandlerFinished.countDown();
+                    }
+                }
+                catch ( Exception e )
+                {
+                    e.printStackTrace();
+                }
+            }
+        };
+
+        EventHandler<Event> queue = new ThreadedStreamConsumer( h );
+
+        System.gc();
+
+        SECONDS.sleep( 5L );
+
+        server.bindEventHandler( queue, new CountdownCloseable( new 
DummyCloseable(), 1 ), new DummyReadableChannel() );
+        server.bindCommandReader( new DummyCommandReader(), null );
+
         Thread t = new Thread()
         {
             @Override
@@ -127,58 +171,12 @@ public class E2ETest
         t.setDaemon( true );
         t.start();
 
-        server.connectToClient();
-
-        final AtomicLong readTime = new AtomicLong();
-
-        EventHandler<Event> h = new EventHandler<Event>()
-        {
-            private final AtomicInteger counter = new AtomicInteger();
-            private volatile long t1;
-
-            @Override
-            public void handleEvent( @Nonnull Event event )
-            {
-                try
-                {
-                    if ( counter.getAndIncrement() == 0 )
-                    {
-                        t1 = System.currentTimeMillis();
-                    }
-
-                    long t2 = System.currentTimeMillis();
-                    long spent = t2 - t1;
-
-                    if ( counter.get() % 500_000 == 0 )
-                    {
-                        System.out.println( spent + "ms: " + counter.get() );
-                    }
-
-                    if ( counter.get() == totalCalls - 64 * 1024 )
-                    {
-                        readTime.set( spent );
-                        System.out.println( spent + "ms on read" );
-                        awaitHandlerFinished.countDown();
-                    }
-                }
-                catch ( Exception e )
-                {
-                    e.printStackTrace();
-                }
-            }
-        };
-
-        ThreadedStreamConsumer queue = new ThreadedStreamConsumer( h );
-
-        server.bindEventHandler( queue, new CountdownCloseable( new 
DummyCloseable(), 1 ), new DummyReadableChannel() )
-            .start();
-
-        assertThat( awaitHandlerFinished.await( 30L, TimeUnit.SECONDS ) )
+        assertThat( awaitHandlerFinished.await( 30L, SECONDS ) )
             .isTrue();
 
         factory.close();
         server.close();
-        queue.close();
+        //queue.close();
 
         // 1.0 seconds while using the encoder/decoder
         assertThat( readTime.get() )
@@ -191,8 +189,8 @@ public class E2ETest
     @Test( timeout = 10_000L )
     public void shouldVerifyClient() throws Exception
     {
-        ForkNodeArguments forkNodeArguments = mock( ForkNodeArguments.class );
-        when( forkNodeArguments.getSessionId() ).thenReturn( 
UUID.randomUUID().toString() );
+        ForkNodeArguments forkNodeArguments =
+            new Arguments( UUID.randomUUID().toString(), 1, new 
NullConsoleLogger() );
 
         try ( SurefireForkChannel server = new SurefireForkChannel( 
forkNodeArguments );
               SurefireMasterProcessChannelProcessorFactory client = new 
SurefireMasterProcessChannelProcessorFactory() )
@@ -211,8 +209,6 @@ public class E2ETest
             t.setDaemon( true );
             t.start();
 
-            server.connectToClient();
-
             assertThat( task.get() )
                 .isEqualTo( "client connected" );
         }
@@ -221,9 +217,8 @@ public class E2ETest
     @Test( timeout = 10_000L )
     public void shouldNotVerifyClient() throws Exception
     {
-        ForkNodeArguments forkNodeArguments = mock( ForkNodeArguments.class );
-        String serverSessionId = UUID.randomUUID().toString();
-        when( forkNodeArguments.getSessionId() ).thenReturn( serverSessionId );
+        ForkNodeArguments forkNodeArguments =
+            new Arguments( UUID.randomUUID().toString(), 1, new 
NullConsoleLogger() );
 
         try ( SurefireForkChannel server = new SurefireForkChannel( 
forkNodeArguments );
               SurefireMasterProcessChannelProcessorFactory client = new 
SurefireMasterProcessChannelProcessorFactory() )
@@ -246,31 +241,133 @@ public class E2ETest
 
             e.expect( InvalidSessionIdException.class );
             e.expectMessage( "The actual sessionId 
'6ba7b812-9dad-11d1-80b4-00c04fd430c8' does not match '"
-                + serverSessionId + "'." );
+                + forkNodeArguments.getSessionId() + "'." );
+
+            server.tryConnectToClient();
+            server.bindCommandReader( new DummyCommandReader(), new 
DummyWritableByteChannel() );
 
-            server.connectToClient();
+            server.bindEventHandler( new DummyEventHandler(),
+                new CountdownCloseable( new DummyCloseable(), 1 ), new 
DummyReadableChannel() );
 
             fail( task.get() );
         }
     }
 
+    private static class DummyEventHandler<Event> implements 
EventHandler<Event>
+    {
+        @Override
+        public void handleEvent( @Nonnull Event event )
+        {
+        }
+    }
+
     private static class DummyReadableChannel implements ReadableByteChannel
     {
+        private volatile Thread t;
+
         @Override
-        public int read( ByteBuffer dst )
+        public int read( ByteBuffer dst ) throws IOException
         {
-            return 0;
+            try
+            {
+                t = Thread.currentThread();
+                HOURS.sleep( 1L );
+                return 0;
+            }
+            catch ( InterruptedException e )
+            {
+                throw new IOException( e.getLocalizedMessage(), e );
+            }
         }
 
         @Override
         public boolean isOpen()
         {
-            return false;
+            return true;
         }
 
         @Override
         public void close()
         {
+            if ( t != null )
+            {
+                t.interrupt();
+            }
+        }
+    }
+
+    private static class DummyWritableByteChannel implements 
WritableByteChannel
+    {
+        private volatile Thread t;
+
+        @Override
+        public int write( ByteBuffer src ) throws IOException
+        {
+            try
+            {
+                t = Thread.currentThread();
+                HOURS.sleep( 1L );
+                return 0;
+            }
+            catch ( InterruptedException e )
+            {
+                throw new IOException( e.getLocalizedMessage(), e );
+            }
+        }
+
+        @Override
+        public boolean isOpen()
+        {
+            return true;
+        }
+
+        @Override
+        public void close() throws IOException
+        {
+            if ( t != null )
+            {
+                t.interrupt();
+            }
+        }
+    }
+
+    private static class DummyCommandReader implements CommandReader
+    {
+        private volatile Thread t;
+
+        @Override
+        public Command readNextCommand() throws IOException
+        {
+            try
+            {
+                t = Thread.currentThread();
+                HOURS.sleep( 1L );
+                return null;
+            }
+            catch ( InterruptedException e )
+            {
+                throw new IOException( e.getLocalizedMessage(), e );
+            }
+        }
+
+        @Override
+        public void close()
+        {
+            if ( t != null )
+            {
+                t.interrupt();
+            }
+        }
+
+        @Override
+        public boolean isClosed()
+        {
+            return false;
+        }
+
+        @Override
+        public void tryFlush()
+        {
         }
     }
 
diff --git 
a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
 
b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
index 428ab10..9655c63 100644
--- 
a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
+++ 
b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
@@ -157,11 +157,11 @@ public class ForkChannelTest
             Client client = new Client( uri.getPort(), sessionId );
             client.start();
 
-            channel.connectToClient();
-            channel.bindCommandReader( commandReader, null ).start();
+            channel.tryConnectToClient();
+            channel.bindCommandReader( commandReader, null );
             ReadableByteChannel stdOut = mock( ReadableByteChannel.class );
             when( stdOut.read( any( ByteBuffer.class ) ) ).thenReturn( -1 );
-            channel.bindEventHandler( consumer, cc, stdOut ).start();
+            channel.bindEventHandler( consumer, cc, stdOut );
 
             commandReader.noop();
 
diff --git 
a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/AbstractNoninterruptibleReadableChannel.java
 
b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/AbstractNoninterruptibleReadableChannel.java
index 678d643..2b83e0f 100644
--- 
a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/AbstractNoninterruptibleReadableChannel.java
+++ 
b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/AbstractNoninterruptibleReadableChannel.java
@@ -63,7 +63,13 @@ abstract class AbstractNoninterruptibleReadableChannel 
implements ReadableByteCh
     @Override
     public final void close() throws IOException
     {
-        open = false;
-        closeImpl();
+        try
+        {
+            closeImpl();
+        }
+        finally
+        {
+            open = false;
+        }
     }
 }
diff --git 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
index cc2964c..f0f32d3 100644
--- 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
+++ 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
@@ -20,18 +20,15 @@ package org.apache.maven.surefire.extensions;
  */
 
 import javax.annotation.Nonnull;
-import java.io.Closeable;
 
 /**
  * The base thread class used to handle a stream, transforms the stream to an 
object.
  */
-public abstract class CloseableDaemonThread extends Thread implements Closeable
+public abstract class CloseableDaemonThread extends Thread implements Stoppable
 {
     protected CloseableDaemonThread( @Nonnull String threadName )
     {
         setName( threadName );
         setDaemon( true );
     }
-
-    public abstract void disable();
 }
diff --git 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Completable.java
similarity index 64%
copy from 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
copy to 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Completable.java
index cc2964c..30506b2 100644
--- 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
+++ 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Completable.java
@@ -19,19 +19,24 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import javax.annotation.Nonnull;
-import java.io.Closeable;
+import java.io.IOException;
 
 /**
- * The base thread class used to handle a stream, transforms the stream to an 
object.
+ * Forks the execution of task and the task completion.
+ * The method {@link #complete()} waits for the task to complete or fails.
+ *
+ * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M5
  */
-public abstract class CloseableDaemonThread extends Thread implements Closeable
+public interface Completable
 {
-    protected CloseableDaemonThread( @Nonnull String threadName )
+    Completable EMPTY_COMPLETABLE = new Completable()
     {
-        setName( threadName );
-        setDaemon( true );
-    }
+        @Override
+        public void complete()
+        {
+        }
+    };
 
-    public abstract void disable();
+    void complete() throws IOException, InterruptedException;
 }
diff --git 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkChannel.java
 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkChannel.java
index 62e88be..38d3648 100644
--- 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkChannel.java
+++ 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkChannel.java
@@ -34,8 +34,9 @@ import java.nio.channels.WritableByteChannel;
  * and communicates with a dedicated forked JVM. It represents a server.
  * <br>
  * <br>
- * It connects to a remote client by {@link #connectToClient()}, provides a 
connection string
+ * It connects to a remote client by {@link #tryConnectToClient()}, provides a 
connection string
  * {@link #getForkNodeConnectionString()} needed by the client in forked JVM, 
binds event handler and command reader.
+ * This object is called in one Thread.
  *
  * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
  * @since 3.0.0-M5
@@ -53,7 +54,13 @@ public abstract class ForkChannel implements Closeable
         this.arguments = arguments;
     }
 
-    public abstract void connectToClient() throws IOException;
+    /**
+     * Asynchronously connects to the client.
+     *
+     * @throws IOException if stream fails
+     * @throws InterruptedException if interrupted thread
+     */
+    public abstract void tryConnectToClient() throws IOException, 
InterruptedException;
 
     /**
      * This is server related class, which if binds to a TCP port, determines 
the connection string for the client.
@@ -68,33 +75,38 @@ public abstract class ForkChannel implements Closeable
     public abstract int getCountdownCloseablePermits();
 
     /**
-     * Binds command handler to the channel.
+     * Binds command handler to the channel. Starts a Thread streaming out the 
commands.
      *
      * @param commands command reader, see {@link 
CommandReader#readNextCommand()}
      * @param stdIn    optional standard input stream of the JVM to write the 
encoded commands into it
-     * @return the thread instance to start up in order to stream out the data
      * @throws IOException if an error in the fork channel
+     * @throws InterruptedException channel interrupted
      */
-    public abstract CloseableDaemonThread bindCommandReader( @Nonnull 
CommandReader commands,
-                                                             
WritableByteChannel stdIn )
-        throws IOException;
+    public abstract void bindCommandReader( @Nonnull CommandReader commands, 
WritableByteChannel stdIn )
+        throws IOException, InterruptedException;
 
     /**
+     * Starts a Thread reading the events.
      *
      * @param eventHandler       event eventHandler
      * @param countdownCloseable count down of the final call of {@link 
Closeable#close()}
      * @param stdOut             optional standard output stream of the JVM
-     * @return the thread instance to start up in order to stream out the data
      * @throws IOException if an error in the fork channel
+     * @throws InterruptedException channel interrupted
      */
-    public abstract CloseableDaemonThread bindEventHandler( @Nonnull 
EventHandler<Event> eventHandler,
-                                                            @Nonnull 
CountdownCloseable countdownCloseable,
-                                                            
ReadableByteChannel stdOut )
-        throws IOException;
+    public abstract void bindEventHandler( @Nonnull EventHandler<Event> 
eventHandler,
+                                           @Nonnull CountdownCloseable 
countdownCloseable,
+                                           ReadableByteChannel stdOut )
+        throws IOException, InterruptedException;
 
     @Nonnull
     protected ForkNodeArguments getArguments()
     {
         return arguments;
     }
+
+    public abstract void disable();
+
+    @Override
+    public abstract void close() throws IOException;
 }
diff --git 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Stoppable.java
similarity index 70%
copy from 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
copy to 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Stoppable.java
index cc2964c..e637b3e 100644
--- 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
+++ 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/Stoppable.java
@@ -19,19 +19,12 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import javax.annotation.Nonnull;
 import java.io.Closeable;
 
 /**
- * The base thread class used to handle a stream, transforms the stream to an 
object.
+ * Used in {@link CloseableDaemonThread}.
  */
-public abstract class CloseableDaemonThread extends Thread implements Closeable
+public interface Stoppable extends Closeable
 {
-    protected CloseableDaemonThread( @Nonnull String threadName )
-    {
-        setName( threadName );
-        setDaemon( true );
-    }
-
-    public abstract void disable();
+    void disable();
 }
diff --git 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/util/CountDownLauncher.java
similarity index 51%
copy from 
maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
copy to 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/util/CountDownLauncher.java
index 2ceaf12..6d0e443 100644
--- 
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/InvalidSessionIdException.java
+++ 
b/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/util/CountDownLauncher.java
@@ -1,4 +1,4 @@
-package org.apache.maven.plugin.surefire.extensions;
+package org.apache.maven.surefire.extensions.util;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,21 +19,36 @@ package org.apache.maven.plugin.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
-
 import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * After the authentication has failed, {@link ForkChannel#connectToClient()} 
throws {@link InvalidSessionIdException}
- * and {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter} 
should close {@link CommandlineExecutor}.
+ * Counts down the calls {@link #countDown()} and the last reaching zero 
executes the {@link #job()}.
  *
+ * @author <a href="mailto:[email protected]";>Tibor Digana (tibor17)</a>
  * @since 3.0.0-M5
  */
-public class InvalidSessionIdException extends IOException
+public abstract class CountDownLauncher
 {
-    public InvalidSessionIdException( String actualSessionId, String 
expectedSessionId )
+    private final AtomicInteger countDown;
+
+    public CountDownLauncher( int count )
+    {
+        if ( count <= 0 )
+        {
+            throw new IllegalStateException( "count=" + count + " should be 
greater than zero" );
+        }
+
+        countDown = new AtomicInteger( count );
+    }
+
+    protected abstract void job() throws IOException, InterruptedException;
+
+    public void countDown() throws IOException, InterruptedException
     {
-        super( "The actual sessionId '" + actualSessionId + "' does not match 
'" + expectedSessionId + "'." );
+        if ( countDown.decrementAndGet() == 0 )
+        {
+            job();
+        }
     }
 }
diff --git 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
 
b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1881.java
similarity index 64%
copy from 
surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
copy to 
surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1881.java
index cc2964c..a2b1ec2 100644
--- 
a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/CloseableDaemonThread.java
+++ 
b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1881.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions;
+package org.apache.maven.surefire.its.jiras;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,19 +19,21 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import javax.annotation.Nonnull;
-import java.io.Closeable;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
+import org.junit.Test;
 
 /**
- * The base thread class used to handle a stream, transforms the stream to an 
object.
+ *
  */
-public abstract class CloseableDaemonThread extends Thread implements Closeable
+@SuppressWarnings( "checkstyle:magicnumber" )
+public class Surefire1881 extends SurefireJUnit4IntegrationTestCase
 {
-    protected CloseableDaemonThread( @Nonnull String threadName )
+
+    @Test( timeout = 30_000L )
+    public void test() throws Exception
     {
-        setName( threadName );
-        setDaemon( true );
+        unpack( "/surefire-1881" )
+            .executeVerify()
+            .assertIntegrationTestSuiteResults( 1, 0, 0, 0 );
     }
-
-    public abstract void disable();
 }
diff --git a/surefire-its/src/test/resources/surefire-1881/pom.xml 
b/surefire-its/src/test/resources/surefire-1881/pom.xml
new file mode 100644
index 0000000..c44fca6
--- /dev/null
+++ b/surefire-its/src/test/resources/surefire-1881/pom.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.maven.plugins.surefire</groupId>
+    <artifactId>surefire-1881</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        
<maven.compiler.source>${java.specification.version}</maven.compiler.source>
+        
<maven.compiler.target>${java.specification.version}</maven.compiler.target>
+        
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <contributors>
+        <contributor>
+            <name>Alexander Kriegisch</name>
+            <roles>
+                <role>author</role>
+            </roles>
+            
<url>https://github.com/kriegaex/Maven_Surefire_PrintToConsoleProblems</url>
+        </contributor>
+    </contributors>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.2.0</version>
+                <configuration>
+                    <forceCreation>true</forceCreation>
+                    <archive>
+                        <manifestEntries>
+                            
<Agent-Class>de.scrum_master.dummy.Agent</Agent-Class>
+                            
<Premain-Class>de.scrum_master.dummy.Agent</Premain-Class>
+                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
+                            
<Can-Retransform-Classes>true</Can-Retransform-Classes>
+                        </manifestEntries>
+                        <addMavenDescriptor>false</addMavenDescriptor>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>${surefire.version}</version>
+                <configuration>
+                    
<argLine>-javaagent:${project.build.directory}/${project.build.finalName}.jar</argLine>
+
+                    <!-- Fix for "[WARNING] Corrupted STDOUT by directly 
writing to native stream in forked JVM" -->
+                    <!-- FIXME:
+                           Using this option is meant to fix "[WARNING] 
Corrupted STDOUT by directly writing to native stream in
+                           forked JVM" and creation of "*-jvmRun1.dumpstream" 
files. Unfortunately, it makes Surefire/Failsafe
+                           freeze if a Java agent prints something to stdOut 
or stdErr. This happens both in M5 and in M6-SNAPSHOT
+                           after both SUREFIRE-1788 and SUREFIRE-1809 have 
been merged in already.
+                         FIXME:
+                           *Not* using this option leads to garbled log output 
when a Java agent writes to both stdOut and stdErr
+                           before/during tests. See comments in class 
de.scrum_master.dummy.Agent.DummyTransformer for examples.
+                         TODO:
+                           If the garbled output would also appear with this 
option activated, cannot be tested at present due to
+                           the Surefire/Failsafe freeze -> re-test after the 
freeze has been fixed.
+                     -->
+                    <forkNode 
implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
+
+                    <!-- TODO:
+                           We have to use this until SUREFIRE-1809 is fixed in 
M6, otherwise there are JPMS problems with
+                           libraries added to the boot class path. -> re-test, 
then remove this option
+                     -->
+                    <useModulePath>false</useModulePath>
+
+                    <!--
+                    <consoleOutputReporter 
implementation="org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter">
+                      &lt;!&ndash; Suppress Surefire/Failsafe output in favour 
of test output only &ndash;&gt;
+                      <disable>true</disable>
+                      <encoding>UTF-8</encoding>
+                    </consoleOutputReporter>
+                    -->
+
+                    <!--
+                    <enableProcessChecker>ping</enableProcessChecker>
+                    
<forkedProcessTimeoutInSeconds>15</forkedProcessTimeoutInSeconds>
+                    -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <goals>
+                            <goal>integration-test</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.1</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git 
a/surefire-its/src/test/resources/surefire-1881/src/main/java/de/scrum_master/dummy/Agent.java
 
b/surefire-its/src/test/resources/surefire-1881/src/main/java/de/scrum_master/dummy/Agent.java
new file mode 100644
index 0000000..3a0c68d
--- /dev/null
+++ 
b/surefire-its/src/test/resources/surefire-1881/src/main/java/de/scrum_master/dummy/Agent.java
@@ -0,0 +1,49 @@
+package de.scrum_master.dummy;
+
+import java.lang.instrument.ClassFileTransformer;
+import java.lang.instrument.Instrumentation;
+import java.security.ProtectionDomain;
+
+public class Agent
+{
+    public static Instrumentation INSTRUMENTATION;
+
+    public static void premain( String commandLineOptions, Instrumentation 
instrumentation )
+    {
+        INSTRUMENTATION = instrumentation;
+        // These console outputs still work in Surefire/Failsafe freeze when 
using option
+        //   <forkNode 
implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
+        System.out.println( "[Agent OUT] Hello world!" );
+        System.err.println( "[Agent ERR] Uh-oh!" );
+        instrumentation.addTransformer( new DummyTransformer(), true );
+    }
+
+    public static class DummyTransformer implements ClassFileTransformer
+    {
+        @Override
+        public byte[] transform( ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] 
classfileBuffer )
+        {
+      /*
+      FIXME: These console outputs make Surefire/Failsafe freeze when using 
option
+        <forkNode 
implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
+
+      FIXME: But when *not* using that option, they mess up console output 
like this (some lines in between deleted):
+        [Transformer OUT] cl[Transformer ERR] className = 
sun/reflect/generics/tree/TypeArgument, class file size = 173
+        assName = sun/reflect/generics/tree/SimpleClassTypeSignature, loader = 
null
+        [Transformer OUT] className = 
java/lang/annotation/Retention[Transformer ERR] className = 
sun/reflect/generics/visitor/Reifier, class file size = 8341
+        Policy, loader = null
+        [Transformer OUT] className = 
org/junit/runner/notification/SynchronizedRunListener, loader[Transformer ERR] 
className = com/sun/proxy/$Proxy0, class file size = 2268
+         = jdk.internal.loader.ClassLoaders$AppClassLoader@5451c3a8
+        [T[Transformer ERR] className = 
org/junit/runners/BlockJUnit4ClassRunner, class file size = 14072
+        ransformer OUT] className = org/junit/runners/ParentRunner, loader = 
jdk.internal.loader.ClassLoaders$AppClassLoader@5451c3a8
+        Transformer ERR] className = 
org/junit/validator/AnnotationsValidator$FieldValidator, class file size = 2179
+        [[Transformer OUT] className = 
org/junit/validator/AnnotationsValidator$FieldValidator, loader = 
jdk.internal.loader.ClassLoaders$AppClassLoader@5451c3a8
+      */
+            System.out.println( "[Transformer OUT] className = " + className + 
", loader = " + loader );
+            System.err.println( "[Transformer ERR] className = " + className + 
", class file size = "
+                + classfileBuffer.length );
+            return null;
+        }
+    }
+}
+
diff --git 
a/surefire-its/src/test/resources/surefire-1881/src/test/java/de/scrum_master/dummy/AgentIT.java
 
b/surefire-its/src/test/resources/surefire-1881/src/test/java/de/scrum_master/dummy/AgentIT.java
new file mode 100644
index 0000000..58ee14f
--- /dev/null
+++ 
b/surefire-its/src/test/resources/surefire-1881/src/test/java/de/scrum_master/dummy/AgentIT.java
@@ -0,0 +1,23 @@
+package de.scrum_master.dummy;
+
+import org.junit.Test;
+
+import java.io.PrintStream;
+import java.net.URL;
+
+public class AgentIT
+{
+    @Test
+    public void test() throws Exception
+    {
+        System.out.println( "[Test OUT] Before manual JRE bootstrap class 
retransformation" );
+        System.err.println( "[Test ERR] Before manual JRE bootstrap class 
retransformation" );
+        // Just for fun, manually apply dummy class transformation to some JRE 
bootstrap classes.
+        // This is unrelated to the Surefire issue, but produces a few more 
log lines with
+        // 'loader = null', signifying the boot loader.
+        Agent.INSTRUMENTATION.retransformClasses( AgentIT.class, String.class, 
URL.class, PrintStream.class,
+            Integer.class );
+        System.out.println( "[Test OUT] After manual JRE bootstrap class 
retransformation" );
+        System.err.println( "[Test ERR] After manual JRE bootstrap class 
retransformation" );
+    }
+}

Reply via email to