株式会社クリアコード > ククログ

ククログ


全文検索エンジンgroongaを囲む夕べ 2: 「groonga村」と「mroongaのベンチマーク」用資料

今年も11月29日に「全文検索エンジンgroongaを囲む夕べ」が開催されました。1年ぶりの開催です。会場は株式会社VOYAGE GROUP(10月に株式会社ECナビから社名変更)でした。会場提供ありがとうございます!とても助かりました。会場提供にあたりこしばさんにとてもお世話になりました。ありがとうございます。

29日なので、もちろん新しいバージョンのリリースも行われています。

今年は、会の内容の概要を紹介する導入用のセッション「groonga村」と、mroongaの性能特性がわかるベンチマーク結果を紹介するセッション「mroongaのベンチマーク」を担当しました。

groonga村

mroongaのベンチマーク

「mroongaのベンチマーク」で紹介した結果はOSC2011.DBで紹介した結果とほとんど同じなので、解説はそちらを参照してください。ただし、OSC2011.DBで紹介した位置情報検索のベンチマークデータが間違っていました。

間違っていた位置情報検索のベンチマーク結果

正しくは、「mroongaの方が劇的に速い」という結果ではなく、以下のように「mroongaの方が速い」という結果でした。

正しい位置情報検索のベンチマーク結果

ここでは、groonga周辺の技術の整理にもなるので「groonga村」の内容を紹介します。

groonga開発チームの期待

まずはじめにこの会でgroonga開発チームが参加者のみなさんに期待していることを伝えました。

groonga開発チームの期待

期待していることは以下の2つです。

  • groonga開発者が増えること
  • groongaユーザーが増えること

どちらもgroongaがよくなることにつながります。

森さんの資料(PDF)や矢田さんの資料(PDF)にもあるとおり、groonga開発チームでやりたいことはいろいろあるのですが、そこまで手がまわっていません。groongaを開発してくれる人が増えると、groongaがより使いやすくより速い全文検索エンジンになります。

ユーザーが増えるといろんな環境でのテストにもなります。問題があったとフィードバックをもらって、それを修正できればgroongaの品質が向上します。また、実際に使うにはどのような機能が求められているかを聞かせてもらえれば、よりgroongaを使いやすくすることにも役立ちます。それ以外にも「このように使っています」や「groongaを使うために便利なツールを作りました」などといった情報を公開してくれるというのも、groongaユーザーにとって有益な情報になります。ぜひ、使って、フィードバックや情報公開をお願いします!

groongaの開発に参加したいという方も、groongaの事例紹介ページに載せてもいいという方も、groongaのメーリングリストまたはgroonga at razil.jp(またはkou@clear-code.com)へご連絡ください!

参加者の期待

今年の会は、参加者のみなさんが以下を期待しているのではないかということで構成しました。

参加者のみなさんの期待

それぞれについて以下のセッションで扱いました。資料は公開できるように調整中です。すでに公開されているものはリンクしています。まだ資料が公開されていないものはUstreamの録画を観てください。

groonga関連ソフトウェアの位置づけ

groongaはライブラリとして利用できるため、MySQLやPostgreSQL、Rubyなど様々なソフトウェアと連携できます。そのため、今回の会でもたくさんの分野の話がでてきます。話をきいているうちにどこの話をしているのかわからなくなってしまうと、話についていけなくなってしまうかもしれません。それを防ぐためにgroongaとその関連ソフトウェアの位置づけを説明しました。

groonga村

これが全体像です。それでは、それぞれの部分を説明します。

コア機能

コア機能

キー管理機能(レコードを参照するときに利用)や転置索引、キーストアなどgroongaが提供する機能のベースになる機能を提供するレイヤーがあります。

DB API

DB API

コア機能の上にデータベース機能を提供するDB APIがあります。このAPIを使うとSQLiteのようにプロセス内にデータベースを持ったアプリケーションを開発することができます。

rroonga

rroonga

DB APIを使ったソフトウェアがrroongaです。RubyからDB APIを使えるようになります。buzztterるりまサーチMilkodeなどがrroongaを使っています。

mroonga

mroonga

MySQLからDB APIを使うためのソフトウェアがmroongaです。SQLを使ってgroongaを使えるようになるため、既存のSQL関連の技術をそのまま使えることが魅力です。例えば、Ruby on RailsでWebアプリケーションを使っている場合はActiveRecord経由でgroongaの高速な全文検索機能を使えることになります。

クエリAPI

クエリAPI

groongaは独自のクエリ言語を持っています。RDBMSでいうSQLのようなものです。このAPIを使うと文字列をやり取りすることでgroongaの機能を使うことができます。

groongaコマンド

groongaコマンド

クエリAPIをネットワーク越しやコマンドライン上で使うためにgroongaコマンドを提供しています。groongaコマンドはHTTPサーバーにもなるため、特別なライブラリではなく通常のHTTPライブラリを使うだけで任意のプログラミング言語からgroongaの機能を使うことができます。塩畑さんの発表にもある通り、ぐるなびさんではHTTPでgroongaの機能を利用しています。

nroonga

nroonga

クエリAPIをNode.jsから使えるようにするライブラリがnroongaです。Node.jsからgroongaの機能を利用するためのnode-groongaというライブラリもありますが、こちらはgroongaコマンドのHTTPサーバーと通信するもので、直接クエリAPIを叩くnroongaとはレイヤーが違います。node-groongaはgroongaコマンドをより便利に使うためのもので、nroongaはgroongaコマンドのHTTPサーバー機能の代替になりうるものです。

groongaコマンドでもHTTPサーバー機能を実現できますが、実は単純なHTTPサーバー機能しかありません。例えば、認証機能もありませんし、WebSocketを実現することもできません。それらの機能も実現できるリッチなHTTPサーバーはNode.jsが提供する機能を利用するのがよいのではないか、というアイディアから生まれたものです。まだ開発が始まったばかりのプロジェクトですが、興味のある方はぜひ開発に参加してください!

textsearch_groonga

groonga with PostgreSQL

PostgreSQLからgroongaの機能を使うためのソフトウェアがtextsearch_groongaです。mroongaと違いDB APIとクエリAPIをうまく組み合わせてgroongaの機能を使っています。

まとめ

groonga村

groongaはライブラリとして利用できるため、このように多くのソフトウェアと連携し、ニーズにあわせた使い方ができるようになっています。ぜひgroongaを使ってみてフィードバックをお願いします!

それでは、また、来年のいい肉の日に会いましょう*1。会場提供をしてくれたVOYAGE GROUPのみなさん、Ustreamで放送してくれたグニャラくんさん、司会をしてくれた坂井さん、受付をしてくれたRuby会議実行委員会のしまださん、すずきさん、発表者のみなさん、会場に来てくれたみなさん、Ustreamで参加してくれたみなさん、ありがとうございました!

おまけ: 参加状況について

今回も前回同様にATNDを使ってイベント告知・参加者登録を行いました。定員100名で告知をし、前日までに160名くらいの登録でキャンセル数は1桁でした。ですが、前回の当日キャンセル数とキャンセルなしで不参加の傾向を考えると、事前に160名の参加登録があったとしても当日はちょうど定員である100名くらいになるのではないかと予想し、「補欠の人も会場にきても大丈夫です」ということにしました。実際、当日にキャンセル数が30くらいになり、キャンセルなしで不参加の人も50人くらいいたため、最終的には80-90名くらいの参加になりました。(当日に参加登録した方もいたようです。)

「補欠の人も会場にきても大丈夫です」というスタイルは会場提供のVOYAGE GROUPさんがOKを出してくれたので実現できたのですが、このくらいのゆるさでやったほうがちょうどよくなるのかもしれませんね。柔軟な対応、ありがとうございました!

