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