/**
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import sun.management.Agent;
import sun.management.AgentConfigurationError;
import sun.management.jmxremote.ConnectorBootstrap.DefaultValues;
import sun.management.jmxremote.ConnectorBootstrap.PropertyNames;
import sun.management.jmxremote.LocalRMIServerSocketFactory;
import sun.misc.VMSupport;
import sun.rmi.server.UnicastServerRef;
import sun.rmi.server.UnicastServerRef2;
import com.sun.jmx.remote.internal.RMIExporter;
/**
* <p>Title: AlternateDomainAgent</p>
* <p>Description: Mimics the standard management agent <b><code>sun.management.Agent</code></b> except it is used for exposing
* non platform MBeanServers</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.satellite.services.attach.AlternateDomainAgent</code></p>
*/
public class AlternateDomainAgent {
/** Platform EOL */
public static final String EOL = System.getProperty("line.separator", "\n");
/** Platform TMP Dir */
public static final String TMP;
/** The agent property prefix for the connector addresses */
public static final String LOCAL_CONNECTOR_ADDRESS_PROP = "com.sun.management.jmxremote.localConnectorAddress";
/** The agent property into which the comma separated MBeanServer default domain names are written */
public static final String JMX_DOMAINS_PROP = "javax.management.agent.domains";
/** 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 agent version */
public static final double VERSION = 1;
/**
* Command line agent startup entry point
* @param args The agent arguments
* @throws Exception thrown on any error
*/
public static void premain(String args) throws Exception {
agentmain(args);
}
static {
String tmpDir = System.getProperty("java.io.tmpdir");
if(!tmpDir.endsWith(File.separator)) {
tmpDir = tmpDir + File.separator;
}
TMP = tmpDir;
}
/**
* Entry point for the attach API
* @param args The agent arguments
*/
public static void agentmain(String args) {
final Set<String> directives = new HashSet<String>();
if(args!=null) {
String[] directiveArgs = args.split("\n");
if(directiveArgs!=null) {
for(String s: directiveArgs) {
if(s==null || s.trim().isEmpty()) continue;
directives.add(s.trim().toLowerCase());
}
}
}
log("Starting AlternateDomainAgent, Directives: " + directives);
// Clear the agent deploy error messages
if(directives.contains("clear.errors")) {
clearAgentProperty(AGENT_DEPLOY_ERR_PROP);
return;
}
// Install the platform mbeanserver
if(directives.contains("install-platform")) {
log("Installing Platform MBeanServer Management Agent");
final String mbeanBuilder = System.getProperty("javax.management.builder.initial", null);
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
System.clearProperty("javax.management.builder.initial");
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
log("Acquired Platform MBeanServer [" + server.getDefaultDomain() + "]");
startAgent(server);
} catch (Exception ex) {
ex.printStackTrace(System.err);
appendAgentException("DefaultDomain", ex);
} finally {
Thread.currentThread().setContextClassLoader(cl);
if(mbeanBuilder!=null) {
System.setProperty("javax.management.builder.initial", mbeanBuilder);
}
}
System.clearProperty("javax.management.builder.initial");
return;
}
//"install-platform"
//final String mbeanBuilder = System.getProperty("javax.management.builder.initial", null);
//System.clearProperty("javax.management.builder.initial");
//try {
for(MBeanServer server : MBeanServerFactory.findMBeanServer(null)) {
String dd = server.getDefaultDomain();
if(agentPropertyArrayContains(JMX_DOMAINS_PROP, ",", dd)) continue;
// Don't load the platform mbean server unless there is a directive override
// (sometimes the default domain for the platform mbeanserver is null)
if(dd==null || dd.trim().isEmpty() || dd.equals("DefaultDomain")) {
if(!directives.contains("include-platform")) {
continue;
}
dd = "DefaultDomain";
}
try {
log("Starting connector server for [" + dd + "]");
startAgent(server);
appendAgentProperty(JMX_DOMAINS_PROP, ",", dd);
} catch (Exception ex) {
appendAgentException(dd, ex);
}
}
// }
// catch (Exception ex) {
// ex.printStackTrace(System.err);
// } finally {
// if(mbeanBuilder!=null) {
// System.setProperty("javax.management.builder.initial", mbeanBuilder);
// }
// }
}
/**
* Retrieves an agent property value
* @param propertyName The name of the property
* @param defaultValue The default value to return if the property is not set
* @return the property value or the default
*/
protected static String getAgentProperty(String propertyName, String defaultValue) {
return VMSupport.getAgentProperties().getProperty(propertyName, defaultValue);
}
/**
* Retrieves an agent property value as a string array
* @param propertyName the property name
* @param delimiter The delimiter to parse the value
* @param defaultValue The default value to return if the property is not set
* @return the parsed array or the default value
*/
protected static synchronized String[] getAgentPropertyArray(String propertyName, String delimiter, String...defaultValue) {
String arr = getAgentProperty(propertyName, null);
if(arr==null || arr.trim().isEmpty()) return defaultValue;
String[] array = arr.trim().split(delimiter);
for(int i = 0; i < array.length; i++) {
if(array[i]!=null) array[i] = array[i].trim();
}
return array;
}
/**
* Determines if the passed value is present in the array in the named agent property
* @param propertyName The agent property name
* @param delimiter The array delimiter
* @param value The value to search for
* @return true if the value is found, false otherwise
*/
protected static synchronized boolean agentPropertyArrayContains(String propertyName, String delimiter, String value) {
if(value==null || value.trim().isEmpty()) return false;
String[] arr = getAgentPropertyArray(propertyName, delimiter, new String[]{});
if(arr.length==0) return false;
return Arrays.binarySearch(arr, value)>=0;
}
/**
* Appends the agent deployment exception to the agent property {@link #AGENT_DEPLOY_ERR_PROP}.
* @param domainName The default domain name of the target MBeanServer for which the agent deploy failed
* @param ex The exception to append the message for
*/
protected static synchronized void appendAgentException(String domainName, Exception ex) {
appendAgentProperty(AGENT_DEPLOY_ERR_PROP, AGENT_DEPLOY_ERR_DELIM,
new StringBuilder("[")
.append(domainName)
.append("]:")
.append(ex.toString())
.toString()
);
}
/**
* Sets or appends the passed agent property
* @param propertyName The agent property name
* @param delimiter The value delimiter
* @param values The values to set or append
*/
protected static synchronized void appendAgentProperty(String propertyName, String delimiter, String...values) {
if(values==null || values.length==0) return;
Properties agentProps = VMSupport.getAgentProperties();
String currentValue = agentProps.getProperty(propertyName, "");
StringBuilder exmessage = new StringBuilder(currentValue);
boolean atLeastOne = false;
for(String v: values) {
if(v==null || v.trim().isEmpty()) continue;
if(exmessage.length()!=0) {
exmessage.append(delimiter);
}
exmessage.append(v.trim()).append(delimiter);
atLeastOne = true;
}
if(atLeastOne) {
for(int i = 0; i < delimiter.length(); i++) {
exmessage.deleteCharAt(exmessage.length()-1);
}
}
agentProps.put(propertyName, exmessage.toString());
}
/**
* Removes the passed values from the implied array in the passed agent property
* @param propertyName The agent property name
* @param delimiter The value delimiter
* @param values The values to remove
*/
protected static synchronized void removeAgentArrProperty(String propertyName, String delimiter, String...values) {
String[] arr = getAgentPropertyArray(propertyName, delimiter, new String[]{});
if(values==null || values.length==0 || arr==null || arr.length==0) return;
Set<String> arrValues = new HashSet<String>(Arrays.asList(arr));
for(String v: values) {
arrValues.remove(v);
}
}
/**
* Clears the agent property identified by the passed property name
* @param propertyName the name of the property to clear
*/
protected static synchronized void clearAgentProperty(String propertyName) {
VMSupport.getAgentProperties().remove(propertyName);
}
/**
* Creates the alt-domain management agent jar in <b><code>${java.io.tmpdir}</code></b>.
* @return the name of the file
* @throws Exception thrown on any error
*/
public static String writeAgentJar() throws Exception {
String fileName = TMP + "altdomain-management-agent.jar";
File file = new File(fileName);
if(!file.exists()) {
JarOutputStream jos = null;
try {
Manifest manifest = new Manifest(new ByteArrayInputStream(manifest().getBytes()));
jos = new JarOutputStream(new FileOutputStream(file), manifest);
JarEntry entry = new JarEntry(AlternateDomainAgent.class.getName().replace('.', '/') + ".class");
jos.putNextEntry(entry);
jos.write(getAgentClassBytes(null));
entry = new JarEntry(AlternateDomainAgent.class.getName().replace('.', '/') + "$PermanentExporter.class");
jos.putNextEntry(entry);
jos.write(getAgentClassBytes(AlternateDomainAgent.class.getName().replace('.', '/') + "$PermanentExporter.class"));
//AlternateDomainAgent.class.getName().replace('.', '/') + ".class"
} finally {
if(jos!=null) {
try { jos.flush(); } catch (Exception e) {}
try { jos.close(); } catch (Exception e) {}
}
}
}
return fileName;
}
public static void main(String[] args) {
log("AgentClassBytes test [" + TMP + "]");
new File(TMP + "altdomain-management-agent.jar").delete();
try {
String agentJar = writeAgentJar();
log("AgentJar:" + agentJar);
JarFile jarFile = new JarFile(agentJar);
Manifest mf = jarFile.getManifest();
if(mf!=null) {
StringBuilder b = new StringBuilder("Manifest Listing:");
b.append("\n\t").append("Main");
for(Map.Entry<Object, Object> antry : mf.getMainAttributes().entrySet()) {
b.append("\n\t\t").append(antry.getKey()).append(":").append(antry.getValue());
}
for(Map.Entry<String, Attributes> entry : mf.getEntries().entrySet()) {
b.append("\n\t").append(entry.getKey());
for(Map.Entry<Object, Object> antry : entry.getValue().entrySet()) {
b.append("\n\t\t").append(antry.getKey()).append(":").append(antry.getValue());
}
}
log(b);
} else {
log("Manifest was null");
}
String ver = jarFile.getManifest().getMainAttributes().getValue("Implementation-Version");
double version = Double.parseDouble(ver.trim());
log("Version:" + version);
// boolean deleted = new File(TMP + "altdomain-management-agent.jar").delete();
// log("AgentJar Deleted:" + deleted);
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
private static byte[] getAgentClassBytes(String clazz) {
ClassLoader cl = AlternateDomainAgent.class.getClassLoader();
InputStream is = null;
byte[] buff = new byte[10240];
int bytesRead = 0;
try {
String resource = clazz==null ? AlternateDomainAgent.class.getName().replace('.', '/') + ".class" : clazz;
log("Reading resource [" + resource + "]");
is = cl.getResourceAsStream(resource);
ByteArrayOutputStream baos = new ByteArrayOutputStream(is.available());
while((bytesRead=is.read(buff))!=-1) {
baos.write(buff, 0, bytesRead);
}
buff = baos.toByteArray();
log("Read Agent Class Bytes:" + buff.length);
return buff;
} catch (Exception ex) {
loge("Failed to read class bytes:" + ex.toString());
throw new RuntimeException("Failed to read class bytes", ex);
} finally {
if(is!=null) try { is.close(); } catch (Exception e) {}
}
}
/**
* Starts the management agent for the passed MBeanServer
* @param server The MBeanServer to start the agent for
* @throws Exception thrown on any error
*/
private static void startAgent(MBeanServer server) throws Exception {
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(server.getClass().getClassLoader());
Properties agentProps = VMSupport.getAgentProperties();
String dd = server.getDefaultDomain();
if(dd==null || dd.trim().isEmpty()) {
dd = "DefaultDomain";
}
JMXConnectorServer cs = startLocalConnectorServer(server);
String address = cs.getAddress().toString();
// Add the local connector address to the agent properties
if("DefaultDomain".equals(dd)) {
agentProps.put(LOCAL_CONNECTOR_ADDRESS_PROP, address);
} else {
agentProps.put(LOCAL_CONNECTOR_ADDRESS_PROP + "." + dd, address);
}
log("Started management connector server for [" + dd + "]");
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
}
private static JMXConnectorServer startLocalConnectorServer(MBeanServer mbs) {
System.setProperty("java.rmi.server.randomIDs", "true");
Map<String, Object> env = new HashMap<String, Object>();
env.put(RMIExporter.EXPORTER_ATTRIBUTE, new PermanentExporter());
// The local connector server need only be available via the
// loopback connection.
String localhost = "localhost";
InetAddress lh = null;
try {
lh = InetAddress.getByName(localhost);
localhost = lh.getHostAddress();
} catch (UnknownHostException x) {
}
// localhost unknown or (somehow) didn't resolve to
// a loopback address.
if (lh == null || !lh.isLoopbackAddress()) {
localhost = "127.0.0.1";
}
try {
JMXServiceURL url = new JMXServiceURL("rmi", localhost, 0);
// Do we accept connections from local interfaces only?
Properties props = Agent.getManagementProperties();
if (props == null) {
props = new Properties();
}
String useLocalOnlyStr = props.getProperty(
PropertyNames.USE_LOCAL_ONLY, DefaultValues.USE_LOCAL_ONLY);
boolean useLocalOnly = Boolean.valueOf(useLocalOnlyStr).booleanValue();
if (useLocalOnly) {
env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
new LocalRMIServerSocketFactory());
}
JMXConnectorServer server =
JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
server.start();
return server;
} catch (Exception e) {
loge("Agent Configuration Exception for JMX Domain [" + mbs.getDefaultDomain() + "]");
throw new AgentConfigurationError("Agent Configuration Exception for JMX Domain [" + mbs.getDefaultDomain() + "]", e, e.toString());
}
}
/**
* Out logging
* @param msg the message to log
*/
protected static void log(Object msg) {
System.out.println("[DomainAgent]" + msg);
}
/**
* Err logging
* @param msg the message to log
*/
protected static void loge(Object msg) {
System.err.println("[DomainAgent]" + msg);
}
/**
* Generates the manifest for the agent jar
* @return the maifest text
*/
public static String manifest() {
return "Manifest-Version: 1.0" + EOL +
"Implementation-Version: " + VERSION + EOL +
"Agent-Class: " + AlternateDomainAgent.class.getName() + EOL +
"Premain-Class: " + AlternateDomainAgent.class.getName() + EOL;
}
/**
* <p>Prevents our RMI server objects from keeping the JVM alive.</p>
*
* <p>We use a private interface in Sun's JMX Remote API implementation
* that allows us to specify how to export RMI objects. We do so using
* UnicastServerRef, a class in Sun's RMI implementation. This is all
* non-portable, of course, so this is only valid because we are inside
* Sun's JRE.</p>
*
* <p>Objects are exported using {@link
* UnicastServerRef#exportObject(Remote, Object, boolean)}. The
* boolean parameter is called <code>permanent</code> and means
* both that the object is not eligible for Distributed Garbage
* Collection, and that its continued existence will not prevent
* the JVM from exiting. It is the latter semantics we want (we
* already have the former because of the way the JMX Remote API
* works). Hence the somewhat misleading name of this class.</p>
*/
private static class PermanentExporter implements RMIExporter {
public Remote exportObject(Remote obj,
int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf)
throws RemoteException {
synchronized (this) {
if (firstExported == null)
firstExported = obj;
}
final UnicastServerRef ref;
if (csf == null && ssf == null)
ref = new UnicastServerRef(port);
else
ref = new UnicastServerRef2(port, csf, ssf);
return ref.exportObject(obj, null, true);
}
// Nothing special to be done for this case
public boolean unexportObject(Remote obj, boolean force)
throws NoSuchObjectException {
return UnicastRemoteObject.unexportObject(obj, force);
}
Remote firstExported;
}
}
/*
*/