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