View Javadoc
1   package org.apache.maven.plugin.surefire.extensions;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
23  import org.apache.maven.surefire.api.booter.Command;
24  import org.apache.maven.surefire.api.booter.MasterProcessCommand;
25  import org.apache.maven.surefire.extensions.CloseableDaemonThread;
26  import org.apache.maven.surefire.extensions.CommandReader;
27  import org.apache.maven.surefire.api.util.internal.ImmutableMap;
28  
29  import javax.annotation.Nonnull;
30  import java.io.IOException;
31  import java.nio.ByteBuffer;
32  import java.nio.channels.ClosedChannelException;
33  import java.nio.channels.NonWritableChannelException;
34  import java.nio.channels.WritableByteChannel;
35  import java.util.HashMap;
36  import java.util.Map;
37  
38  import static java.nio.charset.StandardCharsets.US_ASCII;
39  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
40  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.MAGIC_NUMBER;
41  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
42  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
43  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
44  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
45  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
46  
47  /**
48   * Commands which are sent from plugin to the forked jvm.
49   * <br>
50   *     <br>
51   * magic number : opcode [: opcode specific data]*
52   * <br>
53   *     or data encoded with Base64
54   * <br>
55   * magic number : opcode [: Base64(opcode specific data)]*
56   *
57   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
58   * @since 3.0.0-M5
59   */
60  public class StreamFeeder extends CloseableDaemonThread
61  {
62      private static final Map<MasterProcessCommand, String> COMMAND_OPCODES = opcodesToStrings();
63  
64      private final WritableByteChannel channel;
65      private final CommandReader commandReader;
66      private final ConsoleLogger logger;
67  
68      private volatile boolean disabled;
69      private volatile Throwable exception;
70  
71      public StreamFeeder( @Nonnull String threadName, @Nonnull WritableByteChannel channel,
72                           @Nonnull CommandReader commandReader, @Nonnull ConsoleLogger logger )
73      {
74          super( threadName );
75          this.channel = channel;
76          this.commandReader = commandReader;
77          this.logger = logger;
78      }
79  
80      @Override
81      @SuppressWarnings( "checkstyle:innerassignment" )
82      public void run()
83      {
84          try ( WritableByteChannel c = channel )
85          {
86              for ( Command cmd; ( cmd = commandReader.readNextCommand() ) != null; )
87              {
88                  if ( !disabled )
89                  {
90                      MasterProcessCommand cmdType = cmd.getCommandType();
91                      byte[] data = cmdType.hasDataType() ? encode( cmdType, cmd.getData() ) : encode( cmdType );
92                      c.write( ByteBuffer.wrap( data ) );
93                  }
94              }
95          }
96          catch ( ClosedChannelException e )
97          {
98              // closed externally
99          }
100         catch ( IOException | NonWritableChannelException e )
101         {
102             exception = e.getCause() == null ? e : e.getCause();
103         }
104         catch ( IllegalArgumentException e )
105         {
106             logger.error( e.getLocalizedMessage() );
107         }
108     }
109 
110     public void disable()
111     {
112         disabled = true;
113     }
114 
115     public Throwable getException()
116     {
117         return exception;
118     }
119 
120     @Override
121     public void close() throws IOException
122     {
123         channel.close();
124     }
125 
126     /**
127      * Public method for testing purposes.
128      *
129      * @param cmdType command type
130      * @param data data to encode
131      * @return command with data encoded to bytes
132      */
133     public static byte[] encode( MasterProcessCommand cmdType, String data )
134     {
135         if ( !cmdType.hasDataType() )
136         {
137             throw new IllegalArgumentException( "cannot use data without data type" );
138         }
139 
140         if ( cmdType.getDataType() != String.class )
141         {
142             throw new IllegalArgumentException( "Data type can be only " + String.class );
143         }
144 
145         return encode( COMMAND_OPCODES.get( cmdType ), data )
146             .toString()
147             .getBytes( US_ASCII );
148     }
149 
150     /**
151      * Public method for testing purposes.
152      *
153      * @param cmdType command type
154      * @return command without data encoded to bytes
155      */
156     public static byte[] encode( MasterProcessCommand cmdType )
157     {
158         if ( cmdType.getDataType() != Void.class )
159         {
160             throw new IllegalArgumentException( "Data type can be only " + cmdType.getDataType() );
161         }
162 
163         return encode( COMMAND_OPCODES.get( cmdType ), null )
164             .toString()
165             .getBytes( US_ASCII );
166     }
167 
168     /**
169      * Encodes opcode and data.
170      *
171      * @param operation opcode
172      * @param data   data
173      * @return encoded command
174      */
175     private static StringBuilder encode( String operation, String data )
176     {
177         StringBuilder s = new StringBuilder( 128 )
178             .append( ':' )
179             .append( MAGIC_NUMBER )
180             .append( ':' )
181             .append( operation );
182 
183         if ( data != null )
184         {
185             s.append( ':' )
186                 .append( data );
187         }
188 
189         return s.append( ':' );
190     }
191 
192     private static Map<MasterProcessCommand, String> opcodesToStrings()
193     {
194         Map<MasterProcessCommand, String> opcodes = new HashMap<>();
195         opcodes.put( RUN_CLASS, "run-testclass" );
196         opcodes.put( TEST_SET_FINISHED, "testset-finished" );
197         opcodes.put( SKIP_SINCE_NEXT_TEST, "skip-since-next-test" );
198         opcodes.put( SHUTDOWN, "shutdown" );
199         opcodes.put( NOOP, "noop" );
200         opcodes.put( BYE_ACK, "bye-ack" );
201         return new ImmutableMap<>( opcodes );
202     }
203 }