View Javadoc
1   package org.apache.maven.surefire.booter;
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.surefire.providerapi.ProviderParameters;
23  import org.apache.maven.surefire.providerapi.SurefireProvider;
24  import org.apache.maven.surefire.report.LegacyPojoStackTraceWriter;
25  import org.apache.maven.surefire.report.StackTraceWriter;
26  import org.apache.maven.surefire.suite.RunResult;
27  import org.apache.maven.surefire.testset.TestSetFailedException;
28  
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.FileNotFoundException;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.PrintStream;
35  import java.lang.management.ManagementFactory;
36  import java.lang.reflect.InvocationTargetException;
37  import java.security.AccessControlException;
38  import java.security.AccessController;
39  import java.security.PrivilegedAction;
40  import java.util.concurrent.ScheduledExecutorService;
41  import java.util.concurrent.ScheduledThreadPoolExecutor;
42  import java.util.concurrent.Semaphore;
43  import java.util.concurrent.ThreadFactory;
44  import java.util.concurrent.atomic.AtomicBoolean;
45  
46  import static java.lang.Math.max;
47  import static java.lang.Thread.currentThread;
48  import static java.util.concurrent.TimeUnit.MILLISECONDS;
49  import static java.util.concurrent.TimeUnit.SECONDS;
50  import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_BYE;
51  import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_ERROR;
52  import static org.apache.maven.surefire.booter.ForkingRunListener.encode;
53  import static org.apache.maven.surefire.booter.SystemPropertyManager.setSystemProperties;
54  import static org.apache.maven.surefire.util.ReflectionUtils.instantiateOneArg;
55  import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
56  import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
57  
58  /**
59   * The part of the booter that is unique to a forked vm.
60   * <br>
61   * Deals with deserialization of the booter wire-level protocol
62   * <br>
63   *
64   * @author Jason van Zyl
65   * @author Emmanuel Venisse
66   * @author Kristian Rosenvold
67   */
68  public final class ForkedBooter
69  {
70      private static final long DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS = 30L;
71      private static final long PING_TIMEOUT_IN_SECONDS = 30L;
72      private static final long ONE_SECOND_IN_MILLIS = 1000L;
73  
74      private final CommandReader commandReader = CommandReader.getReader();
75      private final PrintStream originalOut = System.out;
76  
77      private volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS;
78      private volatile PingScheduler pingScheduler;
79  
80      private ScheduledThreadPoolExecutor jvmTerminator;
81      private ProviderConfiguration providerConfiguration;
82      private StartupConfiguration startupConfiguration;
83      private Object testSet;
84  
85      private void setupBooter( String tmpDir, String dumpFileName, String surefirePropsFileName,
86                                String effectiveSystemPropertiesFileName )
87              throws IOException, SurefireExecutionException
88      {
89          BooterDeserializer booterDeserializer =
90                  new BooterDeserializer( createSurefirePropertiesIfFileExists( tmpDir, surefirePropsFileName ) );
91          // todo: print PID in debug console logger in version 2.20.2
92          pingScheduler = isDebugging() ? null : listenToShutdownCommands( booterDeserializer.getPluginPid() );
93          setSystemProperties( new File( tmpDir, effectiveSystemPropertiesFileName ) );
94  
95          providerConfiguration = booterDeserializer.deserialize();
96          DumpErrorSingleton.getSingleton().init( dumpFileName, providerConfiguration.getReporterConfiguration() );
97  
98          startupConfiguration = booterDeserializer.getProviderConfiguration();
99          systemExitTimeoutInSeconds =
100                 providerConfiguration.systemExitTimeout( DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS );
101 
102         ClasspathConfiguration classpathConfiguration = startupConfiguration.getClasspathConfiguration();
103         if ( startupConfiguration.isManifestOnlyJarRequestedAndUsable() )
104         {
105             classpathConfiguration.trickClassPathWhenManifestOnlyClasspath();
106         }
107 
108         ClassLoader classLoader = currentThread().getContextClassLoader();
109         classLoader.setDefaultAssertionStatus( classpathConfiguration.isEnableAssertions() );
110         startupConfiguration.writeSurefireTestClasspathProperty();
111         testSet = createTestSet( providerConfiguration.getTestForFork(),
112                                        providerConfiguration.isReadTestsFromInStream(), classLoader );
113     }
114 
115     private void execute()
116     {
117         try
118         {
119             runSuitesInProcess();
120         }
121         catch ( InvocationTargetException t )
122         {
123             DumpErrorSingleton.getSingleton().dumpException( t );
124             StackTraceWriter stackTraceWriter =
125                     new LegacyPojoStackTraceWriter( "test subsystem", "no method", t.getTargetException() );
126             StringBuilder stringBuilder = new StringBuilder();
127             encode( stringBuilder, stackTraceWriter, false );
128             encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n" );
129         }
130         catch ( Throwable t )
131         {
132             DumpErrorSingleton.getSingleton().dumpException( t );
133             StackTraceWriter stackTraceWriter = new LegacyPojoStackTraceWriter( "test subsystem", "no method", t );
134             StringBuilder stringBuilder = new StringBuilder();
135             encode( stringBuilder, stackTraceWriter, false );
136             encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n" );
137         }
138         acknowledgedExit();
139     }
140 
141     private Object createTestSet( TypeEncodedValue forkedTestSet, boolean readTestsFromCommandReader, ClassLoader cl )
142     {
143         if ( forkedTestSet != null )
144         {
145             return forkedTestSet.getDecodedValue( cl );
146         }
147         else if ( readTestsFromCommandReader )
148         {
149             return new LazyTestsToRun( originalOut );
150         }
151         return null;
152     }
153 
154     private void cancelPingScheduler()
155     {
156         if ( pingScheduler != null )
157         {
158             try
159             {
160                 AccessController.doPrivileged( new PrivilegedAction<Object>()
161                                                {
162                                                    @Override
163                                                    public Object run()
164                                                    {
165                                                        pingScheduler.shutdown();
166                                                        return null;
167                                                    }
168                                                }
169                 );
170             }
171             catch ( AccessControlException e )
172             {
173                 // ignore
174             }
175         }
176     }
177 
178     private PingScheduler listenToShutdownCommands( Long pluginPid )
179     {
180         commandReader.addShutdownListener( createExitHandler() );
181         AtomicBoolean pingDone = new AtomicBoolean( true );
182         commandReader.addNoopListener( createPingHandler( pingDone ) );
183 
184         PingScheduler pingMechanisms = new PingScheduler( createPingScheduler(),
185                                                           pluginPid == null ? null : new PpidChecker( pluginPid ) );
186         if ( pingMechanisms.pluginProcessChecker != null )
187         {
188             Runnable checkerJob = processCheckerJob( pingMechanisms );
189             pingMechanisms.pingScheduler.scheduleWithFixedDelay( checkerJob, 0L, 1L, SECONDS );
190         }
191         Runnable pingJob = createPingJob( pingDone, pingMechanisms.pluginProcessChecker );
192         pingMechanisms.pingScheduler.scheduleAtFixedRate( pingJob, 0L, PING_TIMEOUT_IN_SECONDS, SECONDS );
193 
194         return pingMechanisms;
195     }
196 
197     private Runnable processCheckerJob( final PingScheduler pingMechanism )
198     {
199         return new Runnable()
200         {
201             @Override
202             public void run()
203             {
204                 try
205                 {
206                     if ( pingMechanism.pluginProcessChecker.canUse()
207                                  && !pingMechanism.pluginProcessChecker.isProcessAlive()
208                                  && !pingMechanism.pingScheduler.isShutdown() )
209                     {
210                         DumpErrorSingleton.getSingleton().dumpText( "Killing self fork JVM. Maven process died." );
211                         kill();
212                     }
213                 }
214                 catch ( Exception e )
215                 {
216                     DumpErrorSingleton.getSingleton()
217                             .dumpText( "System.exit() or native command error interrupted process checker." );
218                 }
219             }
220         };
221     }
222 
223     private CommandListener createPingHandler( final AtomicBoolean pingDone )
224     {
225         return new CommandListener()
226         {
227             @Override
228             public void update( Command command )
229             {
230                 pingDone.set( true );
231             }
232         };
233     }
234 
235     private CommandListener createExitHandler()
236     {
237         return new CommandListener()
238         {
239             @Override
240             public void update( Command command )
241             {
242                 Shutdown shutdown = command.toShutdownData();
243                 if ( shutdown.isKill() )
244                 {
245                     DumpErrorSingleton.getSingleton()
246                             .dumpText( "Killing self fork JVM. Received SHUTDOWN command from Maven shutdown hook." );
247                     kill();
248                 }
249                 else if ( shutdown.isExit() )
250                 {
251                     cancelPingScheduler();
252                     DumpErrorSingleton.getSingleton()
253                             .dumpText( "Exiting self fork JVM. Received SHUTDOWN command from Maven shutdown hook." );
254                     exit( 1 );
255                 }
256                 // else refers to shutdown=testset, but not used now, keeping reader open
257             }
258         };
259     }
260 
261     private Runnable createPingJob( final AtomicBoolean pingDone, final PpidChecker pluginProcessChecker  )
262     {
263         return new Runnable()
264         {
265             @Override
266             public void run()
267             {
268                 if ( !canUseNewPingMechanism( pluginProcessChecker ) )
269                 {
270                     boolean hasPing = pingDone.getAndSet( false );
271                     if ( !hasPing )
272                     {
273                         DumpErrorSingleton.getSingleton().dumpText( "Killing self fork JVM. PING timeout elapsed." );
274                         kill();
275                     }
276                 }
277             }
278         };
279     }
280 
281     private void encodeAndWriteToOutput( String string )
282     {
283         byte[] encodeBytes = encodeStringForForkCommunication( string );
284         //noinspection SynchronizationOnLocalVariableOrMethodParameter
285         synchronized ( originalOut )
286         {
287             originalOut.write( encodeBytes, 0, encodeBytes.length );
288             originalOut.flush();
289         }
290     }
291 
292     private void kill()
293     {
294         kill( 1 );
295     }
296 
297     private void kill( int returnCode )
298     {
299         commandReader.stop();
300         Runtime.getRuntime().halt( returnCode );
301     }
302 
303     private void exit( int returnCode )
304     {
305         launchLastDitchDaemonShutdownThread( returnCode );
306         System.exit( returnCode );
307     }
308 
309     private void acknowledgedExit()
310     {
311         final Semaphore barrier = new Semaphore( 0 );
312         commandReader.addByeAckListener( new CommandListener()
313                                           {
314                                               @Override
315                                               public void update( Command command )
316                                               {
317                                                   barrier.release();
318                                               }
319                                           }
320         );
321         encodeAndWriteToOutput( ( (char) BOOTERCODE_BYE ) + ",0,BYE!\n" );
322         launchLastDitchDaemonShutdownThread( 0 );
323         long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS );
324         acquireOnePermit( barrier, timeoutMillis );
325         cancelPingScheduler();
326         commandReader.stop();
327         System.exit( 0 );
328     }
329 
330     private RunResult runSuitesInProcess()
331         throws SurefireExecutionException, TestSetFailedException, InvocationTargetException
332     {
333         ForkingReporterFactory factory = createForkingReporterFactory();
334         return invokeProviderInSameClassLoader( factory );
335     }
336 
337     private ForkingReporterFactory createForkingReporterFactory()
338     {
339         final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
340         return new ForkingReporterFactory( trimStackTrace, originalOut );
341     }
342 
343     private synchronized ScheduledThreadPoolExecutor getJvmTerminator()
344     {
345         if ( jvmTerminator == null )
346         {
347             ThreadFactory threadFactory =
348                     newDaemonThreadFactory( "last-ditch-daemon-shutdown-thread-" + systemExitTimeoutInSeconds + "s" );
349             jvmTerminator = new ScheduledThreadPoolExecutor( 1, threadFactory );
350             jvmTerminator.setMaximumPoolSize( 1 );
351         }
352         return jvmTerminator;
353     }
354 
355     @SuppressWarnings( "checkstyle:emptyblock" )
356     private void launchLastDitchDaemonShutdownThread( final int returnCode )
357     {
358         getJvmTerminator().schedule( new Runnable()
359                                         {
360                                             @Override
361                                             public void run()
362                                             {
363                                                 kill( returnCode );
364                                             }
365                                         }, systemExitTimeoutInSeconds, SECONDS
366         );
367     }
368 
369     private RunResult invokeProviderInSameClassLoader( ForkingReporterFactory factory )
370         throws TestSetFailedException, InvocationTargetException
371     {
372         return createProviderInCurrentClassloader( factory )
373                        .invoke( testSet );
374     }
375 
376     private SurefireProvider createProviderInCurrentClassloader( ForkingReporterFactory reporterManagerFactory )
377     {
378         BaseProviderFactory bpf = new BaseProviderFactory( reporterManagerFactory, true );
379         bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
380         bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
381         ClassLoader classLoader = currentThread().getContextClassLoader();
382         bpf.setClassLoaders( classLoader );
383         bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
384         bpf.setProviderProperties( providerConfiguration.getProviderProperties() );
385         bpf.setRunOrderParameters( providerConfiguration.getRunOrderParameters() );
386         bpf.setDirectoryScannerParameters( providerConfiguration.getDirScannerParams() );
387         bpf.setMainCliOptions( providerConfiguration.getMainCliOptions() );
388         bpf.setSkipAfterFailureCount( providerConfiguration.getSkipAfterFailureCount() );
389         bpf.setShutdown( providerConfiguration.getShutdown() );
390         bpf.setSystemExitTimeout( providerConfiguration.getSystemExitTimeout() );
391         String providerClass = startupConfiguration.getActualClassName();
392         return (SurefireProvider) instantiateOneArg( classLoader, providerClass, ProviderParameters.class, bpf );
393     }
394 
395     /**
396      * This method is invoked when Surefire is forked - this method parses and organizes the arguments passed to it and
397      * then calls the Surefire class' run method. <br> The system exit code will be 1 if an exception is thrown.
398      *
399      * @param args Commandline arguments
400      */
401     public static void main( String... args )
402     {
403         ForkedBooter booter = new ForkedBooter();
404         try
405         {
406             booter.setupBooter( args[0], args[1], args[2], args.length > 3 ? args[3] : null );
407             booter.execute();
408         }
409         catch ( Throwable t )
410         {
411             DumpErrorSingleton.getSingleton().dumpException( t );
412             t.printStackTrace();
413             booter.cancelPingScheduler();
414             booter.exit( 1 );
415         }
416     }
417 
418     private static boolean canUseNewPingMechanism( PpidChecker pluginProcessChecker )
419     {
420         return pluginProcessChecker != null && pluginProcessChecker.canUse();
421     }
422 
423     private static boolean acquireOnePermit( Semaphore barrier, long timeoutMillis )
424     {
425         try
426         {
427             return barrier.tryAcquire( timeoutMillis, MILLISECONDS );
428         }
429         catch ( InterruptedException e )
430         {
431             return true;
432         }
433     }
434 
435     private static ScheduledExecutorService createPingScheduler()
436     {
437         ThreadFactory threadFactory = newDaemonThreadFactory( "ping-" + PING_TIMEOUT_IN_SECONDS + "s" );
438         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor( 1, threadFactory );
439         executor.setKeepAliveTime( 3L, SECONDS );
440         executor.setMaximumPoolSize( 2 );
441         return executor;
442     }
443 
444     private static InputStream createSurefirePropertiesIfFileExists( String tmpDir, String propFileName )
445             throws FileNotFoundException
446     {
447         File surefirePropertiesFile = new File( tmpDir, propFileName );
448         return surefirePropertiesFile.exists() ? new FileInputStream( surefirePropertiesFile ) : null;
449     }
450 
451     private static boolean isDebugging()
452     {
453         for ( String argument : ManagementFactory.getRuntimeMXBean().getInputArguments() )
454         {
455             if ( "-Xdebug".equals( argument ) || argument.startsWith( "-agentlib:jdwp" ) )
456             {
457                 return true;
458             }
459         }
460         return false;
461     }
462 
463     private static class PingScheduler
464     {
465         private final ScheduledExecutorService pingScheduler;
466         private final PpidChecker pluginProcessChecker;
467 
468         PingScheduler( ScheduledExecutorService pingScheduler, PpidChecker pluginProcessChecker )
469         {
470             this.pingScheduler = pingScheduler;
471             this.pluginProcessChecker = pluginProcessChecker;
472         }
473 
474         void shutdown()
475         {
476             pingScheduler.shutdown();
477             if ( pluginProcessChecker != null )
478             {
479                 pluginProcessChecker.destroyActiveCommands();
480             }
481         }
482 
483         boolean isShutdown()
484         {
485             return pingScheduler.isShutdown();
486         }
487     }
488 }