Java 6 has a neat feature that has received little attention: the Attach API
Using this API, you can hook your own code into a process already running on the JVM. It’s meant to build code-profilers and things of that sort, but with a little fooling around, you can attach to a running Applet and play with the live objects. I’ll describe my method – if you know a better way to do this, please let me know.
First, you need to turn off Applet Security. I’m lazy and went with a shotgun approach. Create a new .policy file:
grant { permission java.security.AllPermission; };
Then, set the browser plugin to use this policy. On Windows, try this:
- Open the Control Panel
- [doubleclick] Java Control Panel
- [click] Java tab
- [click] Java Applet Runtime Settings
- [click] View
- [click] Java Runtime Parameters
- [type] -Djava.security.policy=C:path_toyour.policy
- [click] OK
Same thing for Linux, just figure out where your Applet Runtime settings are.
Now you need a way to run your Agent. Annoyingly, the Agent must live in a jar file. You can build it on the command line using JAR, but I chose to do it programmatically to avoid the extra step. This program takes a single command line argument, the PID (process id) of the JVM the applet is running on. There’s different ways to find it. On Windows, try running “tasklist”. On Linux, “ps -A”. Place this file at “com/stuff/Runner.java”:
import java.io.File; import java.util.ArrayList; import java.util.List; import com.sun.tools.attach.VirtualMachine; public class Runner { public static void main(String args[]) throws Exception { //create .jar including all .class files under directory: com/stuff/ List<String> jarFiles = new ArrayList<String>(); for(String file : (new File("com/stuff")).list()) if(file.endsWith(".class")) jarFiles.add("com/stuff/"+file); String[] filenames = jarFiles.toArray(new String[]{}); String jarFile = System.getProperty("user.dir")+"/agent.jar"; JarUtil.jar(filenames, jarFile); //find PID of process to monitor String pid = args[0]; //attach agent.jar if(Integer.parseInt(pid) > 0) { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(jarFile, null); } else { System.out.println("Bad PID: " + pid); } } }
Rad, so now for the Agent itself. When the JVM calls our Agent, it
passes an instance of Instrumentation, which has the interesting method
getAllLoadedClasses(). A Class isn’t very useful unless there are
static methods to get instances of the Class. Fortunately,
AppletPanelCache.getAppletPanels() comes to the rescue as a way to get
at Objects instead of Classes. Here’s MyAgent.java:
import java.lang.instrument.Instrumentation; public class MyAgent { public static void agentmain(String agentArgs, Instrumentation inst) { for(Class klass : inst.getAllLoadedClasses()) { if(klass.getName().endsWith("AppletPanelCache")) { Method m = klass.getMethod("getAppletPanels", new Class[]{}); Object[] panels = (Object[])m.invoke(null, new Object[]{}); for(Object panel : panels) { //do something interesting with an instance of Panel } } } } }
What can you do with a Panel? Well, let’s see, for an instance to be of any use in an Applet, it is likely connected *somehow* to the rest of those top level Panels where everything is shown. With a heavy dose of Reflection, you can recursively explore each Panel’s children, ultimately getting a reference to most (is it most or all? does anyone know?) of the live objects. Once you have a live object, you can do whatever you want – call methods, inspect fields, etc. The methods getComponents() and getWindows() are a good place to start.
So, how to run it? Depends where you have Java installed. Make sure tools.jar is on your classpath. I use this invocation:
- compile javac -classpath /your/path/to/tools.jar:. com.stuff.*
- run (first, open an applet in your webbrowser)
- java -classpath /your/path/to/tools.jar:. com.stuff.Runner -Djava.security.policy=/home/path_to/your.policy
Annoyances – When an Agent calls System.out.println(), it gets the Applet’s PrintStream instead of printing to the Console. My solution? Well, Runner *is* still connected to the Console, so have Agent send all its output to Runner over a socket. There may be a better way. Also, I’ve been unable to attach twice without shutting down and restarting Firefox – not sure exactly why.
One more neat thing you might try – hooking your own AWTEventListener into the AWT Event Processing pipeline:
if(klass.getName().endsWith(“Toolkit”)) { long mask = Long.MAX_VALUE; //call our listener on ALL event types Method m = klass.getMethod("getDefaultToolkit", new Class[]{}); Toolkit toolkit = (Toolkit)m.invoke(null, new Object[]{}); Method n = klass.getMethod("addAWTEventListener", new Class[]{AWTEventListener.class, long.class}); n.invoke(toolkit, new Object[]{new MyAWTEventListener(), mask}); }
One cute idea – dispatch MouseEvent and KeyEvent to make a video game “play” itself.
Remember to reset your Java Applet Runtime Settings, if you are concerned about such things.
References: Hotpatching a Java 6 Application by Jack Shirazi
Tags: java