*1 groonga開発チーム主催のgroonga勉強会は年に一度ですが、ユーザーのみなさんはいい肉の日以外にもgroonga勉強会を開催して大丈夫です!

タグ: Groonga

この記事の続き

2011-12-01

デバッグ力: よく知らないプログラムの直し方

クリアコードではMozilla製品やRuby関連の開発だけではなく、広くフリーソフトウェアのサポートもしています。もちろん、サポート対象のソフトウェアの多くは私達が開発したものではありません。しかし、それらのソフトウェアに問題があった場合は調査し、必要であれば修正しています。

このようなサポートが提供できるのは、もともと、私達がフリーソフトウェアを利用したり開発したりしているときに日常的に問題の調査・修正をしていたからです。ソフトウェアを利用していると、問題に遭遇することはよくあることです。そのソフトウェアがフリーソフトウェアの場合は、開発者に問題を報告し、可能ならパッチを添えます。このとき、そのソフトウェアの内容を完全に把握していることはほとんどありません。しかし、それでも修正することができます。

それはどうしてでしょうか?今まではどのようにやっているのかを自分達でもうまく説明できなかったのですが、最近、少し説明できるようになってきた気がするのでその方法を紹介します。題材はruby-trunk - Bug #5688: Solaris10 で spawn を繰り返すとメモリリークする - Ruby Issue Tracking Systemです。この問題はメモリリークなので、プログラムを動かし続けてもメモリ使用量が増えなくなれば解決ということになります。

基本的な流れ

まず、よく知らないプログラムをデバッグするときの流れを整理してみました。整理してみると、以下のような流れで作業をしていました。

  1. 問題を再現する小さなプログラムを作成する。
  2. 問題のソフトウェアを変更しながら原因となっている箇所を特定する。
  3. 問題を修正する。

問題を再現する小さなプログラムの作成

問題が発覚するときは問題のある処理と問題のない処理が混ざっていますが、この状態のままで修正作業に着手するのは現実的ではありません。例えば、問題が再現するまでに時間がかかる・問題を再現させることが面倒・調査対象の範囲が広くなってしまう、などといった理由で修正作業が大変になってしまいます。ですのでこのような時は、問題が再現する必要最低限の条件(再現条件、再現手順*1)をなるべく正確に特定しておく事が大事です。

また、再現条件はプログラムやスクリプト、自動テストなどのような形で、誰がやっても何度でも正確に問題を再現できるという状態にしておく事が望ましいです*2。今回はRubyそのものの問題の調査なので、自動化された再現手順として、問題を再現する小さなプログラムを作成する事にしました。

この時点では、あくまでもソフトウェアの外から見るという視点で問題をざっくりと切り分けていきます。今回の例の問題に遭遇したときは、同じような処理を実行するRubyスクリプトをいくつか動かしていたときでした。その中の一部のRubyスクリプトではメモリ使用量が増えていき、それ以外のRubyスクリプトではメモリ使用量が増えませんでした。そこで、それらの違いを抽出して、Redmineにある以下のような外部プロセスを実行するだけのRubyスクリプトを作成しました。

1
2
3
4
5
6
#!/usr/bin/env ruby
ARGV[0].to_i.times do |n|
  spawn("sleep", "5")
  sleep 0.2
  GC.start if n % 100 == 0
end

これでも問題が再現したため、原因はRubyのspawn内にあると考えることができます。このように、十分小さな再現プログラムを作成できたら次のステップに進みます。*3

問題のソフトウェアを変更して原因となっている箇所を特定

再現プログラムができたら、問題となっているソフトウェアについてあたりをつけることができます。今回の例では、ライブラリを使っているわけではないので、Ruby本体が怪しいと言えます。

再現プログラムでは、外から問題の原因を絞り込んでいましたが、このステップでは、中から絞り込んでいきます。今回の例では、forkの実体であるrb_fork_err()の中が絞り込み対象です。rb_fork_err()を簡略化すると以下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rb_pid_t
rb_fork_err(...)
{
    rb_pid_t pid;

    for (; before_fork(), (pid = fork()) < 0; prefork()) {
        ...;
    }
    if (!pid) {
        ...; /* 子プロセス用の処理 */
    }
    after_fork();
    return pid;
}

このうち、「for (...) {...}」は「forkに失敗しても何回か試してみる」という処理なのでさらに簡略化して以下のように考えることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
rb_pid_t
rb_fork_err(...)
{
    rb_pid_t pid;

    before_fork();
    pid = fork();
    if (!pid) {
        ...; /* 子プロセス用の処理 */
    }
    after_fork();
    return pid;
}

ここで、どこが原因の切り分けポイントになるかを考えます。まず、上記の処理の中を大きく分けると以下のようになります。

  1. before_fork()
  2. fork()
  3. if (!pid) {...}
  4. after_fork()

このうち、「if (!pid) {...}」は子プロセス専用の処理なので今回は考えなくてもよいことがわかります。なぜなら、今回の再現プログラムでは親プロセスのメモリ使用量が増えることを問題視していて、子プロセスに関しては気にしていないからです。すると以下が問題の切り分けポイントになることがわかります。

  1. 何も実行しなかったらどうなるか。
  2. before_fork()のみ実行したらどうなるか。
  3. before_fork()fork()のみ実行したらどうなるか。

それぞれの結果で以下のようなことがわかります。

試すこと メモリリークした場合 メモリリークしなかった場合
何もせずにすぐにreturn rb_fork_err()の前に原因あり。(今回はこのケースはないはず。) before_fork()fork()after_fork()に原因あり。
before_fork()の後にすぐにreturn before_fork()の中に原因あり。 fork()after_fork()に原因あり。
fork()の後にすぐにreturn fork()の中に原因あり。(fork()はOSが提供しているので、今回はこのケースはないはず。) after_fork()に原因あり。

それぞれを試すコードは以下のようになります。

何もせずにreturn:

1
2
3
4
5
rb_pid_t
rb_fork_err(...)
{
    return -1;
}

before_fork()の後にすぐにreturn:

1
2
3
4
5
6
rb_pid_t
rb_fork_err(...)
{
    before_fork();
    return -1;
}

fork()の後にすぐにreturn:

1
2
3
4
5
6
7
8
9
10
11
12
rb_pid_t
rb_fork_err(...)
{
    rb_pid_t pid;

    before_fork();
    pid = fork();
    if (!pid) {
       _exit(EXIT_FAILURE); /* 子プロセスは気にしないのですぐ終了する。 */
    }
    return pid;
}

実際に試すと以下のことがわかりました。

試すこと メモリリークしたか
何もせずにすぐにreturn しない
before_fork()の後にすぐにreturn しない
fork()の後にすぐにreturn しない

よって、after_fork()の中に問題があることがわかります。

続いて、上記の手順と同様にafter_fork()の中を調べ、原因を絞り込んでいくことを繰り返すと、rb_thread_create_timer_thread()の中のpthread_attr_tに原因があることがわかります。ここで大事なことは「上記の手順と同様に…繰り返す」という部分です。少し原因を絞り込めたからといって、一足飛びに手順を飛ばして原因を見つけようとすると迷子になってしまいます。十分に絞り込めるまではコツコツと一歩ずつ確実に原因を絞り込んでいくことが重要です。さくさくとデバッグを進めていく様子だけを見ると、手順を飛ばして一気に問題を解決しているように見えるかもしれませんが、そんなことはありません。一歩ずつ確実に進めています。ただ、その一歩ずつがテキパキと手早く進んでいるだけなのです。

原因を十分に絞り込めたら問題を修正します。

問題の修正

