2011/12/30

X-CTUを使ったAE-UM232R経由でのXBeeの構成方法

ひさしぶりにAE-UM232Rを引っ張り出してきてXBee (Series2)と接続して、X-CTUによるセットアップを行ないました。 接続やら何やら忘れそうなのでメモを残します。

秋月謹製USB-シリアルコンバータ:AE-UM232RによるXBeeの設定 by X-CTU

普通のUSBデバイスと同じように、接続前にFTDI製のドライバをインストールしてあれば、特に迷うことはなく進められると思います。

ただしXBeeを使うためには基本的な構成などを理解する必要があります。 図書カードを使って「XBeeで作るワイヤレスセンサネットワーク」を入手して手元に置いていますが、XBee Series2に対応していて、情報が新しいので2011年12月時点ではお勧めです。

構成

XBeeは秋月で一緒に購入した2.54mmピッチ変換ボードに載せています。 1番ピンには5V(+4V〜20V)を与える必要があります。

AE-UM232RとXBeeとの接続方法

ブレッドボードにAE-UM232Rとピッチ変換ボードに載せたXBeeを並べています。 電源はUSBから5VをVCCに出力させるため、AE-UM232RのJ2ジャンパはショートさせています。

電源用にAE-UM232RとXBeeのVCC, GNDをブロッドボードのバスに接続している他には、TXD⇔DIN(3番ピン)、RXD⇔DOUT(2番ピン)の接続に2本の線を使っただけで、USBケーブルを除けば、AE-UM232RとXBeeそれぞれの4端子以外はオープンです。

「XBeeで作るワイヤレスセンサネットワーク」では、ArdionoからATMEGAを外して、TXD⇔DOUT, RXD⇔DINを接続してUSB接続していました。

X-CTU接続時のパラメータ

PC Settingsタブの"Test/Query"ボタンでテスト接続に成功した時のパラメータは次の通りです。

  • Baud: 9600
  • Flow Control: None
  • Data Bits: 8
  • Parity: None
  • Stop Bits: 1

テストした結果は次のように表示されていて、Modem Configurationのタブから設定をするためにメモをしておきます。

  • Modem type = XB24-B
  • Modem firmware version = 2264
ファンクションセットの設定

テストが成功した後は、Modem Configurationタブに移って作業を行ないます。

表示されたModem typeはXB24-Bでしたが、ここでは真ん中のプルダウンメニューで次のように選択しました。

  • Modem XBEE: XB24-ZB
  • Function Set: ZIGBEE COORDINATOR AT
  • Version: 208C

あとはWriteボタンを押すと更新が終わります。 PC Settingsタブに戻ってQueryボタンを押すと、次のような表示になりました。

  • Modem type = XB24-B
  • Modem firmware version = 208C

もう一つのXBeeは同様にZIGBEE ROUTER ATに設定して、それぞれどのFunction Setを導入したかラベルを張っておきます。

この記事で取り上げた品々

2011/04/06

Visual Studio 2005でCrypto++ライブラリを使ってみる

手元のVisual Studio 2005はVB2005を使って、以前働いていた職場でプログラムを作る他はもっぱら自分の遊びようでしたが、今回はひさしぶりにVC++2005を使ってみる事にしました。

いまどきVC#じゃなくて、VC++を使ったのはIPropertySetStorageを使うプログラミングをするためにC#を使うのは大変でオーバーヘッドが大きくメリットがなかったからです。

IPropertySetStorageの使い方はいろいろ資料があるので、今回はそれと一緒に組み合せたCrypto++ライブラリの使い方についてメモを残しておきます。

Visual C++ 2005と組み合せる方法

www.cryptopp.com には、VC++との組み合せについて参考になるドキュメントはみつかりませんでした。

そこでGoogleでいくつか検索して参考にしたのは mogproject.blogspot.com の「Crypto++ 5.6.1のビルド」でした。

入手できるcryptopp561.zipを展開したディレクトリに含まれているslnファイルをVS2005から開きました。

この全体をDebug用とRelease用の2つのタイプに分けてビルドした後は、ひとまず閉じて、自分のプロジェクトファイルを開き直します。

使い方 - cryptopp.dll OR cryptlib.lib

どちらのファイルを使うにしても、ヘッダーファイルが含まれている、アーカイブを展開したフォルダのトップを「追加のインクルードディレクトリ」に含めておきます。

DLLを使う場合には、リンカオプションでDLLを含むディレクトリへのパスを通しておき、"dll.h"をインクルードするだけで直接 cryptopp.dll ファイルの名前を指定する必要はありません。

cryptlib.libを使う場合には、「追加の依存ファイル」に"cryptlib.lib"を含めておきます。

基本的にはこれだけだったのですが、デフォルトの設定で動かしていると次のような問題が発生しました。

自分のプロジェクトからCrypto++を使う時の問題

ビルドしたところ、DLLはうまく作れたけれど、LIBファイルとのリンクには失敗したという事が起こりました。

遭遇したエラーメッセージ

1>msvcprt.lib(MSVCP80.dll) : error LNK2005: "public: __thiscall std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >::~basic_string<char,struct std::char_traits<char>,class std::allocator<char> >(void)" (??1?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ) は既に cryptlib.lib(iterhash.obj) で定義されています。
1>msvcprt.lib(MSVCP80.dll) : error LNK2005: "public: __thiscall std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >(char const *)" (??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@PBD@Z) は既に cryptlib.lib(iterhash.obj) で定義されています。
1>LINK : warning LNK4098: defaultlib 'LIBCMT' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。
1>C:\Users\yasu\Documents\Visual Studio 2005\Projects\TestCryptoPP\Release\TestCryptoPP.exe : fatal error LNK1169: 1 つ以上の複数回定義されているシンボルが見つかりました。

これは「ランタイムライブラリ」の設定で「マルチスレッドDLLl(/MD)」が選択されていた事が原因です。

リンクする対象がDLLかスタティックライブラリかによって、オプションを変更しなきゃいけないのは理解できますが、どこを変更すれば良いのかは調べるのに時間がかかりました。

/MD, /MD等のメニュー

ランタイムライブラリ切り替えダイアログ

DLL v.s. スタティックライブラリ

簡単な文字列のSHA1 Digestを計算して、表示するアプリケーションを作成して比較しました。

DLLの場合は、DLLファイルが1.2MB程度で、オブジェクトコードは7KBほど。

スタティックライブラリの場合は、オブジェクトコードは88KBほど。

とりあえず自分の用途では、スタティックライブラリの方が扱いは楽かなぁと感じています。

Crypto++を扱う上でのリファレンス、マニュアル等

Googleなんかで検索すると、Crypto++のドキュメントがなさすぎるという記述を目にします。

確かにドキュメントやチュートリアルは少なくて、基本的な操作を知っている既にプログラマな人以外お断り的な雰囲気は感じます。

ただ、どういう風にメソッドやクラスを使えばいいのかというと、サンプルが付いてくるので、cryptestの中を除くとだいたい必要そうな情報はあるように思いました。

ちゃんと使っているわけじゃないので、なんともいえないですが、典型的な操作についてはテスト用のコードを追えばなんとかなりそうな気がします。

2011/04/02

Make Permanent Pinned Tabs with Google Chrome

Google Chrome 10 has a great feature about the pinned tab. Before that, some people uses the "--pinned-tab-count" command line option to create the pinned tab.

But some problems are still remaining if you want to use a permanent pinned tab like as the bookmark manager page.

The "permanent pinned tab" will open a link in new tab. The "Open PinnedTab Link" extension provides this feature.

The extension opens a link in new tab and has some options to control the behavior.

The "Google News" page, for instance, will open the external link in new tab and opens the link of the same site within the pinned tab.

This extension also provides the similar feature through the context menu. After checking the "Exclude links in same domain" of the context menu, the link behavior of the page is similar to the Google News page.

Options

Extensions page, chrome://extensions/, has a link to the option page of Open PinnedTab Link. The default settings of context menu can be changed from this page.

Open PinnedTab Link - Options page

L10N Messages

The google chrome extension framework is internationalized, so this extension uses this feature for the message translation.

Currently, English and Japanese message catalog is provided.

Known issues

I tried to close the context-menu on the no pinned tab page.

Platforms

This extension should work on every platforms supported by google chrome. It is tested on following platforms:

  • Windows Vista SP2 (32bit)
  • Windows XP SP3 (32bit)
  • Ubuntu 10.04 LTS 64bit
  • MacOS 10.6

Installation

This extension is available from the chrome web store.

Source Codes

The source code is hosted at Gitorious : https://gitorious.org/open-pinnedtab-link

2011/03/31

chrome.tabsイベントの動きを追って、graphvizで図にしてみた

Open PinnedTab LinkというGoogle chromeの機能拡張を作成したので、 Google Chrome Extensionsのデベロッパーガイドを読む必要がありました。

特に Browser Interaction/Tabs (chrome.tabs package)にあるイベントハンドラーの動きがちょっと分かりづらかったので、備忘録的にどういう風に呼ばれるのか図にしてみました。

各ハンドラーの先頭にconsole.log()を入れるローレベルな方法で動きを追ったので、事前条件やテストの方法がまずかったりして違う動きになるかもしれません。

まずは、Graphviz(dot)で図にしてみた

タブを開くたびに上からNormal stateまでのevent handlerメソッド(chrome.tabs.on*())が呼ばれます。

"Normal state"は通常のWebブラウジングをしている状態です。

Statechart diagram of chrome.tabs event handlers

遷移するアクションの説明

説明のない矢印は自動で次の状態に遷移する事を示していて、Chromeのイベントは楕円で示しています。

  • create_tab(): Chrome上で新しいタブを開いたり、"Open Link in New Tab"を選択した場合のアクション
  • (un)pin_tab(): Pinを固定したり、外したりを選択した場合のアクション
  • reloase(): C-rや"Reload"などで明示的にページを更新した場合のアクション
  • close_tab(): C-wや"Close Tab"を選択した場合のアクション
  • (un)dock_tab(): タブをドラッグして、別ウィンドウに開いたり、別ウィンドウに統合した場合のアクション
  • change_focus(): 単純にタブを選択したり、別のタブを閉じたりした事でフォーカスが当った場合のアクション

chrome.tabs.OnRemovedが呼ばれた時には、そのタブは閉じますが、別のタブにフォーカスが当たるので、矢印が2つ出ている事になります。

明示的にTabIDやガード条件のようなものは書いていませんが、適当に読み取れると思います。

楕円で示したところは Events を示していますが、念のため対応する完全修飾のイベント名を列挙しておきます。

  • onCreated(): chrome.tabs.onCreated イベント
  • onUpdated(): chrome.tabs.onUpdated イベント
  • onSelectionChanged(): chrome.tabs.onSelectionChanged イベント
  • onDetached(): chrome.tabs.onDetached イベント
  • onAttached(): chrome.tabs.onAttached イベント
  • onMoved(): chrome.tabs.onMoved イベント
  • onRemoved(): chrome.tabs.onRemoved イベント

図を生成する元のdotファイル

図を生成するためのa.dotファイル



// Statechart diagram of chrome.tabs.on*()

digraph G {

  start [shape=circle, label="", style=filled];
  normal [shape=rect label="Normal state"];
  closed [shape=doublecircle, label="", style=filled];

  onCreated [label="onCreated()"];
  onSelectionChanged [label="onSelectionChanged()"];
  onSelectionChanged_ [label="onSelectionChanged()"];
  onUpdated [label="onUpdated()"];
  onRemoved [label="onRemoved()"];
  onMoved [label="onMoved()"];
  onDetached [label="onDetached()"];
  onAttached [label="onAttached()"];

  start -> onCreated [label="create_tab()"];
  onCreated -> onSelectionChanged_;
  onSelectionChanged_ -> onUpdated;
  onUpdated -> normal;

  // normal state
  normal -> normal;
  normal -> onSelectionChanged [label="change_focus()"];
  onSelectionChanged -> normal;

  // change tab position
  normal -> onMoved;
  onMoved -> normal;

  // pin or unpin tab
  normal -> onUpdated [label="(un)pin_tab() /\nreload()"];

  // dock or undock tab
  normal -> onDetached [label="(un)dock_tab()"];
  onDetached -> onAttached;
  onAttached -> onSelectionChanged [label="change_focus()"];

  // close tab
  normal -> onRemoved [label="close_tab()"];
  onRemoved -> onSelectionChanged [label="change_focus()"];
  onRemoved -> closed;
}

これを図にするには、dotコマンドを使って次のようなコマンドラインを使っています。

$ dot -Tpng -o a.png a.dot

困ったこと

複数のタブを保存して開いた時に、chrome.tabs.onUpdated が呼ばれずに、いきなり chrome.tabs.onSelectionChanged が呼ばれる場合がありました。

さらに悪いことに、この場合には Pin Tab かどうか確実に判別することができませんでした。

そのためタブを保存した状態のChromeを起動して、偶然この問題に遭遇するとPin/Unpinを判別するような機能拡張がうまく動作しない場合があります。

Open PinnedTab Linkでは、この他にもUnpinの場合にContext Menuを無効にするようにしたかったのですが、いまのところ良い手がなく全てのタブの中にメニューを表示しています。

