YouDebugでGroovyスクリプトやその向こうのJava系ライブラリをデバッグする

はじめに

YouDebugという、Hudson作者のid:kkawaさんによるデバッガツールがあるんですが、これGroovyみたいなスクリプト on JVM系のデバッグに非常に便利っすね。すごいっす。

YouDebugの何が便利か

  • IDE不要
    • Groovyコード書くときは未だターミナル&vimなので・・・。
  • デバッグ用のYouDebugスクリプトはGroovyで書く
    • 好みの問題ですけど、GroovyのDSLになってます。
    • Java Debug InterfaceのAPI仕様に引きづられるので、配列やリストのイテレートとかはGroovyっぽくないんですけどね。
  • ブレークポイントを仕掛けるクラスのソースコードさえ入手できれば実行時の値を自由に参照・書き換えできる。
    • つまり、依存先ライブラリやGroovy本体の特定のコードに対しても、試行錯誤が簡単にできるわけです。

環境を整える

起動オプションとかいちいち覚えてられないので、自分の場合は下のようなシェルスクリプトを用意しました。自分用なのでいい加減な作りです。どちらも、~/bin に突っ込んであります。

デバッグ対象プログラムの起動用
$ cat ~/bin/with_ydb
#!/bin/sh
if [ "$2" == "" ];then
    echo "Usage: `basename $0` <PORT> <TARGET_COMMAND>..."
    exit 1
fi
PORT=$1
shift 1
env JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,address=$PORT $JAVA_OPTS" $*

使うときはこんな感じ。
上記の通りJAVA_OPTSをつけてるだけなのでたいていのJava系プログラムはなんでもいけると思います。

$ with_ydb 5005 groovy hoge.groovy
$ with_ydb 5005 gradle test

5005ポートを使うのがkkawa流?

YouDebugスクリプトの起動用

あらかじめ、http://maven.dyndns.org/2/org/kohsuke/youdebug/ からDLした依存関係のライブラリ全部入りのyoudebug-1.x-jar-with-dependencies.jarを~/binに突っ込んでます。

$ cat ~/bin/ydb
#!/bin/sh
if [ "$2" == "" ];then
    echo "Usage: `basename $0` <PORT> <YDB_SCRIPT>"
    exit 1
fi
PORT=$1
shift 1
java -jar ~/bin/youdebug-1.3-jar-with-dependencies.jar -socket $PORT $*

使うときはこんな感じ。
直接YouDebugスクリプトを第2引数に指定します。

$ ydb 5005 debugScript.ydb

拡張子はydbとかつけるとそれっぽいみたいです。

試してみる(1): Hello World

Debug対象のスクリプトを用意する

Hello worldでOk。

$ cat hello.groovy 
def message = "Hello, world"
println message

あえて、message変数を使ってるのは後で値の参照・変更をするため。

普通に実行するとこんな感じ。

$ groovy hello.groovy 
Hello, world
YouDebugスクリプトを用意する

こんな感じで。

$ cat hello.ydb 
vm.breakpoint("hello", 2) {     // arg1: 対象のクラス名(FQCN)、arg2:ブレークポイントをしかける行番号
    println "YDB: $message"     // hello.groovyの1行目で宣言されたmessage変数の値を出力
    message = "Goodbye, world?" // message変数の中身を書き換える
    println "YDB: Modified!"
}
デバッグ実行してみる

まず対象スクリプトをデバッグオプション付きで実行します。

$ with_ydb 5005 groovy hello.groovy
Listening for transport dt_socket at address: 5005
(待ち状態)

Listeningという行がでた後、プロンプトがかえってこなくなります。待ち状態。


この状態でもう一枚ターミナルを用意してYouDebugスクリプトを起動すると・・・

$ ydb 5005 hello.ydb
YDB: Hello, world!
YDB: Modified!
$

と出力されます。


さっきのwith_ydbを実行したターミナルを見てみると、

$ with_ydb 5005 groovy hello.groovy
Listening for transport dt_socket at address: 5005
Goodbye, world
$