今回の例では、原因を絞り込むとpthread_attr_tを使った場合のみメモリ使用量が増えることがわかりました。簡略化すると以下のように使用していました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
rb_thread_create_timer_thread(void)
{
    ...;
    if (!timer_thread_id) {
        pthread_attr_t attr;
        ...;
        pthread_attr_init(&attr);
        ...;
        err = pthread_create(&timer_thread_id, &attr, thread_timer, 0);
        ...;
    }
    ...;
}

同じソースコード内のpthread_attr_tの他の使用例を見ると、以下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int
native_thread_create(rb_thread_t *th)
{
    ...;
    else {
        pthread_attr_t attr;
        ...;
        CHECK_ERR(pthread_attr_init(&attr));
        ...;
        err = pthread_create(&th->thread_id, &attr, thread_start_func_1, th);
        ...;
        CHECK_ERR(pthread_attr_destroy(&attr));
        ...;
    }
    ...;
}

違いはpthread_attr_destroy()を呼んでいるかいないかです。pthread_attr_destroy()は名前からして必要のなくなったリソースを解放する関数にみえるので、rb_thread_create_timer_thread()pthread_attr_destroy()を呼び忘れていることが原因と考えてよさそうです。実際にpthread_attr_destroy()を追加して再現プログラムを実行するとメモリ使用量が増えなくなることが確認できました。これで修正できたと考えてよさそうです。ということで、パッチができあがることになります。

まとめ

Rubyでのメモリリークの問題を例にして、私達がやっている「よく知らないプログラムの直し方」を説明しました。今回のような直し方では「それぞれのコードがどうしてそのようなコードになっているのか」という点はほとんど考慮にいれていません。単純に「その処理を実行したら問題が再現するかどうか」だけで絞り込んでいます。そのため、プログラム全体を把握する必要がなくなり、よく知らないプログラムに対しても使える技術になっています。もちろん、この技術はよく知っているプログラムに対しても使えます。

おさらいすると、プログラムの直し方は以下のステップになります。

  1. 問題を再現する小さなプログラムを作成する。
  2. 問題のソフトウェアを変更しながら原因となっている箇所を特定する。
  3. 問題を修正する。

このうち、「問題を再現する小さなプログラムを作成する」ことはよく聞くのではないでしょうか。バクレポートを報告する文脈でも、「再現プログラムをつけてくれるとうれしい」というのはよく言われます。しかし、「問題のソフトウェアを変更しながら原因となっている箇所を特定する」についてはあまり言及されていないようです。今回は処理を実行するかどうかで判断する方法を使いましたが、print文を埋め込んだり、デバッガを使ったりすることもあります。これらは場合によって使い分けます。それらのやり方よりも大事なことは、「この結果で何がわかるか」を意識しながら一歩一歩着実に原因を絞り込んでいくことです。多くの場合、原因がわかったら修正することはそれほど難しいことではありません。

たとえよく知らないプログラムでも、問題を見つけたときはその問題を直す人が増えることを願っています。華麗に回避することもできるでしょうが、次に同じ道を進んできた人が同じ問題にぶつからずにすんだらステキではないでしょうか。

*1 Steps To Reproduce、略してSTRと言われる事もあります。

*2 言葉で再現条件を説明した場合、読み違いが原因で、人によっては問題を再現できないといった事が起こり得ます。また、デバッグ中に何度も再現試験を実施する際に、ヒューマンエラーで再現手順を間違えてしまうという事もあり得ます。

*3 この後にforkしてすぐにexitするだけでも問題が再現するところまで再現プログラムを小さくしていました。そのため、実際は、次のステップに進む前にspawnというよりもfork内に原因があるということまで絞り込めていました。

2011-12-06

Ruby関連のコンテストっぽいものへの応募例

Rubyに関する取り組みは無理のない範囲で応援しています。コンテストっぽいものもそのうちの1つで、ちょうど募集している時期にそれっぽいものがあれば応募しています*1。これは、応募者が少しでも増えると、次回の開催時には少しは盛り上がるのではないかという期待からです。「応募したくなる人が増えるには」という方向で考えた応援の仕方です。

逆に、応募する敷居を下げる方法もあるでしょう。その1つに「どのように応募すればよいかのヒントを提供する」ことがあるのではないでしょうか*2。ということで、Rubyアソシエーション:2011年度助成金公募への応募内容と第3回フクオカRuby大賞への応募内容を紹介します。Rubyアソシエーションの方は落ちた応募で、第3回フクオカRuby大賞は受かった応募なので参考にするときは注意してください。なお、どちらも現在(2011-12-12)は募集していませんので、気になった方は次回の募集時に応募してみてください。

Rubyアソシエーションの2011年度助成金への応募内容

目的が似ていそうな原さんの世界の誰でも読めるRubyリファレンスマニュアル(に向けて)が採択されています。その観点から、「この応募内容では何が足りないのか」を考えると参考にしやすいのではないでしょうか。なお、2011年12月18日(日) 13:00から秋葉原でRubyの次世代リファレンスマニュアルを構想する会あるそうなので、興味のある方は参加してみてはいかがでしょうか。

----
* 応募者名(個人名又は団体名)

株式会社クリアコード

* 担当者名

須藤功平

* 連絡先電子メールアドレス

kou@clear-code.com

* 略歴

  * 2006年7月25日: 設立
  * 2008年8月5日: 代表取締役を須藤に交代
  * 2009年6月: 日本Ruby会議2009にスポンサー・発表者として参加
  * 2009年10月: とちぎRuby会議02にスポンサー・発表者として参加
  * 2009年12月: 札幌Ruby会議02にスポンサー・発表者として参加
  * 2010年8月: 日本Ruby会議2010にスポンサー・発表者として参加
  * 2011年3月: 第3回フクオカRuby大賞でコミュニティ特別賞を受賞
  * 2011年6月: 本社移転
  * 2011年6月: 日本Ruby会議2011にスポンサー・発表者として参加

* プロジェクト名

Ruby用ドキュメントツールの国際化対応

* プロジェクトの詳細

以前のRubyは他のプログラミング言語と比較してドキュメントが弱
いと言われていたが、最近ではるりまプロジェクト(*1)や"Ruby
1.9.3 Documentation Challenge"(*2)などによりRubyのドキュメン
トが改善されている。

(*1) http://redmine.ruby-lang.org/projects/rurema/wiki
(*2) http://blog.segment7.net/2011/05/09/ruby-1-9-3-documentation-challenge

しかし、上記の2つのプロジェクトはそれぞれ独立に動いており、片
方の成果をもう一方が取り込む、といったことが難しい状況となっ
ている。ドキュメントを改善する作業はあまり人々の関心を得られ
ず、慢性的にリソースが足りない状況である。それにも関わらず
リソースが分散してしまっており、日本語のRubyのドキュメントも
英語のRubyのドキュメントもRuby全体を網羅したものにはなってい
ないのが現状である。

このような状態になってしまっているのは、ドキュメントツールが
国際化に対応していないことが問題であると考える。そこで、この
プロジェクトではRuby用のドキュメントツールであるYARD(*3)に国
際化機能を実装し、貴重なドキュメント改善作業のリソースを分散
せずに協力して作業できる仕組みを提供する。

(*3) http://yardoc.org/

なお、すでにこの作業は開始しており、YARDのメーリングリストで
国際化対応について議論を行なった(*4)。国際化対応作業の方向に
ついてYARDの作者から合意を得ており、実際にいくつかの機能は実
装済み(*5)である。これより、以下のことが言えるため本プロジェ
クトの実現可能性は高いと考える。

  * YARD開発チームとの関係が良好である
  * すでに方向性が決まっている
  * すでに動き出している

(*4) http://groups.google.com/group/yardoc/browse_thread/thread/9aecb2fe3c6c9d5
(*5) https://github.com/kou/yard