問題はいろいろありますが、タブをブックマーク的に使う初期の目標は達成できたので良しとしましょう。 でも当然ユーザーは不満に思いますよね、「無駄なら消してよ」って。うーん、困ったなぁ。

2011/03/29

LinuxのVMWare Workstationで録画したスクリーンキャプチャにMacのLogic 8でBGMをつけてみる

手元のLinux版のVMWare Workstation 7.1.3には画面操作を録画する機能があります。

形式はAVI形式なのですが、中身のコーデックがWindowsベッタリなのかMac側のLogic 8やCompressorには直接取り込む事ができません。

全般的な流れはOgg Theoraを経由し、AVI → MOVファイル変換を行なう事になっています。

簡単ですが、備忘録的にログを残しておきます。

作業の手順

Linux上でVMWareが作成したAVIファイルをエンコーディングをOgg Theoraに変更する。

$ oggconvert

変換したファイルはMacに転送する。

$ scp vmware_movie.ogv macmini:

Mac(macmini)側のデスクトップに移動し、作業を継続する。

MacのQuickTimeにxiph.org/quicktimeからXiphQT 0.1.9をダウンロードし、Library/Components/XiphQT.componentに配置する。

成功すると、転送したvmware_movie.ogvファイルをQuickTime 7で再生することができるようになる。

そのQuickTime 7でvmware_movie.ogvを開いたまま、 ファイルメニューから「書き出し」を選択し、QuitkTime(mov)形式に変換する。

作成したファイルをQuickTimeのメニューにある「ムービー」から読み込み、必要なオーディオトラックの編集をする。

MOVファイルをYouTubeへアップロードする場合に、うまく行かないと感じた場合はComposerを使って、mpeg-4やh264形式に変換する。

以上、ここまで。

Google Chrome用の機能拡張 "Open PinnedTab Link" を作ってみた

結論からいうと、この記事ではGoogle Chrome用に作成したOpen PinnedTab Linkという拡張機能を紹介しています。

ブックマーク代りのピン化したタブ

いまのところLinux 64bit版のGoogle Chrome 10.0.648.204をメインに使っています。

Ubuntu 10.04ではmozilla-daily-ppaを経由してFirefox 4を導入していますが、ピンにしたタブから開くリンクは新しいタブで開くところがとても便利だと思っています。

Google Chromeでもタブをピンにすることはできますが、標準ではピンになったタブの中でリンクが開いてしまい、ブックマーク代りに使うには適していません。

TabLinkは全部のリンクを書き換えてしまう…

似たようなことを考える人はいて、Pin Tab Links should open new windowというHelp forumのトピックからTabLinkという機能拡張を試してみました。

TabLinkは問題なく動いたのですが、全部のリンクが書き変わってしまうので、自分の目的には合いませんでした。 そのため今回はピン化したタブからのリンクだけは新しいタブで開くというOpen PinnedTab Linkを作りました。

特別なことはしていません。内部的にはピン化したタブかどうかを判断する処理の後はTabLinkと同じことをしています。

ただし、ピンを外した後は全リンクの"target"属性を空にすることをしているので、元々新しいタブを開くリンクは違う動きをするかもしれません。

さいごに

ピンタブを使うと、アイコンでその機能を見分ける必要がでてくるので、サイトがfavicon.icoを設定していないと微妙に不便になるでしょう。

2011/03/24

RubyでFastCGIとGetTextモジュールを組み合せる

Rubyで多言語化を行なうにはMutohさんのRuby-GetText-Packageが便利そうです。

通常の po → mo の変換を行なえばメッセージを m17n できるところが良いので、作業の内容は一般的なものです。

単純な置換なら手でも作れますが、将来的に複数の言語に対応するなら、自分でロジックを組む手間が削減できます。

処理速度が速くなるかどうかは微妙で、ステータスをThread毎に格納して、全体のメッセージ文はRuntimeでキャッシュされるので手作業で置換ロジックを書くよりも多少はスピードアップが期待できそうです。ただメモリを消費してもよいならシンプルに正規表現のリストと置換する文字列の対を作った方が早いんじゃなかろうかと想像しています。

ERBだと<%= _("message") %>のように埋め込む手間がかかり、可読性は多少低下するデメリットもありそうです。

それでもGetTextモジュールを使うことで作業手順が標準化できるので、置換対象の文字列を管理する後々のメンテナンスを考えると総合的に利点が上回ると思います。

GetText自体はcgiクラスとの組み合せを想定しているので、FastCGIについてはドキュメントがなさそうでした。 そのためGetTextとFastCGIについてまとめておきます。

開発環境について

いつものようにUbuntu 10.04 LTS x86_64版を使用しています。

  • OS: Ubuntu 10.04 LTS x86_64
  • Ruby 1.9.2-p136 (/usr/local/bin/ruby) + gettext + fcgi
  • Apache 2.2.14 + libapache2-mod-fcgid

FastCGIとCGIの違い

FastCGIは標準添付ライブラリにはないため、fcgiモジュールをgemなどでインストールする必要があります。

CGIとの違いは起動済みのプロセスにWebブラウザからアクセスする仕組みなので、プロセスの起動にかかる時間が短縮される分、メモリを常に使いますが処理スピードは速いです。

またプロセス自体は終了せずに(Rubyの場合)スレッドが各ブラウザからのリクエストを処理するため、インスタンス変数やクラス変数を適切に使うことで、キャッシュの効果により全体的なレスポンスを向上させる事もできます。

その反面、処理する単位はオブジェクト単位にしてシンプルに切り分けないと、キャッシュしたくない内容が残ったりしてプライバシー上の問題を引き起す可能性もあります。

一般的なWebコンテンツのホスティングサービスでは、FastCGIのようにプロセスが常駐するとメモリを消費するため、必要に応じてリソースを消費するCGIやPHPが一般的です。

FastCGIもCGIもWebの初期からある仕組みなので古典的に扱われますが、Web以前から起動済みのプロセスにオンライントランザクションを処理させる仕組みは、決済やら問い合せ処理やらを高速に処理する仕組みとしてよく知られていました。

起動時間とその起動処理を無視できることは、アクセスが増えていく中で安定的なレスポンスを得るための重要な要件の一つです。

PHPはApacheのモジュールとしてサーバのプロセスイメージの中で処理が完結するため速いはずですが、基本的にはあらかじめ処理内容をキャッシュするわけではないので、時間は短縮できますが、接続毎にいろいろな初期化処理が必要となる点が独特です。

さて「FastCGIは古くない!」という主張は十分したので、GetTextとの組み合せについてまとめます。

FastCGIとGetTextの組み合せ

GetText付属のサンプルをみると、CGIモジュールに対応したスクリプトがありますが、そのままではFastCGIに適用できません。

その理由はFastCGIのスレッドをプールして使い回すという性質にあります。

違いを説明する前に雛型になりそうなスクリプトは次のようになります。

完全に動作するFastCGIスクリプト全体

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

$:.unshift "/app/lib"
ENV['GEM_HOME'] = "/app/gems"

require 'rubygems'
require 'fcgi'
require 'cgi'
require 'erb'
require 'gettext/cgi'
require 'gettext/tools/parser/erb'

## Render
class SimpleRender

  def initialize(query, env)
    @query = query
    @env = env
    Locale::clear
    Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
    GetText::set_output_charset("UTF-8")
    GetText::bindtextdomain("sample", "/app/data/locale")
  end
  
  def render
    ret = ""
    content = "/app/sample.erb"

    ret += ERB.new(open(content, "r:utf-8").read).result(binding)
    return ret
  end

  def _(msgid)
    GetText::_(msgid)
  end
end

## Controller
class Main
  def initialize(request)
    @env = request.env
    @query = check_query_string(CGI.parse(@env['QUERY_STRING']))
  end
  
  def run
    ret = "Content-Type: text/html; charset=UTF-8\r\n\r\n"
    case @query["mode"]
    when "search"
      sr = SimpleRender.new(@query, @env)
      ret += sr.render()
    end
    return ret
  end

  private
  def check_query_string(q)
    ret = {}
    label = "mode"
    ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "search"
    label = "lang"
    ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "ja"
    return ret
  end
end

###############
## main loop ##
###############

FCGI.each {|request|
  main = Main.new(request)
  request.out.print main.run
  request.finish
}

スクリプトが参照している外部ファイルについて

gemsを使うかどうかは問題ではないのですが、requireしているモジュール・ライブラリにアクセスできるように、先頭の6行は適切なパスに書き直す必要があります。

/app/sample.erbはコンテンツ本体で、次のような内容になっています。

sample.erb本体

<html>
<body>
<h1><%= _("Hello World") %></h1>
</body>
</html>

GetTextモジュールの処理の中心となる翻訳データを保存するためには /app/data/localeディレクトリを作成しています。 作成方法はmoファイルを生成するところで行なっています。

結果として、ライブラリを除いた/appディレクトリの構造は次のようになっています。

$ find . -type f
./po/en/sample.po
./po/ja/sample.po
./po/sample.pot
./Rakefile
./data/locale/en/LC_MESSAGES/sample.mo
./data/locale/ja/LC_MESSAGES/sample.mo
./sample.erb
解説:Thread毎のキャッシュの破棄

LocaleはThread.current[:current_request]に必要なデータ構造をキャッシュします。

スレッドが消滅しないかもしれないので、接続毎に処理が終った段階でデータを破棄する必要があります。

処理が終った段階」を徹底するために、処理を始める前にデータを破棄しています。 このLocale::clearはnilを代入するだけの処理なので、スレッドが立ち上がった最初に実行しても問題ない内容になっています。

処理開始時にLocale::clearを呼び出しているところ

    Locale::clear
    Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
    GetText::set_output_charset("UTF-8")
    GetText::bindtextdomain("sample", "/app/data/locale")
解説:クライアントの言語情報の登録

前項にあるコードの2行目 Locale::set_request()メソッド が、クライアントが利用する言語を登録しているところです。

CGIモジュールを使うサンプルでは、Locale::set_cgi()を使用していますが、今回はCGIオブジェクトは使えないため直接その内部で使用している Locale::set_requestメソッド を呼び出しています。

第一引数に指定した言語から優先度が高く、第二引数にはCookieに保存した言語情報を格納する想定ですが、cookieを使っていないので空にしています。

GetText関連ファイル(po,moファイル)の作成

今回の例では、moファイルにアクセスするために、/app/data/localeを指定しています。

必要なファイルを作成するために、まず次のような内容の/app/Rakefileファイルを作成しました。 これはMutohさんのページの説明ほぼそのままです。

/app/Rakefileの全体

$:.unshift "/app/lib"

desc "Update pot/po files."
task :updatepo do
  require 'gettext/tools'
  GetText.update_pofiles("sample", Dir.glob("*.erb"), "sample 1.0.0")
end

desc "Create mo-files"
task :makemo do
  require 'gettext/tools'
  GetText.create_mofiles
end

先頭の $:.unshift は、FastCGIスクリプトと同じで、GetTextモジュール(gettext.rb)へのパスです。

デフォルトのパスにインストールしていれば不要ですが、今回は特別な場所にモジュールを配置しているので追加しています。

ファイルを配置した後は、poファイルを作成します。

$ cd /app
$ rake updatepo

"/app/po/sample.pot"が作成されるので、それを言語毎のディレクトリにコピーをして翻訳を行ないます。

$ cd /app
$ mkdir po/ja po/en
$ cp po/sample.pot po/ja/sample.po
$ cp po/sample.pot po/en/sample.po

2つのsample.poの翻訳が終ったら moファイル を作成します。

$ rake makemo

これが無事に終ると /app/data/locale/ja/LC_MESSAGES/sample.mo/app/data/locale/en/LC_MESSAGES/sample.mo のファイルが作成されているはずです。

po/ja/sample.poファイルの内容

# Copyright (C) 2011 Yasuhiro ABE yasu@yasundial.org
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: sample 1.0.0\n"
"POT-Creation-Date: 2011-03-24 09:25+0900\n"
"PO-Revision-Date: 2011-03-24 09:25+0900\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: sample.erb:3
msgid "Hello World"
msgstr "こんにちは"

FastCGIスクリプトの実行

Apacheを適切に設定して、Options ExecCGIが設定された場所に配置してWebブラウザから呼び出します。

URLの最後に ?lang=ja?lang=enをつけて呼び出して内容が変化することを確認します。

GetTextモジュールを使っていて気がついたこと

rmsgmergeコマンドが使えない

たぶん一斉置換か何かの影響か、gettext/tools/rmsgmerge.rb の中がおかしくなっています。

rmsgmergeの修正個所

#      refpot = parser.parse_file(config.refstrrefstr, PoData.new, false)
      refpot = parser.parse_file(config.refpot, PoData.new, false)

これで終りかと思いきや、-hで表示される第一引数と第二引数は逆にしないといけない模様です。

いまのところRakefileの方が便利そうなので困ってはいませんが、コマンドを使って更新をpoファイルに反映したい場合には注意が必要です。

困った事にdebパッケージでruby 1.8.x用に入っているrmsgmergeも動きません。

rgettextコマンド内部で参照しているrgettext.rbへのパスがおかしい

require 'gettext/rgettext'require 'gettext/tools/rgettext'に変更しました。

rgettextコマンド内部にあるrubyコマンドへのパスを変更する

