Wednesday, 11 March 2009

Java: using JPDA to write a debugger

The Java Platform Debugger Architecture (JPDA) API included in the JDK lets you connect to a Java debug session and receive debug events. This code allows you to do the same things you would normally do with jdb or an IDE debugger. This is useful if you want to write your own debug, diagnostics or metrics tools.

Here is some sample code that will be debugged:

import java.util.Random;

public class Test {

  int foo;

  public static void main(String[] args) {
    Random random = new Random();
    Test test = new Test();
    for (int i = 0; i < 10; i++) {
      test.foo = random.nextInt();
      System.out.println(test.foo);
    }
  }

}

The plan is to monitor changes to the foo member variable.

NOTE: Sun JDK version 6 is used throughout.

Connecting to the VM

Different implementations provide different mechanisms for attaching to the VM via connectors. This allows users to connect by knowing the VM process ID (PID) or a TCP/IP host name and port, for example.

Here is the console output for performing the task using jdb on Windows XP:

X:\Debug>start /MIN cmd /C java -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y -cp .\bin Test

X:\Debug>jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8000
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
>
VM Started: No frames on the current call stack

main[1] watch Test.foo
Deferring watch modification of Test.foo.
It will be set after the class is loaded.
main[1] resume
All threads resumed.
> Set deferred watch modification of Test.foo

Field (Test.foo) is 0, will be 346777565: "thread=main", Test.main(), line=37 bci=26

main[1] exit

X:\Debug>

The sequence of commands goes like this:

  • Start the Test main class (the sample code) minimized in a new console (start /MIN cmd /C) in debug mode (-Xdebug). Have the VM listen on port 8000 (address=8000). Wait for the debugger to attach before running (suspend=y).
  • Start the debugger and attach to port 8000 (hostname=localhost,port=8000). Note the connector (com.sun.jdi.SocketAttach).
  • Use watch Test.foo to start watching the field.
  • Use resume to tell the VM to resume.
  • Use exit to quit the debugger. Note that resume would have resumed until the field was hit again in the next iteration of the loop.

Using Java code to connect to the VM

In order to use the JPDA API, you need to add the JDK tools.jar to the classpath. It can be found in the JDK lib directory.

To interact with the VM, it is necessary to acquire a VirtualMachine object instance. The the same connector is used.

Code to connect to the VM:

public class VMAcquirer {

  /**
   * Call this with the localhost port to connect to.
   */
  public VirtualMachine connect(int port)
      throws IOException {
    String strPort = Integer.toString(port);
    AttachingConnector connector = getConnector();
    try {
      VirtualMachine vm = connect(connector, strPort);
      return vm;
    catch (IllegalConnectorArgumentsException e) {
      throw new IllegalStateException(e);
    }
  }

  private AttachingConnector getConnector() {
    VirtualMachineManager vmManager = Bootstrap
        .virtualMachineManager();
    for (Connector connector : vmManager
        .attachingConnectors()) {
      System.out.println(connector.name());
      if ("com.sun.jdi.SocketAttach".equals(connector
          .name())) {
        return (AttachingConnectorconnector;
      }
    }
    throw new IllegalStateException();
  }

  private VirtualMachine connect(
      AttachingConnector connector, String port)
      throws IllegalConnectorArgumentsException,
      IOException {
    Map<String, Connector.Argument> args = connector
        .defaultArguments();
    Connector.Argument pidArgument = args.get("port");
    if (pidArgument == null) {
      throw new IllegalStateException();
    }
    pidArgument.setValue(port);

    return connector.attach(args);
  }

}

For the sake of brevity I'm assuming that you can figure out the com.sun.jdi imports yourself.

Monitoring Test.foo with code

Once a VirtualMachine has been acquired, we can use its EventRequestManager to instruct it to notify us of events. For example, createClassPrepareRequests can be used to instruct the VM to notify us when classes are loaded. The VirtualMachine EventQueue is then used to process the generated events.

Code that monitors Test.foo:

public class FieldMonitor {

  public static final String CLASS_NAME = "Test";
  public static final String FIELD_NAME = "foo";

  public static void main(String[] args)
      throws IOException, InterruptedException {
    // connect
    VirtualMachine vm = new VMAcquirer().connect(8000);

    // set watch field on already loaded classes
    List<ReferenceType> referenceTypes = vm
        .classesByName(CLASS_NAME);
    for (ReferenceType refType : referenceTypes) {
      addFieldWatch(vm, refType);
    }
    // watch for loaded classes
    addClassWatch(vm);

    // resume the vm
    vm.resume();

    // process events
    EventQueue eventQueue = vm.eventQueue();
    while (true) {
      EventSet eventSet = eventQueue.remove();
      for (Event event : eventSet) {
        if (event instanceof VMDeathEvent
            || event instanceof VMDisconnectEvent) {
          // exit
          return;
        else if (event instanceof ClassPrepareEvent) {
          // watch field on loaded class
          ClassPrepareEvent classPrepEvent = (ClassPrepareEventevent;
          ReferenceType refType = classPrepEvent
              .referenceType();
          addFieldWatch(vm, refType);
        else if (event instanceof ModificationWatchpointEvent) {
          // a Test.foo has changed
          ModificationWatchpointEvent modEvent = (ModificationWatchpointEventevent;
          System.out.println("old="
              + modEvent.valueCurrent());
          System.out.println("new=" + modEvent.valueToBe());
          System.out.println();
        }
      }
      eventSet.resume();
    }
  }

  /** Watch all classes of name "Test" */
  private static void addClassWatch(VirtualMachine vm) {
    EventRequestManager erm = vm.eventRequestManager();
    ClassPrepareRequest classPrepareRequest = erm
        .createClassPrepareRequest();
    classPrepareRequest.addClassFilter(CLASS_NAME);
    classPrepareRequest.setEnabled(true);
  }

  /** Watch field of name "foo" */
  private static void addFieldWatch(VirtualMachine vm,
      ReferenceType refType) {
    EventRequestManager erm = vm.eventRequestManager();
    Field field = refType.fieldByName(FIELD_NAME);
    ModificationWatchpointRequest modificationWatchpointRequest = erm
        .createModificationWatchpointRequest(field);
    modificationWatchpointRequest.setEnabled(true);
  }

}

The above code is demo code and not very robust, though note it allows that the Test class might be loaded before or after the VM is resumed.

Sample output when run against the test class:

com.sun.jdi.SocketAttach
old=0
new=-301449374

old=-301449374
new=913937357

...

2 comments:

  1. I am getting connect refused. Any ideas ?

    ReplyDelete
    Replies
    1. This is not enough information to resolve the problem.

      Consider posting the steps you have taken to stackoverflow.com along with details of your runtime environment.

      Delete

All comments are moderated