Ruby標準添付のRDocではなくYARDに対して国際化対応を行う理由は
以下のとおりである。

  * 国際化機能を実装するにあたり、RDocでは他のライブラリを利
    用しづらいため。
    (他のライブラリもRubyに標準添付しなければいけないため。)
  * YARDはRDocの上位互換であり、RDocで書かれたドキュメントも
    YARDで扱えるので、YARDの方が国際化機能を適用できる範囲が
    広いため。
  * YARDの方が拡張を意識した作りとなっており、実装しやすいた
    め。

YARDを国際化対応した結果、使い勝手がよく継続的に利用できるも
のであると判断できた場合はRDocへのポーティングも検討すること
を考えているが、本プロジェクトの範囲とはしない。

* プロジェクトの成果物

以下を成果物とする。

  * 国際化機能付きのYARD
  * 国際化されたYARDのドキュメント(日本語のドキュメント)

ただし、YARDのリリースはYARDの開発者が行うため、期間内に上記
のYARDがリリースされるとは限らない。

実装した国際化機能を用いて実際にYARD自体のドキュメントを国際
化する。これにより国際化機能が利用しやすいか・継続可能な仕組
みになっているかを評価する。Ruby本体や多くのライブラリは、一
度作ったら完成するものではなく、日々改良が続けられていくもの
である。そのため、ドキュメントも一度作成したら完成ではなく、
改良に追従していく必要がある。国際化機能そのものも重要である
が、ドキュメントの改良が無理なく続けられる仕組みであることも
重要であるため、その観点でも評価する。
----

第3回フクオカRuby大賞への応募内容

第3回フクオカRuby大賞は予備審査と本審査があり、予備審査は応募内容を使って審査するそうです。この応募内容で予備審査を通過しました。1年ほど前のものなので内容が古くなっていたりしますね。

----
○ 応募日

平成22年11月13日

○ ソフトウェア、取組等の名称

るりまサーチ

○ 応募者区分

法人・団体として応募

○ 応募者

○○ 応募者名

須藤功平

○○ 応募者名ふりがな

すとうこうへい

○○ 法人・団体 代表者名

須藤功平

○○ 法人・団体 代表者役職

代表取締役

○ 所在地

○○ <都道府県>

東京都

○○ <市区町村名、番地>

文京区本郷1-25-4

○○ <ビル名等>

ベルスクエア本郷5F

○ URL

http://www.clear-code.com/

○ 連絡担当者

○○ 担当者 氏名

須藤功平

○○ 担当者 氏名ふりがな

すとうこうへい

○○ 担当者 所属部署

所属なし

○○ 担当者 役職

代表取締役

○○ 連絡先TEL

03-6231-7270

○○ 連絡先FAX

03-6231-7271

○○ 連絡先e-mail

kou@clear-code.com

○ 1.ソフトウェア、取組等の概要について

○○ (機能・特徴を簡潔に説明してください。)

るりまサーチはRuby本体のリファレンスマニュアルを高速に検索す
るWebアプリケーションです。説明文などテキスト情報から全文検索
して目的のマニュアルを検索する機能はもちろん、マニュアルの記
述対象の種類(クラスについて記述している、インスタンスメソッ
ドについて記述しているなど)など付加的な情報からも絞り込んで
いける機能を提供しています。この多角的に絞り込んでいく機能に
より、少ない労力で目的のマニュアルを見つけ出せることが特徴で
す。

○○ (ソフトウェア、取組等について、参考となるURLを記載してください。)

URL: http://rurema.clear-code.com/

○ 2.ソフトウェア、取組等の目的、ねらいについて

○○ (開発の目的や取組の動機、ターゲット等を記述してください。)

開発の目的:

目的は「Rubyのドキュメントが十分実用的であると評価されるよう
にすること」です。開発に用いる言語を選択する場合の指標のひと
つにドキュメントの充実度が挙げられることがよくあります。その
時、PHPやPythonなどは充実しているが、Rubyはそれほどでもない、
と評価されます。これは、せっかく充実してきたリファレンスマニュ
アルを十分に活用するためのインターフェイスがないことが原因だ
と考えています。

従来のリファレンスマニュアル閲覧Webアプリケーションにも検索機
能がありましたが、全文検索に数十秒かかる、全文検索以外の検索
方法がない、などの理由により、せっかく充実してきたリファレン
スマニュアルを十分に活用することができませんでした。るりまサー
チを開発することにより、検索機能を充実させ、リファレンスマニュ
アルを活用できる環境を提供することで目的の達成に近づくと考え
ています。

ターゲット: Rubyを用いて開発している開発者すべて

○ 3.Rubyを用いた理由、またはRubyに注目した理由について

Ruby本体のリファレンスマニュアルを検索するシステムにはRubyを
用いることが必然だからです。

○ 4.ソフトウェア、取組等の特徴について

○○ 1)優位性、セールスポイントについて、具体的に記載してください。

まず高速であることです。すぐに結果が返ってこないと格段に使い
勝手が悪くなります。処理に何秒もかかった上に「0件ヒット」と
なるようなシステムは使われなくなります。

次にリンクを辿るだけで絞り込んでいけることです。

キーワードがわかっている場合はそのキーワードを入力するだけで
目的のマニュアルをすぐに見つけることができますが、そうでない
場合の方がほとんどです。その場合は絞り込み条件を増やすことに
より、検索結果を絞り込んでいき目的のマニュアルを見つけていき
ます。この操作を簡単にできる仕組みを提供しています。

検索結果内にあらかじめ絞り込み条件をリンクとして挿入しておき
ます。例えば、エンコーディング関連を調べているとします。まず
「enc」というキーワードで絞り込みます。

  http://rurema.clear-code.com/query:enc/

このとき、ページ左のサイドバーには「インスタンスメソッド」や
「定数」など、種類での絞り込み条件を表示します。リンクの右側
に表示されている「(...)」は絞り込み後のヒット数です。アプリ
ケーション側は絞り込みリクエストを受け取る前にすでにヒット数
を知っているので、絞り込み後にヒット数が0になる条件はここに表
示しません。つまり、「絞り込んだ後にヒット数が0になる」という
無駄な操作をユーザがしなくても済むようになっています。

また、ページ本体の検索結果表示部分にも「Rubyのバージョンによ
る絞り込みリンク」、「インスタンスメソッドなどの種類」、「所
属するクラスなど関連する付加情報」などによる絞り込みリンク表
示しています。これにより、リンクを辿るだけでどんどん絞り込ん
でいけるため、少ない労力で目的のマニュアルを見つけることがで
きます。

上記のように高速である、リンクを辿るだけで絞り込んでいけると
いう機能が優位性、セールスポイントになります。

○○ 2)新規性、革新性について、具体的に記載してください。

上記の優位性、セールスポイントは先進的なECサイトや情報提供サ
イトではすでに導入されています。(例えば、amazon.co.jpやぐる
なびなどでも導入されています。)このような機能は今後の検索シ
ステムでは標準的な機能になっていくと考えています。

るりまサーチはそのような先進的な検索機能をRubyで実現できるこ
とを示した、実際に動くオープンソースソフトウェアであることに
新規性があります。先進的なECサイトなどの実装がオープンソース
ソフトウェアとして公開されることはないため、同様の機能を実現
するためには1から自分で調べる必要があります。しかし、るりま
サーチはオープンソースソフトウェアとして公開されているため、
どのように機能を実現しているかを参考にしたり、改変して利用す
ることなどができます。

○○ 3)社会的効果、インパクトについて、具体的に記載してください。

上述の内容と重複しますが、以下の2点が社会的効果となります。

  * Rubyのドキュメント環境が改善されることによりRubyの普及に
    つながる。
  * オープンソースソフトウェアとして公開されているため、同様
    の検索機能を実装する場合に有用である。

○ 5.ソフトウェア、取組等の実績について

