package cz.cuni.mff.d3s.been.hostruntime.task;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cz.cuni.mff.d3s.been.bpk.BpkNames;
import cz.cuni.mff.d3s.been.bpk.JavaRuntime;
import cz.cuni.mff.d3s.been.core.task.Java;
import cz.cuni.mff.d3s.been.core.task.ModeEnum;
import cz.cuni.mff.d3s.been.core.task.TaskDescriptor;
import cz.cuni.mff.d3s.been.hostruntime.TaskException;
/**
* Command line builder for JVM based tasks.
*
* @author Tadeas Palusga
* @author Martin Sixta
* @author Kuba Brecka
*
*/
class JVMCmdLineBuilder implements CmdLineBuilder {
private static final String CP_WILDCARD = "*";
private static final Logger log = LoggerFactory.getLogger(JVMCmdLineBuilder.class);
/** Name of Java's executable. This is overkill, isn't it? */
private static final String JAVA_EXECUTABLE = "java";
/** Name of Java's classpath argument. See {@link #JAVA_EXECUTABLE}. */
private static final String JAVA_CLASSPATH_ARG = "-cp";
/** Name of the java Task Runner */
static final String TASK_RUNNER_CLASS = "cz.cuni.mff.d3s.been.taskapi.TaskRunner";
/**
* Java debug parameter template of Java's classpath argument. See
* {@link #JAVA_EXECUTABLE}. See <a
* href="http://docs.oracle.com/javase/1.5.0/docs/guide/jpda/conninv.html"
* >Oracle documentation</a> <br/>
* <br>
* <br>
* There are 3 string placeholders in the template. <br>
* <br>
* <b>1. - 'server' (official documentation follows)</b><br>
* <i>Default: "n"</i>
* <p>
* If "y", listen for a debugger application to attach; otherwise, attach to
* the debugger application at the specified address. * If "y" and no address
* is specified, choose a transport address at which to listen for a debugger
* application, and print the address to the standard output stream.
* </p>
*
* <b>2. - 'address' (official documentation follows)</b><br>
* <p>
* Transport address for the connection. If server=n, attempt to attach to
* debugger application at this address. If server=y, listen for a connection
* at this address.
* </p>
* <b>3. - 'suspend' (official documentation follows)</b><br>
* <i>Default: "y"</i>
* <p>
* If "y", VMStartEvent has a suspendPolicy of SUSPEND_ALL. If "n",
* VMStartEvent has a suspendPolicy of SUSPEND_NONE.
* </p>
*/
private static final String JAVA_DEBUG_ARG_TEMPLATE = "-agentlib:jdwp=transport=dt_socket,server=%s,address=%s,suspend=%s";
/** task library directory */
private final File libDir;
private File fileDir;
/** underlying task descriptor */
private final TaskDescriptor taskDescriptor;
/** underlying java runtime */
private JavaRuntime runtime;
/**
* @param taskDir
* task home directory - from this directory is determined library
* directory ({@link BpkNames#LIB_DIR})
* @param taskDescriptor
* associated TaskDescriptor
* @param runtime
* Java runtime definition
*/
public JVMCmdLineBuilder(File taskDir, TaskDescriptor taskDescriptor, JavaRuntime runtime) {
this.taskDescriptor = taskDescriptor;
this.runtime = runtime;
this.libDir = new File(taskDir, BpkNames.LIB_DIR);
this.fileDir = new File(taskDir, BpkNames.FILES_DIR);
}
/**
* {@inheritDoc}
*/
@Override
public TaskCommandLine build() throws TaskException {
TaskCommandLine cmdLine = new TaskCommandLine(JAVA_EXECUTABLE);
addClassPath(cmdLine);
addJavaOptsFromTaskDescriptor(cmdLine);
addDebugParameters(cmdLine);
addMainClass(cmdLine);
addArgsFromTaskDescriptor(cmdLine);
return cmdLine;
}
private void addMainClass(TaskCommandLine cmdLine) throws TaskException {
Java java = taskDescriptor.getJava();
if (useTaskRunner(java)) {
cmdLine.addArgument(TASK_RUNNER_CLASS);
}
String finalMainClass = java.getMainClass();
cmdLine.addArgument(finalMainClass);
}
private boolean useTaskRunner(Java java) {
boolean isValueSpecified = java != null && java.isSetUseTaskRunner();
if (isValueSpecified) {
return java.isUseTaskRunner();
} else {
return true; // default to true
}
}
/**
* Generates classpath value argument. Joins all absolute paths of files in
* library directory ({@link JVMCmdLineBuilder#libDir}) to single string.
* (Path join is platform independent - it means that ':' is used as the path
* delimiter on Microsoft OS and ':' on UNIX/Linux OS)
*
* @param cmdLine
* command line to which the generated argument should be added
*/
private void addClassPath(TaskCommandLine cmdLine) {
String filesClasspath = fileDir.toPath().toString() + File.separator + CP_WILDCARD;
String libClasspath = libDir.toPath().toString() + File.separator + CP_WILDCARD;
cmdLine.addArgument(JAVA_CLASSPATH_ARG).addArgument(concat(filesClasspath, libClasspath));
}
private String concat(String... paths) {
return StringUtils.toString(paths, File.pathSeparator);
}
/**
* Searches for java options in task descriptor and appends these options to
* given {@link CommandLine}
*
* @param cmdLine
* command line to which the options should be appended
*/
void addJavaOptsFromTaskDescriptor(TaskCommandLine cmdLine) {
boolean javaElementDefined = taskDescriptor.isSetJava();
if (javaElementDefined) {
boolean javaOptionsDefined = taskDescriptor.getJava().isSetJavaOptions();
if (javaOptionsDefined) {
for (String option : taskDescriptor.getJava().getJavaOptions().getJavaOption()) {
cmdLine.addArgument(option);
}
}
}
}
/**
* Searches for debug options in task descriptor. If debug is defined and
* debug mode is [{@link ModeEnum#CONNECT} or {@link ModeEnum#LISTEN} ] then
* the debug argument is generated from defined template (see
* {@link JVMCmdLineBuilder#JAVA_DEBUG_ARG_TEMPLATE} for detailed informations
* about debug argument.)
*
* @param cmdLine
* Task's command line
* @throws TaskException
*/
private void addDebugParameters(TaskCommandLine cmdLine) throws TaskException {
if (isDebugSectionDefinedInTaskDescriptor()) {
switch (taskDescriptor.getDebug().getMode()) {
case CONNECT: {
String host = taskDescriptor.getDebug().getHost();
int port = taskDescriptor.getDebug().getPort();
cmdLine.suspended = taskDescriptor.getDebug().isSuspend();
cmdLine.addArgument(createDebugParam(false, host + ":" + port, cmdLine.suspended));
break;
}
case LISTEN: {
int port = taskDescriptor.getDebug().getPort();
if (port == 0) {
port = detectRandomPort();
}
cmdLine.debugPort = port;
cmdLine.debugListeningMode = true;
cmdLine.suspended = taskDescriptor.getDebug().isSuspend();
cmdLine.addArgument(createDebugParam(true, "" + port, cmdLine.suspended));
log.info("Debugged process is listening on port {}", port);
break;
}
case NONE:
return;
default:
return;
}
}
}
/**
* Searches for program arguments in task descriptor and appends these
* arguments to given {@link CommandLine}
*
* @param cmdLine
* command line to which the generated argument should be added
*/
private void addArgsFromTaskDescriptor(TaskCommandLine cmdLine) {
boolean hasArguments = taskDescriptor.isSetArguments();
if (hasArguments) {
for (String taskOpt : taskDescriptor.getArguments().getArgument()) {
cmdLine.addArgument(taskOpt);
}
}
}
/**
* Detects <b>random available</b> port on localhost.
*
* @return detected port
* @throws TaskException
* in port cannot be detected from some reaosn
*/
private int detectRandomPort() throws TaskException {
try (ServerSocket ss = new ServerSocket(0)) {
return ss.getLocalPort();
} catch (IOException e) {
throw new TaskException("Cannot detect random port on localhost for debugging", e);
}
}
/**
* Creates transport debug parameter. For detailed parameter description see
* {@link JVMCmdLineBuilder#JAVA_DEBUG_ARG_TEMPLATE}
*
* @param server
* listen for a debugger application to attach
* @param address
* transport address for the connection
* @param suspend
* if <i>true</i> ... suspend policy = SUSPEND_ALL. If <i>false</i>
* ... suspend policy = SUSPEND_NONE
* @return created parameter
*/
private String createDebugParam(boolean server, String address, boolean suspend) {
return String.format(JAVA_DEBUG_ARG_TEMPLATE, (server ? "y" : "n"), address, (suspend ? "y" : "n"));
}
/**
* Tells if debugging section is defined (beware: defined != enabled)
*
* @return whether the debug section is defined
*/
private boolean isDebugSectionDefinedInTaskDescriptor() {
return taskDescriptor.isSetDebug();
}
}