rmsgmergeでも同じでしたが、コマンドの先頭にある#! /usr/bin/rubyのパスを#!/usr/local/bin/rubyに変更しました。

$:変数にライブラリディレクトリを加える

またRuby 1.9.2では$:はカレントディレクトリを指さないので、gettext.rbのあるディレクトリへのパスを$:に登録しています。

いろいろ修正したrgettextスクリプト全体

#! /usr/local/bin/ruby
# -*- coding: utf-8 -*-
=begin
  rgettext - ruby version of xgettext

  Copyright (C) 2005-2009  Masao Mutoh
  
  You may redistribute it and/or modify it under the same
  license terms as Ruby.

=end

$:.unshift File::join([File::dirname($0),"..","lib"])

begin
  require 'gettext/tools/rgettext'
rescue LoadError
  begin
    require 'rubygems'
    require 'gettext/tools/rgettext'
  rescue LoadError
    raise 'Ruby-GetText-Package are not installed.'
  end
end

GetText.rgettext

ドキュメントが微妙に古くて通用しない記述もいろいろありましたが、ちゃんと動いています。

m17n のためには、gettextの標準的な作法が使えるというのは強力だと思います。

2011/03/17

「不謹慎」を狩るのが流行っているみたい

プロがコンサートを開くといって「不謹慎」だと叩かれているような状況が報道されています。

そもそも「つつしみ」がない、不足している状態をみて、「それは不謹慎だ」などというわけですが、 「つつしみ」を内に秘めて行動した場合、あるいは言葉が足りていない場合には、それが相手に伝わらずに反発を招くことは十分に予想されます。

そういった場合には、返す刀で斬るのではなく、「誤解を与えたようですが、〜です。」などと冷静に対応したいものです。

インターネットや書籍など主に文字を使った表現では、相手の息遣いや表情、言葉の抑揚や体の動きといったコミュニケーションに必要な要素がかなり省かれた状態であることに常に留意するべきで、それがリテラシーを備えている状態といえるでしょう。

そもそも不謹慎な言葉が狩られている様をみて、厳しい生活を送っている人達は喜びもしないし、おもしろくもないと思います。

「不謹慎じゃないか」そういう指摘をする事は、自由に行なわれるべきです。 しかし、それを大義名分に刀を振るうのは間違いだと思います。

関西ウォーカーTVにみる「不謹慎」への反応

昨日、関西ウォーカーが震災時のボランティア活動について、専門家を招いて、この時期に何をするべきか、いつボランティアが必要になるのか、という話をしていました。

その放送の最後に「不謹慎」について言及し、関西方面のメディアが、かなり神経を使っている様子がみてとれます。

「買い占め」や関東方面への個人的な物資輸送は、物流を混乱させる要素になるので、被災地への一定の影響があるかもしれません。そういう行動は「つつしみ」を持てばいいのだと思います。

極端な例として、あまりに有名なアーティスト達がウッドストックみたいなコンサートを企画すれば、関東圏含めて大勢が移動することで輸送機関に影響があるかもしれません。

しかし「不謹慎」という理由は十分ではないように思えます。その機材の移動に使うバスを流用する事はできるかもしれませんが、多くの人が日常を取り戻して買い占めみたいな行動を抑制できるかもしれません。 これはバランスの問題です。

そんな風に個別に与える影響の可能性を考えつつ「つつしみ」を持って行動し、またその様子や経緯が読み聞きする相手に伝わることが必要なのでしょう。

けっきょく、みんな不安なだけ

そもそも文字ベースのコミュニケーションでは、思いが全て伝わらない事は、気がつくかどうか別にして、常に起っていることです。

「考える」という事が苦手な人達や、教条的な生活を良とする人達は、一律に派手な活動や言動を「不謹慎」という言葉で狩るという行動にでても、不思議ではありません。

そういった行動の背景に思いを馳せると、何かをしなきゃと不謹慎という印象を与えかねない発言をする人も、不謹慎を気にするアナウンサーも、不謹慎と言葉を狩る人達も、漠然とした不安感を抱えている様子が想像できます。

そして、その心の奥底にある気持ち、行動を引き起す衝動の源である不安感は、いま被災地にいる人達と共通の感情なのではないでしょうか。

非生産的な漠然とした不安感は解消するべき

もし自分が被災地周辺のお店に並んで、自分が買う事で後に続く人達が手にする物資が減っていく、そういった事を想像するのは被災地から遠く離れていても多くの人達が共有可能な感情なはずです。

ある側面からみれば良い/悪いと判断がつくかもしれませんが、何をするときも、感情を爆発させるのではなくて、抑制を効かせて「つつしみ」を持って行動する、それがいま求められていることだと思います。

「不謹慎」だという事象はきっと確かに存在するのでしょう。

けれどそれを指摘する人たち自身が「つつしみ」を持っているか、立ち止まって考えて欲しいと思います。

そういう抑制の効いた様をみて、あるいは後からその事実を知って、被災者の方々はきっとポジティブなメッセージを受け取るでしょう。

そして「不謹慎」にみえても日常を取り戻す事は不安感を解消するために必要なことなのです。 それが出来るところにいるのであれば、ぜひそうしてください。

2011/03/11

DB2 Express-C 9.7とRuby(IBM_DB Driver)を使ってみる

最近はNonSQL DBばかりに注力していたので、ひさしぶりにRDBMSに回帰してみました。

データは最近扱っているiptablesログか郵便番号か、どちらにしようかと思ったのですが、MongoDBでは郵便番号情報を扱っていたので、今回はRubyを使って郵便番号DBを作成しています。

さいしょに感想らしきものを一言

MongoDBとCouchDBの比較はいろいろありますが、DB2を使ってみて改めて感じるのはチューニングポイントが沢山あって使いこなすマニアックな喜びはありそうだという点です。

けれど、ACIDがなくてもOptimisticな処理で十分対応できる用途に対しては、RDBMSを導入する利点よりもメンテナンスコストが上回ってしまう気がします。

あと、DB2と関係ないですが、MongoDBが物理メモリをかなり消費する点も気になっています。 本体の消費メモリは少ないはずですが、memmapを使ってファイルにアクセスしているようです。

CouchDBが動いているErlangのbeamプロセスは負荷をかけてもだいたい30MB前後、MongoDBは物理メモリの搭載量にも依存するようですが、できるだけ空き領域をキャッシュとして使うようにみえます。

DB2は比較にならないほどのプロセス数とスレッド数とメモリを消費してくれますが、NODEディレクトリにある物理ファイルのサイズはMongoDBよりも小さいです。

もっともMongoDBは使うファイルを最初に領域を確保してしまいますから、db.stats()で表示されるdataSizeをみると純粋なデータサイズはかなり小さくですけどね。

今回は ibm_db ライブラリを使って、Ruby から DB2 CLI Driver を呼び出しています。

オフィシャルのibm_db API documentだけでは役に立たない模様で、DeveloperWorksの参考書を読まないと何がなんだかさっぱりでした。

環境の説明

今回は次のような環境で作業を行ないました。

  • CPU: PhenomII 940 X4
  • Memory: 8GB (FileCache: 約4GB)
  • Disks: 500GBx2 (Software RAID-1)
  • DB2 Express-C 9.7 (db2leve: DB2 v9.7.0.2)
  • Ruby: 1.9.2-p136
  • Ruby CLI Driver: IBM_DB 2.5.6

Ruby CLI Driverの導入

gemを使ってダウンロードしますが、gemsの仕組みは使わないので、ibm_dbのlibディレクトリだけコピーしてきます。

$ export IBM_DB_LIB=~/sqllib/lib
$ export IBM_DB_INCLUDE=~/sqllib/include
$ gem install --install-dir tmp_rubylib ibm_db
$ cp -r tmp_rubylib/gems/ibm_db-2.5.6/lib .

ここから先はコピーしたlibディレクトリのあるディレクトリで作業を行ないます。

DBの作成とテーブルの定義

Databaseの作成は最初にちょっとするだけなので、手動でやっておきます。

$ db2 create db postaldb using codeset UTF-8 territory en

しばらくはリモート接続をしないので、nodeの作成やらDB2COMMの設定などはしないでおきます。

次はテーブルを作成します。 分割はせずに一つの巨大なテーブルを作成しています。

POSTALテーブル作成スクリプト

#!/bin/bash

db2 'connect to postaldb'
db2 'DROP TABLE POSTAL'
db2 'CREATE TABLE POSTAL ( SERNUM INTEGER PRIMARY KEY NOT NULL, CITYID  INTEGER, PCODEOLD  CHAR(6), PCODE CHAR(8), PREFKANA  GRAPHIC(7), CITYKANA  GRAPHIC(25), STREETKANA  GRAPHIC(70), PREF  GRAPHIC(7), CITY  GRAPHIC(25), STREET  GRAPHIC(70), OP0  INTEGER, OP1  INTEGER, OP2  INTEGER, OP3  INTEGER, OP4  INTEGER, OP5  INTEGER )' 
db2 'terminate'

preparedステートメントを使ったデータのINSERT

Rubyを使ったINSERT文の使い方はドキュメントになくて、executeUpdateに相当するメソッドもないようなので、普通にexecuteメソッドを使いました。

ken_all.utf8.csvファイルをPOSTALテーブルにINSERTするRubyスクリプト (insert_csv_prepare.rb)

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

require 'csv'

$:.unshift "lib"
require 'ibm_db'

conn = IBM_DB::connect("postaldb","","")
IBM_DB::autocommit(conn, IBM_DB::SQL_AUTOCOMMIT_ON)

sql = "INSERT INTO POSTAL (SERNUM,CITYID,PCODEOLD,PCODE,PREFKANA,CITYKANA,STREETKANA,PREF,CITY,STREET,OP0,OP1,OP2,OP3,OP4,OP5) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
pstmt = IBM_DB::prepare(conn,sql)
sernum = 1
opts={}
opts[:headers] = [:i,:pcode_old,:pcode,:pref_kana,:city_kana,:street_kana,:pref,:city,:street,:op0,:op1,:op2,:op3,:op4,:op5]
CSV.new(open("ken_all.utf8.csv"), opts).each do |row|
  IBM_DB::execute(pstmt, [sernum.to_i, row[:i].to_i,
                          row[:pcode_old], row[:pcode], row[:pref_kana],row[:city_kana],row[:street_kana],row[:pref],row[:city],row[:street],row[:op0].to_i,row[:op1].to_i,row[:op2].to_i,row[:op3].to_i,row[:op4].to_i,row[:op5].to_i])
  sernum += 1
end

IBM_DB::close(conn)

このスクリプトを準備するために、郵便局のWebサイトから郵便番号データをダウンロードしておきます。

$ unzip ken_all.zip
$ nkf -w ken_all.csv > ken_all.utf8.csv
$ ruby insert_csv_prepare.rb

約12万件(元データ約17MB)分のテーブルを作成するのに、2分30秒ほどかかりました。 Prepared statement(プリペアード・ステートメント)を使わずにSQLを毎回生成すると、7分30秒ほどでしたから、だいたい処理効率は3倍くらい違いがあります。

パフォーマンスについて

MongoDBでもDISTINCT(PREF),PREFKANAの結果を47行出力させてみましたが、MongoDBではPREFに対するINDEXがかなりパフォーマンスに寄与しました。

DB2でINDEXを作成せずにSQLを投げると、だいたいdb2start直後で接続時間を省いて3秒前後くらいです。 2回目以降はキャッシュが効くのか、0.2秒くらいになりました。

DB2のINDEXを使ってどうなるのか。 いろいろ謎なパラメータが沢山あるので、使い方によってはマッチしないんじゃないかなという心配がありました。

そんな理由で調査にはdb2advisが便利そうだったので、EXPLAIN表を作ってからSQLを実行して、DB2に最適なINDEXを考えさせました。

$ db2 connect to postaldb
$ db2 -tvf /opt/ibm/db2/V9.7.2/misc/EXPLAIN.DDL
$ db2advis -d postaldb -s "select distinct(pref),prefkana from postal"
-- LIST OF RECOMMENDED INDEXES
-- ===========================
-- index[1],    0.771MB
   CREATE INDEX "YASU    "."IDX1103110446310" ON "YASU    "."POSTAL"
   ("PREFKANA" ASC, "PREF" ASC) ALLOW REVERSE SCANS COLLECT SAMPLED DETAILED STATISTICS;
   COMMIT WORK ;

おなじことをしてみた結果は、予想どおり、この程度のデータ量ではあまり変化はなく、むしろDB2の接続にかかる時間が全体のパフォーマンスを低下させています。

RubyスクリプトでMongDBとDB2で同じような結果になるようにして時間を計測しましたが、MongoDBはdistinctしたPREFに対応するPREFKANA列を別に検索するカーソルの1ページ分の結果をprefetchするロジック的には効率は低いはずです。 また、結果はそれぞれ数回実行した後のものを載せています。

MongoDB | 2.182[s] (index無。接続時間含む)
MongoDB | 0.282[s] (index有。接続時間含む)
DB2     | 3.225[s] (index無。connect時間含)
DB2     | 1.751[s] (index有。connect時間含)
DB2     | 0.164[s] (index無。connect済)
DB2     | 0.121[s] (index有。connect済)

当たり前の結果ですが、DB2を使うならDBPoolingの仕組みは大切だということになりそうです。

