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.plugin.surefire.log.api.ConsoleLogger;
23  import org.apache.maven.surefire.providerapi.ProviderParameters;
24  import org.apache.maven.surefire.providerapi.SurefireProvider;
25  import org.apache.maven.surefire.report.LegacyPojoStackTraceWriter;
26  import org.apache.maven.surefire.testset.TestSetFailedException;
27  
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.FileNotFoundException;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.lang.management.ManagementFactory;
34  import java.lang.management.ThreadInfo;
35  import java.lang.management.ThreadMXBean;
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.ProcessCheckerType.ALL;
51  import static org.apache.maven.surefire.booter.ProcessCheckerType.NATIVE;
52  import static org.apache.maven.surefire.booter.ProcessCheckerType.PING;
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.NL;
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 = 1_000L;
73      private static final String LAST_DITCH_SHUTDOWN_THREAD = "surefire-forkedjvm-last-ditch-daemon-shutdown-thread-";
74      private static final String PING_THREAD = "surefire-forkedjvm-ping-";
75  
76      private final CommandReader commandReader = CommandReader.getReader();
77      private final ForkedChannelEncoder eventChannel = new ForkedChannelEncoder( System.out );
78      private final Semaphore exitBarrier = new Semaphore( 0 );
79  
80      private volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS;
81      private volatile PingScheduler pingScheduler;
82  
83      private ScheduledThreadPoolExecutor jvmTerminator;
84      private ProviderConfiguration providerConfiguration;
85      private ForkingReporterFactory forkingReporterFactory;
86      private StartupConfiguration startupConfiguration;
87      private Object testSet;
88  
89      private void setupBooter( String tmpDir, String dumpFileName, String surefirePropsFileName,
90                                String effectiveSystemPropertiesFileName )
91              throws IOException
92      {
93          BooterDeserializer booterDeserializer =
94                  new BooterDeserializer( createSurefirePropertiesIfFileExists( tmpDir, surefirePropsFileName ) );
95          setSystemProperties( new File( tmpDir, effectiveSystemPropertiesFileName ) );
96  
97          providerConfiguration = booterDeserializer.deserialize();
98          DumpErrorSingleton.getSingleton()
99                  .init( providerConfiguration.getReporterConfiguration().getReportsDirectory(), dumpFileName );
100 
101         if ( isDebugging() )
102         {
103             DumpErrorSingleton.getSingleton()
104                     .dumpText( "Found Maven process ID " + booterDeserializer.getPluginPid() );
105         }
106 
107         startupConfiguration = booterDeserializer.getStartupConfiguration();
108 
109         forkingReporterFactory = createForkingReporterFactory();
110 
111         ConsoleLogger logger = (ConsoleLogger) forkingReporterFactory.createReporter();
112         pingScheduler = isDebugging() ? null : listenToShutdownCommands( booterDeserializer.getPluginPid(), logger );
113 
114         systemExitTimeoutInSeconds = providerConfiguration.systemExitTimeout( DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS );
115 
116         AbstractPathConfiguration classpathConfiguration = startupConfiguration.getClasspathConfiguration();
117 
118         if ( classpathConfiguration.isClassPathConfig() )
119         {
120             if ( startupConfiguration.isManifestOnlyJarRequestedAndUsable() )
121             {
122                 classpathConfiguration.toRealPath( ClasspathConfiguration.class )
123                         .trickClassPathWhenManifestOnlyClasspath();
124             }
125             startupConfiguration.writeSurefireTestClasspathProperty();
126         }
127 
128         ClassLoader classLoader = currentThread().getContextClassLoader();
129         classLoader.setDefaultAssertionStatus( classpathConfiguration.isEnableAssertions() );
130         boolean readTestsFromCommandReader = providerConfiguration.isReadTestsFromInStream();
131         testSet = createTestSet( providerConfiguration.getTestForFork(), readTestsFromCommandReader, classLoader );
132     }
133 
134     private void execute()
135     {
136         try
137         {
138             runSuitesInProcess();
139         }
140         catch ( InvocationTargetException e )
141         {
142             Throwable t = e.getTargetException();
143             DumpErrorSingleton.getSingleton().dumpException( t );
144             eventChannel.consoleErrorLog( new LegacyPojoStackTraceWriter( "test subsystem", "no method", t ), false );
145         }
146         catch ( Throwable t )
147         {
148             DumpErrorSingleton.getSingleton().dumpException( t );
149             eventChannel.consoleErrorLog( new LegacyPojoStackTraceWriter( "test subsystem", "no method", t ), false );
150         }
151         finally
152         {
153             acknowledgedExit();
154         }
155     }
156 
157     private Object createTestSet( TypeEncodedValue forkedTestSet, boolean readTestsFromCommandReader, ClassLoader cl )
158     {
159         if ( forkedTestSet != null )
160         {
161             return forkedTestSet.getDecodedValue( cl );
162         }
163         else if ( readTestsFromCommandReader )
164         {
165             return new LazyTestsToRun( eventChannel );
166         }
167         return null;
168     }
169 
170     private void cancelPingScheduler()
171     {
172         if ( pingScheduler != null )
173         {
174             try
175             {
176                 AccessController.doPrivileged( new PrivilegedAction<Object>()
177                                                {
178                                                    @Override
179                                                    public Object run()
180                                                    {
181                                                        pingScheduler.shutdown();
182                                                        return null;
183                                                    }
184                                                }
185                 );
186             }
187             catch ( AccessControlException e )
188             {
189                 // ignore
190             }
191         }
192     }
193 
194     private PingScheduler listenToShutdownCommands( String ppid, ConsoleLogger logger )
195     {
196         PpidChecker ppidChecker = ppid == null ? null : new PpidChecker( ppid );
197         commandReader.addShutdownListener( createExitHandler( ppidChecker ) );
198         AtomicBoolean pingDone = new AtomicBoolean( true );
199         commandReader.addNoopListener( createPingHandler( pingDone ) );
200         PingScheduler pingMechanisms = new PingScheduler( createPingScheduler(), ppidChecker );
201         if ( ppidChecker != null )
202         {
203             logger.debug( ppidChecker.toString() );
204         }
205 
206         ProcessCheckerType checkerType = startupConfiguration.getProcessChecker();
207 
208         if ( ( checkerType == ALL || checkerType == NATIVE ) && pingMechanisms.pluginProcessChecker != null )
209         {
210             Runnable checkerJob = processCheckerJob( pingMechanisms );
211             pingMechanisms.pingScheduler.scheduleWithFixedDelay( checkerJob, 0L, 1L, SECONDS );
212         }
213 
214         if ( checkerType == ALL || checkerType == PING )
215         {
216             Runnable pingJob = createPingJob( pingDone, pingMechanisms.pluginProcessChecker );
217             pingMechanisms.pingScheduler.scheduleWithFixedDelay( pingJob, 0L, PING_TIMEOUT_IN_SECONDS, SECONDS );
218         }
219 
220         return pingMechanisms;
221     }
222 
223     private Runnable processCheckerJob( final PingScheduler pingMechanism )
224     {
225         return new Runnable()
226         {
227             @Override
228             public void run()
229             {
230                 try
231                 {
232                     if ( pingMechanism.pluginProcessChecker.canUse()
233                                  && !pingMechanism.pluginProcessChecker.isProcessAlive()
234                                  && !pingMechanism.pingScheduler.isShutdown() )
235                     {
236                         DumpErrorSingleton.getSingleton()
237                                 .dumpText( "Killing self fork JVM. Maven process died."
238                                         + NL
239                                         + "Thread dump before killing the process (" + getProcessName() + "):"
240                                         + NL
241                                         + generateThreadDump() );
242 
243                         kill();
244                     }
245                 }
246                 catch ( RuntimeException e )
247                 {
248                     DumpErrorSingleton.getSingleton()
249                             .dumpException( e, "System.exit() or native command error interrupted process checker." );
250                 }
251             }
252         };
253     }
254 
255     private CommandListener createPingHandler( final AtomicBoolean pingDone )
256     {
257         return new CommandListener()
258         {
259             @Override
260             public void update( Command command )
261             {
262                 pingDone.set( true );
263             }
264         };
265     }
266 
267     private CommandListener createExitHandler( final PpidChecker ppidChecker )
268     {
269         return new CommandListener()
270         {
271             @Override
272             public void update( Command command )
273             {
274                 Shutdown shutdown = command.toShutdownData();
275                 if ( shutdown.isKill() )
276                 {
277                     ppidChecker.stop();
278                     DumpErrorSingleton.getSingleton()
279                             .dumpText( "Killing self fork JVM. Received SHUTDOWN command from Maven shutdown hook."
280                                     + NL
281                                     + "Thread dump before killing the process (" + getProcessName() + "):"
282                                     + NL
283                                     + generateThreadDump() );
284                     kill();
285                 }
286                 else if ( shutdown.isExit() )
287                 {
288                     ppidChecker.stop();
289                     cancelPingScheduler();
290                     DumpErrorSingleton.getSingleton()
291                             .dumpText( "Exiting self fork JVM. Received SHUTDOWN command from Maven shutdown hook."
292                                     + NL
293                                     + "Thread dump before exiting the process (" + getProcessName() + "):"
294                                     + NL
295                                     + generateThreadDump() );
296                     exitBarrier.release();
297                     exit1();
298                 }
299                 else
300                 {
301                     // else refers to shutdown=testset, but not used now, keeping reader open
302                     DumpErrorSingleton.getSingleton()
303                             .dumpText( "Thread dump for process (" + getProcessName() + "):"
304                                     + NL
305                                     + generateThreadDump() );
306                 }
307             }
308         };
309     }
310 
311     private Runnable createPingJob( final AtomicBoolean pingDone, final PpidChecker pluginProcessChecker  )
312     {
313         return new Runnable()
314         {
315             @Override
316             public void run()
317             {
318                 if ( !canUseNewPingMechanism( pluginProcessChecker ) )
319                 {
320                     boolean hasPing = pingDone.getAndSet( false );
321                     if ( !hasPing )
322                     {
323                         DumpErrorSingleton.getSingleton()
324                                 .dumpText( "Killing self fork JVM. PING timeout elapsed."
325                                         + NL
326                                         + "Thread dump before killing the process (" + getProcessName() + "):"
327                                         + NL
328                                         + generateThreadDump() );
329 
330                         kill();
331                     }
332                 }
333             }
334         };
335     }
336 
337     private void kill()
338     {
339         kill( 1 );
340     }
341 
342     private void kill( int returnCode )
343     {
344         commandReader.stop();
345         Runtime.getRuntime().halt( returnCode );
346     }
347 
348     private void exit1()
349     {
350         launchLastDitchDaemonShutdownThread( 1 );
351         System.exit( 1 );
352     }
353 
354     private void acknowledgedExit()
355     {
356         commandReader.addByeAckListener( new CommandListener()
357                                           {
358                                               @Override
359                                               public void update( Command command )
360                                               {
361                                                   exitBarrier.release();
362                                               }
363                                           }
364         );
365         eventChannel.bye();
366         launchLastDitchDaemonShutdownThread( 0 );
367         long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS );
368         acquireOnePermit( exitBarrier, timeoutMillis );
369         cancelPingScheduler();
370         commandReader.stop();
371         System.exit( 0 );
372     }
373 
374     private void runSuitesInProcess()
375         throws TestSetFailedException, InvocationTargetException
376     {
377         createProviderInCurrentClassloader( forkingReporterFactory ).invoke( testSet );
378     }
379 
380     private ForkingReporterFactory createForkingReporterFactory()
381     {
382         final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
383         return new ForkingReporterFactory( trimStackTrace, eventChannel );
384     }
385 
386     private synchronized ScheduledThreadPoolExecutor getJvmTerminator()
387     {
388         if ( jvmTerminator == null )
389         {
390             ThreadFactory threadFactory =
391                     newDaemonThreadFactory( LAST_DITCH_SHUTDOWN_THREAD + systemExitTimeoutInSeconds + "s" );
392             jvmTerminator = new ScheduledThreadPoolExecutor( 1, threadFactory );
393             jvmTerminator.setMaximumPoolSize( 1 );
394         }
395         return jvmTerminator;
396     }
397 
398     @SuppressWarnings( "checkstyle:emptyblock" )
399     private void launchLastDitchDaemonShutdownThread( final int returnCode )
400     {
401         getJvmTerminator()
402                 .schedule( new Runnable()
403                 {
404                     @Override
405                     public void run()
406                     {
407                         DumpErrorSingleton.getSingleton()
408                                 .dumpText( "Thread dump for process ("
409                                         + getProcessName()
410                                         + ") after "
411                                         + systemExitTimeoutInSeconds
412                                         + " seconds shutdown timeout:"
413                                         + NL
414                                         + generateThreadDump() );
415 
416                         kill( returnCode );
417                     }
418                 }, systemExitTimeoutInSeconds, SECONDS
419         );
420     }
421 
422     private SurefireProvider createProviderInCurrentClassloader( ForkingReporterFactory reporterManagerFactory )
423     {
424         BaseProviderFactory bpf = new BaseProviderFactory( reporterManagerFactory, true );
425         bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
426         bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
427         bpf.setForkedChannelEncoder( eventChannel );
428         ClassLoader classLoader = currentThread().getContextClassLoader();
429         bpf.setClassLoaders( classLoader );
430         bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
431         bpf.setProviderProperties( providerConfiguration.getProviderProperties() );
432         bpf.setRunOrderParameters( providerConfiguration.getRunOrderParameters() );
433         bpf.setDirectoryScannerParameters( providerConfiguration.getDirScannerParams() );
434         bpf.setMainCliOptions( providerConfiguration.getMainCliOptions() );
435         bpf.setSkipAfterFailureCount( providerConfiguration.getSkipAfterFailureCount() );
436         bpf.setShutdown( providerConfiguration.getShutdown() );
437         bpf.setSystemExitTimeout( providerConfiguration.getSystemExitTimeout() );
438         String providerClass = startupConfiguration.getActualClassName();
439         return (SurefireProvider) instantiateOneArg( classLoader, providerClass, ProviderParameters.class, bpf );
440     }
441 
442     /**
443      * This method is invoked when Surefire is forked - this method parses and organizes the arguments passed to it and
444      * then calls the Surefire class' run method. <br> The system exit code will be 1 if an exception is thrown.
445      *
446      * @param args Commandline arguments
447      */
448     public static void main( String[] args )
449     {
450         ForkedBooter booter = new ForkedBooter();
451         run( booter, args );
452     }
453 
454     /**
455      * created for testing purposes.
456      *
457      * @param booter booter in JVM
458      * @param args arguments passed to JVM
459      */
460     private static void run( ForkedBooter booter, String[] args )
461     {
462         try
463         {
464             booter.setupBooter( args[0], args[1], args[2], args.length > 3 ? args[3] : null );
465             booter.execute();
466         }
467         catch ( Throwable t )
468         {
469             DumpErrorSingleton.getSingleton().dumpException( t );
470             t.printStackTrace();
471             booter.cancelPingScheduler();
472             booter.exit1();
473         }
474     }
475 
476     private static boolean canUseNewPingMechanism( PpidChecker pluginProcessChecker )
477     {
478         return pluginProcessChecker != null && pluginProcessChecker.canUse();
479     }
480 
481     private static void acquireOnePermit( Semaphore barrier, long timeoutMillis )
482     {
483         try
484         {
485             barrier.tryAcquire( timeoutMillis, MILLISECONDS );
486         }
487         catch ( InterruptedException e )
488         {
489             // cancel schedulers, stop the command reader and exit 0
490         }
491     }
492 
493     private static ScheduledExecutorService createPingScheduler()
494     {
495         ThreadFactory threadFactory = newDaemonThreadFactory( PING_THREAD + PING_TIMEOUT_IN_SECONDS + "s" );
496         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor( 1, threadFactory );
497         executor.setKeepAliveTime( 3L, SECONDS );
498         executor.setMaximumPoolSize( 2 );
499         return executor;
500     }
501 
502     private static InputStream createSurefirePropertiesIfFileExists( String tmpDir, String propFileName )
503             throws FileNotFoundException
504     {
505         File surefirePropertiesFile = new File( tmpDir, propFileName );
506         return surefirePropertiesFile.exists() ? new FileInputStream( surefirePropertiesFile ) : null;
507     }
508 
509     private static boolean isDebugging()
510     {
511         for ( String argument : ManagementFactory.getRuntimeMXBean().getInputArguments() )
512         {
513             if ( "-Xdebug".equals( argument ) || argument.startsWith( "-agentlib:jdwp" ) )
514             {
515                 return true;
516             }
517         }
518         return false;
519     }
520 
521     private static class PingScheduler
522     {
523         private final ScheduledExecutorService pingScheduler;
524         private final PpidChecker pluginProcessChecker;
525 
526         PingScheduler( ScheduledExecutorService pingScheduler, PpidChecker pluginProcessChecker )
527         {
528             this.pingScheduler = pingScheduler;
529             this.pluginProcessChecker = pluginProcessChecker;
530         }
531 
532         void shutdown()
533         {
534             pingScheduler.shutdown();
535             if ( pluginProcessChecker != null )
536             {
537                 pluginProcessChecker.destroyActiveCommands();
538             }
539         }
540     }
541 
542     private static String generateThreadDump()
543     {
544         StringBuilder dump = new StringBuilder();
545         ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
546         ThreadInfo[] threadInfos = threadMXBean.getThreadInfo( threadMXBean.getAllThreadIds(), 100 );
547         for ( ThreadInfo threadInfo : threadInfos )
548         {
549             dump.append( '"' );
550             dump.append( threadInfo.getThreadName() );
551             dump.append( "\" " );
552             Thread.State state = threadInfo.getThreadState();
553             dump.append( "\n   java.lang.Thread.State: " );
554             dump.append( state );
555             StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
556             for ( StackTraceElement stackTraceElement : stackTraceElements )
557             {
558                 dump.append( "\n        at " );
559                 dump.append( stackTraceElement );
560             }
561             dump.append( "\n\n" );
562         }
563         return dump.toString();
564     }
565 
566     private static String getProcessName()
567     {
568         return ManagementFactory.getRuntimeMXBean()
569                 .getName();
570     }
571 }