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