SELECT文を発行するRubyスクリプトはprepared statementを利用する必要はありませんが、参考までにprepared state版を載せておきます。

SELECTを行なうSQL文 ibm_db driver/prepared statement版 (select_distinct_pref_pstmt.rb)

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-

$:.unshift "lib"
require 'ibm_db'

conn = IBM_DB::connect("postaldb","","")
IBM_DB::autocommit(conn, IBM_DB::SQL_AUTOCOMMIT_ON)

sql = "SELECT DISTINCT(PREF),PREFKANA FROM POSTAL"
pstmt = IBM_DB::prepare(conn,sql)
if IBM_DB::execute(pstmt, [])
  while row = IBM_DB::fetch_array(pstmt)
    puts "#{row[0].strip},#{row[1].strip}."
  end
end

IBM_DB::close(conn)

2011/03/04

YALTools: 手作業によるデータベースコピーの効率

CouchDBをメンテナンスするために作成したlscouchdbのパフォーマンスを考えてみました。

対象はDTI VPSにホスティングしているサーバー上のCouchDB 1.0.2です。

lscouchdbをコピーしてloopbackのCouchDBのポートに直接接続しているので、計測したデータの大部分はサーバのCPUとDisk I/Oのパフォーマンスに依存しています。

テストの概要

次のようなイディオムを使って、郵便番号DBに入っているView定義によるインデックス分を除く全データ(約220MB、約12万件)のコピーを作成しています。

作業手順

$ sbin/mkdb testp
$ time bin/lsdocs postal -u 15 | grep -v "_design/" | bin/postdocs -u 15 testp
$ sbin/rmdb testp

何回か-u 15の数字を増やして様子をみてみます。

PhenomII X4 940 (Mem: 8GB)での結果

物理メモリの半分ほどはファイルキャッシュに使われていて、今回のデータは十分この範囲に収まるようになっています。手元のPCを使った場合の結果は次のようになりました。

PhenomII X4 940での挙動: time bin/lsdocs postal -u 200 | grep -v _design | bin/postdocs testp -u 200


real	18m13.589s
user	1m3.160s
sys	0m8.400s

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 200 | grep -v _design | bin/postdocs testp -u 50


real	17m43.949s
user	1m7.320s
sys	0m8.520s

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 50 | grep -v _design | bin/postdocs testp -u 200


real	61m45.391s
user	1m12.870s
sys	0m12.770s

読み出し単位を4倍にして時間が1/4になっているので、読み出し回数は低く抑える方が良さそうです。

ちなみにDTI@VPSのエントリーレベル(256MB)で実行すると次のようになります。 Muninで観察している範囲では使用メモリは、この時間帯は180-190MBの範囲で遷移していました。

DTI@VPSでの挙動: time bin/lsdocs postal -u 220 | grep -v _design | bin/postdocs testp -u 100


real	22m51.496s
user	0m46.429s
sys	0m4.081s

ポイントは読み込み処理の効率化らしい

結局のところはデータを保存するパフォーマンスよりも、読み出しの単位を効率的なところにおさめるのが必要そうです。PhenomII X4 940 8GBで、データを取ってみました。

50〜3000件を一度に取得するようなコマンドラインの実行時間をtimeコマンドで取って、real時間をグラフにしてみました。

データ取得用のコマンドライン

$ i=50; time bin/lsdocs postal -u $i -p 1 > /dev/null

横軸がunitで、縦軸に処理時間をとったグラフ

/_all_docsの効率的な数字は環境によるはずですが、1秒辺りの処理件数は2000件で最高の836件になったので、2000前後にしておくのが良さそうです。

ここら辺を踏まえつつ、どうせ読み込み時間が線形に伸びるなら大きな数字にしてしまえと、やった結果が次のようになりました。

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 2000 | grep -v _design | bin/postdocs testp -u 200


real	5m4.372s
user	0m59.750s
sys	0m6.710s

書き込みの単位を増やしても、ほとんど影響がみられませんでした。

PhenomII X4 940での挙動: $ time bin/lsdocs postal -u 2000 | grep -v _design | bin/postdocs testp -u 2000


real	4m57.758s
user	0m54.100s
sys	0m6.500s

Proxy的な中間層がある場合の処理

注意しなければならないのは、今回の作業はバックエンドに直接接続しているということです。

これがstunnelなどを介している場合には、中間層のオーバーヘッドが問題になって時には処理が滞留することもあります。

Stunnelを使用した時の読み込み速度

読み出し側はだいたい10%程度のパフォーマンスダウンですが、書き込み時の単位を50件にすると処理は進みません。

手元の環境では25程度の書き込みにしないとCouchDBへの書き込みが発生しませんでした。

PhenomII X4 940での挙動:$ time bin/lsdocs postal -u 2000 -x stunnel.admin | grep -v _design | bin/postdocs testp -u 25 -x stunnel.admin


real	12m5.389s
user	1m36.620s
sys	0m7.260s

2011/02/26

Ruby::ゾンビプロセスを量産するopenメソッドの使い方 - IO::popenと%x{}の違い

DTIサーバのディスク容量監視にyadaemon.rbを使ったRubyスクリプトを走らせています。

ディスク容量を把握するのは、ちょっと面倒なので内部では手を抜いてopen("|df")を実行しています。

しばらく走らせてみたらdefunctプロセス(いわゆるゾンビプロセス)が大量発生したので、その原因を考えてみました。

問題のあったコード

"df"プロセスが終了していなかったのは次の部分です。

問題のあるコード部分抜粋

module YaWatchDisks
  def yield_df_perms
    open("|df ").each_line do |line|
      #       "Filesystem"  "1K-blocks"    "Used"    "Available" "Use%" "Mounted on"
      # perms: ["/dev/md0", "484041584", "453132936", "6514352", "99%", "/"]
      perms = line.split(/\s+/)
      yield perms if perms.length == 6 and perms[1].to_i > 0 and perms[2].to_i > 0 and perms[3].to_i > 0
    end
  end
...

考えてみれば、each_lineメソッドを実行する主体はeach_lineメソッドのブロックを終了してからcloseメソッドを呼び出すなんていう責任はないわけです。

内部ではopen("|df")の部分がGCの対象になるまで常に滞留していたはずです。

でも実際にはGCの対象にもならず、元プロセスは常駐していますから、プロセスが大量に残っていたという事になります。

問題の修正

変に省略せずに、ブロックをつけて呼び出してあげれば、何の問題もなくcloseされるようになります。

修正したコード部分抜粋

module YaWatchDisks
  def yield_df_perms
    open("|df ") do |f|
      f.each_line do |line|
        #       "Filesystem"  "1K-blocks"    "Used"    "Available" "Use%" "Mounted on"
        # perms: ["/dev/md0", "484041584", "453132936", "6514352", "99%", "/"]
        perms = line.split(/\s+/)
        yield perms if perms.length == 6 and perms[1].to_i > 0 and perms[2].to_i > 0 and perms[3].to_i > 0
      end
    end
  end
...

しばらくしてからmuninで確認すると、プロセス数が純増していたグラフからzombie分が消えていて解決したことが確認できました。

ruby-forum.comで見つけた理由らしきもの

Googleでドキュメントを検索していたらhttp://www.ruby-forum.com/topic/62435で、外部プログラムの結果を得るなら「バッククォートや%x{}構文を使ったら?」という記述がありました。

これを、試してみると古いコードでも確かにゾンビプロセスは発生しなくなり、問題なく動くようになりました。

2011/02/25

RDFaを理解する際に便利かもしれないCheatSheetの作成

手持ちのWebサイトではしばらく前からXHTML+RDFa-1.0をDTD宣言に使用していましたが、十分に対応できている状況ではありませんでした。

今は個人的使うCMSというか静的HTML/RSS生成ツールを作成していて、RDFの構造を意識しようとしています。

RDFaをちゃんと使うために神崎さんの「セマンティックHTML/XHTML」を読み直していて、RDFグラフの典型的な型をちゃんと理解しないといけないなぁと思ったところです。

CMSみたいなツールを考えてみると、文書自体の構造はある程度自動化できる可能性がありそうなのですが、文書に含める画像、アンカーなどのリソースは手動でrel, propertyなどの属性を付与する必要があると強く思っています。

コンテンツを書くために毎回本を開くのも大変なので、自分の理解の範囲でまとめた図を作ってみました。

RDFグラフの基本形

RDFa関連の資料を読むためには、リソースとリテラルを区別することが必要です。

Turtle構文が理解できる事は必要ですが、それは神崎さんの本を呼んでもらう事にして、次のような図に対応するTurtle構文RDF/XML構文XHTML+RDFa構文を理解できれば一つの目安になるかなと思います。

RDFaグラフの基本形

最後の形は閉じたRDFの形ですが、RDFaではまだ記述する事ができません。

詳細はRDFa Working Group WikiのContainersAndCollectionsに詳しく載っています。

XHTML+RDFaからのグラフへの変換

太字になっているのは属性に指定されている値のことです。 ここではrdf:typeだけが小文字で理解を補助するために付与しています。

XHTMLからRDFaグラフへの変換

relはリソース用、propertyはリテラル用

ここには書いていませんが、XHTMLのlink, metaタグも、指定する対象がリソースかリテラルかで使い分けています。

さいごに

これで網羅できているわけではないですが、リソース、リテラルの区別と、空白ノードが出現するタイミングを理解することがとりあえずの目標です。

おそらくこれぐらいは覚えておかないと、RDFaを意識したタグ付けはストレスな作業になるでしょう。

自動化できそうなのは、ダブリンコアやFOAFで定義されているタグの種類を提示したり、それがリソース用なのかリテラル用なのか判別するところぐらいでしょうか。

この記事で取り上げた品々

2011/02/22

Ruby 1.9.2以降(移行後?) - YAML::unescapeの行方

作成しているアプリケーションの中で設定を変更するために、YAMLデータ形式を経由してsave/restore機能を付けています。

便利に使っていたのですが、やや本気で使ってみたところ、UTF-8な日本語文字列をファイルに書き出したタイミングでエスケープされた形式になってしまう事に気がつきました。

YAML形式でファイルを書き出すsave_yamlモジュールメソッド

  def save_yaml(file, hash)
    require 'syck/encoding'
    open(file, "w") do |f|
      f.write(YAML::dump(hash))
      f.flush
    end
  end
  module_function :save_yaml

次のように、出力の一部は人間が読み書きできる形式ではなくなっています。

...
title: "\xE3\x83\x97\xE3\x83..."
...

日本語に限らずヨーロッパ系言語も1バイトに収まらないものはあるので、困っている人は他にも居たようで、調べてみるとYAML::unescape()を使う workaround をみつけました。→ Ruby to_yaml utf8 string

しかしRuby 1.9.2移行はYAML関連のパッケージが syck.rb にまとめられてしまったので、モジュール名がSyckになっています。

結局はSyck::unescapeを使う事で無事に解決しました。

変更後のモジュールメソッド

  def save_yaml(file, hash)
    require 'syck/encoding'
    open(file, "w") do |f|
      f.write(Syck::unescape(YAML::dump(hash)))
      f.flush
    end
  end
  module_function :save_yaml

2011/02/06

Ubuntu 10.04 LTSでlibreofficeが文字化けする件について

わりと普通にPPAをみてUbuntu 10.04 LTS (Lucid) 用のlistファイルを作成してLibreOffice 3.3を導入したところ見事に文字化けしてしまいました。

LibreOfficeの起動画面に日本語が正しく表示されないところ

cat /etc/apt/sources.list.d/libreoffice.list の出力結果

deb http://ppa.launchpad.net/libreoffice/ppa/ubuntu lucid main 
deb-src http://ppa.launchpad.net/libreoffice/ppa/ubuntu lucid main
$ curl http://keyserver.ubuntu.com:11371/pks/lookup?op=get&search=0x83FBA1751378B444 | sudo apt-key add  -
$ sudo apt-get update
$ sudo apt-get purge 'openoffice*.*'
$ sudo apt-get install libreoffice libreoffice-help-ja

実際はlibreoffice-gnomeを入れたんですが、原因はlibreoffice-gtkパッケージが抜けていたことのようでした。

ダブルクォートのオートコレクトについて

前々回にOpenOfficeを使っていてダブルクォートがスペシャルキャラクタに変換されてしまう挙動についての記事を投稿しました。

LibreOfficeで調べてみたところ、どうもデフォルトではReplaceにチェックは入っていないようです。

Windows版とか、マック版とかに依存しない挙動である事は確認していますが、言語環境とかにも影響されるのか、ちょっとなぞです。

さいごに

最近のPPAパッケージの導入手順はadd-apt-repositoryコマンドを使うのが一般的なようです。 やっている事は/etc/apt/sources.list*以下のメンテナンスとkey-addを自動的に行なってくれることで、やっている事は同じです。

たしかにすっきりするんですけどね。 便利さを追求すると、初心者には優しいけれど、中級者のスキルアップには厳しいシステムになりがちなところが、ちょっと問題かな。

2011/02/04

sf.jpが提供するPersonalForgeをしばらく使ってみた感想

PersonalForgeはSourceForge.jp (sf.jp)が提供する個人向けのGitリポジトリを提供するサービスで、2011年1月13日にリリースされています。

