/**
* 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.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import javax.management.MBeanNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;
import org.helios.apmrouter.jmx.ConfigurationHelper;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.vm.VirtualMachine;
import org.helios.vm.VirtualMachineDescriptor;
import org.helios.vm.VirtualMachineInvocationException;
/**
* <p>Title: JVM</p>
* <p>Description: Represents an attachable Java Virtual Machine.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.satellite.services.attach.JVM</code></p>
*/
public class JVM extends NotificationBroadcasterSupport implements JVMMBean {
/** 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";
/** 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~";
/** The System prop/env variable override for the JMX notification thread pool size */
public static final String NOTIF_THREAD_POOL_PROP = "javax.management.agent.deploy.error";
/** The default JMX notification thread pool size */
public static final int NOTIF_THREAD_POOL_SIZE = 2;
/** The JMX notification id serial */
public static final AtomicLong notificationSerial = new AtomicLong(0L);
/** The JVM attached notification type prefix*/
public static String JVM_ATTACH_NOTIF_PREFIX = "org.helios.jvms.attach";
/** The JVM attached notification type */
public static String JVM_ATTACHED_NOTIF = JVM_ATTACH_NOTIF_PREFIX + ".attached";
/** The JVM detached notification type */
public static String JVM_DETACHED_NOTIF = JVM_ATTACH_NOTIF_PREFIX + ".detached";
/** The JVM attach exception notification type */
public static String JVM_ATTACH_EX_NOTIF = JVM_ATTACH_NOTIF_PREFIX + ".exception";
/** The JVM connected notification type prefix*/
public static String JVM_CONNECT_NOTIF_PREFIX = "org.helios.jvms.connect";
/** The JVM connected notification type */
public static String JVM_CONNECTED_NOTIF = JVM_CONNECT_NOTIF_PREFIX + ".connected";
/** The JVM disconnected notification type */
public static String JVM_DISCONNECT_NOTIF = JVM_CONNECT_NOTIF_PREFIX + ".disconnected";
/** The JVM connection exception notification type */
public static String JVM_CONNECTED_EX_NOTIF = JVM_CONNECT_NOTIF_PREFIX + ".exception";
/** The JVM mount notification type prefix*/
public static String JVM_MOUNT_NOTIF_PREFIX = "org.helios.jvms.mount";
/** The JVM MBeanServer mounted notification type */
public static String JVM_MOUNTED_NOTIF = JVM_MOUNT_NOTIF_PREFIX + ".mounted";
/** The JVM MBeanServer unmounted notification type */
public static String JVM_UNMOUNTED_NOTIF = JVM_MOUNT_NOTIF_PREFIX + ".unmounted";
/** The JVM MBeanServer mount exception notification type */
public static String JVM_MOUNTED_EX_NOTIF = JVM_MOUNT_NOTIF_PREFIX + ".exception";
/** The notifications emitted by a JVM MBean */
protected static final MBeanNotificationInfo[] NOTIF_TYPES = new MBeanNotificationInfo[]{
new MBeanNotificationInfo(new String[]{JVM_ATTACHED_NOTIF, JVM_DETACHED_NOTIF, JVM_ATTACH_EX_NOTIF}, Notification.class.getName(), "Notification emitted when a JVM's attached state changes"),
new MBeanNotificationInfo(new String[]{JVM_CONNECTED_NOTIF, JVM_DISCONNECT_NOTIF, JVM_CONNECTED_EX_NOTIF}, Notification.class.getName(), "Notification emitted when a JVM's connected state changes"),
new MBeanNotificationInfo(new String[]{JVM_MOUNTED_NOTIF, JVM_UNMOUNTED_NOTIF, JVM_MOUNTED_EX_NOTIF}, Notification.class.getName(), "Notification emitted when a JVM's mounted state changes")
};
/** Shared JMX notification pool */
protected static final Executor notificationThreadPool = Executors.newFixedThreadPool(
ConfigurationHelper.getIntSystemThenEnvProperty(NOTIF_THREAD_POOL_PROP, NOTIF_THREAD_POOL_SIZE), new ThreadFactory(){
private final AtomicInteger serial = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "JVMAttachNotificationThread#" + serial.incrementAndGet());
t.setDaemon(true);
return t;
}
});
/** The virtual machine instance for this JVM */
protected 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;
/** Indicates if this JVM is attached */
protected final AtomicBoolean attached = new AtomicBoolean(false);
/** The most recent attach exception */
protected final AtomicReference<Throwable> lastAttachException = new AtomicReference<Throwable>(null);
/** The most recent attach timestamp */
protected final AtomicLong lastAttachTime = new AtomicLong(-1L);
/** The most recent detach timestamp */
protected final AtomicLong lastDetachTime = new AtomicLong(-1L);
/**
* Creates a new JVM
* @param descriptor The Attach API supplied {@link VirtualMachineDescriptor}
*/
public JVM(VirtualMachineDescriptor descriptor) {
super(notificationThreadPool, NOTIF_TYPES);
this.descriptor = descriptor;
cleanDisplayName = cleanDisplayName(descriptor);
objectName = JMXHelper.objectName("org.helios.jvms", "main", cleanDisplayName, "pid", descriptor.id());
JMXHelper.registerMBean(this, objectName);
}
/**
* Sends a JMX notification
* @param notifType The notification type
* @param message The notification message
* @param userData The user data for the notification
*/
protected void jvmNotification(String notifType, String message, Serializable userData) {
Notification notif = new Notification(notifType, objectName, notificationSerial.incrementAndGet(), System.currentTimeMillis(), message);
if(userData!=null) notif.setUserData(userData);
this.sendNotification(notif);
}
/**
* Sends a JMX notification
* @param notifType The notification type
* @param message The notification message
*/
protected void jvmNotification(String notifType, String message) {
jvmNotification(notifType, message, null);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#isAttached()
*/
@Override
public boolean isAttached() {
return attached.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#attach()
*/
@Override
public void attach() throws Exception {
if(!attached.get()) {
synchronized(descriptor) {
try {
virtualMachine = VirtualMachine.attach(descriptor);
attached.set(true);
lastAttachTime.set(System.currentTimeMillis());
jvmNotification(JVM_ATTACHED_NOTIF, "Attached to JVM [" + descriptor.displayName() + "/" + virtualMachine.id() + "]");
} catch (Throwable t) {
lastAttachException.set(t);
jvmNotification(JVM_ATTACH_EX_NOTIF, "JVM Attach Failed for [" + descriptor.displayName() + "/" + virtualMachine.id() + "]", t);
t.printStackTrace(System.err);
}
}
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.AttachServiceMBean#detach()
*/
@Override
public void detach() {
if(attached.get()) {
synchronized(descriptor) {
try { virtualMachine.detach(); } catch (Exception ex) {}
attached.set(false);
lastDetachTime.set(System.currentTimeMillis());
jvmNotification(JVM_DETACHED_NOTIF, "JVM Detached [" + descriptor.displayName() + "/" + virtualMachine.id() + "]");
// use static descriptor to detect if JVM still exists.
// if NOT, dipose this instance and unregister the MBean.
}
}
}
/**
* Tests the attach state of the JVM.
* If the test fails, throws a RuntimeException and marks the JVM detached.
* @return true if the JVM is still attached, false otherwise
*/
protected boolean tesAttachState() {
if(!attached.get()) return false;
try {
virtualMachine.getAgentProperties();
return true;
} catch (Exception ex) {
lastAttachException.set(ex);
detach();
return false;
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getAgentProperties()
*/
@Override
public Properties getAgentProperties() {
if(!attached.get()) return null;
try {
return virtualMachine.getAgentProperties();
} catch (VirtualMachineInvocationException vex) {
detach();
return null;
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getSystemProperties()
*/
@Override
public Properties getSystemProperties() {
if(!attached.get()) return null;
return virtualMachine.getSystemProperties();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getDisplayName()
*/
@Override
public String getDisplayName() {
return descriptor.displayName();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getId()
*/
@Override
public String getId() {
return descriptor.id();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getProviderName()
*/
@Override
public String getProviderName() {
return descriptor.provider().name();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getProviderType()
*/
@Override
public String getProviderType() {
return descriptor.provider().type();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getLocalConnectorAddress()
*/
@Override
public String getLocalConnectorAddress() {
if(!attached.get()) return null;
try {
return virtualMachine.getAgentProperties().getProperty(JMX_CONN_ADDR);
} catch (Exception ex) {
ex.printStackTrace(System.err);
return null;
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getMainClass()
*/
@Override
public String getMainClass() {
if(!attached.get()) return null;
return virtualMachine.getAgentProperties().getProperty(JVM_MAIN_CLASS);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getJavaVersion()
*/
@Override
public String getJavaVersion() {
if(!attached.get()) return null;
return virtualMachine.getAgentProperties().getProperty(JVM_JAVA_VERSION);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getHeliosAgentName()
*/
@Override
public String getHeliosAgentName() {
if(!attached.get()) return null;
return virtualMachine.getAgentProperties().getProperty(JVM_HELIOS_NAME);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getCleanDisplayName()
*/
@Override
public String getCleanDisplayName() {
return cleanDisplayName;
}
/**
* 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();
return cleanDisplayName(display);
}
/**
* Examines the VM display name (command line main class) and attempts to clean it up for use as a key
* @param display 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(String display) {
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;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getLastAttachException()
*/
@Override
public Throwable getLastAttachException() {
return lastAttachException.get();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getLastAttachDate()
*/
@Override
public Date getLastAttachDate() {
long ts = lastAttachTime.get();
if(ts==-1L) return null;
return new Date(ts);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.satellite.services.attach.JVMMBean#getLastDetachDate()
*/
@Override
public Date getLastDetachDate() {
long ts = lastDetachTime.get();
if(ts==-1L) return null;
return new Date(ts);
}
}