○○ (具体的な実績、市場等からの評価があれば記載してください。)

Rubyのリファレンスマニュアルの公式検索サービスに採用されまし
た。(現在はメンテナンス中のようです。)

  http://doc.ruby-lang.org/ja/search/

るりまサーチをもっと便利に利用するためのソフトウェアを開発し
てくれた方がいました。

  http://codnote.net/2010/09/20/rurema-instant/
  https://github.com/sorah/rurema_instant

○ 6.今後の展開について

○○ (今後の目標や事業展開について記載してください。)

るりまサーチはRubyのリファレンスマニュアルを便利に検索する機
能を提供するという目的の他に、バックエンドで利用している全文
検索エンジンgroonga(*)のデモという目的もあります。

(*) groonga: http://gronoga.org/

クリアコードではgroongaを利用した検索システムの開発・開発支援
も行っています。groongaを利用した場合にどのような検索システム
を作ることができるか、ということを実際に動くるりまサーチとい
うアプリケーションでデモできます。

るりまサーチ自体はこれまで通り、groongaの機能を活かした使い
やすい機能を追加していきます。るりまサーチはデモとして使い、
新しい開発案件などにつなげていきます。るりまサーチ自体は今後
もオープンソースソフトウェアのままです。

○ 7.添付資料について(添付資料は返却しません。)

○○ (概要説明図やこれまでの発表資料、新聞・雑誌の記事等)

るりまサーチ: Rubyでgroonga使ってリファレンスマニュアルを全
文検索 - ククログ(2010-04-27):
  http://www.clear-code.com/blog/2010/4/27.html

日本Ruby会議2010発表資料: るりまサーチの作り方 - Ruby 1.9で
groonga使って全文検索 - ククログ(2010-09-01):
  http://www.clear-code.com/blog/2010/9/1.html

○ 8.ソフトウェアの動作環境について(ソフトウェアのみ)

○○ (OS、ソフトウェア、ハードウェア構成等について記載してください。)

OS: Linux 64bit
    (rurema.clear-code.comではDebian GNU/Linux lenny 64bitを利用)
ソフトウェア:
  * Ruby 1.9.x
  * groonga
  * Rack
  * rroonga
  * racknga
ハードウェア構成:
  * CPU: 64bit対応のもの
  * メモリ: 512MB以上
----

まとめ

Rubyアソシエーション:2011年度助成金公募への応募内容(落選)と第3回フクオカRuby大賞への応募内容(書類審査通過)を紹介しました。このようなコンテストっぽいものに応募しようとしている人の参考になり、応募者が増えたら、紹介した甲斐があるというものです。

*1 それっぽいものがないときは無理して用意しません。

*2 実際、「第3回フクオカRuby大賞」はどのような内容で応募したかを聞かれたことがあるため、少しは必要とする人がいるはず。

タグ: Ruby
2011-12-12

Netscape Communicator 4.5以降のプロファイル情報の解析

クリアコードではMozillaサポート事業を行っていますが、その一環としてNetscape Communicator 4からFirefoxやThunderbirdへの移行のお手伝いもしています。今回は、NC4.5からThunderbirdへの移行の際に特に問題となった、NC4.5のユーザプロファイル一覧の取得方法の調査の解説を通じて、クリアコードでのオープンソース製品のサポート業務の一例をご紹介したいと思います。

NC4.5のユーザプロファイルの困った事情

本題の前に、まずは背景事情を説明しましょう。

Thunderbirdは、設計的にはNC4(Netscape Messenger)の子孫と言えますが、製品としてはNC4の後継ではありません。そのため、ThunderbirdのNC4からの移行支援の対象は非常に限られており、実際に「設定とデータのインポート」機能ではNC4のユーザ情報からはローカルに保存されたメールしかインポートできません。それ以上の情報をインポートしようと思うと、現状ではそのための仕組みを独自に用意する必要があります。

この時に問題になる事の1つとして、NC4のプロファイルの一覧やそれぞれのプロファイルの内容の保存場所をどのように取得すればよいのか?という事が挙げられます。

今のWindowsはマルチユーザ前提の設計になっており、"C:\Users\<ユーザ名のフォルダ>\AppData\Roaming" (Windows XP以前では "C:\Documents and Settings\<ユーザ名>\Application Data")内にユーザごとの設定を保存するのが当たり前になっています。しかし、昔のWindowsは「1台のPCを使うのは1人だけ」という前提で設計されていたため、複数人で1台のPCを共用しているなどの理由で複数のユーザ設定を使い分けたい場合には、アプリケーションの側で独自にマルチユーザ対応のための仕組みを持つ必要がありました。

NC4もそういった時代に作られた古いアプリケーションの1つで、既定の状態ではプロファイルは "C:\Program Files\Netscape\Users" 以下に保存されるようになっています。

ただ、プロファイルの実体は任意の場所に作成できるため、単純に "C:\Program Files\Netscape\Users" のサブフォルダを辿るだけでは見落としが発生してしまいます。すべてのプロファイルをもれなく把握するためには、そのPC上にあるプロファイルの一覧を取得するための正しい手順を踏まなくてはなりません。実は、Thunderbird 2までのバージョンにはnsreg.datの内容を解釈した結果を取得するためのAPIが含まれていたのですが、Thunderbird 3までの間でそのAPIは削除されてしまったので、今回は代わりの方法を考える必要がありました。

そのプロファイルの一覧の保存先なのですが、NC4の初期のバージョンではWindowsのレジストリに情報が保存されていたのに対し、Netscape 4.5以降では "C:\Windows\nsreg.dat" というバイナリファイルに情報が保存されるように仕様が変更されました。よって、NC4.5以降からThunderbirdへの移行を支援するためには、このバイナリファイルに立ち向かう必要があります。

前置きが長くなりましたが、このバイナリファイル(nsreg.dat)はどのような形式になっていて、どのようにすれば内容を読み取る事ができるのか、というのがこの記事での主題となります。

nsreg.datの内容を読み解くヒントを探す

バイナリファイルの内容自体は、以下のようにしてバイト列として読み込む事ができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
// nsIFileでnsreg.datを渡すと仮定
function readBinaryFrom(aFile) {
  var fileStream = Cc['@mozilla.org/network/file-input-stream;1']
                     .createInstance(Ci.nsIFileInputStream);
  fileStream.init(aFile, 1, 0, false);
  var binaryStream = Cc['@mozilla.org/binaryinputstream;1']
                       .createInstance(Ci.nsIBinaryInputStream);
  binaryStream.setInputStream(fileStream);
  var bytes = binaryStream.readByteArray(fileStream.available());
  binaryStream.close();
  fileStream.close();
  return bytes;
}

この関数によって読み込まれた内容は、1バイト(8ビット)単位の内容がNumberとして格納されたArrayとして返されます*1。ですので、後はこの単純なバイト列の中に埋め込まれた「意味」を読み取ればよいという事になります。

nsreg.datの内容について解説したドキュメントは無いものかと思いまずは簡単に検索してみましたが、断片的な情報はいくつか見つかるものの、ちゃんとした技術情報には辿り着けませんでした。なのでとりあえず、ファイルの中身を見てみる事にしました。

このファイルをバイナリエディタ*2で開くと、「ProfileLocation」やプロファイルの位置と思われる文字列がNULL文字(終端文字)を伴う形で、一定の規則に則って埋め込まれている事が見て取れます。そこで、最初はこれらを単純にパターンマッチングで取り出してプロファイルの一覧として列挙するという事を試みました。

しかしながら、実際に使い込まれた環境のnsreg.datで検証すると、この方法では同じ名前のプロファイルが何度も列挙されるといった結果になってしまいました。どうやら、nsreg.datの内容は新しい情報が追記されていく一方で古い情報には削除フラグが立てられるだけという作りになっている模様で、単純なパターンマッチングでは余計な情報が大量にヒットしてしまう事があるようです。