GitHubも同時に使い始めましたが、sf.jp全般は本家のSourceForge.net (sf.net)と比べてガイドが分かりやすく、次に何をするべきか把握することができました。

sf.netを使った時には、Gitリポジトリのパスのみが表示されて、どうやってコードを配置するか直接的なガイドはありませんでした。もちろんドキュメントは探せばあります。

ガイドはGitHubもちゃんとしていますが、日本語である点も含めてsf.jpを使うメリットはあるだろうと感じました。

それに自分のパソコンの外にコードを置くのはハードディスクのクラッシュといった災害から守る意味もあります。

Gitをとりあえず個人的に使ってみたくて、最初にどう使ったらよくわからないならPersonalForgeがお勧めです。

もちろんコードにオープンソース系のライセンスを付ける事が必要ですけどね。

Subversion v.s. Git

Gitを使うと決めてしまうと、Google CodeはSubversionのリポジトリのみを提供しているため選択肢から外れてしまいます。 それがlscouchdbの開発にsf.netを選択した理由です。

GitHubやGitorisは、PersonalForgeよりもリッチな感じですけれど、それでもコードを管理するだけの機能しかないですからね。

そんなわけで、今年に入ってから本格的にPersonalForge以外にもGitを使っています。

個人的にかなり初期からSVNリポジトリを自宅に構築してきたので、経験の差はありますが、それでも個人利用であればGitの方がいろいろなストレスから開放されそうです。

SVNのブランチやタグはどちらも大きな違いはなくて、高速かつ簡単にトップディレクトリ全体を別の場所にコピーするのと同じです。 SVNでは簡単にブランチを作成する事はできますが、そんなブランチ間を移動するのは、Gitを使った今はかなり面倒に感じます。

Gitの場合はもっとドライで、ブランチ間の移動がスムーズにできるよう設計されています。

けれど、根本的にSVNとGitではリポジトリの主従関係が逆転しています。

SVNを使っていると、バックアップの有無とかに関係なく、サーバに何かあったらどうしようかと心配になります。

Gitの自分の手元のリポジトリがメインで、サーバ側がその安全なスナップショットを反映するミラーにしか過ぎないというのは、地味だけれどGitに対するポジティブな印象を持つ大きな理由でしょう。

この一点だけでもSVNを捨てようかなと真剣に検討しているほどです。

SVN固有の機能は個人開発に必要なのか

SVNではファイル名を変更してもコードの変更履歴を追跡できるところがメリットですが、Gitではそこに注力してはいません。

SVNではファイル名を変更して、さらにその上のディレクトリ名を変更したり、重複した変更を一度にコミットするような複雑な変更を一度にして競合が発生した経験があります。

gitでも似たような事をしてしまいましたが、Gitはすんなりとその有り様を受け入れました。

これはフェアな比較ではありませんが、個人利用ではSVNの機能の多くは実際は必要ないと感じました。 履歴を追跡できる事は必ずしも重要ではないわけです。

その反面、企業ユースではむしろSVNの機能が必要な場合もあり得るでしょう。

学生であればSVNを学んでおく事は必要だと思いますが、SCM初心者の選択肢としては、特に分散リポジトリが必要なくても、SVNではなくてGitを利用するメリットはあると思います。

2011/02/03

OpenOffice Calcでダブルクォートが特殊文字に変換される現象について

CSVファイルを扱っていて、OpenOfficeのデフォルトの挙動に困った事がありました。

それはデータとしてセルに"name"のように単純にダブルクォートで囲んだ場合に、特殊文字に展開されてしまうところです。

これを回避する方法は簡単にでメニューバーの「ツール」から「オートコレクトオプション…」を選択して、「ユーザ定義引用符」置換のチェックを外すだけです。

オートコレクトメニューの変更が必要な個所を赤丸で表示しています

Excelを使った場合のダブルクォートの扱い

CSVについてはrfc4180が一応、後出しですがまとまった情報という事になっています。

これによれば、以前も書きましたが、ダブルクォートは""のように2つを続けて出力することでエスケープできます。

最近はCouchDBにPUTするDocumentをCSVから生成できる方法を模索しているので、Map関数を定義してみました。

Excel2007でCouchDBのMap関数を定義してみた

language,views,all,map
javascript,,,"function(doc) {
  if(doc.doctype && doctype == ""log"")
    emit(doc.src);
}"

注目するのは、== ""log""のエスケープされたところです。

OpenOfficeを使った場合

すでに説明していますが、オートコレクトの挙動によって入力したダブルクォートは\u201c(0x28809c), \u201d(0xe2809d)に変換されてしまいます。

"_id";"language";"views";"all";"map";"views";"all";"reduce"
"_design/all";"javascript";;;"function(doc) {
  if (doc.doctype && doc.doctype == “log”) {
    emit(doc,null);
  }
}";;;"_count"

Unicodeの"Left Double Quotation Mark"や"Right ..."はかなり見分けるには厳しい違いなので、できればデフォルトの挙動としては勘弁して欲しいんですけどね。

とりあえず先頭に書いたようにオートコレクトのオプションを操作して、CSVファイルを生成しています。

2011/02/02

CouchDB: 1.0.1から1.0.2のリリースアップ手順 (xstow対応版)

家にあるalixやらworkstationやらhttp://www.yadiary.net/にあるソフトウェアのバージョン管理にはxstowを利用しています。

最初にコンパイルしたapache couchdb version 1.0.1は/usr/local/stow/couchdb-1.0.1をconfigureの'--prefix'に指定しています。

今回は始めてのバージョンアップになります。

まぁstowのマニュアルには/usr/localをprefixに指定して、make install時に変数を上書きするような方法をとるわけですが、そうすると間違って/usr/local/stow以下でコマンドを実行しても、それなりに動いてしまうのが痛いところなわけです。

さて、今回はapache-couchdb-1.0.2がリリースされてしばらく経ち、いろいろテストしてみてもそのまま移行して問題なさそうな感じなので、その作業をまとめたログです。

現行環境の確認

インストールする環境はDTIのVPSで使っているDebian squeezeです。

stowディレクトリの構造

local.iniなどはバージョンに対して不変なので、stowディレクトリには含めていません。

深さ2ぐらいまでのディレクトリ構造は次のようになっています。

$ find /usr/local/stow/couchdb-1.0.1 -maxdepth 2
/usr/local/stow/couchdb-1.0.1/etc
/usr/local/stow/couchdb-1.0.1/etc/init.d
/usr/local/stow/couchdb-1.0.1/etc/logrotate.d
/usr/local/stow/couchdb-1.0.1/share
/usr/local/stow/couchdb-1.0.1/share/man
/usr/local/stow/couchdb-1.0.1/share/couchdb
/usr/local/stow/couchdb-1.0.1/share/doc
/usr/local/stow/couchdb-1.0.1/bin
/usr/local/stow/couchdb-1.0.1/bin/couchdb
/usr/local/stow/couchdb-1.0.1/bin/couchjs
/usr/local/stow/couchdb-1.0.1/lib
/usr/local/stow/couchdb-1.0.1/lib/couchdb
テスト環境で発見した不具合

このままstow側にないディレクトリ(例: var/, etc/couchdb)を同じように削除して、バージョンアップすると、ちょっとした問題が発生しました。

stowに含めないファイルはバージョンに依存しない情報を持っている事が前提だったのですが、default.iniファイルにはいろいろとパス名を含めた情報が記述されています。

$code_cap

...
util_driver_dir = /usr/local/lib/couchdb/erlang/lib/couch-1.0.1/priv/lib
...

このため$ sudo xstow -D couchdb-1.0.1を実行してから、/usr/local/etc/couchdb/default* ファイルたちを /usr/local/stow/couchdb-1.0.1/etc/couchdb 以下に退避してから、$ sudo xstow couchdb-1.0.2を実行する手間がかかっています。

置き換え手前までの作業

今回はノーマルなcouchdb-1.0.2のパッケージに加えて、自作のパッチを適用しています。

準備したファイルは次の通りです。

gropu_numrows関係のファイルは差分ではないので、直接置き換えます。 差分はGitHub上で確認できます。

コンパイルとインストール
$ tar xvzf apache-couchdb-1.0.2.tar.gz
$ mkdir apache-couchdb-1.0.2.extras
$ cd apache-couchdb-1.0.2.extras
$ curl -o couch_db.hrl https://github.com/YasuhiroABE/CouchDB-Group_NumRows/raw/couchdb-1.0.2/couch_db.hrl
$ curl -o couch_httpd_view.erl https://github.com/YasuhiroABE/CouchDB-Group_NumRows/raw/couchdb-1.0.2/couch_httpd_view.erl
$ cp couch_db.hrl couch_httpd_view.erl ../apache-couchdb-1.0.2/src/couchdb/
$ cd ../apache-couchdb-1.0.2
$ ./configure --prefix=/usr/local/stow/couchdb-1.0.2
$ make
$ sudo mkdir /usr/local/stow/couchdb-1.0.2
$ sudo chown $(id -un) /usr/local/stow/couchdb-1.0.2
$ make install
$ sudo chown -R root:couchdb /usr/local/stow/couchdb-1.0.2
stowで管理しない不要なファイル・ディレクトリの削除

インストールまでは、これで終りで次にvarディレクトリなど/usr/local/以下に直接配置するファイルやディレクトリを削除していきます。

新規に導入する場合には、削除ではなくて、対応する/usr/localの場所にmvする事になります。

$ sudo rm -rf /usr/local/stow/couchdb-1.0.2/var
$ sudo rm -rf /usr/local/stow/couchdb-1.0.2/etc/couchdb/local.*
$ sudo rm -rf /usr/local/stow/couchdb-1.0.2/etc/default
埋め込まれた"stow/couchdb-1.0.2/"パスの削除

スクリプトファイルなどの中からstow/を含むパスを通じて参照している部分を書き換えて/usr/local/stowを参照するようにします。

候補は次のコマンドラインで表示されますが、lib/couchdb/以下のファイルは設定ファイルで指定するので作業を行ないません。

ファイルを開いて書き換えますが、viを使った場合のコマンドラインは次のようになります → :%s!stow/couchdb-1.0.2/!!

 $ find /usr/local/stow/couchdb-1.0.2/ -type f | while read file ; do grep stow/ $file > /dev/null 2>&1 && echo $file ;  done
/usr/local/stow/couchdb-1.0.2/etc/init.d/couchdb
/usr/local/stow/couchdb-1.0.2/etc/logrotate.d/couchdb
/usr/local/stow/couchdb-1.0.2/etc/couchdb/default.ini
/usr/local/stow/couchdb-1.0.2/lib/couchdb/erlang/lib/couch-1.0.2/ebin/couch.app
/usr/local/stow/couchdb-1.0.2/lib/couchdb/erlang/lib/couch-1.0.2/priv/lib/couch_icu_driver.la
/usr/local/stow/couchdb-1.0.2/bin/couchjs
/usr/local/stow/couchdb-1.0.2/bin/couchdb

これで準備作業は全て完了しました。

couchdb-1.0.1からのバージョンアップ

最初に説明したように、通常であればcouchdbで停止してxstowコマンドで切り替えるだけなのですが、今回はstowで管理するファイルにdefault.iniとdefault.dを追加したのでxstowでcouchdb-1.0.1のリンクを削除した後で、このファイルを手動で移動する必要があります。

たぶんコマンドラインは次のようになるはずです。

$ cd /usr/local/stow
$ sudo /etc/init.d/couchdb stop
$ ps auxwww|grep couchdb          ## ← eralangプロセス(beam)の停止を確認
$ sudo xstow -D couchdb-1.0.1
$ sudo mkdir couchdb-1.0.1/etc/couchdb
$ sudo mv ../etc/couchdb/default.* couchdb-1.0.1/etc/couchdb
$ sudo xstow couchdb-1.0.2
$ sudo /etc/init.d/couchdb start

とりあえずこの手順通りで無事にhttp://www.yadiary.net/で動いているCouchDBを1.0.1から.1.0.2に移行する事ができました。

2011/01/29

重複起動を防止するRuby用のdaemonクラスライブラリ

SourceForge.jpにできたPersonalForgeのGitリポジトリ機能を使ってみました。

登録したのはCouchDB周りでDBのメンテナンス用Daemonを作るために作成したRuby用Daemonライブラリyadaemon.rbとサンプルコードです。

有名なdaemonライブラリは便利そうにみえますが、今回は使わない機能が豊富で、欲しかった機能は重複起動を防ぐ機能だったので自分で作成する事にしました。

今回はRuby 1.9.2用に作成しました。

雛型コード

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
require 'yadaemon'
opts = { "daemon"=>true,"euid"=>1000 }
daemon = YaDaemon.new("testapp","test.pid","/tmp",opts)
daemon.run do |pid|
  i = 0
  while daemon.running
    open("/tmp/testapp/test.log","w") do |f|
      f.write(format("updated: %d\n", i))
      f.flush
    end
    i += 1
    sleep 5
  end
end

PersonalForge

Gitリポジトリ

よくあるGitリポジトリのビューがPersonalForge Summaryページから提供されています。

コードのチェックアウト

gitを使ってチェックアウトする事ができます。

$ git clone git://git.pf.sourceforge.jp/gitroot/y/ya/yasundial/MyDaemonWrapper4Ruby.git

基本的な挙動

