// The following classes are from the Java Debugger Platform Architecture jpda
// The ship with SDK 1.3+ in tools.jar
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.StackFrame;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.LaunchingConnector;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.MethodEntryEvent;
import com.sun.jdi.event.MethodExitEvent;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.MethodEntryRequest;
import com.sun.jdi.request.MethodExitRequest;
// IO classes
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintStream;
// util classes
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* This class produces outlines of call sequences for other Java programs
* by using debugger hooks. It is useful for building UML sequence diagrams.
* See the documentation for {@link #main} for usage. See UML::Sequence
* on CPAN for Perl scripts to make the diagrams. In particular, see
* genericseq.pl and UML::Sequence::JavaSeq.pm.
*/
public class Seq {
VirtualMachine vm;
Process process;
SeqEventHandler handler;
EventRequestManager requestManager;
MethodEntryRequest initialEntryRequest;
MethodEntryRequest regularEntryRequest;
MethodExitRequest regularExitRequest;
static final int INITIAL_EVENT_STATUS = 1;
static final int REGULAR_EVENT_STATUS = 2;
int eventStatus = INITIAL_EVENT_STATUS;
int indent = 0;
boolean everythingIsInteresting = false;
HashMap interestingMethods;
ArrayList interestingClasses;
PrintStream outputStream;
String excludeFilter;
// objects is keyed by object hash code storing object's number.
// Numbers are per class and are issued sequentially from 1 during
// constructor processing.
HashMap objects = new HashMap();
// nextObjectNumber is keyed by class name storing the most recently
// used number. Preincrement this to use it.
HashMap nextObjectNumber = new HashMap();
/**
* Most callers will use only this method.
* Builds a connection to the debugger, launches the supplied program,
* directs production outpuer.
* @param interestingMethodsFile name of a file listing methods or classes
* you want to see in your output
* see Hello.methods for an example with
* documenation
* @param outputFileName name of a file where output will go, standard out
* can't be used, since the program you are tracing
* might be using it
* @param args the java class to invoke and any command line arguments
* it needs
*/
public Seq(
String interestingMethodsFile,
String outputFileName,
String[] args
) throws Exception {
setupInterestingThings(interestingMethodsFile);
openOutput(outputFileName);
LaunchingConnector conn = findConnector();
vm = conn.launch(getArgsMap(conn, args));
process = vm.process();
dumpProcessOutput(process);
handler = new SeqEventHandler(this);
requestManager = vm.eventRequestManager();
initialEntryRequest = requestManager.createMethodEntryRequest();
// wait for a method in the starting class
initialEntryRequest.addClassFilter(args[0]);
initialEntryRequest.enable();
}
private void setupInterestingThings(String file) throws IOException {
interestingMethods = new HashMap();
HashMap classHash = new HashMap();
interestingClasses = new ArrayList();
FileReader fr = new FileReader(file);
BufferedReader reader = new BufferedReader(fr);
Integer dummyInt = new Integer(1);
String line;
while ((line = reader.readLine()) != null) {
// skip comments and blanks
if (line.startsWith("#") || line.trim().length() == 0) {
continue;
}
// turn off method name checking if file has an 'ALL' line
else if (line.equals("ALL")) {
everythingIsInteresting = true;
}
// assume everything else is a method/class name
// There is no harm in putting extraneous things into the
// interestingMethods hash. If everythingIsInteresting,
// the hash will be completely ignored. In other cases,
// actual methods are looked up in the hash, if they are there
// they print, otherwise not. No one cares if extra entries
// are there.
else {
interestingMethods.put(line, dummyInt);
classHash .put(grabClassName(line), dummyInt);
}
}
reader.close();
fr.close();
// turn classHash into a List, this could be folded into
// switchToRegularStatus
Set classKeys = classHash.keySet();
Iterator iter = classKeys.iterator();
while (iter.hasNext()) {
interestingClasses.add(iter.next());
}
}
private String grabClassName(String line) {
// look for opening (
int parenPos = line.indexOf('(');
// there is one, this is a signature
if (parenPos >= 0) {
// remove arg list
String methodName = line.substring(0, parenPos);
// look for last dot
int lastDot = methodName.lastIndexOf('.');
// there is one, there is a package name
if (lastDot >= 0) {
// remove the method name, leaving all packages and the class
String className = methodName.substring(0, lastDot);
return className;
}
else {
return methodName; // unlikely
}
}
// no opening paren, the whole line is a class name
else {
return line;
}
}
private void openOutput(String file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
outputStream = new PrintStream(fos);
}
public VirtualMachine getVM() { return vm; }
// sets the main attribute of the connection argument hash to the
// name of the program to trace, concatenated with its arguments,
// the list is delimited by spaces
private Map getArgsMap(Connector conn, String[] args) {
Map argsMap = conn.defaultArguments();
Connector.Argument mainArg = (Connector.Argument)argsMap.get("main");
StringBuffer sb = new StringBuffer();
int argCount = args.length;
int maxArg = argCount - 1;
for (int i = 0; i < argCount; i++) {
sb.append(args[i]);
if (i < maxArg) {
sb.append(" ");
}
}
mainArg.setValue(sb.toString());
return argsMap;
}
// gains a valid LaunchingConnector reference by a name lookup
public LaunchingConnector findConnector() {
List connectors = Bootstrap.virtualMachineManager().allConnectors();
Iterator iter = connectors.iterator();
while (iter.hasNext()) {
Connector conn = (Connector)iter.next();
if (conn.name().equals("com.sun.jdi.CommandLineLaunch")) {
return (LaunchingConnector)conn;
}
}
return null;
}
// when a method exits, adjusts the depth of the call sequence
// and restarts the virtual machine
public void methodExitEvent(MethodExitEvent event) {
indent--;
vm.resume();
}
// the virtual machine is up, ask it to start
public void vmStartEvent(Event event) {
vm.resume();
}
// If this is the first entry event, swithToRegularStatus.
// In all cases, print the method signature, if the
// user is interested in it. Then increment the call sequence depth
// and restart the virtual machine.
public void methodEntryEvent(MethodEntryEvent event) {
if (eventStatus == INITIAL_EVENT_STATUS) {
switchToRegularStatus();
}
Method method = event.method();
String signature = grabSignature(method);
String objectName = grabInstanceName(event, method);
Object includeIt = interestingMethods.get(signature);
if (everythingIsInteresting || includeIt != null) {
outputStream.println(formIndentString() + objectName + signature);
}
indent++;
vm.resume();
}
// Returns the manufactured name of the instance which is operative
// in the current method (the one called this in that method).
// Maintains the list of objects by number using two hashes:
// objects and nextObjectNumber. For example the second Roller object
// used in a program will yield roller2.
private String grabInstanceName(MethodEntryEvent event, Method method) {
ObjectReference thisRef = grabStackTopReference(event);
if (thisRef == null) { // the top method is static => no this instance
return "";
}
String type = thisRef.referenceType().name();
Integer thisCode = new Integer(thisRef.hashCode());
Integer countI = null;
if (method.isConstructor()) { // store the hash code
countI = (Integer)nextObjectNumber.get(type);
int count = 0;
if (countI == null) { count = 1; }
else { count = countI.intValue() + 1; }
countI = new Integer(count);
nextObjectNumber.put(type, countI);
objects.put(thisCode, countI);
}
else { // regular instance method (not constructor, not static)
countI = (Integer)objects.get(thisCode);
}
return lcfirst(type) + countI.toString() + ":";
}
// Examines the current stack frame returning the ObjectReference
// of the instance operative in the method on top of the stack.
// Caller can fish in the returned reference for the type name of
// the operative instance
private ObjectReference grabStackTopReference(MethodEntryEvent event) {
ThreadReference thread = event.thread();
StackFrame frame = null;
try {
frame = thread.frame(0);
} catch (Exception e) { }
// com.sun.jdi.IncompatibleThreadStateException
return frame.thisObject();
}
// This should be part of java.lang.String. It takes a String and
// returns it with the first character in lower case Roller becomes roller.
private static String lcfirst(String in) {
String first = in.substring(0, 1);
String rest = in.substring(1);
return first.toLowerCase() + rest;
}
// turn off initial entry request
// make new entry and exit requests for each class the user want to see
private void switchToRegularStatus() {
eventStatus = REGULAR_EVENT_STATUS;
initialEntryRequest.disable();
Iterator iter = interestingClasses.iterator();
if (iter.hasNext()) {
while (iter.hasNext()) { // make one filter for each class
String className = (String)iter.next();
MethodEntryRequest entryRequest;
MethodExitRequest exitRequest;
entryRequest = requestManager.createMethodEntryRequest();
exitRequest = requestManager.createMethodExitRequest();
entryRequest.addClassFilter(className);
exitRequest .addClassFilter(className);
entryRequest.enable();
exitRequest .enable();
}
}
else { // no classes were named, here comes the flood
MethodEntryRequest entryRequest;
MethodExitRequest exitRequest;
entryRequest = requestManager.createMethodEntryRequest();
exitRequest = requestManager.createMethodExitRequest();
entryRequest.enable();
exitRequest .enable();
}
}
// builds an official signature like
// com.company.package.ClassName.method(java.lang.String[], float)
// uses assembleArgs to make the argument list
private String grabSignature(Method method) {
return method.declaringType().name()
+ "." + method.name() + "("
+ assembleArgs(method) + ")";
}
// gives a string which can be printed before the signature to
// show the current call sequence depth visually
private String formIndentString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < indent; i++) {
sb.append(" ");
}
return sb.toString();
}
// asks the method for its types, then assembles them for proper printing
private String assembleArgs(Method method) {
List argTypes = method.argumentTypeNames();
Iterator iter = argTypes.iterator();
StringBuffer sb = new StringBuffer();
while (iter.hasNext()) {
sb.append(iter.next());
if (iter.hasNext()) {
sb.append(", ");
}
}
return sb.toString();
}
// the debugger must have a way to expell error message, lest it die
// from full buffers, this method arranges that
private void dumpProcessOutput(Process proc) {
dumpOutput(proc.getErrorStream());
dumpOutput(proc.getInputStream());
}
// spawns a thread so dumpStream can run concurrently with other threads
private void dumpOutput(final InputStream stream) {
Thread thread = new Thread() {
public void run() {
try {
dumpStream(stream);
}
catch (Exception e) {
System.err.println("dump failed for " + stream);
}
}
};
thread.setPriority(Thread.MAX_PRIORITY - 1);
thread.start();
}
// continually issues blocking reads stdin, or stderr from the virtual
// machines process
// prints the result to standard err.
private void dumpStream(InputStream stream) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = in.readLine()) != null) {
System.err.println(line);
}
}
public static void printUsage() {
System.err.println("usage: java Seq methods_file output_file"
+ " class [args...]");
}
/**
* This is meant to be used, as shown in {@link #printUsage} above.
* @param args
method_file
output_file
class
*
[args_for_class...]
*/
public static void main(String[] args) throws Exception {
if (args.length < 3) {
printUsage();
System.exit(1);
}
String[] passThroughArgs = new String[args.length - 2];
for (int i = 2; i < args.length; i++) {
passThroughArgs[i - 2] = args[i];
}
Seq s = new Seq(args[0], args[1], passThroughArgs);
}
}