/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.satellite.services.attach;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectionNotification;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.apmrouter.metric.AgentIdentity;
import org.helios.apmrouter.satellite.services.cascade.Cascader;
import org.helios.apmrouter.util.SimpleLogger;
import org.helios.vm.VirtualMachine;
import org.helios.vm.VirtualMachineBootstrap;
import org.helios.vm.VirtualMachineDescriptor;
/**
* <p>Title: AttachService</p>
* <p>Description: Provides satellite <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/attach/">Attach Services</a></p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.satellite.AttachService</code></p>
*/
public class AttachService implements AttachServiceMBean, NotificationListener, NotificationFilter {
/** */
private static final long serialVersionUID = -2338420252724673586L;
/** Indicates if this VM was able to load the attach service */
public static final boolean available;
/** The Attach service bootstrap */
protected static final VirtualMachineBootstrap bootstrap;
/** The mounted virtual machines */
protected final Map<VirtualMachine, String> jvmMountPoints = new ConcurrentHashMap<VirtualMachine, String>();
/** The VM id for this JVM */
public static final String JVM_ID = "" + ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
/** The ObjectName template for AttachService MBean instances */
public static final String OBJECT_NAME_TEMPLATE = "org.helios.vm:service=JVM,name=%s,id=%s";
/** The cascade mountpoint template */
public static final String MOUNT_TEMPLATE = "//%s/%s/%s";
/** The regex to extract the mount point from an expired ObjectName */
public static final Pattern MOUNT_POINT_REGEX = Pattern.compile("(//.*?/.*?)/.*");
/** A white space splitter regex */
public static final Pattern WHITE_SPACE_EXPR = Pattern.compile("\\s+");
/** A display name splitter for main class names */
public static final Pattern SLASH_AND_DOT_EXPR = Pattern.compile("\\\\|/|\\.|\\s+");
/** A display name splitter for main jars */
public static final Pattern SLASH_EXPR = Pattern.compile("\\\\|/|\\s+");
/** The assigned cleaned name if one cannot be determined */
public static final String UNKNOWN = "Unknown";
/** The MBeanServer builder override system property */
public static final String MBEANSERVER_BUILDER_PROP = "javax.management.builder.initial";
/** The agent property name where we store an exception message on an agent deploy failure */
public static final String AGENT_DEPLOY_ERR_PROP = "javax.management.agent.deploy.error";
/** The agent deploy failure message delimiter */
public static final String AGENT_DEPLOY_ERR_DELIM = "\t~";
/** Known extensions that might be used as a JVM launch main class directive */
public static final Set<String> ARCHIVE_NAMES = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
"jar", "zip", "war", "ear", "sar"
)));
/** The agent property name for the jmx connector address */
public static final String JMX_CONN_ADDR = "com.sun.management.jmxremote.localConnectorAddress";
/** The agent property name for the JVM's main class */
public static final String JVM_MAIN_CLASS = "sun.java.command";
/** The system property name for the JVM's java version */
public static final String JVM_JAVA_VERSION = "java.version";
/** The system property name for the JVM's helios agent name */
public static final String JVM_HELIOS_NAME = "org.helios.agent";
/** All the attached virtual machines */
protected static final Map<AttachService, JMXConnector> VMS = new ConcurrentHashMap<AttachService, JMXConnector>();
/** The virtual machine instance for this JVM */
protected final VirtualMachine virtualMachine;
/** The virtual machine descriptor for this JVM */
protected final VirtualMachineDescriptor descriptor;
/** The cleaned display name */
protected final String cleanDisplayName;
/** The ObjectName that this instance will be registered with */
protected final ObjectName objectName;
/** The mount point for this JVM */
protected String mountPoint = null;
/** The mount id for this JVM */
protected String mountId = null;
/*
*
When a mounted and cascaded JVM stops, an MBeanServerNotification will be emitted by [JMImplementation:type=MBeanServerDelegate]
The notification type is: JMX.mbean.unregistered
A cascaded notification will look like:
javax.management.MBeanServerNotification
[source=JMImplementation:type=MBeanServerDelegate]
[type=JMX.mbean.unregistered]
[message=]
[mbeanName=//local.hserval/7361/JMImplementation:type=MBeanServerDelegate]
*/
/**
* Registers all the located JVMs except this one.
*/
public static void registerAll() {
if(!available) return;
for(VirtualMachineDescriptor vmd : VirtualMachineDescriptor.getVirtualMachineDescriptors()) {
if(JVM_ID.equals(vmd.id())) continue;
try {
final JVM jvm = new JVM(vmd);
// if(as.getLocalConnectorAddress()==null) {
// try { as.installManagementAgent(); } catch (Exception ex) {/* No Op */}
// }
// if(as.getLocalConnectorAddress()!=null) {
// try {
// JMXServiceURL surl = new JMXServiceURL(as.getLocalConnectorAddress());
// final JMXConnector connector = JMXConnectorFactory.connect(surl);
// connector.addConnectionNotificationListener(as, null, null );
// VMS.put(as, connector);
// } catch (Throwable ex) {/* No Op */}
// }
} catch (Exception ex) {
SimpleLogger.error("Failed to register JVM [", vmd.id(), "]", ex);
}
}
}
/**
* Mounts all the attached vms
*/
public static void mountAll() {
if(!Cascader.available) return;
for(AttachService as: VMS.keySet()) {
try { as.mount(); } catch (Throwable ex) {/* No Op */}
}
}
static {
VirtualMachineBootstrap _bootstrap = null;
boolean _available = false;
try {
_bootstrap = VirtualMachineBootstrap.getInstance();
_available = true;
SimpleLogger.info("AttachService Loaded");
} catch (Exception ex) {
_available = false;
_bootstrap = null;
SimpleLogger.info("AttachService Not Available");
}
available = _available;
bootstrap = _bootstrap;
}
/**
* Initializes the attach service
*/
public static void initAttachService() {
if(!available) {
SimpleLogger.warn("Unable to initialize AttachService.");
return;
}
StringBuilder b = new StringBuilder("Attach Service Located JVMs:");
for(VirtualMachineDescriptor vmd: VirtualMachine.list()) {
if(JVM_ID.equals(vmd.id())) {
b.append("\n\t").append(vmd.id()).append("\t:\t (ME) ").append(vmd.displayName());
} else {
b.append("\n\t").append(vmd.id()).append("\t:\t").append(vmd.displayName());
}
}
SimpleLogger.info(b);
}
/**
* Creates a new AttachService
* @param virtualMachine The virtual machine instance for this JVM
* @param descriptor The virtual machine descriptor for this JVM
*/
public AttachService(VirtualMachine virtualMachine, VirtualMachineDescriptor descriptor) {
if(JVM_ID.equals(descriptor.id())) {
throw new RuntimeException("DON'T try to register a VirtualMachine inside itself. It's not natural.", new Throwable());
}
this.descriptor = descriptor;
this.virtualMachine = virtualMachine;
cleanDisplayName = cleanDisplayName(this.descriptor);
objectName = JMXHelper.objectName(String.format(OBJECT_NAME_TEMPLATE, cleanDisplayName, this.virtualMachine.id()));
if(!JMXHelper.getHeliosMBeanServer().isRegistered(objectName)) {
JMXHelper.registerMBean(this, objectName);
}
try { JMXHelper.getHeliosMBeanServer().addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, this, this, null); } catch (Exception ex) {}
//if(Cascader.available) mount();
}
/**
* {@inheritDoc}
* @see javax.management.NotificationFilter#isNotificationEnabled(javax.management.Notification)
*/
@Override
public boolean isNotificationEnabled(Notification notification) {
if(notification!=null && (notification instanceof JMXConnectionNotification)) {
String type = notification.getType();
if(JMXConnectionNotification.CLOSED.equals(type) || JMXConnectionNotification.FAILED.equals(type)) {
return true;
}
}
if(!(notification instanceof MBeanServerNotification) || !notification.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) return false;
return mountPoint!=null && ((MBeanServerNotification)notification).getMBeanName().toString().startsWith(mountPoint);
}
/**
* {@inheritDoc}
* @see javax.management.NotificationListener#handleNotification(javax.management.Notification, java.lang.Object)
*/
@Override
public void handleNotification(Notification notification, Object handback) {
if(notification==null) return;
if(notification instanceof JMXConnectionNotification) {
String type = notification.getType();
if(JMXConnectionNotification.CLOSED.equals(type) || JMXConnectionNotification.FAILED.equals(type)) {
detach();
VMS.remove(this);
}
return;
}
Matcher matcher = MOUNT_POINT_REGEX.matcher(((MBeanServerNotification)notification).getMBeanName().toString());
if(matcher.matches() && mountPoint!=null) {
String _mountPoint = matcher.group(1);
if(mountPoint.equals(_mountPoint)) {
mountPoint = null;
SimpleLogger.info("Cascade Unloaded. Detaching JVM [", virtualMachine.id(), "]");
detach();
}
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#installManagementAgent()
*/
@Override
public synchronized void installManagementAgent() {
if(!getSystemProperties().getProperty(MBEANSERVER_BUILDER_PROP, "").isEmpty()) {
// this means an alternate mbeanserver-builder is probably in place.
// in some cases (like jboss4) this may mean that:
// the platform agent has a null default domain
// the platform agent may not have been created yet
installAltDomainManagementAgent("install-platform");
} else {
String connectorAddress = getLocalConnectorAddress();
if(connectorAddress==null || connectorAddress.trim().isEmpty()) {
String javaHome = getSystemProperties().getProperty("java.home");
String managementAgent = javaHome + "/lib/management-agent.jar";
virtualMachine.loadAgent(managementAgent, "com.sun.management.jmxremote");
}
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#installAltDomainManagementAgent()
*/
@Override
public synchronized void installAltDomainManagementAgent() {
installAltDomainManagementAgent((String[])null);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#installAltDomainManagementAgent(java.lang.String[])
*/
@Override
public synchronized void installAltDomainManagementAgent(String...directives) {
String agentFile = null;
try {
agentFile = AlternateDomainAgent.writeAgentJar();
} catch (Exception e) {
SimpleLogger.error("Failed to create AlternateDomainAgent jar", e);
throw new RuntimeException("Failed to create AlternateDomainAgent jar", e);
}
// FIXME: Should only do this once
try {
StringBuilder b = new StringBuilder("");
for(String s: directives) {
if(s==null || s.trim().isEmpty()) continue;
b.append(s.trim()).append("\n");
}
if(b.length()>0) b.deleteCharAt(b.length()-1);
virtualMachine.loadAgent(agentFile, b.toString());
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#mount(java.lang.String, java.lang.String)
*/
@Override
public void mount(String host, String agent) {
if(!jvmMountPoints.containsKey(virtualMachine)) {
synchronized(jvmMountPoints) {
if(!jvmMountPoints.containsKey(virtualMachine)) {
if(!Cascader.available) throw new RuntimeException("The cascading service is not available");
if(host==null || host.trim().isEmpty()) host = AgentIdentity.ID.getHostName();
if(agent==null || agent.trim().isEmpty()) {
agent = getHeliosAgentName();
if(agent==null || agent.trim().isEmpty()) {
agent = getId();
}
}
String connectorAddress = getLocalConnectorAddress();
JMXServiceURL serviceURL = null;
try {
if(connectorAddress==null || connectorAddress.trim().isEmpty()) {
installManagementAgent();
connectorAddress = getLocalConnectorAddress();
}
if(connectorAddress==null || connectorAddress.trim().isEmpty()) {
return;
}
serviceURL = new JMXServiceURL(connectorAddress);
JMXConnector connector = VMS.get(this);
if(connector==null) {
synchronized(VMS) {
connector = VMS.get(this);
if(connector==null) {
connector = JMXConnectorFactory.connect(serviceURL);
VMS.put(this, connector);
}
}
}
mountPoint = String.format(MOUNT_TEMPLATE, host, cleanDisplayName, connector.getMBeanServerConnection().getDefaultDomain());
if(!JMXHelper.getHeliosMBeanServer().queryMBeans(JMXHelper.objectName(mountPoint + "*:*"), null).isEmpty()) {
mountPoint = String.format(MOUNT_TEMPLATE, host, (cleanDisplayName + "#" + virtualMachine.id()), connector.getMBeanServerConnection().getDefaultDomain());
}
mountId = Cascader.mount(serviceURL, null, null, mountPoint);
jvmMountPoints.put(virtualMachine, mountPoint);
} catch (Exception ex) {
ex.printStackTrace(System.err);
throw new RuntimeException("Failed to mount JVM [" + virtualMachine.id() + "]", ex);
}
} else {
throw new RuntimeException("JVM [" + virtualMachine.id() + "] is already mounted");
}
}
} else {
throw new RuntimeException("JVM [" + virtualMachine.id() + "] is already mounted");
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#mount(java.lang.String)
*/
@Override
public void mount(String agent) {
mount(null, agent);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#mount()
*/
@Override
public void mount() {
mount(null, null);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#unmount()
*/
@Override
public void unmount() {
Cascader.unmount(mountId);
jvmMountPoints.remove(virtualMachine);
mountId = null;
mountPoint = null;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#detach()
*/
@Override
public void detach() {
virtualMachine.detach();
jvmMountPoints.remove(virtualMachine);
JMXHelper.unregisterMBean(objectName);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getAgentProperties()
*/
@Override
public Properties getAgentProperties() {
return virtualMachine.getAgentProperties();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getSystemProperties()
*/
@Override
public Properties getSystemProperties() {
return virtualMachine.getSystemProperties();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getDisplayName()
*/
@Override
public String getDisplayName() {
return descriptor.displayName();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getId()
*/
@Override
public String getId() {
return virtualMachine.id();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getProviderName()
*/
@Override
public String getProviderName() {
return virtualMachine.provider().name();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getProviderType()
*/
@Override
public String getProviderType() {
return virtualMachine.provider().type();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getLocalConnectorAddress()
*/
@Override
public String getLocalConnectorAddress() {
return virtualMachine.getAgentProperties().getProperty(JMX_CONN_ADDR);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getMainClass()
*/
@Override
public String getMainClass() {
return virtualMachine.getAgentProperties().getProperty(JVM_MAIN_CLASS);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getJavaVersion()
*/
@Override
public String getJavaVersion() {
return getSystemProperties().getProperty(JVM_JAVA_VERSION);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getHeliosAgentName()
*/
@Override
public String getHeliosAgentName() {
return getSystemProperties().getProperty(JVM_HELIOS_NAME);
}
/**
* Returns the mount point name for this jvm's cascade
* @return the mount point name for this jvm's cascade or null if it is not mounted
*/
@Override
public String getMountPoint() {
return mountPoint;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#getCleanDisplayName()
*/
@Override
public String getCleanDisplayName() {
return cleanDisplayName;
}
/**
* Returns the mount id for this jvm's cascade
* @return the id name for this jvm's cascade or null if it is not mounted
*/
@Override
public String getMountId() {
return mountId;
}
/**
* Determines if the passed display name has a recognized archive as the first argument in the display name
* @param displayName The display name to test
* @return true if the first arg of the display name is a recognized archive, false otherwise
*/
public static boolean hasArchiveMain(String displayName) {
if(displayName==null || displayName.trim().isEmpty()) return false;
String[] frags = SLASH_AND_DOT_EXPR.split(displayName);
if(frags!=null && frags.length>0) {
String ext = frags[frags.length-1];
if(ext!=null && !ext.trim().isEmpty()) {
return ARCHIVE_NAMES.contains(ext.trim().toLowerCase());
}
}
return false;
}
/**
* Examines the VM display name (command line main class) and attempts to clean it up for use as a key
* @param vmd The VirtualMachineDescriptor of the target JVM
* @return The cleaned name, or <b><code>"Unknown"</code></b> if the display name did not conform to a recognized pattern
*/
public static String cleanDisplayName(VirtualMachineDescriptor vmd) {
if(vmd==null) return UNKNOWN;
String display = vmd.displayName();
String[] dfragments = WHITE_SPACE_EXPR.split(display);
if(dfragments.length>0 && dfragments[0]!=null && !dfragments[0].trim().isEmpty()) {
String name = UNKNOWN;
if(hasArchiveMain(dfragments[0])) {
dfragments = SLASH_EXPR.split(dfragments[0]);
} else {
dfragments = SLASH_AND_DOT_EXPR.split(dfragments[0]);
}
name = dfragments[dfragments.length-1];
return name;
}
return UNKNOWN;
}
}