View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.booter.stream;
20  
21  import javax.annotation.Nonnull;
22  
23  import java.io.BufferedOutputStream;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.nio.channels.ReadableByteChannel;
30  import java.util.concurrent.FutureTask;
31  
32  import org.apache.maven.surefire.api.booter.Command;
33  import org.apache.maven.surefire.api.booter.MasterProcessCommand;
34  import org.apache.maven.surefire.api.booter.Shutdown;
35  import org.apache.maven.surefire.api.fork.ForkNodeArguments;
36  import org.apache.maven.surefire.api.stream.AbstractStreamDecoder;
37  import org.apache.maven.surefire.api.stream.MalformedChannelException;
38  import org.apache.maven.surefire.api.stream.SegmentType;
39  
40  import static org.apache.maven.surefire.api.booter.Command.BYE_ACK;
41  import static org.apache.maven.surefire.api.booter.Command.NOOP;
42  import static org.apache.maven.surefire.api.booter.Command.SKIP_SINCE_NEXT_TEST;
43  import static org.apache.maven.surefire.api.booter.Command.TEST_SET_FINISHED;
44  import static org.apache.maven.surefire.api.booter.Command.toRunClass;
45  import static org.apache.maven.surefire.api.booter.Command.toShutdown;
46  import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_COMMANDS_BYTES;
47  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.COMMAND_TYPES;
48  import static org.apache.maven.surefire.api.stream.SegmentType.DATA_STRING;
49  import static org.apache.maven.surefire.api.stream.SegmentType.END_OF_FRAME;
50  import static org.apache.maven.surefire.api.stream.SegmentType.STRING_ENCODING;
51  import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
52  
53  public class CommandDecoder extends AbstractStreamDecoder<Command, MasterProcessCommand, SegmentType> {
54      private static final int DEBUG_SINK_BUFFER_SIZE = 64 * 1024;
55      private static final int NO_POSITION = -1;
56  
57      private static final SegmentType[] COMMAND_WITHOUT_DATA = new SegmentType[] {END_OF_FRAME};
58  
59      private static final SegmentType[] COMMAND_WITH_ONE_STRING =
60              new SegmentType[] {STRING_ENCODING, DATA_STRING, END_OF_FRAME};
61  
62      private final ForkNodeArguments arguments;
63      private final OutputStream debugSink;
64  
65      public CommandDecoder(@Nonnull ReadableByteChannel channel, @Nonnull ForkNodeArguments arguments) {
66          super(channel, arguments, COMMAND_TYPES);
67          this.arguments = arguments;
68          debugSink = newDebugSink();
69      }
70  
71      @Override
72      public Command decode(@Nonnull Memento memento) throws IOException, MalformedChannelException {
73          try {
74              MasterProcessCommand commandType = readMessageType(memento);
75              if (commandType == null) {
76                  throw new MalformedFrameException(
77                          memento.getLine().getPositionByteBuffer(),
78                          memento.getByteBuffer().position());
79              }
80  
81              for (SegmentType segmentType : nextSegmentType(commandType)) {
82                  switch (segmentType) {
83                      case STRING_ENCODING:
84                          memento.setCharset(readCharset(memento));
85                          break;
86                      case DATA_STRING:
87                          memento.getData().add(readString(memento));
88                          break;
89                      case DATA_INTEGER:
90                          memento.getData().add(readInteger(memento));
91                          break;
92                      case END_OF_FRAME:
93                          memento.getLine()
94                                  .setPositionByteBuffer(memento.getByteBuffer().position());
95                          memento.getLine().clear();
96                          return toMessage(commandType, memento);
97                      default:
98                          memento.getLine().setPositionByteBuffer(NO_POSITION);
99                          arguments.dumpStreamText(
100                                 "Unknown enum (" + SegmentType.class.getSimpleName() + ") " + segmentType);
101                 }
102             }
103         } catch (MalformedFrameException e) {
104             if (e.hasValidPositions()) {
105                 int length = e.readTo() - e.readFrom();
106                 memento.getLine().write(memento.getByteBuffer(), e.readFrom(), length);
107             }
108             return null;
109         } catch (RuntimeException e) {
110             getArguments().dumpStreamException(e);
111             return null;
112         } catch (IOException e) {
113             if (!(e.getCause() instanceof InterruptedException)) {
114                 printRemainingStream(memento);
115             }
116             throw e;
117         } finally {
118             memento.reset();
119         }
120 
121         throw new MalformedChannelException();
122     }
123 
124     @Nonnull
125     @Override
126     protected final byte[] getEncodedMagicNumber() {
127         return MAGIC_NUMBER_FOR_COMMANDS_BYTES;
128     }
129 
130     @Nonnull
131     @Override
132     protected SegmentType[] nextSegmentType(@Nonnull MasterProcessCommand commandType) {
133         switch (commandType) {
134             case NOOP:
135             case BYE_ACK:
136             case SKIP_SINCE_NEXT_TEST:
137             case TEST_SET_FINISHED:
138                 return COMMAND_WITHOUT_DATA;
139             case RUN_CLASS:
140             case SHUTDOWN:
141                 return COMMAND_WITH_ONE_STRING;
142             default:
143                 throw new IllegalArgumentException("Unknown enum " + commandType);
144         }
145     }
146 
147     @Nonnull
148     @Override
149     protected Command toMessage(@Nonnull MasterProcessCommand commandType, @Nonnull Memento memento)
150             throws MalformedFrameException {
151         switch (commandType) {
152             case NOOP:
153                 checkArguments(memento, 0);
154                 return NOOP;
155             case BYE_ACK:
156                 checkArguments(memento, 0);
157                 return BYE_ACK;
158             case SKIP_SINCE_NEXT_TEST:
159                 checkArguments(memento, 0);
160                 return SKIP_SINCE_NEXT_TEST;
161             case TEST_SET_FINISHED:
162                 checkArguments(memento, 0);
163                 return TEST_SET_FINISHED;
164             case RUN_CLASS:
165                 checkArguments(memento, 1);
166                 return toRunClass((String) memento.getData().get(0));
167             case SHUTDOWN:
168                 checkArguments(memento, 1);
169                 return toShutdown(
170                         Shutdown.parameterOf((String) memento.getData().get(0)));
171             default:
172                 throw new IllegalArgumentException("Missing a branch for the event type " + commandType);
173         }
174     }
175 
176     @Override
177     protected void debugStream(byte[] array, int position, int remaining) {
178         if (debugSink == null) {
179             return;
180         }
181 
182         try {
183             debugSink.write(array, position, remaining);
184         } catch (IOException e) {
185             // logger file was deleted
186             // System.out is already used by the stream in this decoder
187         }
188     }
189 
190     private OutputStream newDebugSink() {
191         final File sink = arguments.getCommandStreamBinaryFile();
192         if (sink == null) {
193             return null;
194         }
195 
196         try {
197             OutputStream fos = new FileOutputStream(sink, true);
198             final OutputStream os = new BufferedOutputStream(fos, DEBUG_SINK_BUFFER_SIZE);
199             addShutDownHook(new Thread(new FutureTask<>(() -> {
200                 os.close();
201                 return null;
202             })));
203             return os;
204         } catch (FileNotFoundException e) {
205             return null;
206         }
207     }
208 
209     @Override
210     public void close() throws IOException {
211         if (debugSink != null) {
212             debugSink.close();
213         }
214     }
215 }