/*******************************************************************************
* Copyright (c) 2013 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.runner.forking;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
/**
* A default forking implementation. This should be suitable for most execution environments. If not, it is designed to
* be easily extensible, or alternatively you can of course create an entirely own {@link Forker} implementation.
*
* @author Rene Schneider - initial API and implementation
*
*/
public class DefaultForker implements Forker {
/**
* Name and path of the Java executable to use for starting a new JVM.
*/
private static final String JAVA_EXECUTABLE = System.getProperties().getProperty("java.home") + File.separatorChar
+ "bin" + File.separatorChar + "java";
@Override
public ForkedProcess fork(String[] someCommandLineArguments, String aForkName, long aRandomSeed)
throws ForkException {
int tempPortNumber = getPortToUse();
List<String> tempArgs = createArgumentList(someCommandLineArguments, tempPortNumber, aForkName, aRandomSeed);
return createProcess(tempArgs, tempPortNumber);
}
/**
* Creates the argument list used to fork the process.
*
* @param someCommandLineArguments
* the command line arguments provided to {@link #fork(String[], int, String)}.
* @param aPortNumber
* the port number to use by the fork
* @param aForkName
* the name for the new fork
* @param aRandomSeed
* the seed for the RNG of the fork
* @return the argument list
*/
protected List<String> createArgumentList(String[] someCommandLineArguments, int aPortNumber, String aForkName,
long aRandomSeed) {
List<String> tempArgs = new ArrayList<String>();
addJavaExecutable(tempArgs);
addClassPath(tempArgs);
addForkInformation(tempArgs, aPortNumber, aForkName, aRandomSeed);
addJVMArguments(tempArgs);
addClassName(tempArgs);
addApplicationArguments(tempArgs, someCommandLineArguments);
return tempArgs;
}
/**
* Adds the name of the java executable to the argument list provided. Default implementation tries to determine
* this using the system property "java.home".
*
* @param anArgumentList
* the argument list to extend
*/
protected void addJavaExecutable(List<String> anArgumentList) {
anArgumentList.add(JAVA_EXECUTABLE);
}
/**
* Adds the classpath parameter to the argument list. Default implementation uses the system property
* "java.class.path".
*
* @param anArgumentList
* the argument list to extend
*/
protected void addClassPath(List<String> anArgumentList) {
String tempClasspath = System.getProperty("java.class.path");
if (tempClasspath != null && tempClasspath.length() > 0) {
anArgumentList.add("-cp");
anArgumentList.add(tempClasspath);
}
}
/**
* Adds the system property definitions telling the fork on which port it should listen and which name it has.
*
* @param anArgumentList
* the argument list to extend
* @param aPortNumber
* the port number for the fork
* @param aForkName
* the name for the fork
* @param aRandomSeed
* the seed for the RNG of the fork
*/
protected void addForkInformation(List<String> anArgumentList, int aPortNumber, String aForkName,
long aRandomSeed) {
String tempHostInterface = getHostInterfaceToUse();
if (tempHostInterface != null) {
anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_HOST + "=" + tempHostInterface);
}
anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_PORT + "=" + aPortNumber);
anArgumentList.add("-D" + Forker.SYSPARAM_FORK_NAME + "=" + aForkName);
anArgumentList.add("-D" + Forker.SYSPARAM_FORK_SEED + "=" + aRandomSeed);
}
/**
* Adds any random JVM configuration arguments to the argument list. Default implementation asks the
* {@link RuntimeMXBean}.
*
* @param anArgumentList
* the argument list to extend
*/
protected void addJVMArguments(List<String> anArgumentList) {
RuntimeMXBean tempMXBean = ManagementFactory.getRuntimeMXBean();
for (String tempArg : tempMXBean.getInputArguments()) {
// filter out Eclipse debug stuff
if (!tempArg.startsWith("-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:")) {
anArgumentList.add(tempArg);
}
}
}
/**
* Adds the name of the main class to the argument list. Default implementation uses {@link #guessMainClassName()}
* to search stack traces for the class.
*
* @param anArgumentList
* the argument list to extend
*/
protected void addClassName(List<String> anArgumentList) {
anArgumentList.add(guessMainClassName());
}
/**
* Adds the application-specific arguments to the argument list.
*
* @param anArgumentList
* the argument list to extend
* @param someCommandLineArguments
* the applications' arguments, as provided to Integrity
*/
protected void addApplicationArguments(List<String> anArgumentList, String[] someCommandLineArguments) {
if (someCommandLineArguments != null) {
for (String tempCommandLineArgument : someCommandLineArguments) {
anArgumentList.add(tempCommandLineArgument);
}
}
}
/**
* Create a process using the given argument list.
*
* @param anArgumentList
* the arguments
* @return the forked process
* @throws ForkException
*/
protected LocalForkedProcess createProcess(List<String> anArgumentList, int aPortNumber) throws ForkException {
ProcessBuilder tempBuilder = new ProcessBuilder(anArgumentList);
try {
return new LocalForkedProcess(tempBuilder.start(), aPortNumber);
} catch (IOException exc) {
throw new ForkException("Error forking process", exc);
}
}
/**
* Tries to determine the fully qualified name of the main class, since you cannot ask the JVM for it unfortunately.
*
* @return the name of the main class
*/
protected String guessMainClassName() {
for (Entry<Thread, StackTraceElement[]> tempEntry : Thread.getAllStackTraces().entrySet()) {
if ("main".equals(tempEntry.getKey().getName().toLowerCase())) {
return tempEntry.getValue()[tempEntry.getValue().length - 1].getClassName();
}
}
throw new UnsupportedOperationException(
"Could not determine main class name. You probably need to implement your own Forker in order to use forking!");
}
/**
* The maximum possible port number.
*/
private static final int MAX_PORT_NUMBER = 65535;
/**
* The minimum possible port number.
*/
private static final int MIN_PORT_NUMBER = 1024;
/**
* This determines the port to use by the fork to communicate with the master. The fork must be able to bind to this
* port. The default implementation finds a free port on the machine by randomly checking ports above
* {@link #MIN_PORT_NUMBER}.
*
* @return the port to use
*/
protected int getPortToUse() {
int tempPort = 0;
do {
tempPort = (int) Math.floor(Math.random() * (double) (MAX_PORT_NUMBER - MIN_PORT_NUMBER)) + MIN_PORT_NUMBER;
} while (!isPortAvailable(tempPort));
return tempPort;
}
/**
* Determines the host interface to bind the fork to.
*
* @return the host interface name (IP or hostname)
*/
protected String getHostInterfaceToUse() {
return "localhost";
}
/**
* Checks whether a given port is available on the local machine.
*
* @param aPort
* the port to check
* @return true if the port is available, false if not
*/
protected boolean isPortAvailable(int aPort) {
InetAddress tempInterface;
try {
tempInterface = Inet4Address.getByName("localhost");
} catch (UnknownHostException exc1) {
// This is almost impossible to occur!
throw new RuntimeException(exc1);
}
ServerSocket tempServerSocket = null;
DatagramSocket tempDatagramSocket = null;
try {
tempServerSocket = new ServerSocket(aPort, 1, tempInterface);
tempServerSocket.setReuseAddress(true);
tempDatagramSocket = new DatagramSocket(aPort, tempInterface);
tempDatagramSocket.setReuseAddress(true);
return true;
} catch (IOException exc) {
// nothing to do
} finally {
if (tempDatagramSocket != null) {
tempDatagramSocket.close();
}
if (tempServerSocket != null) {
try {
tempServerSocket.close();
} catch (IOException exc) {
// ignore
}
}
}
return false;
}
}