/*
* Copyright (c) 2014-2015 the original author or authors
*
* Licensed 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.
*/
package io.werval.commands;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import io.werval.api.exceptions.WervalException;
import static io.werval.util.IllegalArguments.ensureNotEmpty;
import static io.werval.util.IllegalArguments.ensureNotNull;
/**
* Start Command.
*/
public class StartCommand
implements Runnable
{
/**
* Execution model.
*/
public static enum ExecutionModel
{
/**
* Run in isolated threads, good for build plugins.
*/
ISOLATED_THREADS,
/**
* Fork a java process, good for CLI.
*/
FORK
}
private static final long DAEMON_THREAD_JOIN_TIMEOUT = 15000;
private static final boolean STOP_UNRESPONSIVE_DAEMON_THREADS = false;
private static final boolean CLEANUP_DAEMON_THREADS = true;
private final ExecutionModel executionModel;
private final String mainClass;
private final String[] arguments;
private final URL[] classpath;
private final String configResource;
private final File configFile;
private final URL configUrl;
public StartCommand( ExecutionModel executionModel, String mainClass, String[] arguments, URL[] classpath )
{
this( executionModel, mainClass, arguments, classpath, null, null, null );
}
public StartCommand(
ExecutionModel executionModel,
String mainClass, String[] arguments, URL[] classpath,
String configResource, File configFile, URL configUrl
)
{
ensureNotNull( "Execution Model", executionModel );
ensureNotEmpty( "Main class", mainClass );
this.executionModel = executionModel;
this.mainClass = mainClass;
this.arguments = arguments == null ? new String[ 0 ] : arguments;
this.classpath = classpath == null ? new URL[ 0 ] : classpath;
this.configResource = configResource;
this.configFile = configFile;
this.configUrl = configUrl;
}
@Override
public void run()
{
switch( executionModel )
{
case ISOLATED_THREADS:
runIsolatedThreads();
break;
case FORK:
runFork();
break;
default:
throw new InternalError();
}
}
private void runFork()
{
// java -cp class:path main.Class arg um ents
List<String> cmd = new ArrayList<>();
cmd.add( "java" );
cmd.add( "-cp" );
StringBuilder cpBuilder = new StringBuilder();
Iterator<URL> cpIt = Arrays.asList( classpath ).iterator();
while( cpIt.hasNext() )
{
URL cpUrl = cpIt.next();
if( "file".equals( cpUrl.getProtocol() ) )
{
cpBuilder.append( cpUrl.getPath() );
}
else
{
cpBuilder.append( cpUrl.toString() );
}
if( cpIt.hasNext() )
{
cpBuilder.append( File.pathSeparator );
}
}
String classpathString = cpBuilder.toString();
cmd.add( classpathString );
if( configResource != null )
{
cmd.add( "-Dconfig.resource=\"" + configResource + "\"" );
}
if( configFile != null )
{
cmd.add( "-Dconfig.file=\"" + configFile.getAbsolutePath() + "\"" );
}
if( configUrl != null )
{
cmd.add( "-Dconfig.url=\"" + configUrl.toString() + "\"" );
}
cmd.add( mainClass );
cmd.addAll( Arrays.asList( arguments ) );
try
{
Process process = new ProcessBuilder( cmd ).start();
int status = process.waitFor();
if( status != 0 )
{
throw new WervalException(
"An exception occured while executing the Werval Application, status was: " + status
);
}
}
catch( IOException ex )
{
throw new WervalException( "An exception occured while executing the Werval Application.", ex );
}
catch( InterruptedException ex )
{
Thread.interrupted();
throw new WervalException( "An exception occured while executing the Werval Application.", ex );
}
}
private void runIsolatedThreads()
{
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( mainClass /* name */ );
Thread bootstrapThread = new Thread(
threadGroup,
() ->
{
try
{
invokeMain();
}
catch( NoSuchMethodException ex )
{
Thread.currentThread().getThreadGroup().uncaughtException(
Thread.currentThread(),
new Exception(
"The specified mainClass doesn't contain a main method with appropriate signature.",
ex
)
);
}
catch( InvocationTargetException ex )
{
Thread.currentThread().getThreadGroup().uncaughtException(
Thread.currentThread(),
ex.getCause()
);
}
catch( Exception ex )
{
Thread.currentThread().getThreadGroup().uncaughtException(
Thread.currentThread(),
ex
);
}
},
mainClass + ".main()"
);
bootstrapThread.setContextClassLoader( new URLClassLoader( classpath ) );
bootstrapThread.start();
joinNonDaemonThreads( threadGroup );
if( CLEANUP_DAEMON_THREADS )
{
terminateThreads( threadGroup );
try
{
threadGroup.destroy();
}
catch( IllegalThreadStateException ex )
{
System.err.println( "Couldn't destroy threadgroup " + threadGroup );
ex.printStackTrace( System.err );
}
}
synchronized( threadGroup )
{
if( threadGroup.uncaughtException != null )
{
throw new WervalException(
"An exception occured while executing the Werval Application. "
+ threadGroup.uncaughtException.getMessage(),
threadGroup.uncaughtException
);
}
}
}
private void invokeMain()
throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
Method main = Thread.currentThread().getContextClassLoader().loadClass(
mainClass
).getMethod(
"main",
new Class[]
{
String[].class
}
);
if( !main.isAccessible() )
{
main.setAccessible( true );
}
if( !Modifier.isStatic( main.getModifiers() ) )
{
throw new IllegalArgumentException(
"Can't call main(String[]) method because it is not static."
);
}
if( configResource != null )
{
System.setProperty( "config.resource", configResource );
}
if( configFile != null )
{
System.setProperty( "config.file", configFile.getAbsolutePath() );
}
if( configUrl != null )
{
System.setProperty( "config.url", configUrl.toString() );
}
main.invoke(
null,
new Object[]
{
arguments
}
);
}
/**
* a ThreadGroup to isolate execution and collect exceptions.
*/
class IsolatedThreadGroup
extends ThreadGroup
{
private Throwable uncaughtException; // synchronize access to this
public IsolatedThreadGroup( String name )
{
super( name );
}
@Override
public void uncaughtException( Thread thread, Throwable throwable )
{
if( throwable instanceof ThreadDeath )
{
return; // harmless
}
synchronized( this )
{
if( uncaughtException == null ) // only remember the first one
{
uncaughtException = throwable; // will be reported eventually
}
}
throwable.printStackTrace( System.err );
}
}
private void joinNonDaemonThreads( ThreadGroup threadGroup )
{
boolean foundNonDaemon;
do
{
foundNonDaemon = false;
Collection<Thread> threads = getActiveThreads( threadGroup );
for( Thread thread : threads )
{
if( thread.isDaemon() )
{
continue;
}
foundNonDaemon = true; // try again; maybe more threads were created while we were busy
joinThread( thread, 0 );
}
}
while( foundNonDaemon );
}
private void joinThread( Thread thread, long timeoutMsecs )
{
try
{
// System.out.println( "joining on thread " + thread );
thread.join( timeoutMsecs );
}
catch( InterruptedException e )
{
Thread.currentThread().interrupt(); // good practice if don't throw
System.err.println( "interrupted while joining against thread " + thread ); // not expected!
e.printStackTrace( System.err );
}
if( thread.isAlive() ) // generally abnormal
{
System.err.println(
"thread " + thread + " was interrupted but is still alive after at least " + timeoutMsecs + "msecs"
);
}
}
private void terminateThreads( ThreadGroup threadGroup )
{
long startTime = System.currentTimeMillis();
Set<Thread> uncooperativeThreads = new HashSet<>(); // these were not responsive to interruption
for( Collection<Thread> threads = getActiveThreads( threadGroup );
!threads.isEmpty();
threads = getActiveThreads( threadGroup ), threads.removeAll( uncooperativeThreads ) )
{
// Interrupt all threads we know about as of this instant (harmless if spuriously went dead (! isAlive())
// or if something else interrupted it ( isInterrupted() ).
for( Thread thread : threads )
{
// System.out.println( "interrupting thread " + thread );
thread.interrupt();
}
// Now join with a timeout and call stop() (assuming flags are set right)
for( Thread thread : threads )
{
if( !thread.isAlive() )
{
continue; // and, presumably it won't show up in getActiveThreads() next iteration
}
if( DAEMON_THREAD_JOIN_TIMEOUT <= 0 )
{
joinThread( thread, 0 ); // waits until not alive; no timeout
continue;
}
long timeout = DAEMON_THREAD_JOIN_TIMEOUT - ( System.currentTimeMillis() - startTime );
if( timeout > 0 )
{
joinThread( thread, timeout );
}
if( !thread.isAlive() )
{
continue;
}
uncooperativeThreads.add( thread ); // ensure we don't process again
if( STOP_UNRESPONSIVE_DAEMON_THREADS )
{
System.err.println( "thread " + thread + " will be Thread.stop()'ed" );
thread.stop();
}
else
{
System.err.println(
"thread " + thread + " will linger despite being asked to die via interruption"
);
}
}
}
if( !uncooperativeThreads.isEmpty() )
{
System.err.println( "NOTE: " + uncooperativeThreads.size()
+ " thread(s) did not finish despite being asked to via interruption."
+ " This is not a problem with Werval Run, it is a problem with the running code."
+ " Although not serious, it should be remedied." );
}
else
{
int activeCount = threadGroup.activeCount();
if( activeCount != 0 )
{
// TODO this may be nothing; continue on anyway; perhaps don't even log in future
Thread[] threadsArray = new Thread[ 1 ];
threadGroup.enumerate( threadsArray );
System.err.println(
"strange; " + activeCount + " thread(s) still active in the group " + threadGroup
+ " such as " + threadsArray[0]
);
}
}
}
private Collection<Thread> getActiveThreads( ThreadGroup threadGroup )
{
Thread[] threads = new Thread[ threadGroup.activeCount() ];
int numThreads = threadGroup.enumerate( threads );
Collection<Thread> result = new ArrayList<>( numThreads );
for( int i = 0; i < threads.length && threads[i] != null; i++ )
{
result.add( threads[i] );
}
return result; // note: result should be modifiable
}
}