| package org.apache.maven.plugin.surefire.extensions; |
| |
| /* |
| * 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. |
| */ |
| |
| import org.apache.maven.plugin.surefire.booterclient.output.NativeStdOutStreamConsumer; |
| import org.apache.maven.surefire.api.event.Event; |
| 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.extensions.ForkNodeArguments; |
| import org.apache.maven.surefire.extensions.util.CountdownCloseable; |
| import org.apache.maven.surefire.extensions.util.LineConsumerThread; |
| |
| import javax.annotation.Nonnull; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.net.Inet4Address; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.SocketOption; |
| import java.nio.channels.AsynchronousServerSocketChannel; |
| import java.nio.channels.AsynchronousSocketChannel; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.channels.WritableByteChannel; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| import static java.net.StandardSocketOptions.SO_KEEPALIVE; |
| import static java.net.StandardSocketOptions.SO_REUSEADDR; |
| import static java.net.StandardSocketOptions.TCP_NODELAY; |
| import static java.nio.channels.AsynchronousChannelGroup.withThreadPool; |
| import static java.nio.channels.AsynchronousServerSocketChannel.open; |
| import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel; |
| 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; |
| |
| /** |
| * 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 |
| * {@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 |
| * objects to the {@link EventHandler event handler}. |
| * <br> |
| * The objects {@link WritableByteChannel} and {@link ReadableByteChannel} are forked process streams |
| * (standard input and output). Both are ignored in this implementation but they are used in {@link LegacyForkChannel}. |
| * <br> |
| * The channel is closed after the forked JVM has finished normally or the shutdown hook is executed in the plugin. |
| */ |
| final class SurefireForkChannel extends ForkChannel |
| { |
| private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool( newDaemonThreadFactory() ); |
| |
| private final AsynchronousServerSocketChannel server; |
| private final String localHost; |
| private final int localPort; |
| private volatile AsynchronousSocketChannel worker; |
| private volatile LineConsumerThread out; |
| |
| SurefireForkChannel( @Nonnull ForkNodeArguments arguments ) throws IOException |
| { |
| super( arguments ); |
| server = open( withThreadPool( THREAD_POOL ) ); |
| setTrueOptions( SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE ); |
| InetAddress ip = Inet4Address.getLoopbackAddress(); |
| server.bind( new InetSocketAddress( ip, 0 ), 1 ); |
| InetSocketAddress localAddress = (InetSocketAddress) server.getLocalAddress(); |
| localHost = localAddress.getHostString(); |
| localPort = localAddress.getPort(); |
| } |
| |
| @Override |
| public void connectToClient() throws IOException |
| { |
| if ( worker != null ) |
| { |
| throw new IllegalStateException( "already accepted TCP client connection" ); |
| } |
| |
| try |
| { |
| worker = server.accept().get(); |
| } |
| catch ( InterruptedException e ) |
| { |
| throw new IOException( e.getLocalizedMessage(), e ); |
| } |
| catch ( ExecutionException e ) |
| { |
| throw new IOException( e.getLocalizedMessage(), e.getCause() ); |
| } |
| } |
| |
| @SafeVarargs |
| private final void setTrueOptions( SocketOption<Boolean>... options ) |
| throws IOException |
| { |
| for ( SocketOption<Boolean> option : options ) |
| { |
| if ( server.supportedOptions().contains( option ) ) |
| { |
| server.setOption( option, true ); |
| } |
| } |
| } |
| |
| @Override |
| public String getForkNodeConnectionString() |
| { |
| return "tcp://" + localHost + ":" + localPort; |
| } |
| |
| @Override |
| public int getCountdownCloseablePermits() |
| { |
| return 3; |
| } |
| |
| @Override |
| public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader commands, |
| WritableByteChannel stdIn ) |
| { |
| // 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() ); |
| } |
| |
| @Override |
| public CloseableDaemonThread bindEventHandler( @Nonnull EventHandler<Event> eventHandler, |
| @Nonnull CountdownCloseable countdownCloseable, |
| ReadableByteChannel stdOut ) |
| { |
| out = new LineConsumerThread( "fork-" + getArguments().getForkChannelId() + "-out-thread", stdOut, |
| new NativeStdOutStreamConsumer( getArguments().getConsoleLogger() ), countdownCloseable ); |
| out.start(); |
| |
| ReadableByteChannel channel = newBufferedChannel( newInputStream( worker ) ); |
| return new EventConsumerThread( "fork-" + getArguments().getForkChannelId() + "-event-thread", channel, |
| eventHandler, countdownCloseable, getArguments() ); |
| } |
| |
| @Override |
| public void close() throws IOException |
| { |
| //noinspection unused,EmptyTryBlock,EmptyTryBlock |
| try ( Closeable c1 = worker; Closeable c2 = server; Closeable c3 = out ) |
| { |
| // only close all channels |
| } |
| } |
| } |