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.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
60
61
62
63
64
65
66
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
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
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
444
445
446
447
448 public static void main( String[] args )
449 {
450 ForkedBooter booter = new ForkedBooter();
451 run( booter, args );
452 }
453
454
455
456
457
458
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
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 }