/*
* Copyright 2008 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 org.rioproject.impl.exec;
import com.sun.jini.config.Config;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import org.rioproject.exec.ExecDescriptor;
import org.rioproject.servicebean.ServiceBean;
import org.rioproject.servicebean.ServiceBeanContext;
import org.rioproject.deploy.ServiceBeanInstantiationException;
import org.rioproject.impl.jmx.JMXConnectionUtil;
import org.rioproject.impl.jmx.JMXUtil;
import org.rioproject.impl.servicebean.ServiceElementUtil;
import org.rioproject.opstring.ServiceElement;
import org.rioproject.sla.SLA;
import org.rioproject.system.ComputeResourceUtilization;
import org.rioproject.system.MeasuredResource;
import org.rioproject.system.SystemWatchID;
import org.rioproject.system.capability.PlatformCapability;
import org.rioproject.impl.system.measurable.SigarHelper;
import org.rioproject.impl.system.measurable.cpu.CPU;
import org.rioproject.impl.system.measurable.cpu.ProcessCPUHandler;
import org.rioproject.impl.system.measurable.memory.Memory;
import org.rioproject.impl.system.measurable.memory.ProcessMemoryMonitor;
import org.rioproject.watch.WatchDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.MBeanServerConnection;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Provides support to execute an external service. If the external service is
* a Java Virtual Machine, this utility will try to attach to the Java Virtual
* Machine using the
* <a href="http://java.sun.com/javase/6/docs/technotes/guides/attach/index.html">
* JMX Attach API</a>. This is only possible using Java 6 or greater.
* If the runtime is Java 5, external service utilization monitoring is not
* provided.
*
* <p>In order to obtain the process identifier required to attach to
* the exec'd JVM, the external process may create a pid file, containing the
* process identifier of the process. If this is the case, then configuring the
* deployment to declare the path of the produced pid file can be done.
*
* <p>If the exec'd process does not create a pid file,
* <a href="http://www.hyperic.com/products/sigar.html">SIGAR</a>
* is used to traverse the process tree to identify the parent process that
* exec'd the child JVM. If the parent process can be identified
* (see {@link org.rioproject.impl.system.measurable.SigarHelper#matchChild(int, String[])} the
* <tt>ServiceExecutor</tt> will attach to the JVM, and monitor CPU and Memory
* utilization. <a href="http://www.hyperic.com/products/sigar.html">SIGAR</a>
* is also used to monitor the real memory used by the exec'd JVM.
*
* <p>This class also provides configuration support for the following entries:
*
* <ul>
* <li><span
* style="font-weight: bold; font-family: courier new,courier,monospace;">shellTemplate</span><br
*style="font-weight: bold; font-family: courier new,courier,monospace;">
* <table style="text-align: left; width: 100%;" border="0"
* cellpadding="2" cellspacing="2">
* <tbody>
* <tr>
* <td
* style="vertical-align: top; text-align: right; font-weight: bold;">
* Type:<br>
* </td>
* <td style="vertical-align: top;">String<br>
* </td>
* </tr>
* <tr>
* <td
* style="vertical-align: top; text-align: right; font-weight: bold;">
* Default:<br>
* </td>
* <td style="vertical-align: top;"><span
* style="font-family: monospace;">exec-template.sh</span><br>
* </td>
* </tr>
* <tr>
* <td
* style="vertical-align: top; text-align: right; font-weight: bold;">
* Description:<br>
* </td>
* <td style="vertical-align: top;">The template to use for
* generating a script to exec a command. The script template must be
* loadable as a resource, and must provide the following token that get
* replaced by runtime values:<br>
* <br>
* <span style="font-family: monospace; font-weight: bold;">${command}</span>
* The command to execute. This token eirs placed by the command to
* execute.<br>
* <span style="font-family: monospace;">$<span
* style="font-weight: bold;">{pidFile}</span></span> Rio creates a file
* that stores the pid of the executed command. This token is replaced by
* the name of the pid file to create<br>
* <span style="font-family: monospace; font-weight: bold;">${commandLine}</span>
* This token is replaced by the actual command line that is created to
* 'exec' the command above. Inoput arguments, standard error and output
* are also part of the created command line<br>
* </td>
* </tr>
* </tbody>
* </table>
* </li>
* </ul>
* <ul>
* <li>
* <span style="font-weight: bold; font-family: courier new,courier,monospace;">pidFileWaitTime</span>
* <br style="font-weight: bold; font-family: courier new,courier,monospace;">
* <table style="text-align: left; width: 100%;" border="0" cellpadding="2" cellspacing="2">
* <tbody>
* <tr>
* <td style="vertical-align: top; text-align: right; font-weight: bold;">
* Type:<br>
* </td>
* <td style="vertical-align: top;">int<br>
* </td>
* </tr>
* <tr>
* <td style="vertical-align: top; text-align: right; font-weight: bold;">
* Default:<br>
* </td>
* <td style="vertical-align: top;"><span style="font-family: monospace;">60 </span><br>
* </td>
* </tr>
* <tr>
* <td style="vertical-align: top; text-align: right; font-weight: bold;">
* Description:<br>
* </td>
* <td style="vertical-align: top;">
* The amount of time to wait for an exec'd service to create a pid file.
* The value is in seconds, where 60 seconds is the default. The minimum
* value for this property is 5 seconds, the maximum allowed 5 minutes.<br>
* <br>
* This value represents the maximum amount of time to wait. If the pid file is
* created prior to the timeout value, the utility proceeds immediately.<br>
* </td>
* </tr>
* </tbody>
* </table>
* </li>
* </ul>
*
* @author Dennis Reedy
*/
public class ServiceExecutor {
private ServiceBean serviceBean;
private ProcessManager processManager;
private ExecDescriptor execDescriptor;
private String shellTemplate;
private SigarHelper sigar;
private Memory memory;
private CPU cpu;
private long actualPID=-1;
private MBeanServerConnection mbsc;
private ServiceBeanContext context;
private Configuration config;
private static final String COMPONENT = ServiceExecutor.class.getPackage().getName();
private int pidFileWaitTime = 60; // number of seconds
static final Logger logger = LoggerFactory.getLogger(COMPONENT);
public ServiceExecutor() {
sigar = SigarHelper.getInstance();
}
public void setServiceBeanContext(final ServiceBeanContext context)
throws ServiceBeanInstantiationException, IOException, ConfigurationException {
this.context = context;
this.config = context.getConfiguration();
try {
shellTemplate = (String)context.getConfiguration().getEntry(COMPONENT,
"shellTemplate",
String.class,
null);
} catch (ConfigurationException e) {
logger.warn("Cannot get shell template from configuration, continue with default");
}
try {
pidFileWaitTime = Config.getIntEntry(context.getConfiguration(),
COMPONENT,
"pidFileWaitTime",
60, //default is 1 minute
5, //minimum of 5 second wait
60 * 5); // max of 5 minute wait
} catch(ConfigurationException e) {
logger.warn("Getting pidFileWaitTime, using default", e);
}
execDescriptor = context.getServiceElement().getExecDescriptor();
if(execDescriptor==null)
throw new ServiceBeanInstantiationException("An ExecDescriptor is required " +
"by the ServiceExecutor," +
" unable to proceed.");
String cmdLine = getCommandLine(execDescriptor);
if(!cmdLine.startsWith(File.separator)) {
PlatformCapability[] pCaps =
context.getComputeResourceManager().getMatchedPlatformCapabilities();
boolean matched = false;
for(PlatformCapability pCap : pCaps) {
if(pCap.getPath()!=null) {
File toExec = new File(pCap.getPath(), cmdLine);
if(toExec.exists() && toExec.canRead()) {
matched = true;
if(logger.isInfoEnabled()) {
logger.info("Adding PlatformCapability PATH [{}] to declared command line [{}]",
pCap.getPath(), cmdLine);
}
execDescriptor = Util.extendCommandLine(pCap.getPath(), execDescriptor);
break;
}
}
}
if(!matched) {
throw new ServiceBeanInstantiationException(
"ExecDescriptor with command line " +
"["+execDescriptor.getCommandLine()+"] " +
"cannot " +
"be executed, no associated PlatformCapability " +
"found");
}
} else {
if(logger.isInfoEnabled()) {
logger.info("Using command line [{}]", execDescriptor.getCommandLine());
}
}
File toExec = new File(execDescriptor.getCommandLine());
if(!toExec.exists())
throw new ServiceBeanInstantiationException("The command line ["+
execDescriptor.getCommandLine()+
"] can not be found, " +
"unable to continue. Check " +
"that the directory structure " +
"matches that as found on the " +
"executing platform. If the " +
"ServiceExec is a result of " +
"software downloading, make sure " +
"that the installation is " +
"correct and that downloaded " +
"software has been extracted");
exec();
processManager.manage();
String pidFileName = execDescriptor.getPidFile();
if(pidFileName!=null) {
logger.info("Try to obtain actual pid of exec'd process using pid file: "+pidFileName);
long waited = 0;
while(waited < pidFileWaitTime) {
File pidFile = new File(pidFileName);
if(pidFile.exists()) {
actualPID = readPidFromFile(pidFile);
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.warn("Waiting for pid file to appear, abort wait", e);
break;
}
waited++;
}
}
if(actualPID==-1) {
long managedPID = processManager.getPid();
long waited = 0;
long t0 = System.currentTimeMillis();
while(waited < 5) {
String[] ids;
try {
ids = VirtualMachineHelper.listIDs();
} catch (Exception e) {
logger.error("Cannot load the Attach API", e);
break;
}
StringBuilder s = new StringBuilder();
for(int i=0; i<ids.length; i++) {
if(i>0)
s.append(", ");
s.append(ids[i]);
}
System.out.println("JMX pids: ["+s.toString()+"]");
long[] pids = new long[ids.length];
for(int i=0; i<ids.length; i++)
pids[i] = new Long(ids[i]);
/* First check to see if the actualPID is in the list of JMX managed pids */
for(long pid : pids) {
if(pid==managedPID) {
actualPID = managedPID;
long t1 = System.currentTimeMillis();
System.out.println("Time waiting for process to be under JMX management: "+(t1/t0)+" milliseconds");
break;
}
}
if(actualPID!=-1)
break;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.warn("Waiting for pid to appear under JMX management, abort wait", e);
break;
}
waited++;
}
}
if(sigar!=null) {
if(actualPID==-1) {
logger.info("Try to obtain actual pid of exec'd process using SIGAR");
String[] ids = new String[0];
try {
ids = VirtualMachineHelper.listIDs();
} catch (Exception e) {
logger.error("Cannot load the Attach API", e);
}
actualPID = sigar.matchChild(processManager.getPid(), ids);
}
} else {
if(actualPID==-1)
logger.warn("No SIGAR support available, unable to obtain PID");
}
if(actualPID!=-1) {
logger.info("PID of exec'd process obtained: "+actualPID);
try {
mbsc = JMXConnectionUtil.attach(Long.toString(actualPID));
logger.info("JMX Attach succeeded to exec'd JVM with pid: "+actualPID);
createSystemWatches();
setThreadDeadlockDetector(context.getServiceElement());
checkWatchDescriptors(context.getServiceElement());
} catch(Exception e) {
logger.warn("Could not attach to the exec'd JVM with PID: " +
actualPID +
", continue service execution", e);
}
} else {
logger.info("Could not obtain actual PID of exec'd process, " +
"process cpu and java memory utilization are not available");
}
}
public ComputeResourceUtilization getComputeResourceUtilization() {
List<MeasuredResource> mRes;
if(memory!=null && cpu!=null) {
mRes = new ArrayList<MeasuredResource>();
mRes.add(memory.getMeasuredResource());
mRes.add(cpu.getMeasuredResource());
} else {
mRes = Collections.unmodifiableList(new ArrayList<MeasuredResource>());
}
return new ComputeResourceUtilization("", "", "", mRes);
}
private void setThreadDeadlockDetector(final ServiceElement elem) {
ServiceElementUtil.setThreadDeadlockDetector(elem, mbsc);
}
private void createSystemWatches() {
if(sigar!=null) {
int pidToUse = (int)(actualPID==-1?processManager.getPid():actualPID);
memory = new Memory(config);
ProcessMemoryMonitor memMonitor =
(ProcessMemoryMonitor)memory.getMeasurableMonitor();
memMonitor.setPID(pidToUse);
if(mbsc!=null) {
MemoryMXBean memBean =
getPlatformMXBeanProxy(mbsc,
ManagementFactory.MEMORY_MXBEAN_NAME,
MemoryMXBean.class);
memMonitor.setMXBean(memBean);
} else {
memMonitor.setMXBean(null);
}
memory.start();
cpu = new CPU(config, SystemWatchID.PROC_CPU, true);
ProcessCPUHandler cpuMonitor = (ProcessCPUHandler)cpu.getMeasurableMonitor();
cpuMonitor.setPID(pidToUse);
OperatingSystemMXBean opSys =
getPlatformMXBeanProxy(mbsc,
ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME,
OperatingSystemMXBean.class);
cpuMonitor.setMXBean(opSys);
RuntimeMXBean runtime =
getPlatformMXBeanProxy(mbsc,
ManagementFactory.RUNTIME_MXBEAN_NAME,
RuntimeMXBean.class);
cpuMonitor.setStartTime(runtime.getStartTime());
cpu.start();
context.getWatchRegistry().register(memory, cpu);
}
}
private <T> T getPlatformMXBeanProxy(final MBeanServerConnection mbsc,
final String name,
final Class<T> mxBeanInterface) {
return JMXUtil.getPlatformMXBeanProxy(mbsc, name, mxBeanInterface);
}
private void checkWatchDescriptors(ServiceElement elem) {
SLA[] slas = elem.getServiceLevelAgreements().getServiceSLAs();
for(SLA sla : slas) {
WatchDescriptor[] wDescs = sla.getWatchDescriptors();
for(WatchDescriptor wDesc : wDescs) {
if(wDesc.getObjectName()!=null)
wDesc.setMBeanServerConnection(mbsc);
}
}
}
private String getCommandLine(final ExecDescriptor exec) {
String cmd;
if(exec.getWorkingDirectory()!=null) {
String wd = exec.getWorkingDirectory();
if(wd.endsWith(File.separator))
cmd = wd + exec.getCommandLine();
else
cmd = wd + File.separator + exec.getCommandLine();
} else {
cmd = exec.getCommandLine();
}
return cmd;
}
@SuppressWarnings("unused")
public void setExecDescriptor(final ExecDescriptor execDescriptor) {
if (execDescriptor == null)
throw new IllegalArgumentException("ExecDescriptor is null");
this.execDescriptor = execDescriptor;
}
@SuppressWarnings("unused")
public void setServiceBean(final ServiceBean serviceBean) {
this.serviceBean = serviceBean;
}
@SuppressWarnings("unused")
public void preDestroy() {
terminate();
}
public ProcessManager exec() throws IOException {
if(execDescriptor==null)
throw new IllegalStateException("execDescriptor is not set");
return exec(execDescriptor);
}
public ProcessManager exec(final ExecDescriptor exDesc) throws IOException {
Shell shell = ShellFactory.createShell();
if(shellTemplate!=null)
shell.setShellTemplate(shellTemplate);
processManager = shell.exec(exDesc);
processManager.registerListener(new ProcessManager.Listener() {
public void processTerminated(int pid) {
if(logger.isDebugEnabled())
logger.debug("Process [{}] terminated", pid);
if(serviceBean!=null)
serviceBean.destroy(true);
}
});
return processManager;
}
/**
* Close the shell and release all used resources
*/
public synchronized void terminate() {
if(memory!=null)
memory.stop();
if(cpu!=null)
cpu.stop();
if (processManager != null) {
processManager.destroy(true);
}
}
public long readPidFromFile(File f) throws IOException {
long pid = -1;
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(f));
String line = in.readLine().trim();
pid = Long.valueOf(line);
} finally {
if(in!=null)
in.close();
}
return pid;
}
public static void main(String[] args) {
ExecDescriptor exDesc;
try {
if(args.length>0 && args[0].endsWith("config")) {
Configuration config = ConfigurationProvider.getInstance(args);
exDesc = (ExecDescriptor) config.getEntry(COMPONENT,
"descriptor",
ExecDescriptor.class,
null);
} else {
exDesc = new ExecDescriptor();
exDesc.setCommandLine("${RIO_HOME}/bin/cybernode");
exDesc.setWorkingDirectory("${RIO_HOME}/bin");
exDesc.setStdErrFileName("${RIO_HOME}/logs/cybernode.log");
exDesc.setStdOutFileName("${RIO_HOME}/logs/cybernode.log");
}
final ServiceExecutor svcExecutor = new ServiceExecutor();
try {
ProcessManager manager = svcExecutor.exec(exDesc);
manager.manage();
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//svcExecutor.terminate();
}
}).start();
manager.getProcess().waitFor();
System.out.println("Manager returned from waitFor()");
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (ConfigurationException e) {
e.printStackTrace();
}
}
}