/**
* 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.gmx.classloading;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.nio.channels.ServerSocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.GZIPOutputStream;
import javax.management.ObjectName;
import javax.management.loading.MLet;
import javax.management.loading.PrivateMLet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.helios.gmx.Gmx;
import org.helios.gmx.jmx.remote.RemotableMBeanServer;
import org.helios.gmx.util.FreePortFinder;
import org.helios.gmx.util.JMXHelper;
import org.helios.gmx.util.LoggingConfig;
import org.helios.gmx.util.LoggingConfig.GLogger;
import org.helios.gmx.util.URLHelper;
import org.helios.vm.agent.AgentInstrumentation;
/**
* <p>Title: ReverseClassLoader</p>
* <p>Description: An http server to provide remote class loading from a remote JVM.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.gmx.classloading.ReverseClassLoader</code></p>
*/
public class ReverseClassLoader extends AbstractHandler {
/** The Jetty Server instance */
protected Server server;
/** The assigned listening port */
protected int port = -1;
/** The assigned listening binding interface */
protected String bindInterface = null;
/** The instance statistics */
protected StatisticsHandler stats = new StatisticsHandler();
/** The NIO Connector */
protected SelectChannelConnector connector = null;
/** The server thread pool */
protected ExecutorThreadPool threadPool = null;
/** Indicates if the class loader is serving a jar or individual classes */
protected final boolean jarClassLoader;
/** The byte array of the jar is a jar class loader is being used */
protected byte[] jarContent = null;
/** The gzipped byte array of the jar is a jar class loader is being used for remote classloaders reporting gzip support */
protected byte[] gzJarContent = null;
/** The URL of the code source for this class */
protected final URL codeSourceUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
/** The stats MBeanContainer */
protected MBeanContainer container;
/** The base URL of the classloader server */
protected URL baseURL = null;
/** A set of dynamically created class loaders that can load classes that may be requested by a remote class loader */
protected final Map<ClassLoader, ClassLoader> classLoaders = new WeakHashMap<ClassLoader, ClassLoader>();
/** A map of closure bytecode byte arrays keyed by the class resource name */
protected final ByteCodeRepository byteCodeRepo = ByteCodeRepository.getInstance();
/** A map of local file system resources that can be served by the loader keyed by the URI */
protected final Map<String, URL> dynamicResources = new ConcurrentHashMap<String, URL>();
/** An instance GLogger */
protected final GLogger log = LoggingConfig.getInstance().getLogger(getClass());
/** The http classloading URI prefix */
public static final String HTTP_URI_PREFIX = "/classloader/";
/** The http classloading URI suffix for jar loading */
public static final String HTTP_URI_JAR_SUFFIX = "gmx.jar";
/** The server's thread group */
private static final ThreadGroup threadGroup = new ThreadGroup("ReverseClassLoaderThreadGroup");
/** The server's thread name serial number */
private static final AtomicLong threadSerial = new AtomicLong(0L);
/** The singleton instance */
private static volatile ReverseClassLoader instance = null;
/** The singleton instance ctor lock */
private static final Object lock = new Object();
/**
* Returns the URL array that remote class loaders should use to retrieve classes from this class loader.
* If this class was loaded from a jar, the URL will refrence the jar and the standard classpath, otherwise, will reference the standard classpath.
* Referencing the jar is more efficient since remotes can load the entire jar in one call, but the URI for individual classes is
* supported for development environments where these classes may not be packaged into a jar yet.
* @return a classloading URL
*/
public URL[] getHttpCodeBaseURL() {
List<URL> urls = new ArrayList<URL>();
try {
if(jarClassLoader) {
urls.add(new URL(new StringBuilder("http://").append(bindInterface).append(":").append(port).append(HTTP_URI_PREFIX).append(HTTP_URI_JAR_SUFFIX).toString()));
}
urls.add(this.baseURL);
return urls.toArray(new URL[urls.size()]);
} catch (Exception e) {
throw new RuntimeException("Failed to build HttpCodeBaseURL", e);
}
}
/**
* Acquires the ReverseClassLoader instance, starting the server if it is not running.
* @return the ReverseClassLoader instance
*/
public static ReverseClassLoader getInstance() {
if(instance==null) {
synchronized(lock) {
if(instance==null) {
instance = new ReverseClassLoader();
}
}
}
return instance;
}
// /**
// * Indexes the class name of the passed closure class
// * @param closure The closure to register
// */
// public void registerClosure(Closure<?> closure) {
// if(closure==null) throw new IllegalArgumentException("The passed closure was null", new Throwable());
// Class<? extends Closure> closureClass = closure.getClass();
// if(GeneratedClosure.class.isAssignableFrom(closureClass)) {
//
// byte[] bytecode = byteCodeRepo.getByteCode(closureClass);
// if(bytecode!=null) {
// return;
// //throw new RuntimeException("Failed to get bytecode for GeneratedClosure class [" + closureClass.getName() + "]", new Throwable());
// }
// Set<Class<?>> nestedClasses = new HashSet<Class<?>>(Arrays.asList(closureClass.getDeclaredClasses()));
// Collections.addAll(nestedClasses, closureClass.getClasses());
// for(Class<?> clazz: nestedClasses) {
// if(GeneratedClosure.class.isAssignableFrom(clazz)) {
// bytecode = byteCodeRepo.getByteCode(clazz);
// if(bytecode==null) {
// throw new RuntimeException("Failed to get bytecode for GeneratedClosure class [" + closureClass.getName() + "]", new Throwable());
// }
// }
// }
// } else {
// System.out.println("Not a generated closure [" + closure.getClass().getName() + "]");
// }
// }
/**
* Returns the bytecode of the class matching the passed resource class name
* @param className the resource class name
* @return the bytecode of the named class.
*/
@SuppressWarnings("unchecked")
protected byte[] getClassBytes(String className) {
if(className==null) throw new IllegalArgumentException("The passed class name was null", new Throwable());
byte[] bytecode = byteCodeRepo.getByteCodeFromResource(className);
if(bytecode!=null) return bytecode;
InputStream is = null;
List<ClassLoader> loaders = null;
synchronized(classLoaders) {
loaders = new ArrayList<ClassLoader>(classLoaders.size());
loaders.add(getClass().getClassLoader());
loaders.addAll(classLoaders.keySet());
}
for(ClassLoader cl: loaders) {
try {
is = cl.getResourceAsStream(className);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if(is!=null) {
byte[] buffer = new byte[8096];
int bytesRead = -1;
while((bytesRead=is.read(buffer))!=-1) {
baos.write(buffer, 0, bytesRead);
}
} else {
Class<?> clazz = Class.forName(className.replace(".class", "").replace('/', '.'), true, cl);
if(GeneratedClosure.class.isAssignableFrom(clazz)) {
baos.write(byteCodeRepo.getByteCode(clazz));
} else {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(clazz);
oos.flush();
}
}
baos.flush();
return baos.toByteArray();
} catch (Exception e) {
} finally {
if(is!=null) try { is.close(); } catch (Exception e) {}
}
}
// This is a last ditch effort to find a closure class compiled before the class transformer install.
// It could be quite slow.
String clazzName = className.replace(".class", "").replace('/', '.');
Class[] iclasses = {};
try {
iclasses = (Class[])ManagementFactory.getPlatformMBeanServer().getAttribute(AgentInstrumentation.AGENT_INSTR_ON, "AllLoadedClasses");
} catch (Exception e) {}
log.log("Checking [" , iclasses.length , "] Instrumentation classes for [" , clazzName , "]");
for(Class<?> clazz: iclasses) {
if(clazz.getName().equals(clazzName)) {
return byteCodeRepo.getByteCode(clazz);
}
}
//throw new RuntimeException("Failed to load class [" + className + "]", new Throwable().fillInStackTrace());
return null;
}
/**
* Private ctor.
* Starts the server.
*/
private ReverseClassLoader() {
classLoaders.put(getClass().getClassLoader(), getClass().getClassLoader());
jarClassLoader = codeSourceUrl.toString().toLowerCase().endsWith(".jar");
if(jarClassLoader) {
loadJarBytes(codeSourceUrl);
log.log("Loaded Gmx jar bytes.\n\tStandard:" , jarContent.length , "\n\tGZipped:" , gzJarContent.length);
}
server = new Server();
threadPool = new ExecutorThreadPool(Executors.newCachedThreadPool(new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(threadGroup, r, "ReverseClassLoader#" + threadSerial.incrementAndGet());
t.setDaemon(true);
return t;
}
}));
server.setThreadPool(threadPool);
try {
container = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
container.setDomain(getClass().getPackage().getName());
container.addBean(stats);
container.addBean(server);
container.addBean(threadPool);
server.addBean(container);
} catch (Exception e) {
log.elog("Warning: Failed to register stats MBean for ReverseClassLoader. Continuing.");
e.printStackTrace(System.err);
}
startServer();
baseURL = URLHelper.url(new StringBuilder("http://").append(bindInterface).append(":").append(port).append(HTTP_URI_PREFIX));
}
/**
* Loads the Gmx jar into a byte array for serving to remote class loaders
* @param jarUrl The URL of the jar resource.
*/
protected void loadJarBytes(URL jarUrl) {
if(jarUrl==null) throw new IllegalArgumentException("The passed jarUrl was null", new Throwable());
ByteArrayOutputStream baos = null;
BufferedInputStream bis = null;
InputStream is = null;
GZIPOutputStream gzipOut = null;
try {
is = jarUrl.openStream();
bis = new BufferedInputStream(is);
baos = new ByteArrayOutputStream(is.available());
byte[] buffer = new byte[8092];
int bytesRead = -1;
while((bytesRead=bis.read(buffer))!=-1) {
baos.write(buffer, 0, bytesRead);
}
baos.flush();
jarContent = baos.toByteArray();
baos.close();
baos = new ByteArrayOutputStream(jarContent.length);
gzipOut = new GZIPOutputStream(baos, jarContent.length);
gzipOut.write(jarContent);
gzipOut.flush();
gzipOut.finish();
baos.flush();
gzJarContent = baos.toByteArray();
gzipOut.close();
baos.close();
} catch (Exception e) {
throw new RuntimeException("Failed to load bytes for jar [" + jarUrl + "]", e);
} finally {
if(bis!=null) try { bis.close(); } catch (Exception e) {}
if(is!=null) try { is.close(); } catch (Exception e) {}
}
}
/**
* Starts the server.
*/
private void startServer() {
if(server.isStarted() || server.isStarting() || server.isRunning()) {
return;
}
try {
bindInterface = FreePortFinder.hostName();
//port = FreePortFinder.getNextFreePort(bindInterface);
connector = new SelectChannelConnector();
connector.setPort(0);
connector.setMaxIdleTime(30000);
server.addConnector(connector);
stats.setHandler(this);
server.setHandler(stats);
server.start();
port = connector.getPort();
ServerSocketChannel channel = (ServerSocketChannel)connector.getConnection();
port = channel.socket().getLocalPort();
log.olog("Started HTTP Server on [" , bindInterface , ":" , port , "]");
container.addBean(connector);
} catch (Exception e) {
throw new RuntimeException("Failed to start Jetty HTTP Server on [" + bindInterface + ":" + port + "]", e);
}
}
public static void main(String[] args) {
ReverseClassLoader rcl = ReverseClassLoader.getInstance();
rcl.addDynamicResource("file:/home/nwhitehead/services/jboss/jboss-5.1.0.GA/docs/examples/jmx/ejb-management.jar");
try { Thread.currentThread().join(); } catch (Exception e) {}
}
/**
* Stops the server and nulls out the singleton instance.
*/
public void stopServer() {
try { server.stop(); } catch (Exception e) {};
instance=null;
}
/**
* {@inheritDoc}
* @see org.eclipse.jetty.server.Handler#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
log.log("Request Target ", request.getMethod() , " [" , target , "] \n\tfrom [" , request.getRemoteAddr() , ":" , request.getRemotePort() , "]");
boolean jarRequest = (target.equals(HTTP_URI_PREFIX + HTTP_URI_JAR_SUFFIX));
byte[] classBytes = null;
boolean isHead = "HEAD".equals(request.getMethod());
if(!jarRequest) {
String resource = target.replace(HTTP_URI_PREFIX, "");
if(dynamicResources.containsKey(resource)) {
URL url = dynamicResources.get(resource);
classBytes = URLHelper.getBytesFromURL(url);
} else {
classBytes = getClassBytes(resource);
}
}
OutputStream os = response.getOutputStream();
GZIPOutputStream gzipOut = null;
String encodings = request.getHeader("Accept-Encoding");
boolean gzipAgent = false;
if (encodings != null && encodings.indexOf("gzip") != -1) {
// Go with GZIP
gzipAgent = true;
response.setHeader("Content-Encoding", "gzip");
if(!jarRequest) {
gzipOut = new GZIPOutputStream(os, classBytes.length);
}
}
response.setContentType("application/octet-stream");
response.setStatus(HttpServletResponse.SC_OK);
if(jarRequest) {
if(gzipAgent) {
response.setHeader("Content-Encoding", "gzip");
classBytes = gzJarContent;
// os.write(gzJarContent);
// log.log("Wrote [" + gzJarContent.length , "] GZipped for resource [", target, "]");
} else {
classBytes = jarContent;
// os.write(jarContent);
// log.log("Wrote [" + jarContent.length , "] for resource [", target, "]");
}
} else {
if(classBytes==null) {
response.sendError(404, "Class Not Found [" + target + "]");
baseRequest.setHandled(true);
log.log("ERROR: Sent 404 for [" , target , "]");
return;
}
response.setContentLength(classBytes.length);
if(!isHead) {
if(gzipOut!=null) {
gzipOut.write(classBytes);
gzipOut.flush();
gzipOut.finish();
} else {
os.write(classBytes);
}
}
}
os.flush();
log.log("Wrote [" + classBytes.length , "] for resource [", target, "]");
baseRequest.setHandled(true);
}
/**
* Installs the remotable MBeanServer MBean in the MBeanServer as a private Met associated with the passd Gmx
* @param gmx The Gmx connected to the target MBeanServer in which to install the remotable MBeanServer MBean
*/
public void installRemotableMBeanServer(Gmx gmx) {
installRemotableMBeanServer(gmx, true);
}
/**
* Installs the remotable MBeanServer MBean in the MBeanServer associated with the passd Gmx
* @param gmx The Gmx connected to the target MBeanServer in which to install the remotable MBeanServer MBean
* @param privateClassLoader If true, the classloading MBean initially registered will be private, otherwise it will be public.
*/
public void installRemotableMBeanServer(Gmx gmx, boolean privateClassLoader) {
ObjectName classLoaderOn = JMXHelper.objectName(String.format(Gmx.REMOTE_MLET_ON_PREFIX, bindInterface, port));
ObjectName mbeanServerOn = JMXHelper.objectName(String.format(Gmx.REMOTE_MBEANSERVER_ON_PREFIX, gmx.getServerDomain(), bindInterface, port));
ObjectName jbossULR3 = JMXHelper.objectName("JMImplementation:service=LoaderRepository,name=Default");
if(gmx.getMBeanServerConnection().isRegistered(jbossULR3)) {
for(URL url: getHttpCodeBaseURL()) {
try { gmx.getMBeanServerConnection().invoke(jbossULR3, "newClassLoader", new Object[]{url, true}, new String[]{URL.class.getName(), boolean.class.getName()}); } catch (Exception e) {}
}
gmx.getMBeanServerConnection().createMBean(RemotableMBeanServer.class.getName(), mbeanServerOn);
}
if(!gmx.getMBeanServerConnection().isRegistered(classLoaderOn)) {
if(privateClassLoader) {
gmx.getMBeanServerConnection().createMBean(PrivateMLet.class.getName(), classLoaderOn, new Object[]{getHttpCodeBaseURL(), true}, new String[]{URL[].class.getName(), boolean.class.getName()});
} else {
gmx.getMBeanServerConnection().createMBean(MLet.class.getName(), classLoaderOn);
for(URL url: getHttpCodeBaseURL()) {
gmx.getMBeanServerConnection().invoke(classLoaderOn, "addURL", new Object[]{url.toString()}, new String[]{String.class.getName()});
}
}
}
if(!gmx.getMBeanServerConnection().isRegistered(mbeanServerOn)) {
gmx.getMBeanServerConnection().createMBean(RemotableMBeanServer.class.getName(), mbeanServerOn, classLoaderOn, new Object[]{baseURL}, new String[]{URL.class.getName()});
}
try {
gmx.installedRemote(classLoaderOn, mbeanServerOn);
} catch (Exception e) {
throw new RuntimeException("Failed to invoke callback on Gmx install remote", e);
}
}
/**
* @param clazz
* @return
* @see org.helios.gmx.classloading.ByteCodeRepository#getByteCode(java.lang.Class)
*/
public byte[] getByteCode(Class<?> clazz) {
return byteCodeRepo.getByteCode(clazz);
}
/**
* @param className
* @return
* @see org.helios.gmx.classloading.ByteCodeRepository#getByteCode(java.lang.String)
*/
public byte[] getByteCode(String className) {
return byteCodeRepo.getByteCode(className);
}
/**
* @param className
* @return
* @see org.helios.gmx.classloading.ByteCodeRepository#getByteCodeFromResource(java.lang.String)
*/
public byte[] getByteCodeFromResource(String className) {
return byteCodeRepo.getByteCodeFromResource(className);
}
/**
* Adds a dynamic resource
* @param resourceURL The URL of the resource to serve
*/
public void addDynamicResource(URL resourceURL) {
if(resourceURL==null) throw new IllegalArgumentException("The passed URL was null", new Throwable());
String[] frags = resourceURL.getPath().split("/");
String key = frags[frags.length-1];
dynamicResources.put(key, resourceURL);
log.log("Added Dynamic Resource [" , key , "]");
}
/**
* Adds a dynamic resource
* @param resourceURL The URL of the resource to serve
*/
public void addDynamicResource(String resourceURL) {
if(resourceURL==null) throw new IllegalArgumentException("The passed URL was null", new Throwable());
addDynamicResource(URLHelper.url(resourceURL));
}
/**
* Returns the base classloading URL
* @return the baseURL
*/
public URL getBaseURL() {
return baseURL;
}
}
/**
*
* Needs:
* register remotables with Gmx when installed
* set sysprop on install on target server so agents can quickly determine if agent is installed
* unregister on ??
*
*
*
Groovy Code Demonstrating Remote MBeanServer MBean Install
===========================================================
import org.helios.gmx.*;
import org.helios.gmx.util.*;
import javax.management.*;
import javax.management.loading.MLet;
gmx = Gmx.remote("service:jmx:rmi://NE-WK-NWHI-01.CPEX.com:8002/jndi/rmi://NE-WK-NWHI-01.CPEX.com:8003/jmxrmi");
//mlet = new MLet([] as URL[], true) ;
on = new ObjectName("org.helios.gmx:service=ReverseClassLoader");
on2 = new ObjectName("org.helios.gmx:service=MBeanServer");
try { gmx.unregisterMBean(on); } catch (e) {}
try { gmx.unregisterMBean(on2); } catch (e) {}
urls = new URL[1];
urls[0] = new URL("http://localhost:49156/classloader/");
gmx.createMBean(MLet.class.getName(), on, [urls, true] as Object[], [urls.getClass().getName(), boolean.class.getName()] as String[]);
gmx.createMBean("org.helios.gmx.jmx.remote.RemotableMBeanServer", on2, on);
*/