どこが削除フラグなのか、そもそもどこからどこまでが正確に1つの情報の単位なのか、という事をここから探り当てるのは非常に困難です。そこで根本的解決を見るために、Mozilla Seamonkeyのソースツリーの中からnsreg.datのパースに関する処理を探し出す事にしました*3

探索にあたって、まず単純にMXRで「nsreg.dat」を文字列検索してみたところ、nsDogbertProfileMigratorというファイルが見つかりました。MozillaプロジェクトではNetscape製品の事は何故かDogbertと呼ばれていたようなのですが、このモジュールはその名の通り、NC4のプロファイルを移行するための諸々の処理を実装しているようです。

この中でさらにプロファイルの一覧を取得しているらしい関数を探して中身を見てみた所、今度はreg.h(ヘッダファイル)とreg.c(実装)というさらに低レベルのAPIに辿り着きました。

nsDogbertProfileMigrator、もしくはさらに低層のAPIだけをコンパイルしてバイナリ形式のコンポーネントにするという手も無くはないのですが、

  • ビルドのための環境を整えるのには手間がかかりすぎる。
  • バイナリコンポーネントをアドオンに含めるのは今(Gecko 2.0以降)では色々と面倒が多い。
  • そして何より、インターフェース的にもあまり使いやすい物では無さそうである。

という事から、今後*4のためにも、これに相当する使いやすいモジュールをJavaScriptで実装し直してみる事にしました。

ただ、nsDogbertProfileMigrator.cppは2200行程、reg.cは4100行程あって、全部を読んで把握するというのはちょっと面倒です。なので、実際のnsreg.datとソースコードの両面から、重要そうと思われるポイントに絞って調査を進めていきました。

nsreg.datのレジストリエントリ

先のソースコードにおけるnsreg.datの読み取り処理と、実際のnsreg.datの内容とを比較しながら見ていったところ、ファイルにはdescriptionと呼ばれる単位でレジストリのエントリが保存されている模様である事が分かりました。また、descriptionの中身は以下のようになっているという事も分かりました。

  • description全体の長さ:32バイト
    • 1バイト目〜4バイト目(location):そのdescriptionの先頭の位置(符号無し32ビット整数)
    • 5バイト目〜8バイト目(name):「名前」の先頭の位置(符号無し32ビット整数)
    • 9バイト目〜10バイト目(nameLength):nameの長さ(符号無し16ビット整数)
    • 11バイト目〜12バイト目(type):ノードの種類のフラグ(符号無し16ビット整数)
    • 13バイト目〜16バイト目(left):同じ階層にある次のノード(兄弟ノード)にあたるdescriptionの先頭の位置(符号無し32ビット整数)
    • 17バイト目〜20バイト目(down):最初の子ノードにあたるdescriptionの先頭の位置(符号無し32ビット整数)
    • 21バイト目〜24バイト目(value):「値」の先頭の位置(符号無し32ビット整数)
    • 25バイト目〜28バイト目(valueLength):valueの長さ(符号無し32ビット整数)
    • 29バイト目〜32バイト目(parent):親ノードにあたるdescriptionの先頭の位置(符号無し32ビット整数)

それぞれの値は8ビット毎のリトルエンディアン形式*5で格納されており、下位の8ビットから順に並べ替える事で実際の値を得られました。

スクリーンショット:実際のnsreg.datの内容をバイナリエディタで表示した様子

実際にnsreg.datの中をもう一度見てみた所、プロファイルの位置と思われる文字列の近くに32ビットの長さのバイナリ部があり、先頭の4バイトがそのバイナリ部の位置そのものを示している事を確認できました。また、nameやvalueが指している位置にはUTF-8バイト列として文字列が格納されており、終端文字まで含めた長さがnameLengthおよびvalueLengthの長さとして格納されていました*6

前述の「削除フラグ」は、この中のtypeという部分に含まれていました。この部分で特定のビットが立っていると、そのdescriptionは削除済みであるということになるようです。また、nsreg.datの中にはツリーの形で情報が保存されており、left、down、parentの各フィールドからツリー構造を辿っていける模様である事も分かりました。

DOM的にnsreg.datの内容を見るための実装

と、ここまで分かった所で、このdescriptionをDOM風のオブジェクトとして扱うための実装を試験的に作成してみる事にしました。

まず、バイト列をJavaScriptの数値や文字列(UCS2)に変換するユーティリティを定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// バイト列→数値
function bytesToNumber(aBytes) {
  var converted = 0;
  aBytes.forEach(function(aValue, aIndex) {
    // リトルエンディアンの符号無し整数なので、
    // 単純に8桁ずつビットシフトした結果を合計すれば
    // 表現されている数値を得られる。
    converted += (aValue << (aIndex * 8));
  });
  return converted;
}

// バイト列→文字列
function bytesToString(aBytes) {
  var converted = '';
  aBytes.some(function(aValue, aIndex) {
    if (!aValue)
      return true;
    // 数値の配列から、1文字が1バイトの値を表す
    // UTF-8バイト列としての文字列に一旦変換する。
    converted += String.fromCharCode(aValue);
    return false;
  });
  return UTF8toUCS2(converted);
}

function UTF8toUCS2(aUTF8Octets) {
  return decodeURIComponent(escape(aUTF8Octets));
}

