NetBeans on windowsでLinux上のrubyプログラムをリモートデバッグするためのproxy

NetBeansのリモートデバッグについて、以前の記事で、以下のように書きましたが、

注意点としては、リモートと同じフルパス上にソースコードを置く必要がある点です。リモート先で、/var/www/railsprjにRoRのプロジェクトコードが置いてあるなら、ローカルでも/var/www/railsprjにプロジェクトコードを置く必要があります。

このままだと、Windows上のNetBeansでLinux上のRailsがリモートデバッグできません。
そこで、NetBeansとFast Debugger(ruby-debug-ide)の間に挟むproxyを用意して、proxyの中でファイルパス変換することでリモートデバッグできるようにしました。本当は、NetBeansのIDE内部でファイルパス変換するように修正するべきだと思いますが、面倒だったのでproxyにしました。

せっかくなので、以下にproxyのソースコードを置いておきます(MIT Licenseにしときました)。

以下の構成で、基本的な機能(変数チェック、ステップ実行、breakpointセット辺り)が動作するのは確認しました。
Railsがデバッグできるかどうかは、まだ試してません。

[NetBeans (Debuger GUI)] on Windows
  ↓↑
[proxy] on Windows/Linux
  ↓↑
[ruby process (+ruby-debug-ide)]  on Linux

・CRuby 1.8.7 および JRuby 1.5.1でproxy動作確認した。

なお、実際にデバッグするときは、NetBeansが動くマシン上にもデバッグ対象のソースコードを置かないとソースコードデバッグできません。
例えば、ソースコードは以下のように配置します。

・ruby-debug-ide側
/home/user/script
|---test.rb
\---lib
      \---somelib.rb

・NetBeans側
C:\win_script
|---test.rb
\---lib
      \---somelib.rb


proxyを使ったデバッグの仕方


以下の手順でproxyの起動やNetBeansからproxyへの接続を行えば、後は普通にデバッグできます。

  1. ruby-debug-ide付きでRuby process起動(以前の記事参照)
  2. proxy起動(Linux上で起動するか、NetBeans on Windows上で起動するかはお好みで)。
  3. NetBeansでproxyへの接続。

1, 2の手順はどちらが先でもOKです。
1は以前の記事と同じなので、2, 3の説明だけ書きます。

2. proxy起動

proxyスクリプトをオプション指定して起動するだけです。proxyスクリプトのオプションは以下の通り。

Usage: ruby-debug-ide-proxy.rb -t  [-p listen_port] [--rdbprefix prefix] [--ideprefix prefix] [-d]

Example: ruby-debug-ide-proxy.rb -t localhost --rdbprefix "/home/user/script" --ideprefix "C:\\win_script" -d

Exampleの例を説明すると、

  • localhostのruby-debug-ideに接続して(接続先portは1234。指定するときはlocalhost:6000のようにする。)、
  • proxyを経由してNetBeansにデバッグデータ(XML)を送るときは、/home/user/script –> C:\\win_script への変換を行い、
  • proxyを経由してNetBeansからデバッグコマンド(breakpointセットなど)を指示するときは、C:\\win_script –> /home/user/scriptへの変換を行い、
  • proxy実行時は、DEBUGログを出力する

という指定になります。

3. NetBeansからproxyへの接続

NetBeansのメニューから「デバッグ -> デバッガを接続」を選択し、proxyが動作するホスト名やポート番号を指定して接続してください。
NetBeansからproxyに接続した時、proxyからruby-debug-ideへの接続が自動的に行われます。

正しくデバッガ接続できた場合、以下のようなログがproxyプログラムの端末に出力されます。

