/*
* Agent Smith - A java hot class redefinition implementation
* Copyright (C) 2007 Federico Fissore
*
* 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 it.fridrik.agent;
import it.fridrik.filemonitor.FileEvent;
import it.fridrik.filemonitor.FileModifiedListener;
import it.fridrik.filemonitor.FileMonitor;
import it.fridrik.filemonitor.JarEvent;
import it.fridrik.filemonitor.JarModifiedListener;
import it.fridrik.filemonitor.JarMonitor;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.Vector;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Agent Smith is an agent with just one aim: redefining classes as soon as they
* are changed. Smith bundles together Instrumentation, FileMonitor and
* JarMonitor
*
* @author Federico Fissore (federico@fissore.org)
* @see FileMonitor
* @see JarMonitor
* @since 1.0
*/
public class Smith implements FileModifiedListener, JarModifiedListener {
/** Min period allowed */
private static final int MONITOR_PERIOD_MIN_VALUE = 500;
/** Lists of active Smith agents */
private static Vector<Smith> smiths = new Vector<Smith>();
/** Called when the agent is initialized via command line */
public static void premain(String agentArgs, Instrumentation inst) {
initialize(agentArgs, inst);
}
/** Called when the agent is initialized after the jvm startup */
public static void agentmain(String agentArgs, Instrumentation inst) {
initialize(agentArgs, inst);
}
private static void initialize(String agentArgs, Instrumentation inst) {
SmithArgs args = new SmithArgs(agentArgs);
if (!args.isValid()) {
throw new RuntimeException(
"Your parameters are invalid! Check the documentation for the correct syntax");
}
Smith smith = new Smith(inst, args);
smiths.add(smith);
}
/** Stops all active Smith agents */
public static void stopAll() {
for (Smith smith : smiths) {
smith.stop();
}
}
private static final Logger log = Logger.getLogger(Smith.class.getName());
private final Instrumentation inst;
private final String classFolder;
private final String jarFolder;
private final ScheduledExecutorService service;
/**
* Creates and starts a new Smith agent. Please note that periods smaller than
* 500 (milliseconds) won't be considered.
*
* @param inst
* the instrumentation implementation
* @param args
* the {@link SmithArgs} instance
*/
public Smith(Instrumentation inst, SmithArgs args) {
this.inst = inst;
this.classFolder = args.getClassFolder();
this.jarFolder = args.getJarFolder();
int monitorPeriod = MONITOR_PERIOD_MIN_VALUE;
if (args.getPeriod() > monitorPeriod) {
monitorPeriod = args.getPeriod();
}
log.setUseParentHandlers(false);
log.setLevel(args.getLogLevel());
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(args.getLogLevel());
log.addHandler(consoleHandler);
service = Executors.newScheduledThreadPool(2);
FileMonitor fileMonitor = new FileMonitor(classFolder, "class");
fileMonitor.addModifiedListener(this);
service.scheduleWithFixedDelay(fileMonitor, 0, monitorPeriod,
TimeUnit.MILLISECONDS);
if (jarFolder != null) {
JarMonitor jarMonitor = new JarMonitor(jarFolder);
jarMonitor.addJarModifiedListener(this);
service.scheduleWithFixedDelay(jarMonitor, 0, monitorPeriod,
TimeUnit.MILLISECONDS);
}
log.info("Smith: watching class folder: " + classFolder);
log.info("Smith: watching jars folder: " + jarFolder);
log.info("Smith: period between checks (ms): " + monitorPeriod);
log.info("Smith: log level: " + log.getLevel());
}
/**
* Stops this Smith agent
*/
public void stop() {
service.shutdown();
}
/**
* When the monitor notifies of a changed class file, Smith will redefine it
*/
public void fileModified(FileEvent event) {
redefineClass(toClassName(event.getSource()), event);
}
/**
* When the monitor notifies of a changed jar file, Smith will redefine the
* changed class file the jar contains
*/
public void jarModified(JarEvent event) {
redefineClass(toClassName(event.getEntryName()), event);
}
/**
* Redefines the specified class
*
* @param className
* the class name to redefine
* @param event
* the event which contains the info to access the modified class
* files
* @throws IOException
* if the inputstream is someway unreadable
* @throws ClassNotFoundException
* if the class name cannot be found
* @throws UnmodifiableClassException
* if the class is unmodifiable
*/
protected void redefineClass(String className, EventObject event) {
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class<?> clazz : loadedClasses) {
if (clazz.getName().equals(className)) {
try {
ClassDefinition definition = new ClassDefinition(clazz,
getByteArrayOutOf(event));
inst.redefineClasses(new ClassDefinition[] { definition });
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Redefined: " + clazz.getName());
}
} catch (Exception e) {
log.log(Level.SEVERE, "error", e);
}
}
}
}
/**
* Factory method. Depending on the event implementation, retrieves the byte
* array of the changed class
*
* @param event
* the event to analize
* @return the byte array of the changed class file
* @throws IOException
* if some problems occur while opening the class file for reading
*/
private byte[] getByteArrayOutOf(EventObject event) throws IOException {
if (event instanceof FileEvent) {
return toByteArray(new FileInputStream(new File(classFolder
+ event.getSource())));
} else if (event instanceof JarEvent) {
JarEvent jarEvent = (JarEvent) event;
JarFile jar = jarEvent.getSource();
return toByteArray(jar.getInputStream(getJarEntry(jar, jarEvent
.getEntryName())));
}
throw new IllegalArgumentException("Event of type "
+ event.getClass().getName() + " is not supported");
}
/**
* Converts an absolute path to a file to a fully qualified class name
*
* @param fileName
* the absolute path of the class file
* @return a fully qualified class name
*/
private static String toClassName(String fileName) {
return fileName.replace(".class", "").replace(File.separatorChar, '.');
}
/**
* Gets the specified jar entry from the specified jar file
*
* @param jar
* the jar file that contains the jar entry
* @param entryName
* the name of the entry contained in the jar file
* @return a JarEntry
* @throws IllegalArgumentException
* if the specified entryname is not contained in the specified jar
* file
*/
private static JarEntry getJarEntry(JarFile jar, String entryName) {
JarEntry entry = null;
for (Enumeration<JarEntry> entries = jar.entries(); entries
.hasMoreElements();) {
entry = entries.nextElement();
if (entry.getName().equals(entryName)) {
return entry;
}
}
throw new IllegalArgumentException("EntryName " + entryName
+ " does not exist in jar " + jar);
}
/**
* Loads .class files as byte[]
*
* @param is
* the inputstream of the bytes to load
* @return a byte[]
* @throws IOException
* if an error occurs while reading file
*/
private static byte[] toByteArray(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = is.read(buffer)) != -1) {
byte[] tmp = new byte[bytesRead];
System.arraycopy(buffer, 0, tmp, 0, bytesRead);
baos.write(tmp);
}
byte[] result = baos.toByteArray();
baos.close();
is.close();
return result;
}
}