次に、これらのユーティリティを使って、「値の内容」「次のノード」などを自ら見つけ出すようなDescriptionクラスを試験的に作成しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
function Description(aBytes, aOffset) {
  // 他のノードを探すためにはnsreg.dat全体のバイト列を
  // 保持しておかないといけない。
  this.allBytes = aBytes;
  this.bytes = aBytes.slice(aOffset, aOffset + this.DESCRIPTION_SIZE);

  this.location = bytesToNumber(this.bytes.slice(0, 3));
  if (this.location != aOffset)
    throw new Error('invalid description at '+aOffset);

  this.type = bytesToNumber(this.bytes.slice(10, 11));

  var nameOffset = bytesToNumber(this.bytes.slice(4, 7));
  var nameLength = bytesToNumber(this.bytes.slice(8, 9));
  this.name = bytesToString(this.allBytes.slice(nameOffset,
                                                nameOffset + nameLength));

  // 他のノードはlazy getterでその都度インスタンス化するため、
  // ここでは位置の情報だけを保持しておく。
  this._left   = bytesToNumber(this.bytes.slice(12, 15));
  this._down   = bytesToNumber(this.bytes.slice(16, 19));
  this._parent = bytesToNumber(this.bytes.slice(28, 31));

  // valueは他のノードを指している事があるので、これも
  // 位置と長さの情報だけを保持しておく。
  this._valueOffset = bytesToNumber(this.bytes.slice(20, 23));
  this._valueLength = bytesToNumber(this.bytes.slice(24, 27));
}
Description.prototype = {
  DESCRIPTION_SIZE : 32,
  // typeフィールドにおける、削除済のレジストリエントリかどうかを
  // 示すフラグ。
  TYPE_DELETED     : 0x80,

  get deleted() {
    return !!(this.type & this.TYPE_DELETED);
  },

  // valueはノードかもしれないし文字列かもしれないので、
  // 両方の可能性を考慮する。
  get value() {
    return this.nodeValue || this.stringValue;
  },
  get stringValue() {
    if (typeof this._stringValue == 'undefined')
      this._stringValue = bytesToString(
        this.allBytes.slice(this._valueOffset,
                            this._valueOffset + this._valueLength));
    return this._stringValue;
  },
  get nodeValue() {
    if (typeof this._nodeValue == 'undefined') {
      try {
        this._nodeValue = new Description(this.allBytes,
                                          this._valueOffset);
      }
      catch(e) {
        this._nodeValue = null;
      }
    }
    return this._nodeValue;
  },

  // left、down、parentに対応するlazy getter。
  // left/downではなくnext/firstChildなのは、その方が
  // オブジェクト的な表現での実態に即しているから。
  get nextDescription() {
    if (this._left && !this._nextDescription)
      this._nextDescription = new Description(this.allBytes, this._left);
    return this._nextDescription;
  },
  get firstChildDescription() {
    if (this._down && !this._firstChildDescription)
      this._firstChildDescription = new Description(this.allBytes, this._down);
    return this._firstChildDescription;
  },
  get parentDescription() {
    if (this._parent && !this._parentDescription)
      this._parentDescription = new Description(this.allBytes, this._parent);
    return this._parentDescription;
  },

  // deletedなdescriptionは実質的には無い物として扱うバージョン。
  // (実際にdeletedなdescriptionが参照されたままになっている事が
  // あるのかどうかはまだ分からないが、念のため。)
  get next() {
    return this.nextDescription && !this.nextDescription.deleted ?
             this.nextDescription : null ;
  },
  get firstChild() {
    return this.firstChildDescription && !this.firstChildDescription.deleted ?
             this.firstChildDescription : null ;
  },
  get parent() {
    return this.parentDescription && !this.parentDescription.deleted ?
             this.parentDescription : null ;
  },

  // deletedでない子ノードを収集する。
  // (実際にdeletedなdescriptionが参照されたままになっている事が
  // あるのかどうかはまだ分からないが、念のため。)
  get children() {
    if (!this._children) {
      this._children = [];
      let child = this.firstChildDescription;
      let found = {}; // 無限ループに陥ってしまわないよう、念のため。
      while (child) {
        if (!child.deleted && !found.hasOwnProperty(child.location))) {
          this._children.push(child);
          found[child.location] = child;
        }
        child = child.nextDescription;
      }
    }
    return this._children;
  },

  // 子ノードが名前を持っている場合に
  // それを簡単に取得するためのユーティリティ。
  getNamedChild : function(aName) {
    var found = null;
    this.children.some(function(aChild) {
      if (aChild.name == aName)
        found = aChild;
      return found;
    }, this);
    return found;
  },
  getNamedChildren : function(aName) {
    return this.children.filter(function(aChild) {
        return aChild.name == aName;
      });
  }
};

これを先のreadBinaryFrom()と組み合わせれば、description単位での調査はずっと容易になります。

nsreg.datの全容

ここまでの調査の過程で、nsreg.datの中には全てのユーザ情報のルートとなるエントリが存在しており、nsDogbertProfileMigratorのGetSourceProfiles()というメソッドの中でそれを起点としてその子ノードを走査しているらしいという事が分かっていました。という事は、その「ユーザ情報のルート」から芋蔓的にプロファイルの一覧を見つけられる(しかも削除済みのレジストリエントリを除外した形で)と考えられます。

残る問題は、そのレジストリエントリがどこにあるのかという事です。

ファイルの先頭から32バイトが全体のルートになるdescriptionなのでは?とも一瞬考えましたが、実際のファイルの先頭には先頭のアドレス(0000)とは似ても似つかない値が入っていたので、この可能性は無いようです。この調子で2バイト目から順番に見ていってもきりが無いですし、そもそもそれで見つかったとしても常にその位置に最初のdescriptionがあるとは限らないので、ここはやはり既存の実装を見るのが一番確実そうです。

という事でソースコードを見ていた所、reg.hの中に「must equal MAGIC_NUMBER*7」や「major version number」といったコメントが付いた、REGHDRといういかにも怪しい名前の構造体の定義がある事に気がつきました*8MAGIC_NUMBERという定数の定義を見ると、nsreg.datの先頭4バイトがまさにこのMAGIC_NUMBERと一致しています。また、REGHDRの中でrootというフィールドにあたる位置(先頭から13バイト目~16バイト目)に書き込まれていた値をdescriptionの位置として参照してみた所、全体のルートとなるdescriptionらしき内容に行き当たりました(このdescriptionの子ノードの中には先の「ユーザ情報のルート」となるdescriptionが含まれている事も確認できました)。さらに駄目押しで既存の実装から「REGHDR」を参照している箇所を探してみた所、レジストリファイルをオープンする処理の中で固定のオフセットとしてファイルの先頭からREGHDR分の長さだけ内容を読み込んでくるという処理も見つかりました。やはりこれがルートと見て間違いなさそうです。

残りの実装

これでようやく、nsreg.datの中を機械的に辿ってユーザプロファイルの一覧を取得する目処が立ちました。後は不足している部分を実装するだけです。

まず、nsreg.datの内容全体をバイト列として渡すとルートにあたるレジストリエントリを返す関数を定義します。

1
2
3
4
5
6
7
function getRootDescription(aBytes) {
  const ROOT_LOCATION        = 0xC;
  const ROOT_LOCATION_LENGTH = 4;
  var root = aBytes.slice(ROOT_LOCATION,
                          ROOT_LOCATION + ROOT_LOCATION_LENGTH - 1);
  return new Description(aBytes, bytesToNumber(root));
}

次に、それを使ってユーザのプロファイル一覧(名前とパスの組)を取得して返す関数を定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getProfilesFromBinary(aBytes) {
  var root = getRootDescription(aBytes);
  var users = root.getNamedChild('Users').children;
  var profiles = users.map(function(aUserNode) {
      return {
        name : aUserNode.name,
        path : aUserNode.nodeValue.stringValue
      };
    });
  profiles.sort(function(aA, aB) {
    return aA.name > aB.name;
  });
  return profiles;
}

最後に、システムのnsreg.datを自動検出してgetProfilesFromBinary()に渡し、その結果を返す関数を定義します*9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getProfiles() {
  try {
    const DirectoryService = Cc['@mozilla.org/file/directory_service;1']
                               .getService(Ci.nsIProperties);
    let file = DirectoryService.get('WinD', Ci.nsIFile);
    file.append('nsreg.dat');
    if (file.exists()) {
      let bytes = readBinaryFrom(file);
      return getProfilesFromBinary(bytes);
    }
  }
  catch(e) {
  }
  return [];
}

以上で必要な実装は揃いました。後はこれらを1つのファイルにまとめてJavaScriptコードモジュールにしておけば、今後またNC4からの移行案件があっても安心して対応できますね。

まとめ

NC4.5以降の古いNetscape製品ではバイナリ形式の独自のレジストリファイルが使われているという事、そのレジストリファイルに関する情報とパーサの実装はMozilla Seamonkeyのソースツリーから見つけられる事、そしてDOM的なアプローチでのパーサの再実装の流れを紹介しました。これ自体は非常に限定的な局面でしか役に立たない情報ですが、クリアコードのMozillaサポート事業やオープンソースソフトウェアのサポート事業では実際にどのような事をやっているのか?という事例紹介*10も兼ねて記事にしてみた次第です。

古い製品であってもソースコードさえあれば、後の時代からでもこのように目的を達成できる可能性が高くなります。ソースが無いと、ここまですんなりとはいかなかったでしょう(本格的なリバースエンジニアリングをしないといけないとなると、どれだけの手間がかかるか考えたくもないですね)。「クローズドソースのパッケージ製品を重宝していたにも関わらず、提供元が倒産等で存在しなくなってしまい、もはやサポートを受けられなくなってしまった」という状況に陥ってしまう将来的なリスクを非常に重く見る場合、オープンソースな製品を導入するというのも現実的な選択肢の1つなのではないでしょうか。

*1 今ならTyped Array を使った方が良いかも知れませんね。

*2 Ubuntu上で「Hexエディタ」を使いました。