I, [2010-09-27T03:43:21.124000 #2240]  INFO -- : proxy will replace /home/user/script with C:\win_script
I, [2010-09-27T03:43:21.468000 #2240]  INFO -- : proxy listens on 0.0.0.0:7000
I, [2010-09-27T03:43:24.530000 #2240]  INFO -- : proxy connects to 192.168.1.7:1234
I, [2010-09-27T03:43:24.530000 #2240]  INFO -- : debug start

後は、普通にNetBeansのデバッガGUIが使えます。
例えば、デバッガGUIからbreakpointをセットすると、以下のログがproxy端末上に出ます。(-dを指定して起動した場合のみ)

D, [2010-09-27T03:43:24.546000 #2240] DEBUG -- : (ide -> proxy) b C:\win_script\test.rb:5
D, [2010-09-27T03:43:24.546000 #2240] DEBUG -- : (proxy -> rdb) b /home/user/script/test.rb:5
D, [2010-09-27T03:43:24.562000 #2240] DEBUG -- : (rdb -> proxy) 
D, [2010-09-27T03:43:24.624000 #2240] DEBUG -- : (proxy -> ide) 

手順は以上です。

Ruby/Railsの場合、デバッガはプログラムの動作理解に使うものというのが私の認識ですデバッグは、printfデバッグの方が効率がいいと思ってます。。なので、Ruby/Rails全般に慣れていない人がデバッガ利用者だと思うので、手順はもう少し簡潔にしたいですが、そうするにはIDE内を修正するしかないかな・・・。

NetBeansからRuby on Rails(RoR)のリモートデバッグ

NetBeansには、rubyのデバッガフロントエンドが同梱されています。
今回は、それを使ってRuby on Rails(以下RoR)のリモートデバッグをするためのメモです。

個人的には、RoRが動いてるマシンにsshでログインした後、emacsとrubydb3x.elを使う方がメリットがあるsshのポート(TCP 22)が開いてることは普通にありますが、デバッガ・デバッギ通信用のポートがfirewallで遮断されることは良くあります。と思いますが、emacsの場合デバッガ用GUIはありません。GUIに慣れてる人はNetBeansの方が楽なので、そういう人向けにまとめます。

なお、NetBeansをインストールしたローカルマシンで、RoRも動かす場合のデバッグ方法は、NetBeansのwikiを参照してください。

まあ、ローカルマシン上でデバッグする場合も、リモートマシンと接続してデバッグする場合もやり方はほとんど変わりません。

どちらの場合も、デバッギプロセス(と同じプロセス上で動くruby-debug-ide)がTCP(デフォルトだと1234)をListenし、Netbeansからそこに接続してデバッグを開始します。Netbeansから接続するデバッギプロセスがlocalhostにいるか、リモート先のホストにいるかの違いしかないです。
接続後、Netbeansからruby-debug-ideへbreakpointをしかける場所などを指示します。

デバッグ方法そのものは、ローカル・リモートともに一緒ですが、リモートのホストに対してローカルのNetBeansを接続してデバッグするには、リモート先のホストで以下の作業が必要です。

  1. rubyインストール (解説略)
  2. RubyGemsインストール (解説略)
  3. RoRインストール・プロジェクト作成 (解説略)
  4. ruby-debug-ideインストール
  5. リモート先のRoRプロジェクトのソースコードを、ローカルにも展開
  6. ruby-debug-ide付きでRoRプロジェクトのWebサーバ起動

4, 5, 6の手順は、あまりネット上で見かけないので順に解説します。

ruby-debug-ideインストール

Linuxだと、gemで簡単にインストールできます。(gcc, ruby-develをインストールしておけば。)
Fedora8だと以下で終わりでした。

$ gem install ruby-debug
$ gem install ruby-debug-ide

Windows(のmswin版ruby)の場合、コンパイル環境の用意が面倒なので、コンパイル済みgemをダウンロードした上でインストールします。最近だと、Visual C++は無料で手に入りますがmswin版rubyをコンパイルしたVisual C++と同じバージョンをインストールする必要しないと「MSC version unmatch」エラーが出てめんどくさいです。http://rubyforge.org/tracker/index.php?func=detail&aid=16774&group_id=1900&atid=7436

> gem install -l linecache-0.43-mswin32.gem
> gem install -l ruby-debug-base-0.10.3-mswin32.gem
> del ruby-debug-base-0.10.3-mswin32.gem
> gem install ruby-debug
> gem install ruby-debug-ide -v 0.4.6

コンパイル済みgem (linecache, ruby-debug-base)は以下2つのリンク先から
それぞれダウンロードしてください。

リモート先のRoRプロジェクトのソースコードを、ローカルにも展開

ローカルにもソースコードを置かないと、breakpointで止まった時にNetbeansにソースコードが読み込まれません。
注意点としては、リモートと同じフルパス上にソースコードを置く必要がある点です。リモート先で、/var/www/railsprjにRoRのプロジェクトコードが置いてあるなら、ローカルでも/var/www/railsprjにプロジェクトコードを置く必要があります。WindowsとLinuxだとフルパスを一致させることが不可能なので、例えば、Windows上のNetBeansからLinux上のRoRがデバッグができないという点でかなりアレな仕様です。誰もやってないなら修正パッチ作るかな。

この辺の挙動理解には、NetBeans wikiのChecking debugger engine functionalityとか、ruby-debug-ide protocolとかを参考にして、実際にruby-debug-ideとtelnetで会話するといいです。

ruby-debug-ide付きでRuby on RailsプロジェクトのWebサーバ起動

以下な感じでRoR Webサーバ起動します。(-dも付けると、NetBeansとの通信時の処理が良く見えます。)

$ rdebug-ide --stop script/server -h 0.0.0.0
Fast Debugger (ruby-debug-ide 0.4.6) listens on 0.0.0.0:1234

NetBeansからruby-debug-ideに接続

NetBeans IDEのメニュー -> デバッグ -> 接続をクリックし、ダイアログにリモート先ホスト名とポート番号(1234)を指定して接続します。
–stopを指定してあるので、RoRの処理先頭でstopするはずです。
あとは、IDEからbreakpoint指定するなり、なんなり自由に操作します。
watch変数として、paramsを追加しておくといい感じです。

JRuby実行中にjdbでアタッチしてrubyプログラムのbacktrace

Ruby自身のデバッガだと、任意のタイミングでアタッチしてbacktrace見るといったことができない(はず)なので、もっと低レベルのデバッガを利用して、それを実現する方法についてメモ。今回はJRubyに対してjdbでアタッチ&backtraceする方法についてのメモです。
なお、CRubyの場合は、以前の記事とか、さらにその先のリンク先を見れば良いです。
ちなみに、今回試したJRubyのversionは以下の通り。OSはCentOS 5.3です。

$ jruby --version
jruby 1.3.1 (ruby 1.8.6p287) (2009-06-15 2fd6c3d) (Java HotSpot(TM) Client VM 1.6.0_16) [i386-java]

jdbによるbacktrace

まず、アタッチ対象例のRubyコードとして以下を用意。

sample.rb
[RUBY]
def foo
p “abc”
bar
end

def bar
p “foge”
sleep(1000)
end

abc
[/RUBY]

そして、デバッガattach用のポートを開けて起動しますここの「Debugger」の項目を参考にオプション設定しました。

$ jruby -J-Xdebug -J-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n sample.rb

barメソッド内のsleepの実行をしているうちに、別のターミナルを開いてjdbでアタッチし、スレッドを停止させます。

$ jdb -attach 8000
> suspend
すべてのスレッドが中断されました

そして、backtraceを表示させます。以下の方法で大体表示できます(いい加減なやり方ですが)。

> threads
グループ system:
  (java.lang.ref.Reference$ReferenceHandler)0x9db Reference Handler 状況待機中
  (java.lang.ref.Finalizer$FinalizerThread)0x9da  Finalizer         状況待機中
  (java.lang.Thread)0x9d9                         Signal Dispatcher 実行中
グループ main:
  (java.lang.Thread)0x1                           main              状況待機中
> thread 0x1
main[1] where
  [1] java.lang.Object.wait (ネイティブ メソッド)
  [2] org.jruby.RubyThread.sleep (RubyThread.java:718)
  [3] org.jruby.RubyKernel.sleep (RubyKernel.java:725)
  [4] org.jruby.RubyKernel$s_method_0_1$RUBYINVOKER$sleep.call (null)
  [5] org.jruby.internal.runtime.methods.JavaMethod$JavaMethodN.call (JavaMethod.java:620)
  ...(以下略)
main[1] up 4
main[5] locals
メソッド引数:
context = instance of org.jruby.runtime.ThreadContext(id=2527)
self = instance of org.jruby.RubyObject(id=2528)
clazz = instance of org.jruby.MetaClass(id=2529)
name = "sleep"
arg0 = instance of org.jruby.RubyFixnum(id=2531)
block = instance of org.jruby.runtime.Block(id=2532)
ローカル変数:
main[5] dump context.frameIndex
 context.frameIndex = 2 #frameStackの上限(たぶん)
main[5] dump context.file + ":" + context.line  
 context.file + ":" + context.line = "sample.rb:7" #現在位置
main[5] dump context.frameStack[2].fileName + ": " + context.frameStack[2].name + ":" + context.frameStack[2].line
 ...(省略)... = "sample.rb: bar:2" #1つ前のスタックフレーム
main[5] dump context.frameStack[1].fileName + ": " + context.frameStack[1].name + ":" + context.frameStack[1].line
 ...(省略)... = "sample.rb: foo:10" #2つ前のスタックフレーム
main[5] dump context.frameStack[0].fileName + ": " + context.frameStack[0].name + ":" + context.frameStack[0].line
 ...(省略)... = ": null:0" #トップレベル

以上になります。

autoloadで自動読み込みされないファイルをユーザアクセス毎に再読み込みする

今回はRuby on Railsについてメモ。

autoloadで自動読み込みされないファイルRailsの場合、クラス定義と定義が記述されたファイルの名前が対応づいてないとautoloadで自動読み込みされません(たぶん)。例えば、FooBarというクラスの定義はfoo_bar.rbというファイルの中で行われてないとautoloadにされません。をcontrollerの中などでrequireを使って読み込んだ場合、そのファイルはdevelopmentモード時でもユーザアクセス毎に再読み込みされません。そのため、ファイルを編集してもwebサーバを再起動しないと変更が反映されないので、デバッグがやりづらくて困ってました。

解決策がないかと思って、ググってみたところ、以下のページに解決策が書いてありました。
http://www.pistolfly.jp/weblog/2007/06/require-dependency.html

requireじゃなくて、require_dependencyでファイル読み込みすれば良いということでした。
require_dependencyの挙動については、上のリンク先を参照してください。
この辺り(autoload周辺)って、Ruby on Rails公式なドキュメントが見当たらないですねえ。・・・実装を読めってことか。

以下、問題のコード例と解決方法。(ruby 1.8.5, rails 2.3.3, WEBrick 1.3.1で実験しました。)

ファイル読み込みの対象となるファイルを用意します。

<rails_project>/lib/hoge_fuga.rb
[RUBY]
class Hoge
def hoge
return “abc”
end
end
[/RUBY]

上のファイルを読み込むcontrollerを用意します。
このとき、requireの代わりにrequire_dependencyでファイルを読み込みます。

<rails_project>/app/controllers/test_controller.rb
[RUBY]
#require ‘hoge_fuga’
require_dependency ‘hoge_fuga’

class TestController < ApplicationController def hoge a = Hoge.new render(:text => a.hoge)
end
end
[/RUBY]

んで、Webサーバを起動。

$ cd <rails_project>
$ ruby script/server

で、ブラウザからtest/hogeにアクセスすると、ブラウザ上にabcが出力されます。
この状況で、<rails_project>/lib/hoge_fuga.rbを以下のように書き換えてみます。
[RUBY]
class Hoge
def hoge
return “123”
end
end
[/RUBY]

変更を保存した後、ブラウザからtest/hogeにアクセスすると、ブラウザ上に123が出力されます。
確かに、Webサーバを再起動しなくても、<rails_project>/lib/hoge_fuga.rbが再読み込みされてますね。

これでデバッグがしやすくなるなあ。

rubyプロセスのコアファイルからバックトレースする。(おまけ: python)

某所で、「rubyプロセスがSEGVとかした時のコアファイルから、rubyスクリプトのバックトレースは取れるの?」って聞かれたので、ちょっと調べてみました。

結論としては、「rubyメソッドの呼び出し位置(ファイル名, 行番号)は取れるけど、呼び出し時の実引数を見るのは難しい」っていう感じです。
rubyのスタックフレームは、当然、rubyプロセスのメモリ上に構築されるので、スタックフレームのデータ構造さえわかれば、ある程度は表示できます。

ただ、コアファイルを出力したrubyプロセスはすでに存在しないので、ruby実装に使われているC関数をデバッガで(正確にはrubyプロセス上で)実行することができません。
そのため、オブジェクトのinspectなどを実行することが難しく、実引数のオブジェクトが何か調べることが困難です。ということで、今回はスタックフレームを表示させる方法を以下で説明します。

rubyスタックフレームを表示するGDBスクリプトは以下になります(rb_dump.gdb)。
(moriyoshiさんのブログ「 GDBで実行中のスクリプト言語のスタックフレームをダンプしてみる試み」のコードをほとんどそのまま使わせていただきました。ありがとうございます。)

[code]
define dump_rb_bt_from_core
set $t = ruby_frame
while $t
printf “[0x%08x] “, $t
if $t->node.nd_file
printf “(%s:%d)\n”, $t->node.nd_file, ($t->node.flags >> 19) & ((1 << (sizeof(NODE*) * 8 - 19)) - 1) else printf "(UNKNOWN)\n" end set $t = $t->prev
end
end

document dump_rb_bt_from_core
dumps the current frame stack from core file. usage: dump_rb_bt_from_core
end
[/code]

今回はサンプルプログラムとして、0アドレスにアクセスするC拡張ライブラリを実行するrubyスクリプトを用意します。
まず、C拡張ライブラリ(segv.c)、
[C]
#include “ruby.h”

VALUE do_segv(VALUE self){
*(char*)(0x0) = 0;
return Qnil;
}

void Init_segv(){
VALUE module;
module = rb_define_module(“Segv”);
rb_define_module_function(module, “do_segv”, &do_segv, 0);
}
[/C]
で、後は適当にextconf.rbを作って、segv.soを作ります。

次にsegv.soを呼び出すRubyスクリプト(test.rb)。
[RUBY]
require ‘segv’

def test1(a)
test2(a)
end

def test2(b)
Segv::do_segv
end

test1(1)
[/RUBY]

さて、サンプルプログラムの用意ができたので、SEGVさせてみます。

$ ulimit -c unlimited
$ ruby test.rb
test.rb:8: [BUG] Segmentation fault
ruby 1.8.5 (2006-08-25) [i386-linux]

アボートしました (core dumped)

これでコアファイルが出力されたので、それをGDBで解析します。

今回はCentOS 5.3のyumでインストールしたrubyから出力されたコアファイルなので、
GDBで解析するには、ここからrubyのdebuginfoをインストールしておく必要があります。

$ wget http://debuginfo.centos.org/5/i386/ruby-debuginfo-1.8.5-5.el5_3.7.i386.rpm
# rpm -i ruby-debuginfo-1.8.5-5.el5_3.7.i386.rpm

それでは、GDBでコアファイルからrubyのバックトレースをさせてみます。

$ gdb ruby core.29443
GNU gdb Fedora (6.8-37.el5)
...
(gdb) backtrace   #通常のC関数レベルのバックトレース。
#0  0x00b3a402 in __kernel_vsyscall ()
#1  0x0056fdf0 in raise () from /lib/libc.so.6
#2  0x00571701 in abort () from /lib/libc.so.6
#3  0x00c514c2 in rb_bug (fmt=) at error.c:214
#4  0x00cbd80b in sigsegv (sig=) at signal.c:537
#5  [signal handler called]
#6  do_segv (self=3086662140) at segv.c:4
#7  0x00c54dd5 in call_cfunc (...) at eval.c:5657
#8  0x00c5c4ab in rb_call0 (...) at eval.c:5810

(gdb) source rb_dump.gdb      #この記事の最初に作ったGDBスクリプトをロード
(gdb) dump_rb_bt_from_core  # rubyバックトレース
[0xbfc1da20] (test.rb:8)
[0xbfc1e0e0] (test.rb:4)
[0xbfc1e7c0] (test.rb:11)
[0x00d0f960] Cannot access memory at address 0x4

以上になります。

pythonもコアファイルからスタックフレームが取れるか調べてみましたが、
DebuggingWithGDBのgdbinitスクリプトを読む限り、
rubyと同様に実行していたpythonスクリプトのファイル名と行番号は取れそうです。

ActiveRecordの予約語とテーブルのカラム名

RubyのActiveRecordActiveRecordってデザインパターン名だったんだね。知らなかった・・・。http://ja.wikipedia.org/wiki/Active_Recordライブラリを使ってDB操作をした際、ハマッたのでメモ。

ActiveRecordを使うと、DBテーブルのレコードをオブジェクトとして取り出せます。
カラムのデータを取得する際は、カラム名をメソッド名としてオブジェクトを操作すれば良いのですが、
ActiveRecordの予約語と同じカラム名が存在すると正しく動作しません。

例えば、type, displayなどが予約語です。

ためしに、下記のようにテーブルとレコードをつくり、

$ mysql -u root test

CREATE TABLE apples
(
    id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
    type VARCHAR(20) NOT NULL
);
INSERT INTO apples (type) VALUES ('fuji');

以下のRubyコードを書いたとします。
[Ruby]
require ‘rubygems’
require ‘activerecord’

ActiveRecord::Base.establish_connection(
:adapter => ‘mysql’,
:host => ‘127.0.0.1’,
:username => ‘root’,
:password => ”,
:database => ‘test’
)

class Apple < ActiveRecord::Base end a = Apple.find(1) p a.type [/Ruby] 上記コードを実行すると下記のような例外がでます。

/usr/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:1434
:in `instantiate’: The single-table inheritance mechanism failed to locate 
the subclass: ‘fuji’. This error is raised because the column ‘type’ is reserved
for storing the class in case of inheritance. Please rename this column if you didn’t
intend it to be used for storing the inheritance class or overwrite 
Apple.inheritance_column to use another column for that information. 
(ActiveRecord::SubclassNotFound)

typeの場合は、上記のようにわかりやすい例外がでます。displayという名前のカラムが
ある場合は特に例外が発生しません。でも、a.displayと実行してもカラムのデータとは異なる値が
返ってくるので厄介です。

対処法としてはカラム名を変更(type -> etypeとか)するのが普通です。

カラム名を変更したくない場合はselectで別名を指定すれば、問題を回避できます。
[Ruby]
a = Apple.find(1, :select => ‘type as etype’)
p a.etype # “fuji”
[/Ruby]

ActiveRecordの予約語として何があるかは、下記リンクを見てください

その他参考文献

http://uruseiyatsura.way-nifty.com/blog/2007/08/ruby_on_rails.html