/*******************************************************************************
* 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;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.math.NumberUtils;
import org.cloudifysource.dsl.utils.ServiceUtils;
import org.cloudifysource.utilitydomain.admin.TimedAdmin;
import org.hyperic.sigar.FileInfo;
import org.hyperic.sigar.ProcState;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;
import org.jini.rio.boot.ServiceClassLoader;
import org.openspaces.admin.esm.ElasticServiceManager;
import org.openspaces.admin.gsc.GridServiceContainer;
import org.openspaces.admin.gsm.GridServiceManager;
import org.openspaces.admin.pu.ProcessingUnit;
import org.openspaces.admin.space.Space;
import org.openspaces.pu.container.support.ResourceApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import com.gigaspaces.internal.sigar.SigarHolder;
/*********
* Utility class for the USM package.
*
* @author barakme
*
*/
public final class USMUtils {
// private static final int POST_SYNC_PROCESS_SLEEP_INTERVAL = 500;
private USMUtils() {
// private constructor to prevent instantiation.
}
private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger(USMUtils.class.getName());
private static TimedAdmin timedAdmin;
/********
* Marks a file as executable by the operating system. On windows, always returns true. Otherwise, attempts will be
* made to mark the file as executable using Java 6 API, if possible. If not, will attempt to use sigar. If this
* fails, a chmod command will be executed.
*
* @param sigar
* an instance of sigar.
* @param file
* the file to make as executable.
* @return true if the file was successfully marked as executable, false otherwise.
* @throws USMException
* in case of an error.
*/
public static boolean markFileAsExecutable(final Sigar sigar, final File file)
throws USMException {
if (!file.exists()) {
return false;
}
if (ServiceUtils.isWindows()) {
return true; // not used in windows
}
if (USMUtils.markFileAsExecutableUsingJdk(file)) {
return true;
}
if (USMUtils.isFileExecutableSigar(file)) {
return true;
}
logger.info("File " + file + " is not executable according to sigar");
return USMUtils.markFileAsExecutableUsingChmod(file);
}
private static boolean markFileAsExecutableUsingJdk(final File file)
throws USMException {
// TODO - refactor this - just catch exception on the whole block
logger.fine("Attempting to set file as executable using REFLECTION");
Method method = null;
try {
method = File.class.getMethod("setExecutable", new Class[] { Boolean.TYPE });
} catch (final SecurityException e) {
throw new USMException("Failed to check if file " + file.getAbsolutePath() + " is executable", e);
} catch (final NoSuchMethodException e) {
// ignore - this can happen
}
if (method == null) {
logger.warning("setExecutable not supported by JDK. "
+ "GigaSpaces Cloudify should be executed on Java Version >= 1.6");
return false;
}
try {
final boolean retval = (Boolean) method.invoke(file, true);
logger.fine("File " + file + " was marked as executable");
if (!retval) {
logger.warning("File: " + file + " is not executable. "
+ "Attempt to mark it as executable has failed. "
+ "Execution of this command may not be possible");
}
return retval;
} catch (final IllegalArgumentException e) {
throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
} catch (final IllegalAccessException e) {
throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
} catch (final InvocationTargetException e) {
throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
}
}
private static boolean isFileExecutableSigar(final File file)
throws USMException {
long mode;
try {
logger.info("Checking if file " + file + " is executable using sigar");
mode = SigarHolder.getSigar().getFileInfo(file.getAbsolutePath()).getMode();
} catch (final SigarException e) {
throw new USMException("Failed to check if file " + file.getAbsolutePath() + " is executable", e);
}
final long executable = mode & FileInfo.MODE_UEXECUTE;
return executable > 0;
}
private static boolean markFileAsExecutableUsingChmod(final File file)
throws USMException {
try {
logger.info("File " + file + " will be marked as executable using chmod command");
final Process p = Runtime.getRuntime().exec(new String[] { "chmod", "+x", file.getAbsolutePath() });
final int result = p.waitFor();
logger.info("chmod command finished with result = " + result);
if (result != 0) {
final String msg =
"chmod command did not terminate successfully. File " + file + " may not be executable!";
logger.warning(msg);
}
return result == 0;
} catch (final IOException e) {
throw new USMException("Failed to execute chmod command: chmod +x " + file.getAbsolutePath(), e);
} catch (final InterruptedException e) {
throw new USMException("Failed to execute chmod command on file " + file, e);
}
}
/*******
* Returns true if current process is a GSC.
*
* @param ctx
* the spring application context.
* @return .
*/
public static boolean isRunningInGSC(final ApplicationContext ctx) {
return ctx.getClassLoader() instanceof ServiceClassLoader;
}
/*************
* Return's the working directory of a PU.
*
* @param ctx
* the spring application context.
* @return the working directory.
*/
public static File getPUWorkDir(final ApplicationContext ctx) {
File puWorkDir = null;
if (isRunningInGSC(ctx)) {
// running in GSC
final ServiceClassLoader scl = (ServiceClassLoader) ctx.getClassLoader();
final URL url = scl.getSlashPath();
URI uri;
try {
uri = url.toURI();
} catch (final URISyntaxException e) {
throw new IllegalArgumentException("Failed to create URI from URL: " + url, e);
}
puWorkDir = new File(uri);
} else {
final ResourceApplicationContext rac = (ResourceApplicationContext) ctx;
try {
final Field resourcesField = rac.getClass().getDeclaredField("resources");
final boolean accessibleBefore = resourcesField.isAccessible();
resourcesField.setAccessible(true);
final Resource[] resources = (Resource[]) resourcesField.get(rac);
for (final Resource resource : resources) {
// find META-INF/spring/pu.xml
final File file = resource.getFile();
if (file.getName().equals("pu.xml") && file.getParentFile().getName().equals("spring")
&& file.getParentFile().getParentFile().getName().equals("META-INF")) {
puWorkDir = resource.getFile().getParentFile().getParentFile().getParentFile();
break;
}
}
resourcesField.setAccessible(accessibleBefore);
} catch (final Exception e) {
throw new IllegalArgumentException("Could not find pu.xml in the ResourceApplicationContext", e);
}
if (puWorkDir == null) {
throw new IllegalArgumentException("Could not find pu.xml in the ResourceApplicationContext");
}
}
if (!puWorkDir.exists()) {
throw new IllegalStateException("Could not find PU work dir at: " + puWorkDir);
}
final File puExtDir = new File(puWorkDir, "ext");
if (!puExtDir.exists()) {
throw new IllegalStateException("Could not find PU ext dir at: " + puExtDir);
}
return puWorkDir;
}
/**
* Returns a cached instance of TimedAdmin, creates it if necessary.
* @return a cached instance of TimedAdmin
*/
public static synchronized TimedAdmin getTimedAdmin() {
if (timedAdmin != null) {
logger.fine("using a cached instance of TimedAdmin");
return timedAdmin;
}
logger.fine("creating a new instance of TimedAdmin");
timedAdmin = new TimedAdmin();
// useful for unit tests
timedAdmin.discoverUnmanagedSpaces();
// Don't discover GridServiceAgent objects, not required for the USM and could potentially generate a lot
// of objects in memory and multiple network connections
final Class[] discoveryServices = new Class[] { GridServiceManager.class, GridServiceContainer.class,
ElasticServiceManager.class, ProcessingUnit.class, Space.class };
timedAdmin.setDiscoveryServices(discoveryServices);
timedAdmin.setStatisticsHistorySize(0);
return timedAdmin;
}
/*********
* Returns the list of parent processes, starting by the child pid and ending with the current pid.
*
* @param childPid
* the list starting point.
* @param myPid
* the curennt process pid.
* @return the pid list.
* @throws USMException
* in case of an error.
*/
public static List<Long> getProcessParentChain(final long childPid, final long myPid)
throws USMException {
final LinkedList<Long> list = new LinkedList<Long>();
if (childPid == 0) {
logger.warning("The child PID is 0 - this usually means that this USM instance had a problem "
+ "during startup. Please check the logs for more details.");
return list;
}
long currentPid = childPid;
while (currentPid != myPid) {
list.add(currentPid);
final long ppid = USMUtils.getParentPid(currentPid);
if (ppid == 0) {
logger.severe("Attempt to create a process chain from child process " + childPid + " this process("
+ myPid + "). Process " + childPid + " is not a descendant of this process. Only process "
+ childPid + " will be included in the chain");
list.clear();
list.add(childPid);
return list;
}
currentPid = ppid;
}
return list;
}
/******
* Returns the parent pid for the given pid.
*
* @param pid
* the process pid.
* @return the parent pid for the given pid.
* @throws USMException
* in case of an error.
*/
public static long getParentPid(final long pid)
throws USMException {
ProcState procState = null;
procState = USMUtils.getProcState(pid);
if (procState == null) {
return 0;
}
return procState.getPpid();
}
/*********
* Checks, using Sigar, is a given process is alive.
*
* @param pid
* the process pid.
* @return true if the process is alive (i.e. not stopped or zombie).
* @throws USMException
* in case of an error.
*/
public static boolean isProcessAlive(final long pid)
throws USMException {
final ProcState procState = USMUtils.getProcState(pid);
return (procState != null && procState.getState() != ProcState.STOP && procState.getState() != ProcState.ZOMBIE);
}
private static ProcState getProcState(final long pid)
throws USMException {
final Sigar sigar = SigarHolder.getSigar();
ProcState procState = null;
try {
procState = sigar.getProcState(pid);
} catch (final SigarException e) {
if ("No such process".equals(e.getMessage())) {
return null;
}
throw new USMException("Failed to check if process with PID: " + pid + " is alive. Error was: "
+ e.getMessage(), e);
}
return procState;
}
/***********
* Given a map, returns a new map where the values are numbers, matching entries from the original list which were
* numbers or strings that can be parsed as numbers. Other entries are discarded.
*
* @param map
* the original map.
* @return the numbers map.
*/
// converts a map of type <String, Object> to a Map of <String, Number>
public static Map<String, Number> convertMapToNumericValues(final Map<String, Object> map) {
final Map<String, Number> returnMap = new HashMap<String, Number>();
for (final Map.Entry<String, Object> entryObject : map.entrySet()) {
if (entryObject.getValue() instanceof Number) {
returnMap.put(entryObject.getKey(), (Number) entryObject.getValue());
} else if (entryObject.getValue() instanceof String) {
if (NumberUtils.isNumber((String) entryObject.getValue())) {
final Number number = NumberUtils.createNumber((String) entryObject.getValue());
returnMap.put(entryObject.getKey(), number);
}
} else {
logger.info("monitor value is expected to be numeric: " + entryObject.getValue().toString());
}
}
return returnMap;
}
/**********
* Closes the cached admin instance.
*
*/
public static synchronized void shutdownAdmin() {
timedAdmin.close();
logger.info("USM timed Admin instance was shut down");
}
/*********
* Returns the exit code of the given process handle, or null if the process has not terminated.
*
* @param processToCheck
* the process.
* @return the exit code, or null.
*/
public static Integer getProcessExitCode(final Process processToCheck) {
if (processToCheck == null) {
return null;
}
try {
int val = processToCheck.exitValue();
return val;
} catch (final Exception e) {
return null;
}
}
}