JDI(Java Debug Interface)のテスト
そもそもやりたかったのは、任意の例外が発生した瞬間にそれを検知してデバッグモードに入るような機能を作ること。そのためにはデバッガもどきを作る必要があって、Pure Javaで記述できるJDIを試していたら、なんだかよく分からないものが出来上がった。
- JDITest.java
import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; import com.sun.jdi.*; import com.sun.jdi.connect.*; import com.sun.jdi.event.*; import com.sun.jdi.request.*; /* * How to compile: * * % javac -cp .:$JDK_HOME/lib/tools.jar -Xlint:all JDITest.java * * How to run: * % java -cp .:$JDK_HOME/lib/tools.jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=4321 JDITest * * API reference * http://java.sun.com/j2se/1.5.0/ja/docs/ja/guide/jpda/jdi/ * * The code is originally written by Yoshiyuki Usui. * http://www.geocities.jp/usui22/JavaProgram/testJDI/testJDI.html * */ public class JDITest { private static Connector findConnector(String name) { VirtualMachineManager vmManager = Bootstrap.virtualMachineManager(); List<Connector> connectors = vmManager.allConnectors(); Iterator<Connector> it = connectors.iterator(); while (it.hasNext()) { Connector connector = it.next(); if (connector.name().equals(name)) return connector; } return null; } public static void main(String[] args) { AttachingConnector connector = (AttachingConnector) findConnector("com.sun.jdi.SocketAttach"); if (connector == null) throw new RuntimeException("No connector"); Map<String, Connector.Argument> arguments = connector.defaultArguments(); arguments.get("hostname").setValue("localhost"); arguments.get("port").setValue("4321"); VirtualMachine vm = null; try { vm = connector.attach(arguments); } catch (IllegalConnectorArgumentsException ex) { throw new RuntimeException(ex); } catch (IOException ex) { throw new RuntimeException(ex); } Debugger debugger = new Debugger(vm); debugger.start(); Parent parent = new Parent(); debugger.exit(); } } class Debugger extends Thread { private VirtualMachine vm; private EventQueue queue; private boolean exitRequested = false; public Debugger(VirtualMachine vm) { this.vm = vm; EventRequestManager manager = vm.eventRequestManager(); // refType = all, notifyCaught = false, notifyUncaught = true ExceptionRequest req = manager.createExceptionRequest(null, false, true); req.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); req.enable(); } public void showThreadInfo(ThreadReference thread) throws IncompatibleThreadStateException { List<StackFrame> frames = thread.frames(); Iterator<StackFrame> it = frames.iterator(); while (it.hasNext()) { StackFrame frame = it.next(); Location loc = frame.location(); Method method = loc.method(); long codeIndex = loc.codeIndex(); System.err.println("StackFarme location:"); try { System.err.println(" sourceName = " + loc.sourceName()); } catch (AbsentInformationException ex) { System.err.println(" sourceName = (Unknown)"); } System.err.println(" lineNumber = " + loc.lineNumber()); System.err.println(" method = " + method); try { byte[] bytecodes = method.bytecodes(); System.err.println(" codeIndex = " + codeIndex + "/" + bytecodes.length); } catch (UnsupportedOperationException ex) { System.err.println(" codeIndex = " + codeIndex); } } } public void processExceptionEvent(ExceptionEvent ee) { ThreadReference thread = ee.thread(); try { // showThreadInfo(thread); thread.suspend(); List<StackFrame> frames = thread.frames(); if (! frames.isEmpty()) { StackFrame frame = frames.get(0); thread.popFrames(frame); } thread.resume(); } catch (IncompatibleThreadStateException ex) { System.err.println("ThreadReference.frame() => " + ex); } } public void run() { queue = vm.eventQueue(); while (! exitRequested) { try { EventSet events = queue.remove(1000); if (events == null) continue; EventIterator it = events.eventIterator(); while (it.hasNext()) { Event event = it.nextEvent(); if (event instanceof ExceptionEvent) { processExceptionEvent((ExceptionEvent) event); } } events.resume(); } catch (InterruptedException ex) { // System.err.println("Debugger: interrupted"); break; } } System.err.println("Debugger: exited."); } public void exit() { exitRequested = true; interrupt(); } }
- Parent.java
class Parent { public Parent() { Child alice = new CleverChild("Alice"); Child bob = new CommonChild("Bob"); Child charlie = new DullChild("Charlie"); try { Thread.sleep(3000); } catch (InterruptedException ex) { } watch(alice); watch(bob); watch(charlie); } public void watch(Child child) { try { child.join(); } catch (InterruptedException e) { // ignored. } } }
- Child.java
abstract class Child extends Thread { private int age; public int getAge() { return age; } abstract boolean isBaby(); public Child(String name) { super(name); age = 0; start(); } public void growUp() { age++; if (isBaby()) throw new RuntimeException("Oops, he/she is too young to speak!"); } public void run() { growUp(); System.err.println("Hello! I'm " + getName() + "."); } } class CleverChild extends Child { public CleverChild(String name) { super(name); } public boolean isBaby() { return getAge() < 1; } } class CommonChild extends Child { public CommonChild(String name) { super(name); } public boolean isBaby() { return getAge() < 2; } } class DullChild extends Child { public DullChild(String name) { super(name); } public boolean isBaby() { return getAge() < 3; } }
実行結果:
$ java -cp .:/usr/lib/java/lib/tools.jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=4321 JDITest Listening for transport dt_socket at address: 4321 Hello! I'm Alice. Hello! I'm Bob. Exception in thread "Charlie" java.lang.RuntimeException: Oops, he/she is too young to speak! at Child.growUp(Child.java:20) at Child.run(Child.java:25) Debugger: exited.
この例のポイントはChildクラスのgrowUpメソッドで、例外が発生するとデバッガに処理が移り、その中でスタックフレームを1つ戻すことでgrowUpメソッドが再実行される。その結果、本来例外が発生するはずのCommonChildで例外がキャンセルされ、"Hello! I'm Bob."という出力が行われている(DebuggerクラスのprocessExceptionEvent()でshowThreadInfo()の呼び出しをコメントアウトしている部分を有効にすると、複数回呼ばれていることがよりはっきり分かる)。
ところで、DullChildクラスのcharlieが例外を投げて終了しているのは何故だろう。この例外捕捉→再実行の仕組みは2回以上動かないのか?
追記
例外発生時にデバッガを起動するには、このようにすればいいらしい(JPDA の接続および呼び出しより)。
$ java -agentlib:jdwp=transport=dt_socket,server=y,onthrow=java.lang.Exception,launch=/usr/lib/java/bin/jdb Test
確かにデバッガは起動するけど、これだとデバッグできないから意味ないな。
- Test.java
public class Test { public static void test() throws Throwable { int x = 1; throw new Exception("test"); } public static void main(String[] args) { try { test(); } catch (Throwable th) { th.printStackTrace(); } } }
追記2
launcher=のところに以下のようなシェルスクリプトを設定したらいけた。
- debugstub.sh:
#!/bin/sh transport=$1 address=$2 jdb -attach localhost:$address
最初$addressをそのまま渡したらConnection refusedになってハマった。
せっかくだからちょっと遊んでみる。
$ java -agentlib:jdwp=transport=dt_socket,server=y,onthrow=java.lang.Exception,launch=./debugstub.sh Test uncaught java.lang.Throwable を設定しました 保留した uncaught java.lang.Throwable を設定しました jdb の初期化中です... > Exception in thread "event-handler" java.lang.NullPointerException at com.sun.tools.example.debug.tty.TTY.exceptionEvent(TTY.java:110) at com.sun.tools.example.debug.tty.EventHandler.exceptionEvent(EventHandler.java:261) at com.sun.tools.example.debug.tty.EventHandler.handleEvent(EventHandler.java:105) at com.sun.tools.example.debug.tty.EventHandler.run(EventHandler.java:79) at java.lang.Thread.run(Thread.java:595) > threads グループ system: (java.lang.ref.Reference$ReferenceHandler)0x151 Reference Handler 状況待機中 (java.lang.ref.Finalizer$FinalizerThread)0x152 Finalizer 状況待機中 (java.lang.Thread)0x153 Signal Dispatcher 実行中 グループ main: (java.lang.Thread)0x155 main 実行中 > thread 0x155 main[1] list 2 { 3 public static void test() throws Throwable 4 { 5 int x = 1; 6 => throw new Exception("test"); 7 } 8 9 public static void main(String[] args) 10 { 11 try { main[1] locals メソッド引数: ローカル変数: x = 1 main[1] exit
なお、ローカル変数の情報を利用するためにはTest.javaのコンパイルの際に-gオプションを付ける必要がある(ということに気付かなくてさらにハマった)。