と出力されてプロンプトになってます。
メッセージが差し替えられてますね。

試してみる(2): GroovyのコアAPIの挙動を変えてみる

次は、同じhello.groovyを使いますが、GroovyのコアAPIブレークポイントをしかけて、挙動を変更してみます。

YouDebugスクリプトを用意する

変更対象の情報としてGroovyのソースコードを入手して見比べながらYouDebugスクリプトを書きます。
当然、ソースのバージョンと、使っているgroovyバイナリのバージョンは一致していないなければだめです。行番号とか、すぐずれますしね。
今回は↓のSVNの最新ソースを前提にしてますので、同じYouDebugスクリプトが他の人の環境でそのまま動くわけではないです。ご注意を。


さて、今回はこんな感じにしてみます。

$ cat groovy.ydb 
vm.breakpoint("groovy.lang.Script", 153) {
    value = "YDB> " + value
    println "YDB: Modified!"
}

groovy.lang.Scriptの153行目は↓のように、スクリプト上で書いたprintlnの実装になってます。
つまり、上のYouDebugスクリプトは、出力される文字に"YDB> "というプレフィックスを強引につけてしまおう、というGroovyコアAPIへの改変を意味してます。

149     public void println(Object value) {
150         Object object;
151 
152         try {
153             object = getProperty("out");
154         } catch (MissingPropertyException e) {
155             DefaultGroovyMethods.println(System.out,value);
156             return;
157         }
158 
159         InvokerHelper.invokeMethod(object, "println", new Object[]{value});
160     }

ちなみに、↑をみるとブレークポイント対象は150行目でもよくね?とか思いますが、実際にはNGです。空振ります。
宣言行とかtryの行はだめなんですね。Eclipseブレークポイントしかけるときと同じで、実処理があるところじゃないとだめなようです。

デバッグ実行してみる

同じように対象スクリプトをデバッグオプション付きで実行します。

$ with_ydb 5005 groovy hello.groovy
Listening for transport dt_socket at address: 5005
(待ち状態)


この状態でもう一枚ターミナルを用意してYouDebugスクリプトを起動すると・・・

$ ydb 5005 groovy.ydb
YDB: Modified!
$

と出力されます。
万が一ここでModified!がでないってことは、FQCNや行番号誤りでブレークポイントがうまくかからなかったってことになります。ソースとにらめっこしてクラス名を見直したり、行番号をずらしたりしてヒットするまで頑張りましょう。


さて、さっきのwith_ydbを実行したターミナルを見てみましょう。

$ with_ydb 5005 groovy hello.groovy
Listening for transport dt_socket at address: 5005
YDB> Hello, world!
$

やりました!!


というわけで、GroovyのコアAPIの挙動までいじれるわけですね。

TIPS: YouDebugスクリプトで試行錯誤する場合

デバッグ対象プログラムを起動→YouDebugスクリプト実行を何度も実行するのがメンドイので、whileとか使ってやると、デバッグ用のターミナル側でばしばしYouDebugスクリプトを実行するだけで済みます。

$ while TRUE; do with_ydb 5005 groovy src/hello.groovy ; done
Listening for transport dt_socket at address: 5005
Goodbye, world?     ←裏で1回目のYouDebugスクリプト実行
Listening for transport dt_socket at address: 5005
Goodbye, world?     ←裏で2回目のYouDebugスクリプト実行
Listening for transport dt_socket at address: 5005
                    ←プロンプトに戻らずに次のYouDebugスクリプトの実行待ち

おわりに

とりあえずさわりだけということでこんな感じでご紹介しましたが、とにかくデバッグ対象クラスのFQCNと行番号、そして変数名やメソッド名などがわかれば、色々といじり倒してしまうことができるので面白いですね。

僕はこれでGroovyのコアAPIに対してほげほげして、パッチっぽいものをJIRAに投げてみたりしました。
groovy-coreの再ビルドなんて毎回してると時間がかかって気力が続かないので、外部からサクサク試行錯誤ができるこの仕組みは非常に助かりました!!!