blob: 3c1857804744314053f1a90a1c9d1fe0a5e4dc51 [file] [log] [blame]
package org.apache.maven.surefire.booter;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.surefire.providerapi.ProviderParameters;
import org.apache.maven.surefire.providerapi.SurefireProvider;
import org.apache.maven.surefire.report.LegacyPojoStackTraceWriter;
import org.apache.maven.surefire.report.ReporterFactory;
import org.apache.maven.surefire.report.StackTraceWriter;
import org.apache.maven.surefire.suite.RunResult;
import org.apache.maven.surefire.testset.TestSetFailedException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.lang.Math.max;
import static java.lang.System.err;
import static java.lang.System.out;
import static java.lang.System.setErr;
import static java.lang.System.setOut;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.maven.surefire.booter.CommandReader.getReader;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_BYE;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_ERROR;
import static org.apache.maven.surefire.booter.ForkingRunListener.encode;
import static org.apache.maven.surefire.booter.SystemPropertyManager.setSystemProperties;
import static org.apache.maven.surefire.util.ReflectionUtils.instantiateOneArg;
import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
/**
* The part of the booter that is unique to a forked vm.
* <p/>
* Deals with deserialization of the booter wire-level protocol
* <p/>
*
* @author Jason van Zyl
* @author Emmanuel Venisse
* @author Kristian Rosenvold
*/
public final class ForkedBooter
{
private static final long DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS = 30;
private static final long PING_TIMEOUT_IN_SECONDS = 20;
private static final long ONE_SECOND_IN_MILLIS = 1000;
private static volatile ScheduledThreadPoolExecutor jvmTerminator;
private static volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS;
/**
* This method is invoked when Surefire is forked - this method parses and organizes the arguments passed to it and
* then calls the Surefire class' run method. <p/> The system exit code will be 1 if an exception is thrown.
*
* @param args Commandline arguments
*/
public static void main( String... args )
{
final CommandReader reader = startupMasterProcessReader();
final ExecutorService pingScheduler = isDebugging() ? null : listenToShutdownCommands( reader );
final PrintStream originalOut = out;
try
{
final String tmpDir = args[0];
final String dumpFileName = args[1];
final String surefirePropsFileName = args[2];
BooterDeserializer booterDeserializer =
new BooterDeserializer( createSurefirePropertiesIfFileExists( tmpDir, surefirePropsFileName ) );
if ( args.length > 3 )
{
final String effectiveSystemPropertiesFileName = args[3];
setSystemProperties( new File( tmpDir, effectiveSystemPropertiesFileName ) );
}
final ProviderConfiguration providerConfiguration = booterDeserializer.deserialize();
DumpErrorSingleton.getSingleton().init( dumpFileName, providerConfiguration.getReporterConfiguration() );
final StartupConfiguration startupConfiguration = booterDeserializer.getProviderConfiguration();
systemExitTimeoutInSeconds =
providerConfiguration.systemExitTimeout( DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS );
final TypeEncodedValue forkedTestSet = providerConfiguration.getTestForFork();
final boolean readTestsFromInputStream = providerConfiguration.isReadTestsFromInStream();
final ClasspathConfiguration classpathConfiguration = startupConfiguration.getClasspathConfiguration();
if ( startupConfiguration.isManifestOnlyJarRequestedAndUsable() )
{
classpathConfiguration.trickClassPathWhenManifestOnlyClasspath();
}
final ClassLoader classLoader = currentThread().getContextClassLoader();
classLoader.setDefaultAssertionStatus( classpathConfiguration.isEnableAssertions() );
startupConfiguration.writeSurefireTestClasspathProperty();
final Object testSet;
if ( forkedTestSet != null )
{
testSet = forkedTestSet.getDecodedValue( classLoader );
}
else if ( readTestsFromInputStream )
{
testSet = new LazyTestsToRun( originalOut );
}
else
{
testSet = null;
}
try
{
runSuitesInProcess( testSet, startupConfiguration, providerConfiguration, originalOut );
}
catch ( InvocationTargetException t )
{
DumpErrorSingleton.getSingleton().dumpException( t );
StackTraceWriter stackTraceWriter =
new LegacyPojoStackTraceWriter( "test subsystem", "no method", t.getTargetException() );
StringBuilder stringBuilder = new StringBuilder();
encode( stringBuilder, stackTraceWriter, false );
encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n" , originalOut );
}
catch ( Throwable t )
{
DumpErrorSingleton.getSingleton().dumpException( t );
StackTraceWriter stackTraceWriter = new LegacyPojoStackTraceWriter( "test subsystem", "no method", t );
StringBuilder stringBuilder = new StringBuilder();
encode( stringBuilder, stackTraceWriter, false );
encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n", originalOut );
}
acknowledgedExit( reader, originalOut, pingScheduler );
}
catch ( Throwable t )
{
DumpErrorSingleton.getSingleton().dumpException( t );
// Just throwing does getMessage() and a local trace - we want to call printStackTrace for a full trace
// noinspection UseOfSystemOutOrSystemErr
t.printStackTrace( err );
cancelPingScheduler( pingScheduler );
// noinspection ProhibitedExceptionThrown,CallToSystemExit
exit( 1 );
}
}
private static void cancelPingScheduler( final ExecutorService pingScheduler )
{
if ( pingScheduler != null )
{
try
{
AccessController.doPrivileged( new PrivilegedAction<Object>()
{
@Override
public Object run()
{
pingScheduler.shutdown();
return null;
}
}
);
}
catch ( AccessControlException e )
{
// ignore
}
}
}
private static CommandReader startupMasterProcessReader()
{
return getReader();
}
private static ExecutorService listenToShutdownCommands( CommandReader reader )
{
reader.addShutdownListener( createExitHandler() );
AtomicBoolean pingDone = new AtomicBoolean( true );
reader.addNoopListener( createPingHandler( pingDone ) );
Runnable pingJob = createPingJob( pingDone );
ScheduledExecutorService pingScheduler = createPingScheduler();
pingScheduler.scheduleAtFixedRate( pingJob, 0, PING_TIMEOUT_IN_SECONDS, SECONDS );
return pingScheduler;
}
private static CommandListener createPingHandler( final AtomicBoolean pingDone )
{
return new CommandListener()
{
public void update( Command command )
{
pingDone.set( true );
}
};
}
private static CommandListener createExitHandler()
{
return new CommandListener()
{
public void update( Command command )
{
Shutdown shutdown = command.toShutdownData();
if ( shutdown.isKill() )
{
kill();
}
else if ( shutdown.isExit() )
{
exit( 1 );
}
// else refers to shutdown=testset, but not used now, keeping reader open
}
};
}
private static Runnable createPingJob( final AtomicBoolean pingDone )
{
return new Runnable()
{
public void run()
{
boolean hasPing = pingDone.getAndSet( false );
if ( !hasPing )
{
kill();
}
}
};
}
private static void encodeAndWriteToOutput( String string, PrintStream out )
{
byte[] encodeBytes = encodeStringForForkCommunication( string );
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized ( out )
{
out.write( encodeBytes, 0, encodeBytes.length );
out.flush();
}
}
private static void kill()
{
Runtime.getRuntime().halt( 1 );
}
private static void exit( int returnCode )
{
launchLastDitchDaemonShutdownThread( returnCode );
System.exit( returnCode );
}
private static void acknowledgedExit( CommandReader reader, PrintStream originalOut, ExecutorService pingScheduler )
{
final Semaphore barrier = new Semaphore( 0 );
reader.addByeAckListener( new CommandListener()
{
@Override
public void update( Command command )
{
barrier.release();
}
}
);
encodeAndWriteToOutput( ( (char) BOOTERCODE_BYE ) + ",0,BYE!\n", originalOut );
launchLastDitchDaemonShutdownThread( 0 );
long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS );
acquireOnePermit( barrier, timeoutMillis );
cancelPingScheduler( pingScheduler );
System.exit( 0 );
}
private static boolean acquireOnePermit( Semaphore barrier, long timeoutMillis )
{
try
{
return barrier.tryAcquire( timeoutMillis, MILLISECONDS );
}
catch ( InterruptedException e )
{
return true;
}
}
private static RunResult runSuitesInProcess( Object testSet, StartupConfiguration startupConfiguration,
ProviderConfiguration providerConfiguration,
PrintStream originalSystemOut )
throws SurefireExecutionException, TestSetFailedException, InvocationTargetException
{
final ReporterFactory factory = createForkingReporterFactory( providerConfiguration, originalSystemOut );
return invokeProviderInSameClassLoader( testSet, factory, providerConfiguration, true, startupConfiguration,
false );
}
private static ReporterFactory createForkingReporterFactory( ProviderConfiguration providerConfiguration,
PrintStream originalSystemOut )
{
final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
return new ForkingReporterFactory( trimStackTrace, originalSystemOut );
}
private static synchronized ScheduledThreadPoolExecutor getJvmTerminator()
{
if ( jvmTerminator == null )
{
ThreadFactory threadFactory =
newDaemonThreadFactory( "last-ditch-daemon-shutdown-thread-" + systemExitTimeoutInSeconds + "s" );
jvmTerminator = new ScheduledThreadPoolExecutor( 1, threadFactory );
jvmTerminator.setMaximumPoolSize( 1 );
return jvmTerminator;
}
else
{
return jvmTerminator;
}
}
private static ScheduledExecutorService createPingScheduler()
{
ThreadFactory threadFactory = newDaemonThreadFactory( "ping-" + PING_TIMEOUT_IN_SECONDS + "s" );
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor( 1, threadFactory );
executor.setMaximumPoolSize( 1 );
executor.prestartCoreThread();
return executor;
}
@SuppressWarnings( "checkstyle:emptyblock" )
private static void launchLastDitchDaemonShutdownThread( final int returnCode )
{
getJvmTerminator().schedule( new Runnable()
{
public void run()
{
Runtime.getRuntime().halt( returnCode );
}
}, systemExitTimeoutInSeconds, SECONDS
);
}
private static RunResult invokeProviderInSameClassLoader( Object testSet, Object factory,
ProviderConfiguration providerConfig,
boolean insideFork,
StartupConfiguration startupConfig,
boolean restoreStreams )
throws TestSetFailedException, InvocationTargetException
{
final PrintStream orgSystemOut = out;
final PrintStream orgSystemErr = err;
// Note that System.out/System.err are also read in the "ReporterConfiguration" instantiation
// in createProvider below. These are the same values as here.
try
{
return createProviderInCurrentClassloader( startupConfig, insideFork, providerConfig, factory )
.invoke( testSet );
}
finally
{
if ( restoreStreams && System.getSecurityManager() == null )
{
setOut( orgSystemOut );
setErr( orgSystemErr );
}
}
}
private static SurefireProvider createProviderInCurrentClassloader( StartupConfiguration startupConfiguration,
boolean isInsideFork,
ProviderConfiguration providerConfiguration,
Object reporterManagerFactory )
{
BaseProviderFactory bpf = new BaseProviderFactory( (ReporterFactory) reporterManagerFactory, isInsideFork );
bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
ClassLoader classLoader = currentThread().getContextClassLoader();
bpf.setClassLoaders( classLoader );
bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
bpf.setProviderProperties( providerConfiguration.getProviderProperties() );
bpf.setRunOrderParameters( providerConfiguration.getRunOrderParameters() );
bpf.setDirectoryScannerParameters( providerConfiguration.getDirScannerParams() );
bpf.setMainCliOptions( providerConfiguration.getMainCliOptions() );
bpf.setSkipAfterFailureCount( providerConfiguration.getSkipAfterFailureCount() );
bpf.setShutdown( providerConfiguration.getShutdown() );
bpf.setSystemExitTimeout( providerConfiguration.getSystemExitTimeout() );
String providerClass = startupConfiguration.getActualClassName();
return (SurefireProvider) instantiateOneArg( classLoader, providerClass, ProviderParameters.class, bpf );
}
private static InputStream createSurefirePropertiesIfFileExists( String tmpDir, String propFileName )
throws FileNotFoundException
{
File surefirePropertiesFile = new File( tmpDir, propFileName );
return surefirePropertiesFile.exists() ? new FileInputStream( surefirePropertiesFile ) : null;
}
private static boolean isDebugging()
{
for ( String argument : ManagementFactory.getRuntimeMXBean().getInputArguments() )
{
if ( "-Xdebug".equals( argument ) || argument.startsWith( "-agentlib:jdwp" ) )
{
return true;
}
}
return false;
}
}