/*******************************************************************************
* Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
*
* 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 org.cloudifysource.usm.launcher;
import groovy.lang.Closure;
import groovy.lang.MissingMethodException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.domain.LifecycleEvents;
import org.cloudifysource.domain.entry.ExecutableDSLEntry;
import org.cloudifysource.domain.entry.ExecutableDSLEntryType;
import org.cloudifysource.dsl.entry.ClosureExecutableEntry;
import org.cloudifysource.dsl.entry.JavaExecutableEntry;
import org.cloudifysource.dsl.entry.ListExecutableEntry;
import org.cloudifysource.dsl.entry.StringExecutableEntry;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.dsl.internal.debug.DebugModes;
import org.cloudifysource.dsl.utils.ServiceUtils;
import org.cloudifysource.dsl.utils.ServiceUtils.FullServiceName;
import org.cloudifysource.usm.USMException;
import org.cloudifysource.usm.USMUtils;
import org.cloudifysource.usm.commands.USMBuiltInCommand;
import org.cloudifysource.usm.dsl.ServiceConfiguration;
import org.hyperic.sigar.Sigar;
import org.openspaces.core.cluster.ClusterInfo;
import org.openspaces.core.cluster.ClusterInfoAware;
import org.openspaces.core.properties.BeanLevelProperties;
import org.openspaces.core.properties.BeanLevelPropertiesAware;
import org.springframework.beans.factory.annotation.Autowired;
import com.gigaspaces.internal.sigar.SigarHolder;
import com.j_spaces.kernel.Environment;
import com.j_spaces.kernel.PlatformVersion;
/*************
* The default process launcher implementation, used by the USM to launch external processes. Includes OS specific code
* to modify a command line according to standard command line conventions.
*
* @author barakme
*
*/
public class DefaultProcessLauncher implements ProcessLauncher, ClusterInfoAware, BeanLevelPropertiesAware {
private static final int POST_SYNC_PROCESS_SLEEP_INTERVAL = 200;
private static final String LINUX_EXECUTE_PREFIX = "./";
private static final String[] WINDOWS_BATCH_FILE_PREFIX_PARAMS = { "cmd.exe", "/c " };
private static final String LOCALCLOUD = "localcloud";
private List<String> groovyCommandLinePrefixParams;
// last command line to be executed
private List<String> commandLine;
private final Sigar sigar = SigarHolder.getSigar();
private ClusterInfo clusterInfo;
private String groovyEnvironmentClassPath;
@Autowired
private ServiceConfiguration configutaion;
private boolean debugAllEvents;
private Set<LifecycleEvents> debugEvents = Collections.emptySet();
private DebugModes debugMode = DebugModes.INSTEAD;
private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger(DefaultProcessLauncher.class
.getName());
private List<String> switchFirstPartOfCommandLine(final List<String> originalCommandLine, final String newPart) {
final List<String> newCommand = new LinkedList<String>();
newCommand.add(newPart);
newCommand.addAll(originalCommandLine.subList(1,
originalCommandLine.size()));
return newCommand;
}
private List<String> createWindowsCommandLineFromLinux(final List<String> linuxCommandLine, final File puWorkDir) {
final AlternativeExecutableFileNameFilter filter = new AlternativeExecutableFileNameFilter() {
private String prefix;
@Override
public boolean accept(final File dir, final String name) {
return name.startsWith(prefix) && (name.endsWith(".bat") || name.endsWith(".exe"));
}
@Override
public void setPrefix(final String prefix) {
this.prefix = prefix;
}
};
return createCommandLineFromAlternativeOS(puWorkDir,
linuxCommandLine,
filter);
}
/**********
* An interface for a Filename filter implementation that looks for alternative executables.
*
* @author barakme
*
*/
private interface AlternativeExecutableFileNameFilter extends FilenameFilter {
void setPrefix(String prefix);
}
private List<String> createCommandLineFromAlternativeOS(final File puWorkDir,
final List<String> alternateCommandLine, final AlternativeExecutableFileNameFilter fileNameFilter) {
if (alternateCommandLine == null || alternateCommandLine.isEmpty()) {
return null;
}
final String executable = alternateCommandLine.get(0);
// groovy files are executable on all OS
if (executable.endsWith(".groovy")) {
return alternateCommandLine;
}
final File executableFile = getFileFromRelativeOrAbsolutePath(puWorkDir,
executable);
if (executableFile == null) {
logger.warning("Could not find an executable file: " + executable + "in working directory " + puWorkDir
+ " in command line: " + alternateCommandLine + ". Alternate command line can't be created.");
return null;
}
final String prefix = getFilePrefix(executableFile);
final File parentDir = executableFile.getParentFile();
fileNameFilter.setPrefix(prefix);
final File[] files = parentDir.listFiles(fileNameFilter);
if (files.length == 0) {
return null;
}
logger.info("Found candidates for command line to replace " + executable + ". Candidates are: "
+ Arrays.toString(files));
final List<String> modifiedCommandLine = switchFirstPartOfCommandLine(alternateCommandLine,
files[0].getName());
return modifiedCommandLine;
}
private String getFilePrefix(final File executableFile) {
String prefixToSearch = executableFile.getName();
final int index = prefixToSearch.lastIndexOf('.');
if (index >= 0) {
prefixToSearch = prefixToSearch.substring(0,
index);
}
return prefixToSearch;
}
private List<String> createLinuxCommandLineFromWindows(final List<String> windowsCommandLine,
final File puWorkDir) {
final AlternativeExecutableFileNameFilter filter = new AlternativeExecutableFileNameFilter() {
private String prefix;
@Override
public boolean accept(final File dir, final String name) {
return name.equals(prefix) || name.startsWith(prefix) && name.endsWith(".sh");
}
@Override
public void setPrefix(final String prefix) {
this.prefix = prefix;
}
};
return createCommandLineFromAlternativeOS(puWorkDir,
windowsCommandLine,
filter);
}
private void modifyGroovyCommandLine(final List<String> commandLineParams, final File workingDir)
throws USMException {
try {
initGroovyCommandLine(workingDir);
} catch (final FileNotFoundException e) {
throw new USMException("Failed to set up groovy command line", e);
}
for (int i = 0; i < groovyCommandLinePrefixParams.size(); i++) {
commandLineParams.add(i,
groovyCommandLinePrefixParams.get(i));
}
}
private void addJarsFromDirectoryToList(final File dir, final List<File> list) {
final File[] jars = getJarFilesFromDir(dir);
if (jars != null) { // possibe if directory does not exist
list.addAll(Arrays.asList(jars));
}
}
private List<File> getJarFilesForGroovyClasspath(final File gsHome, final File workingDir) {
final List<File> list = new LinkedList<File>();
// jar files in PU's lib dir
addJarsFromDirectoryToList(new File(workingDir.getParentFile(), "lib"),
list);
// <GS_HOME>/lib/required
addJarsFromDirectoryToList(new File(gsHome, "lib/required"),
list);
// <GS_HOME>/lib/platform/usm
addJarsFromDirectoryToList(new File(gsHome, "lib/platform/usm"),
list);
addJarsFromDirectoryToList(new File(gsHome, "lib/platform/cloudify"),
list);
addJarsFromDirectoryToList(new File(gsHome, "lib/platform/sigar"),
list);
return list;
}
private File[] getJarFilesFromDir(final File dir) {
final File[] jars = dir.listFiles(new FileFilter() {
@Override
public boolean accept(final File pathname) {
return pathname.getName().endsWith(".jar") && pathname.isFile();
}
});
return jars;
}
private void initGroovyCommandLine(final File workingDir)
throws FileNotFoundException, USMException {
if (this.groovyCommandLinePrefixParams != null) {
return;
}
final String home = Environment.getHomeDirectory();
final File homeDir = new File(home);
final String groovyPath = createGroovyPath(homeDir);
final StringBuilder sb = new StringBuilder();
final List<File> jars = getJarFilesForGroovyClasspath(homeDir,
workingDir);
if (jars != null) {
for (final File jar : jars) {
sb.append(jar.getAbsolutePath()).append(File.pathSeparator);
}
}
final ArrayList<String> groovyCommandParams = new ArrayList<String>();
groovyCommandParams.add(groovyPath);
// pass values as system props to the jvm, as required by XAP
final List<String> envVarsList = new ArrayList<String>();
envVarsList.add("LOOKUP_LOCATORS_PROP");
envVarsList.add("LOOKUP_GROUPS_PROP");
envVarsList.add("RMI_OPTIONS");
// The GS logging configuration uses a custom JDK logger
// JDK logging expects loggers to be available in the system classloader, but groovy loads
// classpath entries into the GroovyClassLoader whoe parent is the System class loader.
// See more details at CLOUDIFY-1694
// envVarsList.add("GS_LOGGING_CONFIG_FILE_PROP");
groovyCommandParams.addAll(convertEnvVarsToSysPropsList(envVarsList));
if (isLocalCloud()) {
groovyCommandParams.add("-D" + CloudifyConstants.MULTICAST_ENABLED_PROPERTY + "=false");
} else {
String extJavaOptionsValue = System.getenv("EXT_JAVA_OPTIONS");
if (!StringUtils.isBlank(extJavaOptionsValue)) {
String[] options = extJavaOptionsValue.split(" ");
for (String option : options) {
groovyCommandParams.add(option);
}
}
}
groovyCommandParams.add("-D" + CloudifyConstants.LRMI_BIND_PORT_CONTEXT_PROPERTY + "="
+ CloudifyConstants.LRMI_BIND_PORT_RANGE);
if (ServiceUtils.isWindows()) {
modifyWindowsCommandLine(groovyCommandParams, workingDir);
}
String classPathEnv = System.getenv("CLASSPATH");
if (classPathEnv == null) {
classPathEnv = "";
}
classPathEnv = classPathEnv + File.pathSeparator + sb.toString();
// We use the classpath env variable, as the full classpath may be too
// long for the command line
// limit imposed by the operating system.
logger.info("Setting ClassPath environment variable for child processes to: " + classPathEnv);
this.groovyEnvironmentClassPath = classPathEnv;
// if ((jars != null) && (jars.size() > 0)) {
// groovyCommandParams.add("-cp");
// groovyCommandParams.add(sb.toString());
// }
logger.info("Setting groovyCommandParams to: " + groovyCommandParams);
this.groovyCommandLinePrefixParams = groovyCommandParams;
}
private String createGroovyPath(final File homeDir)
throws FileNotFoundException, USMException {
final File toolsDir = new File(homeDir, "tools");
final File groovyDir = new File(toolsDir, "groovy");
final File binDir = new File(groovyDir, "bin");
File groovyFile = null;
if (ServiceUtils.isWindows()) {
groovyFile = new File(binDir, "groovy.bat");
} else {
groovyFile = new File(binDir, "groovy");
}
if (!groovyFile.exists()) {
throw new FileNotFoundException("Could not find groovy executable: " + groovyFile.getAbsolutePath());
}
if (ServiceUtils.isLinuxOrUnix()) {
USMUtils.markFileAsExecutable(sigar,
groovyFile);
}
return groovyFile.getAbsolutePath();
}
private void modifyOSCommandLine(final List<String> commandLineParams, final File puWorkDir)
throws USMException {
final String runParam = commandLineParams.get(0);
if (ServiceUtils.isWindows()) {
modifyWindowsCommandLine(commandLineParams,
puWorkDir);
} else {
markLinuxTargetAsExecutable(runParam,
puWorkDir);
modifyLinuxCommandLine(commandLineParams,
puWorkDir);
}
}
// Add "./" to command line, if not present and file is in ext dir
private void modifyLinuxCommandLine(final List<String> commandLineParams, final File puWorkDir) {
String executeScriptName = commandLineParams.get(0);
final File executeFile = new File(puWorkDir, executeScriptName);
final File parent = executeFile.getParentFile();
if (parent != null && parent.equals(puWorkDir)) {
if (executeScriptName.endsWith(".sh") && !executeScriptName.startsWith(LINUX_EXECUTE_PREFIX)) {
commandLineParams.remove(0);
executeScriptName = LINUX_EXECUTE_PREFIX + executeScriptName;
commandLineParams.add(0,
executeScriptName);
}
}
// LinkedList<String> linkedList =
// (LinkedList<String>)commandLineParams;
// linkedList.addAll(0, Arrays.asList("sh", "-c", "\""));
}
private void modifyWindowsCommandLine(final List<String> commandLineParams, final File workingDir) {
final String firstParam = commandLineParams.get(0);
if (firstParam.endsWith(".bat") || firstParam.endsWith(".cmd")) {
for (int i = 0; i < WINDOWS_BATCH_FILE_PREFIX_PARAMS.length; i++) {
commandLineParams.add(i,
WINDOWS_BATCH_FILE_PREFIX_PARAMS[i]);
}
}
// if the file does not exist, this is probably an operating system
// command
File file = new File(firstParam);
if (!file.isAbsolute()) {
file = new File(workingDir, firstParam);
}
if (!file.exists()) {
// this is not an executable file, so add the cmd interpreter
// prefix
for (int i = 0; i < WINDOWS_BATCH_FILE_PREFIX_PARAMS.length; i++) {
commandLineParams.add(i,
WINDOWS_BATCH_FILE_PREFIX_PARAMS[i]);
}
}
// remove quotes
final ListIterator<String> commandIterator = commandLineParams.listIterator();
while (commandIterator.hasNext()) {
final String param = commandIterator.next();
commandIterator.remove();
commandIterator.add(StringUtils.replace(param, "\"", ""));
}
}
private File getFileFromRelativeOrAbsolutePath(final File puWorkDir, final String fileName) {
File file = new File(puWorkDir, fileName);
if (file.exists()) {
return file;
}
file = new File(fileName);
if (file.exists()) {
return file;
}
return null;
}
private void markLinuxTargetAsExecutable(final String originalCommandLineRunParam, final File puWorkDir)
throws USMException {
logger.info("Setting File as executable: " + originalCommandLineRunParam);
final File file = getFileFromRelativeOrAbsolutePath(puWorkDir,
originalCommandLineRunParam);
if (file != null) {
logger.info("File: " + file + " will be marked as executable");
USMUtils.markFileAsExecutable(sigar,
file);
}
}
private List<String> convertCommandLineStringToParts(final String commandLine) {
final List<String> list = new LinkedList<String>();
final String[] parts = commandLine.split(" ");
Collections.addAll(list, parts);
return list;
}
private List<String> getCommandLineFromArgument(final ExecutableDSLEntry arg, final File workDir,
final List<String> params) {
if (arg.getEntryType() == ExecutableDSLEntryType.STRING) {
// Split the command into parts
final List<String> commandLineStringInParts =
convertCommandLineStringToParts(((StringExecutableEntry) arg).getCommand());
// Add the custom command parameters as args to the split commands
// array
if (params != null) {
commandLineStringInParts.addAll(params);
}
return commandLineStringInParts;
}
if (arg.getEntryType() == ExecutableDSLEntryType.MAP) {
@SuppressWarnings("unchecked")
final Map<String, ExecutableDSLEntry> map = (Map<String, ExecutableDSLEntry>) arg;
final String os = System.getProperty("os.name");
if (logger.isLoggable(Level.FINE)) {
logger.fine("Looking for command line for Operating System Name: " + os);
}
final List<String> cmdLineList = lookUpCommandLineForOS(map,
os);
if (cmdLineList != null) {
return cmdLineList;
}
logger.severe("Could not find a matching operating system expression for Operating System: " + os);
logger.severe("Attempting alternative command line: " + os);
final List<String> alternativeCommandLine = createAlternativeCommandLine(map,
workDir);
if (alternativeCommandLine != null) {
return alternativeCommandLine;
}
logger.severe("Could not create alternative command line: " + os);
}
if (arg.getEntryType() == ExecutableDSLEntryType.LIST) {
final List<?> originalList = ((ListExecutableEntry) arg).getCommand();
final List<String> resultList = new ArrayList<String>(originalList.size());
for (final Object elem : originalList) {
resultList.add(elem.toString());
}
return resultList;
}
throw new IllegalArgumentException("Could not find command line for argument " + arg + "!");
}
private List<String> getCommandLineFromValue(final ExecutableDSLEntry value) {
if (value.getEntryType() == ExecutableDSLEntryType.STRING) {
return convertCommandLineStringToParts(((StringExecutableEntry) value).getCommand());
} else if (value.getEntryType() == ExecutableDSLEntryType.LIST) {
final List<String> result = ((ListExecutableEntry) value).getCommand();
return result;
} else {
throw new IllegalArgumentException("The value: " + value
+ " could not be converted to a command line. Only a String, or a List of Strings may be used");
}
}
private List<String> lookUpCommandLineForOS(final Map<String, ExecutableDSLEntry> map, final String os) {
final Set<Entry<String, ExecutableDSLEntry>> entries = map.entrySet();
for (final Entry<String, ExecutableDSLEntry> entry : entries) {
final String key = entry.getKey();
final ExecutableDSLEntry value = entry.getValue();
try {
if (Pattern.matches(key,
os)) {
return getCommandLineFromValue(value);
}
} catch (final PatternSyntaxException pse) {
logger.log(Level.WARNING,
"Opeating System regular expression pattern: " + key
+ " cannot be compiled. It will me compared using basic string matching.",
pse);
if (key.equals(os)) {
return getCommandLineFromValue(value);
}
}
}
return null;
}
private List<String> createAlternativeCommandLine(final Map<String, ExecutableDSLEntry> map, final File workDir) {
if (ServiceUtils.isWindows()) {
List<String> otherCommandLine = null;
if (map.entrySet().size() > 1) {
otherCommandLine = lookUpCommandLineForOS(map, "Linux");
}
if (otherCommandLine == null) {
otherCommandLine = getCommandLineFromValue(map.values().iterator().next());
}
return createWindowsCommandLineFromLinux(otherCommandLine,
workDir);
}
List<String> otherCommandLine = null;
if (map.entrySet().size() > 1) {
otherCommandLine = lookUpCommandLineForOS(map, "Windows");
}
if (otherCommandLine == null) {
otherCommandLine = getCommandLineFromValue(map.values().iterator().next());
}
return createLinuxCommandLineFromWindows(otherCommandLine, workDir);
}
@Override
public Process launchProcessAsync(final ExecutableDSLEntry arg, final File workingDir, final int retries,
final boolean redirectErrorStream, final List<String> params, final LifecycleEvents event)
throws USMException {
return launchAsync(arg,
workingDir,
retries,
redirectErrorStream,
null,
null,
params, event);
}
private Process launchAsync(final ExecutableDSLEntry arg, final File workingDir, final int retries,
final boolean redirectErrorStream, final File outputFile, final File errorFile, final List<String> params,
final LifecycleEvents event)
throws USMException {
if (arg.getEntryType() == ExecutableDSLEntryType.CLOSURE) {
// in process execution of a closure
return launchAsyncFromClosure(((ClosureExecutableEntry) arg).getCommand());
}
commandLine = getCommandLineFromArgument(arg,
workingDir,
params);
return this.launch(commandLine,
workingDir,
retries,
redirectErrorStream,
outputFile,
errorFile, event);
}
private Process launchAsyncFromClosure(final Object arg)
throws USMException {
final Callable<?> closure = (Callable<?>) arg;
Object retval;
try {
retval = closure.call();
} catch (final Exception e) {
logger.log(Level.SEVERE,
"A closure entry failed to execute: " + e,
e);
throw new USMException("Launch of process from closure exited with an exception: " + e.getMessage(), e);
}
if (retval == null) {
return null; // this is the expected behavior
}
if (retval instanceof Process) {
return (Process) retval;
}
logger.warning("The Start closure returned a non-null value that is not a Process. "
+ "This value will be ignored! Returned value was of type: "
+ retval.getClass().getName() + ". Value was: " + retval);
return null;
}
@Override
public Object launchProcess(final ExecutableDSLEntry arg, final File workingDir, final int retries,
final boolean redirectErrorStream, final Map<String, Object> params, final LifecycleEvents event)
throws USMException {
final List<String> paramsList = getParamsListFromMap(params);
if (arg.getEntryType() == ExecutableDSLEntryType.CLOSURE) {
// in process execution of a closure
final Closure<?> closure = ((ClosureExecutableEntry) arg).getCommand();
try {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Closure Parameters: " + paramsList.toString());
}
return closure.call(paramsList.toArray());
} catch (MissingMethodException mme) {
String method = (String) params.get(CloudifyConstants.INVOCATION_PARAMETER_COMMAND_NAME);
String argsStr = Arrays.toString(paramsList.toArray());
String errorMessage = "A closure entry failed to call method \"" + method + "\" with arguments \""
+ argsStr + "\". Invalid method name or arguments.";
logger.log(Level.SEVERE, errorMessage + " Groovy reported error: " + mme.getMessage(), mme);
throw new USMException(errorMessage);
} catch (final Exception e) {
logger.log(Level.SEVERE,
"A closure entry failed to execute: " + e.getMessage(),
e);
throw new USMException("Failed to execute closure " + e.getMessage(), e);
}
}
if (arg.getEntryType().equals(ExecutableDSLEntryType.JAVA)) {
try {
Object command = ((JavaExecutableEntry) arg).getCommand();
return ((USMBuiltInCommand) command).invoke(paramsList.toArray());
} catch (Exception e) {
logger.log(Level.SEVERE,
"A USM built-in command failed to execute: " + e.getMessage(),
e);
throw new USMException("Failed to execute USM built-in command " + e.getMessage(), e);
}
}
final Process proc = launchProcessAsync(arg,
workingDir,
retries,
redirectErrorStream,
paramsList, event);
final BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line = null;
final StringBuilder sb = new StringBuilder();
final String newline = System.getProperty("line.separator");
logger.info("Command Output:");
try {
do {
if (line != null) {
sb.append(line).append(newline);
logger.info(line);
}
line = reader.readLine();
} while (line != null);
} catch (final IOException ioe) {
throw new USMException("Failed to execute command: " + commandLine, ioe);
} finally {
if (reader != null) {
try {
reader.close();
} catch (final IOException e) {
// ignore
}
}
}
try {
final int exitValue = proc.waitFor();
logger.info("Command exited with value: " + exitValue);
if (exitValue != 0) {
logger.severe("Event lifecycle external process exited with abnormal status code: " + exitValue);
final String result = sb.toString();
final String exceptionReason = GroovyExceptionHandler.getExceptionString(result);
logger.log(Level.SEVERE,
"Event lifecycle external process failed: " + result);
throw new USMException("Event lifecycle external process exited with abnormal status code: "
+ exitValue + " " + exceptionReason);
}
} catch (final InterruptedException e) {
logger.warning("Interrupted while waiting for process to exit");
}
// sleeping for a short interval, to make sure process table is cleaned
// of the dead process
try {
Thread.sleep(POST_SYNC_PROCESS_SLEEP_INTERVAL);
} catch (final InterruptedException e) {
// ignore
}
return sb.toString();
}
private List<String> getParamsListFromMap(final Map<String, Object> params) {
final List<String> paramsList = new ArrayList<String>();
int index = 0;
while (true) {
final String param = (String) params.get(CloudifyConstants.INVOCATION_PARAMETERS_KEY + index);
if (param != null) {
paramsList.add(param);
} else {
break;
}
index++;
}
return paramsList;
}
@Override
public String getCommandLine() {
return this.commandLine.toString();
}
private void modifyCommandLine(final List<String> commandLineParams, final File workingDir, final File outputFile,
final File errorFile, final LifecycleEvents event)
throws USMException {
modifyOSCommandLine(commandLineParams, workingDir);
if (commandLineParams.get(0).endsWith(".groovy")) {
modifyGroovyCommandLine(commandLineParams,
workingDir);
}
modifyRedirectionCommandLine(commandLineParams,
outputFile,
errorFile);
if (ServiceUtils.isLinuxOrUnix()) {
if (isDebugEvent(event)) {
// skipping linux command line modifications for debug event
logger.info("Skipping linux command line modifications for debug event");
} else {
// run the whole command in a shell session
logger.fine("Command before shell modification: " + commandLineParams);
final StringBuilder sb = new StringBuilder();
for (final String param : commandLineParams) {
sb.append(param).append(" ");
}
commandLineParams.clear();
commandLineParams.addAll(Arrays.asList("nohup",
"sh",
"-c",
sb.toString()));
logger.fine("Command after shell modification: " + commandLineParams);
}
}
}
private void modifyRedirectionCommandLine(final List<String> commandLineParams, final File outputFile,
final File errorFile) {
if (outputFile == null) {
return; // both files are set, or neither
}
commandLineParams.addAll(createRedirectionParametersForOS(outputFile,
errorFile));
// if(USMUtils.isLinuxOrUnix()) {
// // add the terminating quotes
// commandLineParams.add("\"");
// }
}
private List<String> createRedirectionParametersForOS(final File outputFile, final File errorFile) {
return Arrays.asList(">>", outputFile.getAbsolutePath(), "2>>", errorFile.getAbsolutePath());
}
private Process launch(final List<String> commandLineParams, final File workingDir, final int retries,
final boolean redirectErrorStream, final File outputFile, final File errorFile, final LifecycleEvents event)
throws USMException {
if (outputFile == null && errorFile != null || outputFile != null && errorFile == null) {
throw new IllegalArgumentException("Both output and error files must be set, or none of them");
}
if (redirectErrorStream && (outputFile != null || errorFile != null)) {
throw new IllegalArgumentException(
"If redirectError option is chosen, neither output file or error file can be set");
}
List<String> modifiedCommandLineParams = null;
modifiedCommandLineParams = commandLineParams;
if (isDebugEvent(event)) {
// create environment for debugging the event and modify the command line.
modifyCommandLine(modifiedCommandLineParams, workingDir, outputFile, errorFile, event);
logger.info("DEBUG BREAKPOINT!");
DebugHookInvoker dhi = new DebugHookInvoker();
final ClassLoader loader = this.configutaion.getDslClassLoader();
logger.info("DSL Class Loader is: " + loader);
modifiedCommandLineParams = dhi.setUpDebugHook(this.configutaion.getServiceContext(),
modifiedCommandLineParams, loader, this.debugMode);
} else {
modifyCommandLine(modifiedCommandLineParams, workingDir, outputFile, errorFile, event);
}
// JDK7 on Windows Specific modifications
// See CLOUDIFY-1787 for more details.
File tempBatchFile = null;
if (shouldModifyCommandLineForJDK7(outputFile)) {
tempBatchFile = createTempFileForJDK7(workingDir);
modifyCommandLineForJDK7ProcessBuilder(modifiedCommandLineParams, outputFile, workingDir, tempBatchFile);
}
final String modifiedCommandLine =
org.springframework.util.StringUtils.collectionToDelimitedString(modifiedCommandLineParams,
" ");
this.commandLine = modifiedCommandLineParams;
int attempt = 1;
USMException ex = null;
while (attempt <= retries + 1) {
final ProcessBuilder pb = new ProcessBuilder(modifiedCommandLineParams);
pb.directory(workingDir);
pb.redirectErrorStream(redirectErrorStream);
final Map<String, String> env = createEnvironment();
pb.environment().putAll(env);
try {
logger.fine("Parsed command line: " + commandLineParams.toString());
final String fileInitialMessage =
"Starting service process in working directory:'" + workingDir + "' "
+ "at:'" + new Date() + "' with command:'" + modifiedCommandLineParams + "'"
+ System.getProperty("line.separator");
if (outputFile != null) {
appendMessageToFile(fileInitialMessage,
outputFile);
}
if (errorFile != null) {
appendMessageToFile(fileInitialMessage,
errorFile);
}
return pb.start();
} catch (final IOException e) {
ex = new USMException("Failed to start process with command line: " + modifiedCommandLine, e);
logger.log(Level.SEVERE,
"Process start attempt number " + attempt + " failed",
ex);
}
++attempt;
}
throw ex;
}
private File createTempFileForJDK7(File workingDir) throws USMException {
try {
final File file = File.createTempFile("ProcessLauncherScript", ".bat", workingDir);
file.deleteOnExit();
return file;
} catch (IOException e) {
throw new USMException("Failed to create temporary batch file for command execution. Error was: "
+ e.getMessage(), e);
}
}
private void modifyCommandLineForJDK7ProcessBuilder(
List<String> modifiedCommandLineParams, File outputFile, File workingDir, File tempFile) {
// write the command to a batch file
final String actualCommand = StringUtils.join(modifiedCommandLineParams, " ");
try {
FileUtils.writeStringToFile(tempFile, "@" + actualCommand);
modifiedCommandLineParams.clear();
modifiedCommandLineParams.addAll(Arrays.asList(WINDOWS_BATCH_FILE_PREFIX_PARAMS));
modifiedCommandLineParams.add(tempFile.getAbsolutePath());
} catch (IOException e) {
throw new IllegalStateException("Unable to create a temp file for script launching: " + e.getMessage(), e);
}
}
private boolean shouldModifyCommandLineForJDK7(File outputFile) {
if (!ServiceUtils.isWindows()) {
// issue is only relevant on windows.
return false;
}
if (outputFile == null) {
// if redirect to file is not used, there is no need to change anything.
return false;
}
return true;
}
private void appendMessageToFile(final String msg, final File file)
throws IOException {
FileWriter writer = null;
try {
writer = new FileWriter(file, true);
writer.write(msg);
} finally {
if (writer != null) {
writer.close();
}
}
}
private Map<String, String> createEnvironment() {
final Map<String, String> map = new HashMap<String, String>();
final String groupsProperty = getGroupsProperty();
map.put("LOOKUPGROUPS",
groupsProperty);
final String locatorsProperty = getLocatorsProperty();
if (locatorsProperty != null) {
map.put("LOOKUPLOCATORS",
locatorsProperty);
}
if (this.clusterInfo == null) {
logger.warning("ClusterInfo in Process Launcher is null. "
+ "Child process will have missing environment variables");
} else {
if (clusterInfo.getName() != null) {
map.put(CloudifyConstants.USM_ENV_CLUSTER_NAME,
clusterInfo.getName());
final FullServiceName fullServiceName = ServiceUtils.getFullServiceName(clusterInfo.getName());
map.put(CloudifyConstants.USM_ENV_APPLICATION_NAME,
fullServiceName.getApplicationName());
map.put(CloudifyConstants.USM_ENV_SERVICE_NAME,
fullServiceName.getServiceName());
} else {
logger.warning("PU Name in ClusterInfo is null. "
+ "This should never happen as of 2.5.0");
map.put(CloudifyConstants.USM_ENV_CLUSTER_NAME,
ServiceUtils.getAbsolutePUName(CloudifyConstants.DEFAULT_APPLICATION_NAME,
"USM"));
map.put(CloudifyConstants.USM_ENV_APPLICATION_NAME,
CloudifyConstants.DEFAULT_APPLICATION_NAME);
map.put(CloudifyConstants.USM_ENV_SERVICE_NAME,
"USM");
}
map.put(CloudifyConstants.USM_ENV_PU_UNIQUE_NAME,
clusterInfo.getUniqueName());
map.put(CloudifyConstants.USM_ENV_INSTANCE_ID,
"" + clusterInfo.getInstanceId());
map.put(CloudifyConstants.USM_ENV_NUMBER_OF_INSTANCES,
"" + clusterInfo.getNumberOfInstances());
map.put(CloudifyConstants.USM_ENV_RUNNING_NUMBER,
"" + clusterInfo.getRunningNumber());
map.put(CloudifyConstants.USM_ENV_SERVICE_FILE_NAME,
"" + this.configutaion.getServiceFile().getName());
}
if (groovyEnvironmentClassPath != null && !groovyEnvironmentClassPath.isEmpty()) {
map.put("CLASSPATH",
this.groovyEnvironmentClassPath);
}
// logger.info("Child process additional env variables: " + map);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Adding Environment to child process: " + map);
}
return map;
}
private String getGroupsProperty() {
String groupsProperty = System.getProperty("com.gs.jini_lus.groups");
if (groupsProperty == null) {
groupsProperty = System.getenv("LOOKUPGROUPS");
}
if (groupsProperty == null) {
groupsProperty = "gigaspaces-" + PlatformVersion.getVersionNumber();
}
return groupsProperty.replace("\"", "");
}
private String getLocatorsProperty() {
String property = System.getProperty("com.gs.jini_lus.locators");
if (property == null) {
property = System.getenv("LOOKUPLOCATORS");
}
if (property != null) {
property = property.replace("\"",
"");
}
return property;
}
@Override
public Object launchProcess(final ExecutableDSLEntry arg, final File workingDir, final Map<String, Object> params,
final LifecycleEvents event)
throws USMException {
return launchProcess(arg, workingDir, 0, true, params, event);
}
@Override
public Process launchProcessAsync(final ExecutableDSLEntry arg, final File workingDir, final File outputFile,
final File errorFile, final LifecycleEvents event)
throws USMException {
return launchAsync(arg, workingDir, 0, false, outputFile, errorFile, null, event);
}
@Override
public void setClusterInfo(final ClusterInfo clusterInfo) {
this.clusterInfo = clusterInfo;
}
/**
* Converts the specified environment variables to system properties. Each environment variable is expected to have
* this value pattern: -DsystemPropertyName=systemPropertyValue or this: -DsystemPropertyName=systemPropertyValue
* -DAnotherSystemPropertyName=anotherSystemPropertyValue
*
* @param envVarsList
* @return
*/
private static List<String> convertEnvVarsToSysPropsList(final List<String> envVarsList) {
final List<String> sysPropsList = new ArrayList<String>();
String envVarValue;
for (final String envVarName : envVarsList) {
envVarValue = System.getenv(envVarName);
if (envVarValue != null) {
final String[] sysProps = envVarValue.split("-D");
for (final String sysProp : sysProps) {
if (StringUtils.isNotBlank(sysProp)) {
sysPropsList.add("-D" + sysProp.trim());
}
}
}
}
return sysPropsList;
}
@Override
public void setBeanLevelProperties(final BeanLevelProperties beanLevelProperties) {
Properties contextProperties = beanLevelProperties.getContextProperties();
final String debugAllString = contextProperties.getProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_ALL);
final String debugEvents = contextProperties.getProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_EVENTS);
final String debugModeString = contextProperties.getProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_MODE);
logger.info("Debug All: " + debugAllString + ", Debug events: " + debugEvents + ", Debug mode: "
+ debugModeString);
if (debugAllString != null) {
this.debugAllEvents = Boolean.parseBoolean(debugAllString);
}
if (debugEvents != null) {
String[] parts = debugEvents.split(",");
final Set<LifecycleEvents> debugEventsSet = new HashSet<LifecycleEvents>();
for (String part : parts) {
final String trimmedEventName = part.trim();
final LifecycleEvents event = LifecycleEvents.getEventByName(trimmedEventName);
if (event == null) {
logger.warning("Unknown event name passed in debug configuration: " + trimmedEventName);
} else {
debugEventsSet.add(event);
}
}
this.debugEvents = debugEventsSet;
}
if (debugModeString != null) {
this.debugMode = DebugModes.nameOf(debugModeString);
if (this.debugMode == null) {
throw new IllegalArgumentException("Unknown debug mode: " + debugModeString);
}
}
logger.info("Debug event flags -- All: " + this.debugAllEvents + ", Events: " + this.debugEvents + ", Mode: "
+ this.debugMode);
// logger.info("SETTING DEBUG ALL EVENTS FLAG TO TRUE!");
// TODO - delete this
// this.debugAllEvents = true;
}
private boolean isDebugEvent(final LifecycleEvents event) {
return ServiceUtils.isLinuxOrUnix()
&& (this.debugAllEvents || this.debugEvents.contains(event))
&& !event.equals(LifecycleEvents.START);
}
private boolean isLocalCloud() {
// if this env var is set to "localcloud" - running in localcloud mode
String machineIdEnvVar = System.getenv(CloudifyConstants.GIGASPACES_CLOUD_MACHINE_ID);
return (machineIdEnvVar == null) || LOCALCLOUD.equalsIgnoreCase(machineIdEnvVar);
}
}