*3 NC4の後継ソフトウェアであるNetscape 6〜Netscape 7は、Mozilla Application Suiteというソフトウェアにいくつかの機能を足した物でしたが、差異を最小限にする(Netscape側でないとできないという作業を最小化する)ためにほとんどの機能はApplication Suiteに含める方針だったようで、nsreg.datのパース処理もそういった機能の1つとしてApplication Suiteに含まれていました。そのMozilla Application Suiteは、Netscape亡き後も有志の手によりMozilla Seamonkeyとして開発が継続されています。実はThunderbird 2にもこの処理は含まれていたので、Thunderbird 2のソースコードを調査してもよかったのですが、今回探索を行った時にはその事を失念していました……

*4 あるんでしょうか?

*5 8ビットを超える長さのデータを表現する際の表現形式で、8ビットを1桁と考えた時に上の桁から順に格納する方式をビッグエンディアン、下の桁から順に格納する方式をリトルエンディアンと言います。ビッグエンディアンは人間にとって分かりやすく、リトルエンディアンはコンピュータにとって分かりやすいという利点がそれぞれあります。

*6 ただし、valueLengthは0になっている場合があり、その場合はvalueの位置から32バイトの内容がまた別のdescriptionになっている、という構造になっていました。

*7 定数値の中でも、それ単体を見ても意味が全く分からない上に、他に何の説明も無い物。一意な識別子として使われる事がある。

*8 「hdr」はThunderbirdのソース中では「header」または「handler」の意味で使われている事が多いです。

*9 ソースコードを見た限りではWindows版だけでなくMac OS版やUNIX版のNC4.5もnsreg.datを使用している様子でしたので、この箇所だけ各プラットフォーム向けに実装を分ければWindows/Mac OS/UNIXの3プラットフォームに対応できそうです。

*10 純粋に調査のみの依頼の場合もあれば、今回のように移行案件の中で必要に迫られて行う場合もあります。

タグ: Mozilla
2011-12-19

2011年まとめ

早いもので今年も最終週になりました。今年は手狭になったオフィスを引越し、3倍くらいの広さになりました。社外の方に場所を貸し出せる余裕ができ、社内で一緒に開発するなどの技術交流もできました。今年は「自分たちの技術や知識を気づいたら身につけていたものから誰かに伝えられるものとしてまとめあげること」を目指していたので、それを実現できるよい環境になりました。

今回はククログ上で公開した今年の活動をまとめてみます。

1月

迷惑メール対策ソフトウェアであるmilter managerのスケーラビリティ向上についてと、Rubyのリファレンスマニュアルを全文検索するるりまサーチとドキュメント検索の話題がありました。

milter managerは1月のうちにスケーラビリティを向上したバージョンをリリースし、その後リリースを重ね、最新リリースでは十分に安定したと言えるくらいまでになっています。今年はmilter managerやRubyで実装したmilterを使ったメールシステムが増えた年でもありました。どちらのケースもそれなりの規模のメールシステムです。

この頃にるりまサーチのことを書いていたのは2月に第3回フクオカRuby大賞の本審査があったからです。

2月

第3回フクオカRuby大賞があったこともあり、もっと知られてもいい人たちについて考えていました。

2月は年に一度の肉の日にリリース祭りがありました。

自分たちの知識を「誰かに伝えられるものとしてまとめあげる」シリーズの第1弾としておすすめEmacs設定をまとめました。今年の3番目の人気記事です。おすすめ設定はこのときからさらに更新されているので、1年か数年に1回でもまとめ直していきたいですね。

3月

第3回フクオカRuby大賞でコミュニティ特別賞を受賞しました。ありがたいことです。

めずらしくソフトウェア開発者の募集について詳しく書いていますね。

4月

いつも通りこつこつとソフトウェアを開発していたようです。着実にgroonga関連のソフトウェアをリリースしたり、milter managerをリリースしています。こつこつ着実に続けていくことは地味な上に結構大変なんですよね。

groonga開発者の森さんが書いた検索エンジンはなぜ見つけるのかの紹介もしていました。検索エンジンに興味のある方は参考にしてください。

5月

ソフトウェアのドキュメントの多言語対応についていろいろ検討していました。

まず、リファレンスマニュアルの記述方法について、記述方法ごとにメリットとデメリットをまとめてみました。また、現在あるドキュメントツールがどの記述方法を採用しているかも示しています。これで記述方法については大まかに把握できるはずです。

続いて、Ruby用のドキュメントツールであるRDocとYARDを比較して違いをまとめました。この中ではYARDびいきで書いていますが、RDocも活発に開発されていてすごいです。まさかRDがサポートされるとは思いませんでした*1

ところで、RubyDoc.infoというサイトを知っていますか?このサイトではgemのドキュメントを提供しており、YARDで整形した状態で気になるgemのドキュメントを読めて便利です。gemを作る側で見ても、リリース後に自分でドキュメントをアップロードしなくても勝手にドキュメントを生成してくれるので便利です。

また、Python用のドキュメントツールであるSphinxの国際化機能を使った複数言語用のドキュメントを用意する方法についてもまとめました。なお、groongaのリファレンスマニュアルはSphinxを使って英語と日本語のドキュメントを提供しています。

ドキュメント関連以外では64bit版Windows用のRubyInstallerも作っていました。残念ながらまだ取り込んでもらえていません。当時は動くところまでやったので今でもおそらく動くでしょう。だれか続きをやってもらえないでしょうか。(Windowsユーザーの方がよさそう。)

6月

MySQLでgroongaを使うためのソフトウェアmroongaラッパーモードが発明されました。

7月

本社を移転しました。

日本Ruby会議2011にスポンサー・発表者として参加し、テスティングフレームワークの作り方について話しました。また、日本Ruby会議2011では話さなかった、今年目指していることにも関連する伝えることを伝えることを考えていました。

8月

あまりパッとしたことを書いていませんでした。

9月

自分たちの知識を「誰かに伝えられるものとしてまとめあげる」シリーズの第2弾としておすすめzsh設定をまとめました。今年の2番目の人気記事です。おすすめ設定はこのときからさらに更新されているので、1年か数年に1回でもまとめ直していきたいですね。

また、groongaの位置情報検索の情報もまとめました。まだgroonga本体のドキュメントには反映できていないのが残念なところです。

10月

札幌でのgroonga勉強会用の情報をまとめていました。groongaにデータを登録してからインデックスが更新されるまでの流れgroongaの全文検索処理の流れです。

自分たちの知識を「誰かに伝えられるものとしてまとめあげる」シリーズの第3弾としてどうして開発者がドキュメントを書くべきかをまとめました。あまりこの視点で話されることありませんが大事な視点のはずです。

11月

久しぶりのMozilla関連情報としてFirefoxの技術書「Firefox Hacks Rebooted」の紹介がありました。

また、OSC2011.DBでmroongaのベンチマークを紹介してきました。

12月

いい肉の日に行われる年に1度のgroonga勉強会を今年も開催しました。

自分たちの知識を「誰かに伝えられるものとしてまとめあげる」シリーズの第4弾としてデバッグ力: よく知らないプログラムの直し方をまとめました*2。今年1番の人気記事です。

まとめ

月ごとに今年のクリアコードの活動をまとめました。

去年よりもまた発表する機会が減りましたが、今年目指していた「自分たちの技術や知識を気づいたら身につけていたものから誰かに伝えられるものとしてまとめあげること」については去年よりも実現できていたのではないでしょうか。

今年もありがとうございました。来年もクリアコードをよろしくお願いします。

*1 なお、YARDの方がRDocよりもプラグインの仕組みが整備されているので、簡単にYARDにRDサポートを追加できます。

*2 このあと無事にコミット権をもらっていました。

2011-12-26

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|