よくあるdaemonと同じように、pidファイルを使います。

runメソッドに指定したブロックを実効する前に、いくつかのチェック作業を行ないます。

  • シナリオ# - メインラインシナリオ
  • A1 - ユーザがプロセスを起動する
  • A2 - 既に実行しているプロセスがないかpidファイルの存在をチェックする
  • A3a - pidファイルが存在する場合、中に記述されている番号のプロセスが存在するか確認する
  • A3b - pidファイルが存在しない場合、pidファイルが作成できる事を確認する
  • A4 - piddirが書き込み可能か確認する
  • A5 - forkする場合は、forkする
  • A6 - 既存pidファイルにpidを保存する
  • A7 - rootで起動して、かつ、指定がある場合、eUID, eGIDにswitchする
  • A8 - runメソッドがyieldを実行し、ユーザが指定したジョブを実行する
  • A9 - ユーザが指定したジョブを実行した後は特に何もせず後続の処理を続ける

オプション

インスタンス化の手順

daemon = YaDaemon.new(appname, pidfile, pidpdir, options)

YaDaemonクラスのインスタンスを作成する際に指定できるパラメータは次の通りです。

  • appname: appname used for sub-directory name of the pidpdir.
  • pidfile: pid filename
  • pidpdir: pid parent dir
  • options: Hash object
    • debug => true/false
    • daemon => true/false
    • euid => number
    • egid => number
    • perms => number

意味と使い方を順番に解説していきます。

appname

パスを含まないディレクトリ名にとることができる文字列を記述します。

名前はアプリケーションの名前の意味ですが、実際には"pidpdir"直下にサブディレクトリを作るためのディレクトリ名として使われます。

euidが指定されている場合には、所有者がeuidで指定したユーザになります。

pidfile

パスを含まないファイル名を記述します。

指定したファイルにPID番号が書き出されます。

ファイルオーナはプログラムを実行したユーザのUIDになります。

pidpdir

ディレクトリへの絶対パスを記述します。

pidpdir自体のowner/groupなどのパーミッションは一切変更されません。

ディレクトリが存在していれば、このディレクトリappnameのディレクトリが作成されます。

options: debug (default: false)

オプションが指定されている場合にはデバッグメッセージがpidfileと同じディレクトリ(pidpdir/appname/)に"debug.log"の名前でログファイルが出力されます。

options: daemon (default: false)

trueの場合には、プロセスをforkしてstderr/stdout/stdinを切り離します。

d.j.b.のdaemontoolsと一緒に使う事を想定しているため、標準ではfalseに設定されています。

options: euid (default: Process::euid)

実効UIDを整数値で指定。文字列でグループ名を指定する事はできません。

プロセス特定のユーザで実効する場合には、ファイルの権限変更などに供えて、予防的にこのオプションを指定する事をお勧めします。

過ってrootユーザで実効した場合でも、指定したeuidに遷移した後にrunメソッドが呼ばれます。

options: egid (default: Process::egid)

実効GIDを整数値で指定。文字列でグループ名を指定する事はできません。

options: perms (default: 0711)

整数値で指定する必要がありますが、0711は711とは異なります。

0711,02711のような8進数をベースに指定します。

2011/1/31追記:stop/restartオプションの追加

pidファイルの中にPIDを埋め込んでいるので、これを利用してstop()force_stop()の2つのメソッドを持っています。

使い方の例はsf.jpのGitリポジトリの中に入れてありますが、簡単にサンプルコードだけを載せておきます。

トップにあるコードで、daemon.run()の呼び出しの前に、次のようなコードを書いておきます。

stop()メソッドを使った例

if ARGV[0] == "restart"
  begin
    daemon.stop
    while daemon.check_proc
      sleep 1
    end
    puts "running process was terminated."
  rescue
    puts $!
    puts "failed to restart process."
  end
elsif ARGV[0] == "stop"
  begin
    daemon.stop
    while daemon.check_proc
      sleep 1
    end
    puts "running process was terminated."
  rescue
    puts $!
  end
  exit
end

これを呼ぶと @piddirの直下に"stop.txt"ファイルが作成され、whileの条件式にしているdaemon.running()がfalseを返すようになります。

長期間sleepするような作りになっていると、daemon.running()メソッドが呼ばれるまで何も反応しませんが、ちょっとコードを工夫すれば問題なく安全に停止することができるでしょう。

2011/01/25

WebSVNに"lost+found"を無視させる

自作のコードのいくつかはSubversionで管理していて、Alix上にApacheを入れてWebDAVとWebSVNで管理しています。

今回USBメモリ上にリポジトリを作成したのですが、そこをWebSVN 2.0で表示させるとlost+foundをリポジトリとして扱おうとしてエラーが表示されてしまいます。

リポジトリの表示を自動化せずに、全て手動で$config->addRepository()を/etc/websvn/config.php内で実行するのは、どんどんリポジトリを追加していて、あまりにも面倒なので削除パターンを追加することにしました。

環境

WebSVNは次の環境で稼働しています。

  • HW: Alix 2c3
  • OS: Debian lenny (5.0.8)
  • Apache: 2.2.9-10+lenny9
  • WebSVN: 2.0-4+lenny1

WebSVNの最新版は2.3系列で、それについては最後に少しふれています。

Ubuntu 10.10で確認した範囲ではWebSVN 2.3.1では、apacheプロセスが読み取れない、SVNリポジトリではないディレクトリが存在していても問題なく動いています。

挙動の変更内容

設定ファイルの中でリポジトリが格納されているディレクトリを$config->setParentPath()で指定していると想定しています。

PHPの内部ではopendir()で展開されていますが、lost+foundなどのシステム上必要なディレクトリを省くためにignoreRepoPathPattern変数を追加しました。

既存コードに倣って直接変数への代入ではなく、addIgnoreRepoPathPattern関数を使って追加する仕様にしています。

修正内容

変更を加えたのは設定ファイルのconfig.phpと、それを元にリポジトリをリストアップするconfigclass.phpファイルです。

config.phpへの設定追加

--- /etc/websvn/config.php.orig	2011-01-25 10:45:15.000000000 +0900
+++ /etc/websvn/config.php	2011-01-25 11:02:04.000000000 +0900
@@ -1,4 +1,8 @@
 <?php
+
+// $config->addIgnoreRepoPathPattern("/my/"); // test purpose only
+$config->addIgnoreRepoPathPattern("/lost\+found/");
+
 // WebSVN - Subversion repository viewing via the web using PHP
 // Copyright (C) 2004-2006 Tim Armes
 //

configclass.phpの修正内容

--- /usr/share/websvn/include/configclass.php.orig	2011-01-25 10:10:19.000000000 +0900
+++ /usr/share/websvn/include/configclass.php	2011-01-25 11:00:23.000000000 +0900
@@ -133,6 +133,8 @@
    var $contentEnc;
    var $templatePath;
 
+   var $ignoreRepoPathPattern = array();
+
    // }}}
 
    // {{{ __construct($name, $svnName, $path, [$group, [$username, [$password]]])
@@ -1012,6 +1014,15 @@
 
    // }}}
 
+   // {{{ addIgnoreRepoPathPattern
+   //
+   // Set the ignore path pattern which works with the ParentPath function.
+   function addIgnoreRepoPathPattern($pattern)
+   {
+     $this->ignoreRepoPathPattern[] = $pattern;
+   }
+   // }}}
+
    // {{{ parentPath
    //
    // Automatically set up the repositories based on a parent path
