1 package org.apache.maven.surefire.booter;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
23 import org.apache.maven.surefire.api.booter.BaseProviderFactory;
24 import org.apache.maven.surefire.api.booter.Command;
25 import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
26 import org.apache.maven.surefire.api.booter.ForkingReporterFactory;
27 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
28 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
29 import org.apache.maven.surefire.api.booter.Shutdown;
30 import org.apache.maven.surefire.api.fork.ForkNodeArguments;
31 import org.apache.maven.surefire.api.provider.CommandListener;
32 import org.apache.maven.surefire.api.provider.ProviderParameters;
33 import org.apache.maven.surefire.api.provider.SurefireProvider;
34 import org.apache.maven.surefire.api.testset.TestSetFailedException;
35 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
36 import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
37 import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
38 import org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory;
39
40 import java.io.File;
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.lang.management.ManagementFactory;
46 import java.lang.management.ThreadInfo;
47 import java.lang.management.ThreadMXBean;
48 import java.lang.reflect.InvocationTargetException;
49 import java.security.AccessControlException;
50 import java.security.AccessController;
51 import java.security.PrivilegedAction;
52 import java.util.concurrent.ScheduledExecutorService;
53 import java.util.concurrent.ScheduledThreadPoolExecutor;
54 import java.util.concurrent.Semaphore;
55 import java.util.concurrent.ThreadFactory;
56 import java.util.concurrent.atomic.AtomicBoolean;
57
58 import static java.lang.Thread.currentThread;
59 import static java.util.ServiceLoader.load;
60 import static java.util.concurrent.TimeUnit.SECONDS;
61 import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
62 import static org.apache.maven.surefire.api.util.ReflectionUtils.instantiateOneArg;
63 import static org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
64 import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
65 import static org.apache.maven.surefire.booter.ProcessCheckerType.ALL;
66 import static org.apache.maven.surefire.booter.ProcessCheckerType.NATIVE;
67 import static org.apache.maven.surefire.booter.ProcessCheckerType.PING;
68 import static org.apache.maven.surefire.booter.SystemPropertyManager.setSystemProperties;
69
70
71
72
73
74
75
76
77
78
79
80 public final class ForkedBooter
81 {
82 private static final long DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS = 30L;
83 private static final long PING_TIMEOUT_IN_SECONDS = 30L;
84 private static final String LAST_DITCH_SHUTDOWN_THREAD = "surefire-forkedjvm-last-ditch-daemon-shutdown-thread-";
85 private static final String PING_THREAD = "surefire-forkedjvm-ping-";
86 private static final String PROCESS_CHECKER_THREAD = "surefire-process-checker";
87 private static final String PROCESS_PIPES_ERROR =
88 "The channel (std/out or TCP/IP) failed to send a stream from this subprocess.";
89
90 private final Semaphore exitBarrier = new Semaphore( 0 );
91
92 private volatile MasterProcessChannelEncoder eventChannel;
93 private volatile ConsoleLogger logger;
94 private volatile MasterProcessChannelProcessorFactory channelProcessorFactory;
95 private volatile CommandReader commandReader;
96 private volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS;
97 private volatile PingScheduler pingScheduler;
98
99 private ScheduledThreadPoolExecutor jvmTerminator;
100 private ProviderConfiguration providerConfiguration;
101 private ForkingReporterFactory forkingReporterFactory;
102 private StartupConfiguration startupConfiguration;
103 private Object testSet;
104
105 private void setupBooter( String tmpDir, String dumpFileName, String surefirePropsFileName,
106 String effectiveSystemPropertiesFileName )
107 throws IOException
108 {
109 BooterDeserializer booterDeserializer =
110 new BooterDeserializer( createSurefirePropertiesIfFileExists( tmpDir, surefirePropsFileName ) );
111 setSystemProperties( new File( tmpDir, effectiveSystemPropertiesFileName ) );
112
113 providerConfiguration = booterDeserializer.deserialize();
114 DumpErrorSingleton.getSingleton()
115 .init( providerConfiguration.getReporterConfiguration().getReportsDirectory(), dumpFileName );
116
117 int forkNumber = booterDeserializer.getForkNumber();
118
119 if ( isDebugging() )
120 {
121 DumpErrorSingleton.getSingleton()
122 .dumpText( "Found Maven process ID " + booterDeserializer.getPluginPid()
123 + " for the fork " + forkNumber + "." );
124 }
125
126 startupConfiguration = booterDeserializer.getStartupConfiguration();
127
128 String channelConfig = booterDeserializer.getConnectionString();
129 channelProcessorFactory = lookupDecoderFactory( channelConfig );
130 channelProcessorFactory.connect( channelConfig );
131 boolean isDebugging = isDebugging();
132 boolean debug = isDebugging || providerConfiguration.getMainCliOptions().contains( LOGGING_LEVEL_DEBUG );
133 ForkNodeArguments args = new ForkedNodeArg( forkNumber, debug );
134 eventChannel = channelProcessorFactory.createEncoder( args );
135 MasterProcessChannelDecoder decoder = channelProcessorFactory.createDecoder( args );
136
137 flushEventChannelOnExit();
138
139 forkingReporterFactory = createForkingReporterFactory();
140 logger = forkingReporterFactory.createTestReportListener();
141 commandReader = new CommandReader( decoder, providerConfiguration.getShutdown(), logger );
142
143 pingScheduler = isDebugging ? null : listenToShutdownCommands( booterDeserializer.getPluginPid() );
144
145 systemExitTimeoutInSeconds = providerConfiguration.systemExitTimeout( DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS );
146
147 AbstractPathConfiguration classpathConfiguration = startupConfiguration.getClasspathConfiguration();
148
149 if ( classpathConfiguration.isClassPathConfig() )
150 {
151 if ( startupConfiguration.isManifestOnlyJarRequestedAndUsable() )
152 {
153 classpathConfiguration.toRealPath( ClasspathConfiguration.class )
154 .trickClassPathWhenManifestOnlyClasspath();
155 }
156 startupConfiguration.writeSurefireTestClasspathProperty();
157 }
158
159 ClassLoader classLoader = currentThread().getContextClassLoader();
160 classLoader.setDefaultAssertionStatus( classpathConfiguration.isEnableAssertions() );
161 boolean readTestsFromCommandReader = providerConfiguration.isReadTestsFromInStream();
162 testSet = createTestSet( providerConfiguration.getTestForFork(), readTestsFromCommandReader, classLoader );
163 }
164
165 private void execute()
166 {
167 try
168 {
169 runSuitesInProcess();
170 }
171 catch ( Throwable t )
172 {
173 Throwable e =
174 t instanceof InvocationTargetException ? ( (InvocationTargetException) t ).getTargetException() : t;
175 DumpErrorSingleton.getSingleton().dumpException( e );
176 logger.error( e.getLocalizedMessage(), e );
177 }
178 finally
179 {
180
181 Thread.interrupted();
182
183 if ( eventChannel.checkError() )
184 {
185 DumpErrorSingleton.getSingleton().dumpText( PROCESS_PIPES_ERROR );
186 logger.error( PROCESS_PIPES_ERROR );
187 }
188
189
190 acknowledgedExit();
191 }
192 }
193
194 private Object createTestSet( TypeEncodedValue forkedTestSet, boolean readTestsFromCommandReader, ClassLoader cl )
195 {
196 if ( forkedTestSet != null )
197 {
198 return forkedTestSet.getDecodedValue( cl );
199 }
200 else if ( readTestsFromCommandReader )
201 {
202 return new LazyTestsToRun( eventChannel, commandReader );
203 }
204 return null;
205 }
206
207 private void cancelPingScheduler()
208 {
209 if ( pingScheduler != null )
210 {
211 try
212 {
213 AccessController.doPrivileged( new PrivilegedAction<Object>()
214 {
215 @Override
216 public Object run()
217 {
218 pingScheduler.shutdown();
219 return null;
220 }
221 }
222 );
223 }
224 catch ( AccessControlException e )
225 {
226
227 }
228 }
229 }
230
231 private void closeForkChannel()
232 {
233 if ( channelProcessorFactory != null )
234 {
235 try
236 {
237 channelProcessorFactory.close();
238 }
239 catch ( IOException e )
240 {
241 e.printStackTrace();
242 }
243 }
244 }
245
246 private PingScheduler listenToShutdownCommands( String ppid )
247 {
248 PpidChecker ppidChecker = ppid == null ? null : new PpidChecker( ppid );
249 commandReader.addShutdownListener( createExitHandler( ppidChecker ) );
250 AtomicBoolean pingDone = new AtomicBoolean( true );
251 commandReader.addNoopListener( createPingHandler( pingDone ) );
252 PingScheduler pingMechanisms = new PingScheduler(
253 createScheduler( PING_THREAD + PING_TIMEOUT_IN_SECONDS + "s" ),
254 createScheduler( PROCESS_CHECKER_THREAD ),
255 ppidChecker );
256
257 ProcessCheckerType checkerType = startupConfiguration.getProcessChecker();
258
259 if ( ( checkerType == ALL || checkerType == NATIVE ) && pingMechanisms.processChecker != null )
260 {
261 logger.debug( pingMechanisms.processChecker.toString() );
262 if ( pingMechanisms.processChecker.canUse() )
263 {
264 Runnable checkerJob = processCheckerJob( pingMechanisms );
265 pingMechanisms.processCheckerScheduler.scheduleWithFixedDelay( checkerJob, 0L, 1L, SECONDS );
266 }
267 else if ( !pingMechanisms.processChecker.isStopped() )
268 {
269 logger.warning( "Cannot use process checker with configuration " + checkerType
270 + ". Platform not supported." );
271 }
272 }
273
274 if ( checkerType == ALL || checkerType == PING )
275 {
276 Runnable pingJob = createPingJob( pingDone, pingMechanisms.processChecker );
277 pingMechanisms.pingScheduler.scheduleWithFixedDelay( pingJob, 0L, PING_TIMEOUT_IN_SECONDS, SECONDS );
278 }
279
280 return pingMechanisms;
281 }
282
283 private Runnable processCheckerJob( final PingScheduler pingMechanism )
284 {
285 return new Runnable()
286 {
287 @Override
288 public void run()
289 {
290 try
291 {
292 if ( pingMechanism.processChecker.canUse()
293 && !pingMechanism.processChecker.isProcessAlive()
294 && !pingMechanism.pingScheduler.isShutdown() )
295 {
296 logger.error( "Surefire is going to kill self fork JVM. Maven process died." );
297 DumpErrorSingleton.getSingleton()
298 .dumpText( "Killing self fork JVM. Maven process died."
299 + NL
300 + "Thread dump before killing the process (" + getProcessName() + "):"
301 + NL
302 + generateThreadDump() );
303
304 kill();
305 }
306 }
307 catch ( RuntimeException e )
308 {
309 DumpErrorSingleton.getSingleton()
310 .dumpException( e, "System.exit() or native command error interrupted process checker." );
311 }
312 }
313 };
314 }
315
316 private CommandListener createPingHandler( final AtomicBoolean pingDone )
317 {
318 return new CommandListener()
319 {
320 @Override
321 public void update( Command command )
322 {
323 pingDone.set( true );
324 }
325 };
326 }
327
328 private CommandListener createExitHandler( final PpidChecker ppidChecker )
329 {
330 return new CommandListener()
331 {
332 @Override
333 public void update( Command command )
334 {
335 Shutdown shutdown = command.toShutdownData();
336 if ( shutdown.isKill() )
337 {
338 if ( ppidChecker != null )
339 {
340 ppidChecker.stop();
341 }
342
343 logger.error( "Surefire is going to kill self fork JVM. "
344 + "Received SHUTDOWN {" + shutdown + "} command from Maven shutdown hook." );
345 DumpErrorSingleton.getSingleton()
346 .dumpText( "Killing self fork JVM. Received SHUTDOWN command from Maven shutdown hook."
347 + NL
348 + "Thread dump before killing the process (" + getProcessName() + "):"
349 + NL
350 + generateThreadDump() );
351
352 kill();
353 }
354 else if ( shutdown.isExit() )
355 {
356 if ( ppidChecker != null )
357 {
358 ppidChecker.stop();
359 }
360 cancelPingScheduler();
361 logger.error( "Surefire is going to exit self fork JVM. "
362 + "Received SHUTDOWN {" + shutdown + "} command from Maven shutdown hook." );
363 DumpErrorSingleton.getSingleton()
364 .dumpText( "Exiting self fork JVM. Received SHUTDOWN command from Maven shutdown hook."
365 + NL
366 + "Thread dump before exiting the process (" + getProcessName() + "):"
367 + NL
368 + generateThreadDump() );
369 exitBarrier.release();
370 exit1();
371 }
372 else
373 {
374
375 DumpErrorSingleton.getSingleton()
376 .dumpText( "Thread dump for process (" + getProcessName() + "):"
377 + NL
378 + generateThreadDump() );
379 }
380 }
381 };
382 }
383
384 private Runnable createPingJob( final AtomicBoolean pingDone, final PpidChecker pluginProcessChecker )
385 {
386 return new Runnable()
387 {
388 @Override
389 public void run()
390 {
391 if ( !canUseNewPingMechanism( pluginProcessChecker ) )
392 {
393 boolean hasPing = pingDone.getAndSet( false );
394 if ( !hasPing )
395 {
396 logger.error( "Killing self fork JVM. PING timeout elapsed." );
397 DumpErrorSingleton.getSingleton()
398 .dumpText( "Killing self fork JVM. PING timeout elapsed."
399 + NL
400 + "Thread dump before killing the process (" + getProcessName() + "):"
401 + NL
402 + generateThreadDump() );
403
404 kill();
405 }
406 }
407 }
408 };
409 }
410
411 private void kill()
412 {
413 kill( 1 );
414 }
415
416 private void kill( int returnCode )
417 {
418 commandReader.stop();
419 closeForkChannel();
420 Runtime.getRuntime().halt( returnCode );
421 }
422
423 private void exit1()
424 {
425 launchLastDitchDaemonShutdownThread( 1 );
426 System.exit( 1 );
427 }
428
429 private void acknowledgedExit()
430 {
431 commandReader.addByeAckListener( new CommandListener()
432 {
433 @Override
434 public void update( Command command )
435 {
436 exitBarrier.release();
437 }
438 }
439 );
440 eventChannel.bye();
441 launchLastDitchDaemonShutdownThread( 0 );
442 boolean byeAckReceived = acquireOnePermit( exitBarrier );
443 if ( !byeAckReceived && !eventChannel.checkError() )
444 {
445 eventChannel.sendExitError( null, false );
446 }
447 cancelPingScheduler();
448 commandReader.stop();
449 closeForkChannel();
450 System.exit( 0 );
451 }
452
453 private void runSuitesInProcess()
454 throws TestSetFailedException, InvocationTargetException
455 {
456 createProviderInCurrentClassloader().invoke( testSet );
457 }
458
459 private ForkingReporterFactory createForkingReporterFactory()
460 {
461 final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
462 return new ForkingReporterFactory( trimStackTrace, eventChannel );
463 }
464
465 private synchronized ScheduledThreadPoolExecutor getJvmTerminator()
466 {
467 if ( jvmTerminator == null )
468 {
469 ThreadFactory threadFactory =
470 newDaemonThreadFactory( LAST_DITCH_SHUTDOWN_THREAD + systemExitTimeoutInSeconds + "s" );
471 jvmTerminator = new ScheduledThreadPoolExecutor( 1, threadFactory );
472 jvmTerminator.setMaximumPoolSize( 1 );
473 }
474 return jvmTerminator;
475 }
476
477 @SuppressWarnings( "checkstyle:emptyblock" )
478 private void launchLastDitchDaemonShutdownThread( final int returnCode )
479 {
480 getJvmTerminator()
481 .schedule( new Runnable()
482 {
483 @Override
484 public void run()
485 {
486 if ( logger != null )
487 {
488 logger.error( "Surefire is going to kill self fork JVM. The exit has elapsed "
489 + systemExitTimeoutInSeconds + " seconds after System.exit(" + returnCode + ")." );
490 }
491
492 DumpErrorSingleton.getSingleton()
493 .dumpText( "Thread dump for process ("
494 + getProcessName()
495 + ") after "
496 + systemExitTimeoutInSeconds
497 + " seconds shutdown timeout:"
498 + NL
499 + generateThreadDump() );
500
501 kill( returnCode );
502 }
503 }, systemExitTimeoutInSeconds, SECONDS
504 );
505 }
506
507 private SurefireProvider createProviderInCurrentClassloader()
508 {
509 BaseProviderFactory bpf = new BaseProviderFactory( true );
510 bpf.setReporterFactory( forkingReporterFactory );
511 bpf.setCommandReader( commandReader );
512 bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
513 bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
514 ClassLoader classLoader = currentThread().getContextClassLoader();
515 bpf.setClassLoaders( classLoader );
516 bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
517 bpf.setProviderProperties( providerConfiguration.getProviderProperties() );
518 bpf.setRunOrderParameters( providerConfiguration.getRunOrderParameters() );
519 bpf.setDirectoryScannerParameters( providerConfiguration.getDirScannerParams() );
520 bpf.setMainCliOptions( providerConfiguration.getMainCliOptions() );
521 bpf.setSkipAfterFailureCount( providerConfiguration.getSkipAfterFailureCount() );
522 bpf.setSystemExitTimeout( providerConfiguration.getSystemExitTimeout() );
523 String providerClass = startupConfiguration.getActualClassName();
524 return instantiateOneArg( classLoader, providerClass, ProviderParameters.class, bpf );
525 }
526
527
528
529
530 private void flushEventChannelOnExit()
531 {
532 Runnable target = new Runnable()
533 {
534 @Override
535 public void run()
536 {
537 eventChannel.onJvmExit();
538 }
539 };
540 Thread t = new Thread( target );
541 t.setDaemon( true );
542 ShutdownHookUtils.addShutDownHook( t );
543 }
544
545 private static MasterProcessChannelProcessorFactory lookupDecoderFactory( String channelConfig )
546 {
547 MasterProcessChannelProcessorFactory defaultFactory = null;
548 MasterProcessChannelProcessorFactory customFactory = null;
549 for ( MasterProcessChannelProcessorFactory factory : load( MasterProcessChannelProcessorFactory.class ) )
550 {
551 Class<?> cls = factory.getClass();
552
553 boolean isSurefireFactory =
554 cls == LegacyMasterProcessChannelProcessorFactory.class
555 || cls == SurefireMasterProcessChannelProcessorFactory.class;
556
557 if ( isSurefireFactory )
558 {
559 if ( factory.canUse( channelConfig ) )
560 {
561 defaultFactory = factory;
562 }
563 }
564 else
565 {
566 customFactory = factory;
567 }
568 }
569 return customFactory != null ? customFactory : defaultFactory;
570 }
571
572
573
574
575
576
577
578 public static void main( String[] args )
579 {
580 ForkedBooter booter = new ForkedBooter();
581 run( booter, args );
582 }
583
584
585
586
587
588
589
590 private static void run( ForkedBooter booter, String[] args )
591 {
592 try
593 {
594 booter.setupBooter( args[0], args[1], args[2], args.length > 3 ? args[3] : null );
595 booter.execute();
596 }
597 catch ( Throwable t )
598 {
599 DumpErrorSingleton.getSingleton().dumpException( t );
600 if ( booter.logger != null )
601 {
602 booter.logger.error( t.getLocalizedMessage(), t );
603 }
604 booter.cancelPingScheduler();
605 booter.exit1();
606 }
607 }
608
609 private static boolean canUseNewPingMechanism( PpidChecker pluginProcessChecker )
610 {
611 return pluginProcessChecker != null && pluginProcessChecker.canUse();
612 }
613
614 private static boolean acquireOnePermit( Semaphore barrier )
615 {
616 try
617 {
618 return barrier.tryAcquire( Integer.MAX_VALUE, SECONDS );
619 }
620 catch ( InterruptedException e )
621 {
622
623 return false;
624 }
625 }
626
627 private static ScheduledExecutorService createScheduler( String threadName )
628 {
629 ThreadFactory threadFactory = newDaemonThreadFactory( threadName );
630 ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor( 1, threadFactory );
631 executor.setMaximumPoolSize( executor.getCorePoolSize() );
632 return executor;
633 }
634
635 private static InputStream createSurefirePropertiesIfFileExists( String tmpDir, String propFileName )
636 throws FileNotFoundException
637 {
638 File surefirePropertiesFile = new File( tmpDir, propFileName );
639 return surefirePropertiesFile.exists() ? new FileInputStream( surefirePropertiesFile ) : null;
640 }
641
642 private static boolean isDebugging()
643 {
644 for ( String argument : ManagementFactory.getRuntimeMXBean().getInputArguments() )
645 {
646 if ( "-Xdebug".equals( argument ) || argument.startsWith( "-agentlib:jdwp" ) )
647 {
648 return true;
649 }
650 }
651 return false;
652 }
653
654 private static class PingScheduler
655 {
656 private final ScheduledExecutorService pingScheduler;
657 private final ScheduledExecutorService processCheckerScheduler;
658 private final PpidChecker processChecker;
659
660 PingScheduler( ScheduledExecutorService pingScheduler, ScheduledExecutorService processCheckerScheduler,
661 PpidChecker processChecker )
662 {
663 this.pingScheduler = pingScheduler;
664 this.processCheckerScheduler = processCheckerScheduler;
665 this.processChecker = processChecker;
666 }
667
668 void shutdown()
669 {
670 pingScheduler.shutdown();
671 processCheckerScheduler.shutdown();
672 if ( processChecker != null )
673 {
674 processChecker.destroyActiveCommands();
675 }
676 }
677 }
678
679 private static String generateThreadDump()
680 {
681 StringBuilder dump = new StringBuilder();
682 ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
683 ThreadInfo[] threadInfos = threadMXBean.getThreadInfo( threadMXBean.getAllThreadIds(), 100 );
684 for ( ThreadInfo threadInfo : threadInfos )
685 {
686 dump.append( '"' );
687 dump.append( threadInfo.getThreadName() );
688 dump.append( "\" " );
689 Thread.State state = threadInfo.getThreadState();
690 dump.append( "\n java.lang.Thread.State: " );
691 dump.append( state );
692 StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
693 for ( StackTraceElement stackTraceElement : stackTraceElements )
694 {
695 dump.append( "\n at " );
696 dump.append( stackTraceElement );
697 }
698 dump.append( "\n\n" );
699 }
700 return dump.toString();
701 }
702
703 private static String getProcessName()
704 {
705 return ManagementFactory.getRuntimeMXBean()
706 .getName();
707 }
708 }