| 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; |
| } |
| } |