@@ -1023,6 +1034,19 @@
          // For each file...
          while (false !== ($file = readdir($handle)))
          { 
+
+/* ignore some paths */
+$flag = false;
+foreach($this->ignoreRepoPathPattern as $reg) {
+  if ( preg_match($reg, $file) ) {
+    $flag = true;
+    break;
+  }
+}
+if ($flag) {
+  continue;
+}
+
             // That's also a non hidden directory
             if (is_dir($path.DIRECTORY_SEPARATOR.$file) && $file{0} != ".")
             {

さいごに

最新版の2.3系列のconfigclass.phpをみると、リポジトリの同一名での重複登録を省くための処理と、パターンにマッチした場合にのみリポジトリに加える処理も追加されていました。

addExcludedPath()を呼べばよさそうですが、指定するパスには絶対パスで指定する必要がありそうですが、現在では、Ubuntu 10.10の例にあるように、SVNディレクトリではないディレクトリがネガティブな影響を与える心配はないようです。

一括登録からみれば省くものは限られているケースが想定できて、想定外のパターンを含めてしまう可能性もあるので、バランスですが、一般的にはinclude, excludeは正規表現のパターンでも指定できたほうが便利だとは思いますが、とりあえずは古いWebSVNを動かしているが故のワークアラウンドでした。

2011/01/20

W3CのValidatorを通す方法 - Twitter/Facebookボタン編

W3CのMarkup Validation Service (validator.w3.org)はWebサイトの文書が規格に従っているかどうかを確認する事ができます。

有名なサイトであっても規格そのものに準拠していることは、このサービスを使えばあまり真剣に考えていないようすがみてとれます。

でもルールは守ってこそのルールですから、ここはどうにかしてValidatorサービスを通すようにしてみました。

今回使っている定義はXMTML/RDFaですが、XHTML系列であれば同じだと思います。

お題: Twitter/FacebookのボタンをWebサイトに追加する

作成した郵便番号の検索システムやらにTwitter/Facebookのボタンを追加しようとして、公式ガイドをみたところ見たことのないタグの使い方をしていました。

ちなみにボタンを追加するためのTwitterの公式ガイドは「Resources → Tweet Button」で、Facebookの方は「Like Button」にガイドがあります。

XHTMLではiframeタグが使えずにObjectタグを使うなどの変更が必要だったりすることは知られていますが、今回はその範囲を越えています。

スマートではないですが、規格に準拠しないところはJavaScriptを使って動的に作成することにしました。

Twitterボタンを設置する

オリジナルのコードは次のようになっています。

twitter.comに指示されたボタンを埋め込むためのHTML断片

<a 
  href="http://twitter.com/share"
  class="twitter-share-button" 
  data-count="vertical"
  data-via="YasuhiroABE">Tweet</a>
  <script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
課題

問題はdata-countなどの属性が標準では定義されていないところです。

この部分をJavaScriptを使って追加してあげることにしました。

変更したコード

..
<script src="/js/jquery.min.js" type="text/javascript"></script>
<script src="http://platform.twitter.com/widgets.js" type="text/javascript" charset="utf-8"></script>
..
<script type="text/javascript"><!--
  jQuery(document).ready(function($){
    $("a.twitter-share-button").attr("data-url","http://www.yadiary.net/postal/");
    $("a.twitter-share-button").attr("data-text", "CouchDBを使用した郵便番号検索 @VPS");
    $("a.twitter-share-button").attr("data-via", "YasuhiroABE");
    $("a.twitter-share-button").attr("data-count", "horizontal");
    $("a.twitter-share-button").attr("data-lang", "ja");
  }
--></script>
...
<!-- twitter button --> 
<a href="http://twitter.com/share" class="twitter-share-button">Tweet</a> 
...

Facebookボタンを追加する

ボタンを追加する方法にはiframeタグを使う方法と、FBMLを使う方法が選択可能です。

TwitterボタンではjQueryを使用したので、JavaScriptを使ってみます。

facebookから提示されたボタンを埋め込むためのHTML断片

<script src="http://connect.facebook.net/en_US/all.js#xfbml=1"></script>
<fb:like href="http://www.yadiary.net/postal" show_faces="true" width="60"></fb:like>
課題

fbタグが未定義なので、xmlnsでnamespaceを追加しています。 これは実際に定義されている必要はないので、他にも使われているらしい"http://www.facebook.com/2008/fbml"を使いました。

修正した

<html xml:lang="ja"
  ...
  xmlns:fb="http://www.facebook.com/2008/fbml"
> 
<head>
...
  <script src="/js/jquery.min.js" type="text/javascript"></script> 
  <script type="text/javascript"><!--
  jQuery(document).ready(function($){
    /* for facebook button */
    var fbtag = document.createElement('fb:like');
    fbtag.setAttribute("href","http://www.yadiary.net/postal/");
    fbtag.setAttribute("layout", "button_count");
    fbtag.setAttribute("show_faces","true");
    fbtag.setAttribute("width", "80");
    document.getElementById("facebook-button").appendChild(fbtag);
  });
  --></script>
...
</head>
<body>
...
  <script type="text/javascript" src="http://connect.facebook.net/en_US/all.js#xfbml=1"></script> 
  <span id="facebook-button"></span> 
...
</body>
...

今回jQueryを使っているのは、「既に使っているから」という以外の理由はありません。 コード自体はjQueryには依存していないはずです。

まとめ

はたしてこの方法が妥当なのか微妙ですが、どちらのボタンもJavaScriptを有効にしていないと動かないので、タグをJavaScriptでレンダリングしても良いのかなぁと自分を納得させています。

なんにしても、こんな方法で標準に準拠しないようなタグを使うことも可能です。

結果としてValidatorサービスでエラーなしにする事ができて満足しています。

2011/01/19

StunnelのクライアントモードでCouchDBに接続する

CouchDB自体には通信を暗号化する機能が1.1系列からしか準備されていないので、stunnelを使っています。

普段は「CouchDB: Ruby CouchモジュールをDigest認証対応にする」ようにSSL接続に対応したクライアントを使っています。

とはいえCouchDB自体には、SSL接続に対応した機能がないので、CouchDB同士を接続する必要があるレプリケーション(Replication)を有効にするためにStunnelのクライアント機能を使ってみました。

実際にはCouchDBの間にはインターネットがありますが、おおまかなシステム構成図は次のとおりです。

Stunnel Client: System Overview

Stunnelサーバ設定の確認

CouchDBの起動時にdefault/couchdbファイルに書かれているstunnelを起動するコマンドラインは次のようになっています。

/usr/bin/stunnel -v 3 -a /usr/local/etc/couchdb/sslcerts -d :::6984 -r 127.0.0.1:5984

-aオプションに指定しているディレクトリの中は次のような感じです。

$ sudo ls -l /usr/local/etc/couchdb/sslcerts
lrwxrwxrwx 1 root couchdb   17 Dec  2 11:44 22f12cbd.0 -> demoCA.cacert.pem
lrwxrwxrwx 1 root couchdb   23 Dec  2 11:44 6b0ab199.0 -> stunnel.client.cert.pem
-rw-r----- 1 root root    3664 Dec  2 10:23 demoCA.cacert.pem
-rw-r--r-- 1 root couchdb 3494 Dec  2 11:34 stunnel.client.cert.pem

Stunnelクライアントの設定

サーバ側はSSLクライアント認証が有効なので、普通に接続しようとすると失敗します。

接続用Certificateファイルの作成

いつも通りにCA.plを使って、newcert.pem,newkey.pemファイルを作成します。

CA.plへのパスはUbuntu 10.04 LTSでのものです。環境毎に格納場所が違いますので、locate CA.plで探すか、手動でopensslを実行してください。

$ /usr/lib/ssl/misc/CA.pl -newreq
$ /usr/lib/ssl/misc/CA.pl -sign
$ rm newreq.pem
$ cp newcert.pem couchdb.client.cert.pem
$ cp newkey.pem  couchdb.client.key.pem
$ openssl rsa < couchdb.client.key.pem > couchdb.client.nokey.pem
$ cat couchdb.client.cert.pem couchdb.client.nokey.pem > couchdb.client.pem

最終的にはcouchdb.client.pemファイルを使い、stunnelをクライアント化します。

Stunnelサーバ側でのCertificateファイルの更新

最初のコマンドラインにあるようにStunnelサーバは接続に使うcertificateを/usr/local/etc/couchdb/sslcertsに保存しています。

今回作成したcouchdb.client.cert.pemファイルと"CA.pl -sign"の実行時に使ったCAのcacert.pemファイルをstunnelサーバ側に転送しておきます。

Stunnelサーバ側で次のような操作をしておきます。 cacert.pemが既に存在していて、内容が同じであれば省いてください。 内容が違う場合はファイル名を変更してコピーしておく必要があります。

$ sudo cp couchdb.client.cert.pem cacert.pem  /usr/local/etc/couchdb/sslcerts
$ sudo c_rehash  /usr/local/etc/couchdb/sslcerts
Stunnelクライアントモードでの起動

基本的には次のようなコマンドラインでStunnelサーバに接続します。

/usr/bin/stunnel -c -p /usr/local/etc/couchdb/sslcerts/couchdb.client.pem" -d 127.0.0.1:5985 -r 192.168.x.x:6984

ここでの192.168.x.xはStunnelサーバのIPアドレスです。

Stunnelクライアント側にはcouchdb.client.pemファイルをコピーしておき、やはり次のようなコマンドを実行します。

$ sudo cp couchdb.client.pem /usr/local/etc/couchdb/sslcerts/
$ sudo /usr/bin/stunnel -c -p /usr/local/etc/couchdb/sslcerts/couchdb.client.pem" -d 127.0.0.1:5985 -r 192.168.x.x:6984
$ curl -u admin:xxxxxx http://localhost/:5985/_all_dbs

サーバ側はBasic認証が有効になっているのでadmin:xxxxxsはID(admin)とパスワード(xxxxxx)を':'(コロン)で区切って指定しています。

セキュリティ上の考察

Stunnelクライアントからはログインできるユーザは全てサーバに到達する事が可能になります。

もちろんパスワードがわからなければ接続できませんが、curlコマンドラインを起動する場合にはps auxwwwの出力にはでないですが、bash等、使っているシェルのhistoryには記録されます。

それが気になることはあまりないとは思いますが、こういうところにも気を配る必要があるかないか、環境はちゃんと理解しておくことが必要です。

さいごに

とりあえず、ここまでで無事にレプリケーションを有効にする準備ができました。

やっぱりセキュリティ周りの作りはちょっと不安なんですよね。

2011/01/14

Rubyでのflockの使い方

マニュアルでflockの使い方をみると、File::RDWR|File::CREATを指定していて、"w"を使うと切り詰め(truncate)るからダメだという記述があります。

ruby-1.9.2-p0/file.cに記載されているflockのサンプル

 *     # update a counter using write lock
 *     # don't use "w" because it truncates the file before lock.
 *     File.open("counter", File::RDWR|File::CREAT, 0644) {|f|
 *       f.flock(File::LOCK_EX)
 *       value = f.read.to_i + 1
 *       f.rewind
 *       f.write("#{value}\n")
 *       f.flush
 *       f.truncate(f.pos)
 *     }

たしかに"w"はだめだろうけど"r+"か"w+"ならいけるんじゃなかろうかと思って調べてみました。

ソースコードに記述された各モード文字列の意味

手元にあったruby-1.9.2-p0のio.cをみると、次のような記述があります。

ruby-1.9.2-p0/io.cからの抜粋 (9755-9773行)

 *    Mode |  Meaning
 *    -----+--------------------------------------------------------
 *    "r"  |  Read-only, starts at beginning of file  (default mode).
 *    -----+--------------------------------------------------------
 *    "r+" |  Read-write, starts at beginning of file.
 *    -----+--------------------------------------------------------
 *    "w"  |  Write-only, truncates existing file
 *         |  to zero length or creates a new file for writing.
 *    -----+--------------------------------------------------------
 *    "w+" |  Read-write, truncates existing file to zero length
 *         |  or creates a new file for reading and writing.
 *    -----+--------------------------------------------------------
 *    "a"  |  Write-only, starts at end of file if file exists,
 *         |  otherwise creates a new file for writing.
 *    -----+--------------------------------------------------------
 *    "a+" |  Read-write, starts at end of file if file exists,
 *         |  otherwise creates a new file for reading and
 *         |  writing.
 *    -----+--------------------------------------------------------

"w"や"w+"を使っていると、もしロックに失敗してもファイルの内容が失なわれてしまいます。 もっともLOCK_NBを加えていないサンプルのコードの場合には、ずっと待つので関係ない気がします。

そこでサンプルを考えてみました。2つのスクリプトがtest00.txtというファイルに自分のファイル名を書き込もうとします。

最初に実行するtest00a.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test00.txt"])
File.open(file, "w", 0644) {|f|
  f.flock(File::LOCK_EX)
  f.rewind
  f.write($0)
  f.flush
sleep 10
  f.truncate(f.pos)
}

次に実行するtest00b.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test00.txt"])
File.open(file, "w", 0644) {|f|
  if f.flock(File::LOCK_EX)
    f.rewind
    f.write($0)
    f.flush
    f.truncate(f.pos)
  end
}

別端末でcat test00.txtをループしながら、スクリプトを実行すると2つめの"test00b.rb"を実行した時点で処理が一時停止したかのようにみえます。

$ while true ; do cat test00.txt ; sleep 1 ;done

...
./test00a.rb
./test00a.rb
./test00a.rb  ## ← test00b.rbの実行直後から、内容のないファイルをcatするため画面には何も表示されない
./test00b.rb  ## ← seep 10の処理が終り、ファイルが上書きされ、その内容が出力される
./test00b.rb
./test00b.rb
...

とはいえ、確実にflockで待機していたtest00b.rbが内容を上書きしているので、意図したような動き自体にはなっているはずです。

ここでLOCK_NBを一緒に使う場合を考えると、実際にはファイルを上書きしなくてもファイルサイズが零になるため問題になるでしょう。

スクリプトを少し変更して、File::LOCK_NBを一緒に使うようなサンプルを作成してみます。

File::LOCK_NBを使った排他制御の例

前節と同様にtest01a.rb, test01b.rbを準備して、それぞれからtest01.txtを自身のファイル名で上書きする事を考えます。

ただし今回はファイルオープンに"r+"オプションを指定します。

最初に実行するtest01a.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test01.txt"])
File.open(file, "r+", 0644) {|f|
  f.flock(File::LOCK_EX)
  f.rewind
  f.write($0)
  f.flush
sleep 30
  f.truncate(f.pos)
}

次に実行するtest01b.rb

#!/usr/local/bin/ruby
#-*- coding: utf-8 -*-
@basedir = File::dirname($0)
file = File::join([@basedir,"test01.txt"])
File.open(file, "r+", 0644) {|f|
  if f.flock(File::LOCK_EX|File::LOCK_NB)
    f.rewind
    f.write($0)
    f.flush
    f.truncate(f.pos)
  end
}

これで実行するとファイルの新規作成はできませんが、既存ファイルを準備しておけば期待通りに動きます。

さいごに

表に戻ると、C言語の最初にfopenを習った時は"r"やら"r+"やらのアクセスモードの違いがよく分かりませんでした。

でも確認すればいいんですよね。ただ、経験がない分、それをどういう場面で使えばいいかの想像力が少し十分ではなかったかな。

教えるっていう行為は経験値が足りない人にどう伝えればいいかの部分が難しいんですよね、きっと。

2011/01/09

IPSetを使ったFirewallルールの設定について

以前家のブロードバンドルータにしているAlixにIPSetを導入するところまでを、「ブロードバンドルータにしているalixをDebian squeezeにして、ipsetを導入する」に投稿していました。

しばらく前にいわゆるボットネットの一部といわれているIPとの接続を弾くためにiptablesルールを設定仕直したので、そのログをまとめておきます。

現状のiptablesの設定状況

IPSetの導入と平行してiptablesルールの見直しを行なって、起動時にはiptables-restoreを使ってルールを設定しています。

設定のメインは/etc/network/if-pre-up.d/iptablesスクリプトで、if-pre-up.dディレクトリにあるスクリプトは起動時などifupによってネットワークデバイスが設定される直前に実行されます。

本当は重複して実行される可能性のある/etc/network/if-*.dに配置するのはスマートじゃないけど、iptables/ipsetの設定はNICが認識されているかどうかに無関係に実行できるので、重複起動は気にしないことにしました。

他には/etc/rc2.d/なんかにスクリプトを配置することもできると思います。 ここら辺はdebianで標準的な場所がないので、ホストの使い方によって変化するかもしれません。

もしWorkstationならネットワークデバイスの初期化云々はそれほど重要ではないので、/etc/rc.local辺りで設定をするかもしれません。

常時接続のルータは意図せずiptables設定なしにネットワークに接続するのは嫌なので、if-pre-up.dを使いました。

iptables設定スクリプトと参照するファイル群

このスクリプトの配置場所はいくつかの候補の中から選択するしかありませんが、ファイル名と内容は自由に書けるので、内部でiptables-restoreコマンド等を実行するようにしました。

スクリプトの中身は次のようになっています。

/etc/network/if-pre-up.d/iptablesファイル全体

#!/bin/bash

PATH=/sbin:/usr/sbin
BASEDIR="$(dirname $0)"

ipset --restore < "${BASEDIR}/../ipset.restored"
iptables-restore < "${BASEDIR}/../iptables.restored"
ip6tables-restore < "${BASEDIR}/../ip6tables.restored"

スクリプトでは/etc/network/直下に3つのファイルがある事を前提にしています。

  • /etc/network/ipset.restored
  • /etc/network/iptables.restored
  • /etc/network/ip6tables.restored

今回はip6tablesは範囲外です。ipsetとiptablesの連携について書いていきます。

iptablesとの連携

直接"iptables.restored"ファイルを書く事もできますが、いろいろ危険なのでiptablesコマンドを使って設定した後に"iptables.restored"ファイルを保存しています。

ipsetとの連携は、まずiptables側で空のipsetルールを作成して、それを参照するiptablesルールを加えます。

ipset,iptablesコマンドを使った空のipsetルールを使ったルール作り

#!/bin/bash
PATH=/sbin:/usr/sbin:/bin:/usr/bin

## delete all rules
iptables -F
iptables -t nat -F
iptables -X
iptables -t nat -X
iptables -Z
ipset -F
ipset -X
...
ipset -N denyip iphash --hashsize 36864
ipset -N denynet nethash --hashsize 36864
iptables -A INPUT -m set --match-set denyip src -j DROP
iptables -A INPUT -m set --match-set denynet src -j DROP
iptables -A FORWARD -m set --match-set denyip src -j DROP
iptables -A FORWARD -m set --match-set denynet src -j DROP
iptables -A OUTPUT -m set --match-set denyip dst -j DROP
iptables -A OUTPUT -m set --match-set denynet dst -j DROP
iptables -A FORWARD -m set --match-set denyip dst -j DROP
iptables -A FORWARD -m set --match-set denynet dst -j DROP
...

このスクリプトを実行して、動きに問題がない事を確認してからiptables.restoredファイルを作成しておきます。

$ sudo /sbin/iptables-save > /etc/network/iptables.restored

次にipsetを使い、定義だけされている空のipsetルールに具体的な設定を加えていきます。

まず用意するのはボットネットに組み込まれていると思われるIPアドレスのリスト。

iptables.deny.outboundファイル抜粋

##
## comment string, this line should begin with the '#' char.
109.10x.23x.1xx
...
109.23x.22x.0/24
...

このファイルを処理する

#!/bin/bash
PATH=/sbin:/usr/sbin:/bin:/usr/bin
BASEDIR="$(dirname $0)"
OUTBOUND_BLOCK_FILE="${BASEDIR}/iptables.deny.outbound"

ipset -N tmpip iphash --hashsize 36864
ipset -N tmpnet nethash --hashsize 36864
if test -f "${OUTBOUND_BLOCK_FILE}"; then
  while read ipaddr
  do
    ( echo ${ipaddr} | egrep ^# > /dev/null ) && continue
    ip="$(echo ${ipaddr}|awk -F/ '{print $1}')"
    mask="$(echo ${ipaddr}|awk -F/ '{print $2}')"
    if test "${ip}" != "" -a "${mask}" = "" ; then
      ipset -A tmpip "${ipaddr}"
    else
      ipset -A tmpnet "${ipaddr}"
    fi
  done < "${OUTBOUND_BLOCK_FILE}"
fi
ipset -W denyip tmpip
ipset -W denynet tmpnet
ipset -X tmpip
ipset -X tmpnet

設定したipsetルールは--saveオプションを使って保存します。

$ sudo ipset --save > etc/network/iptables.restored

こういう形でスクリプトを分けたのにはいくつか理由がありますが、ipset/iptablesコマンドを利用して大量のIPアドレスを処理するルールを追加するのにはかなり時間がかかるので処理を分けたかったのが大きな理由です。

次はipsetを使ってみたかったということ。

iptablesを使う事も可能ですが、空のipsetルールを設定したように、空のchainを作成しておいて、後からそのchainに個別のIPアドレスをDROPするルールを追加していくのが良いでしょう。

そうしないと一連のiptablesコマンドの実行が完了するまでの間は、ネットワークに対して脆弱なまま接続する事になるかもしれません。

もちろん直接iptables-restoreコマンドが解釈するようなファイルを生成して時間を短縮することは可能です。

いずれの方法でも、テストを十分にして、いきなり起動時に読み込まれるファイルとして保存しない事が重要です。

ここでリストの挙げられているIPアドレスからの接続を拒否することよりも、そういったサイトへの接続を拒否する事の方が重要です。 もっとも接続してはいけないマスター系ノードのIPアドレスは変化するでしょうし、知られていないものがあるでしょうから、本当にこういうリストが有用なのかは少し疑問が残ります。

何もしないよりはましかな。

CouchDB: Viewでのkeyの並び順(Order)の確認レシピ

CouchDBでViewを作成して、startkeyendkeyで条件を指定する時に、優先順位がいまいち分かりずらいので検証するための環境を作ってみました。

あらかじめ準備しておくものは次のものです。

  • CouchDB本体 (今回は1.0.1を準備しました)
  • Ruby (今回はjsonライブラリが用意されているRuby1.9を使います。Ruby 1.8を使用する場合にはjsonライブラリを別途御準備ください。curlでの代用も可能ですが、十分な注意が必要です)
  • テスト文書作成用DB (今回は"example"を使用しますが、任意の名前で結構です)

併わせて参考のために本家のCouchDB Wiki - View_collationを確認すると良いでしょう。

とりあえず結論

結果だけを知りたいという方のために、最初に今回の結果を載せておきます。

とりあえず"id"は無視して、keyの右辺の並びを上から順に眺めてください。

/example/_design/order/_view/orderの表示結果

{"id"=>"ordercheck.12", "key"=>nil, "value"=>nil}
{"id"=>"ordercheck.2", "key"=>false, "value"=>nil}
{"id"=>"ordercheck.4", "key"=>true, "value"=>nil}
{"id"=>"ordercheck.14", "key"=>-1, "value"=>nil}
{"id"=>"ordercheck.5", "key"=>1, "value"=>nil}
{"id"=>"ordercheck.3", "key"=>10, "value"=>nil}
{"id"=>"ordercheck.11", "key"=>"", "value"=>nil}
{"id"=>"ordercheck.0", "key"=>"a", "value"=>nil}
{"id"=>"ordercheck.15", "key"=>"bcd", "value"=>nil}
{"id"=>"ordercheck.10", "key"=>"z", "value"=>nil}
{"id"=>"ordercheck.13", "key"=>"\uFFF0", "value"=>nil}
{"id"=>"ordercheck.6", "key"=>[0, 1], "value"=>nil}
{"id"=>"ordercheck.9", "key"=>[0, 3, 2], "value"=>nil}
{"id"=>"ordercheck.7", "key"=>[1], "value"=>nil}
{"id"=>"ordercheck.8", "key"=>[1, nil, ""], "value"=>nil}
{"id"=>"ordercheck.16", "key"=>[1, 2], "value"=>nil}
{"id"=>"ordercheck.1", "key"=>{}, "value"=>nil}

nilからfalse,trueの順に並んでいく様子がわかります。

nil → 真偽値(false→true) → 数値 → 文字列 → 配列 → ハッシュ

配列の場合は基本的に先頭から要素の有無でまずソートされ、その次に要素の値でソートされています。 要素の数は重要ではない事がわかります。

この配列の扱いは個人的にViewの定義を考える時に混乱するところですが、Viewがちゃんとソートされていればlimit, skipを使って部分的な結果を得て、そのままWebページなりエンドユーザに出力することが出来るので便利なはずです。

作業の流れ

今回はCouchDB内に実際に文書とViewを作成します。その結果を表示する事で、どういった順序でソートされるのかを確認します。

作成する文書

文書の構造は次の通りです。

{
  "_id":"check_order.11",
  "_rev":"1-77356980318a930bb8afc1e6193fa981",
  "k":""
}

"k"に真偽値やら数値やらを代入していきます。

Map関数

"k"をキーにしています。Reduce関数は定義していません。

function(doc) {
  if(doc._id.indexOf('check_order.') == 0) {
    emit(doc.k, null);
  }
}
作成したViewの表示

最終的には最初に載せたような結果が得られ、優先順位は次のようになっている事がわかります。

nil → 真偽値(false→true) → 数値 → 文字列 → 配列 → ハッシュ

今回はこういう結果を出力するスクリプトを準備しておいて、どういう並び順になるか確認するための環境を作ります。

スクリプトの準備

流れに従って、文書作成用のスクリプトを作成する前にCouchモジュールに対するWrapperモジュールを作成しておきます。

ディレクトリ・ファイル構造

今回は"test"ディレクトリをトップディレクトリとして、相対的にlib, initdb, viewsディレクトリを作成していきます。libディレクトリ名は固定で、各スクリプトから"../lib"にパスを通します。

"lib"ディレクトリと同じレベルに存在すれば、"initdb", "views"ディレクトリ名は任意の名前に変更できます。

  • test/lib … ライブラリディレクトリ ("lib"ディレクトリ名は変更不可)
  • test/lib/couchdb.rb … CouchDB Wikiに掲載されているCouchモジュール
  • test/lib/util.rb … Couchモジュールにエラー処理を追加したWrapperモジュール
  • test/initdb … 文書作成用ディレクトリ (ディレクトリ名は変更可)
  • test/initdb/init_docs.rb … 文書を作成するスクリプト
  • test/initdb/show_all_docs.rb … 作成されている文書を全て表示するスクリプト
  • test/initdb/remove_docs.rb … 任意の_idを持つ文書を削除するスクリプト
  • test/views … View作成用ディレクトリ (ディレクトリ名は変更可)
  • test/views/_design.views.order.rb … Viewを作成するスクリプト
  • test/views/show_views.rb … 作成したViewを表示するスクリプト
lib/util.rbの作成

require 'couchdb'で呼び出しているライブラリは、Couch Wikiの「Getting started with Ruby」に掲載されているCouchモジュールです。

先頭にあるDBNameには文書を作成するために使用する、作成済みDB名を'/'から始めて書いてください。

次にYaCouch::getCouchの中を適宜変更して、Couch::Serverクラスのインスタンスをcouchに代入できるようにオプションを適宜変更します。

util.rbファイル全体

# -*- coding: utf-8 -*-

require 'json'
require 'uri'
require 'couchdb'

module YaCouch
  DBname = '/example'
  def YaCouch::getCouch
    ## couch = Couch::Server.new('user'=>'admin', 'password'=>'')
    couch = YaCouch::Main::getCouchAsAdmin
    return YaCouch::Main.new(couch)
  end
  class Main
    require 'json'
    require 'uri'
    def initialize(couch = nil, debug = false)
      @couch = couch
      @debug = debug
    end
    def get(uri)
      json = Hash.new
      begin
        res = @couch.get(URI.escape(uri))
        json = JSON.parse(res.body)
      rescue
        p $! if @debug
      end
      json = Hash.new if json.has_key?("error")
      json
    end
    def put(uri, json)
      res = nil
      begin
        res = @couch.put(URI.escape(uri), json.to_json)
      rescue
        p $! if @debug
      end
      res
    end
    def post(uri, json)
      res = nil
      begin
        res = @couch.post(URI.escape(uri), json.to_json)
      rescue
        p $! if @debug
      end
      res
    end
    def delete(uri)
      res = nil
      begin
        res = @couch.delete(URI.escape(uri))
      rescue
        p $! if @debug
      end
      res
    end
  end
end
initdb/init_docs.rbの作成

文書名(_id)は、「"check_order." + 数字」にしていますが、何でも構いません。

init_docs.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

@couch = YaCouch::getCouch
@num = 0
def up(json_value)
  uri = YaCouch::DBname + '/check_order.' + @num.to_s
  json = @couch.get(uri)
  json["k"] = json_value
  res = @couch.put(uri, json)
  @num += 1
end

## prepare documents
up("a")
up(Hash.new)
up(false)
up(10)
up(true)
up(1)
up([0,1])
up([1])
up([1,nil,""])
up([0,3,2])
up("z")
up("")
up(nil)
up("\ufff0")
up(-1)
up([0,4])
up("bcd")
up([1,2])
views/_design.views.order.rbの作成

Viewを作成するポイントは "/example/_design/order" です。

_design.views.order.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

@couch = YaCouch::getCouch
uri = YaCouch::DBname + "/_design/order"
json = @couch.get(uri)
json['language'] = 'javascript'
json['views'] = Hash.new
json['views']['order'] = Hash.new
json['views']['order']['map'] = <<-MAP
function(doc) {
  if(doc._id.indexOf('check_order.') == 0) {
    emit(doc.k,null);
  }
}
MAP
res = @couch.put(uri,json)
p res.body
views/show_views.rb

show_views.rbファイル全体

#!/usr/local/bin/ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'
@couch = YaCouch::getCouch

uri = YaCouch::DBname + "/_design/order/_view/order"
json = @couch.get(uri)
json['rows'].each do |row|
  p row
end

このスクリプトの実行結果は、最初に掲載したようなdoc.kをキーとしてソートされた文書のリストになります。

まとめ

タイトルを「〜レシピ」にしたので、その体裁で書こうと思ったものの、挫折しました。

それはさておき、Rubyで使えるライブラリはいろいろありますが、手元の環境ではStunnel4を使い、CouchDBサーバはSSLクライアント認証を有効にしているため、接続部分をカスタマイズする必要があります。

テストのためにApacheのmod_proxyを使ってDigest認証での接続も出きるようにしていますが、いずれにしてもデフォルトの接続処理のセキュリティに満足していないので、低レベルなCouchモジュールに手を入れて使っています。

そんな事をしていないのであれば他のライブラリに慣れるのが良さそうですが、その場合でもこのスクリプトを大きく変更する必要はないと思います。

Appendix. 追加スクリプト

処理の本筋ではない、initdb/show_all_docs.rb と initdb/remove_docs.rb スクリプトを掲載しておきます。

initdb/show_all_docs.rb

show_all_docs.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'
@couch = YaCouch::getCouch
uri = YaCouch::DBname + '/_all_docs?include_docs=true'
json = @couch.get(uri)
json['rows'].each do |row|
  p row['doc']
end
initdb/remove_docs.rb

引数無しに実行すると、内部でdelete_doc_prefix変数に設定されている"_id"名が"check_order."で始まる文書が削除されます。

View定義を削除する時には引数に "_design"を指定してください。

remove_docs.rbファイル全体

#!/usr/bin/env ruby
$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

delete_doc_prefix ='check_order.'
delete_doc_prefix = ARGV[0] if ARGV.length == 1

@couch = YaCouch::getCouch
uri = YaCouch::DBname + '/_all_docs?include_docs=true'
json = @couch.get(uri)
json['rows'].each do |row|
  d = row['doc']
  if d['_id'] =~ /^#{delete_doc_prefix}/
    uri = format("%s/%s?rev=%s", YaCouch::DBname, d['_id'], d['_rev'])
    res = @couch.delete(uri)
    p res.body
  end
end