Truly Ergonomic Keyboard (JIS配列) 使い始めた

職場で Mac OS X を使うようになり、また腱鞘炎にもなっていたところだったので Truly Ergonomic Keyboard を買うことにした。…のが4月になりたての頃。
購入して早速 Mac にぶっ刺してみたところ、どうも動かないキーが多くて困った。裏側にあるDIPスイッチを Mac 向けとか日本語系とかにしてみても、真ん中の Del が Backspace だったり Del と Backspace との区別が(動作上)なかったり、Tabが遠かったりパイプがどこだかわからなかったり、何より「半角全角」キーがないせいで日本語入力ができなかったりして、実質的には使えたもんじゃない状態だった。
幸いにしてこれはファームウェア書き換えをすることによってキー配置を自在に変更できるようになっており、その辺の問題は無事解決できそうに思えた、が、肝心のファームウェア入れ替えはWindowsでしかできないのだった。(発売当初。今はmacでもできるので心配ない)
ということで持ち帰って、ついでに書き換えるのならしばらく Mac でのキー入力を試してみてそれに合った入力方法を選択するのhttp://d.hatena.ne.jp/suu-g/edit#がよいのではないかということになり、そのまま放置されることとなった。ありがちだ。

で、しばらく使ってみるとまあ Mac のキーボードってそんなに押しやすいわけでもないし本体熱くなるしでだんだん別キーボードを使いたい気持ちがふつふつと盛り上がってきて、今回ようやく重い腰を上げて入れ替えをしたってわけ。
suu-g: Mac向けカスタマイズ

今回は上記の構成にしてみた。しばらく使ってみて、また考えたいところ。
未だに悩んでいるのは半角全角をどうするかで、スペースキーはわりとッターンしたいから親指に置きたいけれど、カタカナ・ひらがなと無変換キーも親指で押したいので、これはどうしたものか。スペースキー基本的に左手で打っているようなので、変換(全角半角)キーを右スペースにするのでいいかも知れない。
そのへん直したら適当に追記か書き換えしていくつもり。

=> さっそく右スペースを全角半角にしたバージョン。ど真ん中の※マークにはパイプをおいた。もったいない気もするけどいちばん使うのシェルだし…。

=> さらに更新。パイプ周りとかを修正。アンダースコアが若干遠いことが今の難点だけどさすがにキーが足りないのでしゃーなし。あとは意外とスペースキー右側も使いたいときあるらしい。なんか気分次第で右も使ってた。それと Mac キーは頻繁に使うので左下に配置。Altはあまり使わないから諦めて左上にした。

AironetのMBSSID設定方法

久々に Aironet をきちんとMBSSIDにて設定した。公式の設定例を見ながらだいたいできたのだけど、最後の最後でひとつうまく行かないことがあって、40分くらいはそれで浪費してしまった。

学生のころはこんなもんだろと思っていた設定が、今となってみるとずいぶん不合理に見えてきて、「どこに何が必要か」について把握するのが難しいと感じるのは自分の能力不足とか以上にその設定が分かりにくいからだ、そしてその設定を筋を通して説明するページがないからだ、という考えに至ったので、今回行った設定の考え方をなんとなく記録として残しておく。

無線を落とす

AP をつけたとき、最初にすべきことは、コマンドを打っている最中も容赦なく出力されてくるエラーメッセージを黙らせること。無線を止めることで実現できる。

en
conf t
int dot11 0
shut
int dot11 1
shut
do wr mem

do wr mem は非常にお行儀が悪いけど、消えるよりゃマシだろまぁ。do で書き込むなという話もあるし、いまの推奨は copy running-config startup-config だっけ。

VLANとサブインターフェイス

通常、Aironet の AP にはイーサの口は一つだけ。なので、そこには Tagged VLAN を食わせて MBSSID 対応していくことになる。

その口が interface fa 0 だとしたら、中で利用したいVLAN番号に応じたサブインターフェイスを作っていく。 今回の例では、42は(すべての答えだから)管理用セグメント、398は(サンキュッパで安そうだから)ユーザセグメント。

interface FastEthernet0.42
 encapsulation dot1Q 42
!
interface FastEthernet0.398
 encapsulation dot1Q 398
!

無線の方も、イーサと同様にサブインターフェイスを作る。

interface Dot11Radio0.398
 encapsulation dot1Q 398
!

ブリッジする

実はAironetは、VLANを決めただけでは通信が通るようにはなっていない*1。通信を通したいインターフェイス同士を、同じ「ブリッジグループ」に含める必要がある。

先ほどの二つのインターフェイスを、 bridge-group でまとめる。

interface FastEthernet0.42
 encapsulation dot1Q 42
 bridge-group 2
!
interface FastEthernet0.398
 encapsulation dot1Q 398
 bridge-group 3
!
interface Dot11Radio0.398
 encapsulation dot1Q 398
 bridge-group 3
!

VLANと違い、bridge-groupは1-255くらいまでしか設定できないので、VLANと同じ番号は使えない。VLAN番号の末尾や先頭などにちなんだ、分かりやすいものを選んでおくとよい。 Aironet 本体へ telnet/ssh するために使用する BVI (Bridge Virtual Interface) はデフォルトではVLAN番号 1 上のネットワークに勝手につながるのだけど、それ以外を利用する場合は bridge-group を設定した、ような気がする。

interface BVI 1
 ip address 192.168.0.2 255.255.255.0
 bridge-group 2
!

VLAN と bridge-group と BVI との関係は、 http://packetlife.net/blog/2012/feb/20/aironet-aps-bridge-groups-and-bvi/ の図がわかりやすい。

SSID の設定をする

Aironet の SSID の設定の仕方には歴史があり、昔は SSID を一つしか設定できなかった。その名残りがあるので、SSIDの設定をするときは、以下のような困ったことが起きることがある。

  • 設定の順序を間違えると入らなくなる設定がある
  • ググって出てきた情報が間違っている
  • タブ補完でそれっぽい設定を探しても、それがまったく意味のない設定
  • タブ補完が効かない(あるいは、タブ補完は効くが、コマンド途中の入力は受け付けられない)
  • あとでコンフィグ見るとそこだけインデントが違う

わりとまじでよくある。だるい。

さて、SSIDを設定するにあたって設定する順番は、だいたい以下の通り:

  1. MBSSID(マルチBSSID)宣言をする
  2. SSIDの設定をする
  3. SSIDをVLANに紐付ける
  4. 無線インターフェイスに、VLANごとの暗号化方式を設定する
  5. 無線インターフェイスに、出力するSSIDを宣言する

それぞれについて、以下で説明していく。

MBSSID 宣言

(conf) dot11 mbssid

元来はインターフェイスごとに mbssid を宣言する必要があった。グローバルな設定として宣言しておくと、インターフェイスに書く必要がなくなる。

SSID の設定をする

dot11 ssid GuestUser398
   vlan 1
   authentication open
   authentication key-management wpa version 2
   mbssid guest-mode
   wpa-psk ascii 7 XXXXXXXXXXX
!

特に言うことはないけれど、

  • RADIUS 使わない限りは authentication open はほぼ固定設定
  • WPA2 を利用するとここで宣言しておく
  • WPA2-PSK で認証する場合、ここで wpa-psk とパスワードを入力する。ユーザが接続するときに利用するやつはここで入力する
  • mbssid guest-mode は、まあ、mbssid でかつ guest-mode(SSIDを広報する)ということ。色気を出して guest-mode を消すとかはしないほうがいい。セキュリティが悪化する

そして VLAN を設定。Aironet的には、SSIDはVLANに所属するものになっている。これ大事なポイント。テストに出る。

無線インターフェイスにVLANごとの暗号化方式を設定する

このあたりがどうにも気持ち悪くて、全然覚えられない。なんか筋が通った話はあるんだろうか…。Cisco公式ドキュメントを熟読しないと…。

先ほどは SSID ごとにパスフレーズを設定し、その SSID は VLAN に紐付いていた。 ここでは、無線インターフェイスの VLAN ごとに、通信路の暗号化に利用する方式(AES or TKIP)を設定する。

interface Dot11Radio0
 encryption vlan 398 mode ciphers aes-ccm
 !
 shutdown
!
interface Dot11Radio1
 encryption vlan 398 mode ciphers aes-ccm
 !
 shutdown
!

TKIPの場合は encryption vlan 398 mode ciphers aes-ccm tkip とする。まあいまどきTKIP必須とかないだろうけど。

ここでの注意点は、設定を入れるDot11Radioのインターフェイスがサブじゃないということ。せっかくサブインターフェイスを作ったのに悪いけどさ、あれは bridge-group しか設定しないんだわ。おかしいだろ常識的に考えて…だがこれが現実…。

さて。これで準備は万端だ。あとは数コマンドだけ。

無線インターフェイスSSID を設定する

ところで、無線インターフェイスDot11Radio0Dot11Radio1 とに分かれているのは、 0 が 2.4GHz帯で 1 が 5GHz帯のインターフェイスになっている、というところにある。

インターフェイスごとつまり周波数帯ごとに、吹きたいSSIDが変わってくることがある。 最近だと、SSIDの末尾に -a をつけたものを 5GHz帯に、 -g をつけたものを 2.4GHz帯に、なんてことがよくある。 これは無線インターフェイスごとに別々の SSID を設定していくことで実現可能なわけ。

今回、 GuestUser398 という SSID を 2.4GHz 5GHz 両方に設定するのであれば、以下のような設定をする。

interface Dot11Radio0
 encryption vlan 398 mode ciphers aes-ccm
 !
 ssid GuestUser398
 shutdown
!
interface Dot11Radio1
 encryption vlan 398 mode ciphers aes-ccm
 !
 ssid GuestUser398
 shutdown
!

設定が完了したら、interface の shutdown を外そう。

int dot 0
no shut
int dot 1
no shut

no shutdown すると、Aironetが無線を発し始める。周りの無線のスキャンを行うので、起動までは少し時間がかかることになる。

数十秒ほどして、ランプが緑色になったら、APが準備完了したしるしだ。接続テストをしよう。繋がったら Assoc したとシリアル画面へとログ出力されるし、ランプも青色へと変化する。

以上

まっとうな運用をするのであればもっと色々と設定することはあるけれど、まずは以上の設定を行い SSID を確認できてからでないと話は始まらない。とりあえず Cisco の考え方に身を染めていこう。人間、あきらめが肝心だ。

*1:このへんはルータと考え方が一緒

rbish gem 作成中

erb を組み入れたシェルスクリプトを実行する gem を作成している。とりあえず公開だけはした。
https://rubygems.org/gems/rbish
読みは rubbish と同じ【rʌ'biʃ】。

erb で書かれたシェルスクリプトを動かすだけなら下にある一行くらいでもどうにかなるので、ほとんどの労力はコマンドとして整えるところに行ってることになるかな。

`#{ERB.new(File.read(ARGV[0])).result}` 

今回は thor ではなく mixlib シリーズを使用してコマンドラインを立ててみてる。Thor はサブコマンドを作るときは最高の選択肢のひとつだけど、UNIX style のコマンドを作成するには最適ではない感じがしたので、ほかの諸々を調査してた。で、いくつかの候補の中から mixlib-cli を選択してみた、というわけ。
mixlib には config や log などもあるので、それらと一緒に一通り使ってみて、それから判断しようと思ってる。

現在のバージョンは 0.1.0 ってことにしてる。Semantic Versioning 2.0 によると、公開時は 1.0.0 にしろってことなんだけど、今のものにその数字を背負わせる自信はない。
まずはこちらで認識してる、エラー表示とかテストとかデバッグ出力とか、そのあたり直したらもうちょっとバージョンを上げようかなと思う。

libv8 のバージョンを探す

2013年の Ruby on Rails Advent Calendar に参加しようと思ったら、その日のぶんが空いていたので、当日の 23:45 からプログラムを書いてそのまま記事にするというエクストリームアドベントをすることとなりました。プログラマ的には 33 時までは当日だから問題ないよね。

さて今回は Ruby on Rails というか therubyracer というか libv8 の話です。

Ruby on Rails では therubyracer gem をちょくちょく使いますね。therubyracer gem は Ruby から使用できる v8 エンジンで、環境ごとに適切なバイナリパッケージがインストールされるのですが、時折やたらと時間がかかることがあります。*1
この適切なバージョンを見つけ出すためのスクリプトを作成しました。

これを走らせると、

 $ ruby libv8_check.rb
gem 'libv8', '3.16.14.3'

と、Gemfile に書くべき行を表示してくれます。

何が表示されているか?

libv8 gem には、特定の環境用にビルドされたバイナリ gem と汎用の gem とがあります。この汎用gemをインストールすると非常に時間がかかることになります。
したがって、汎用 gem をインストールするのは避けたいのですが、バイナリパッケージが存在するかどうかの確認は案外手間がかかり、「誰でもできる」とは言い難い状況でした。
ということで、その確認を簡単にするためににこのスクリプトを作成した、ということになります。

このスクリプトは、「存在している libv8 gem の中でもっとも新しい、ユーザ環境で使用できるバイナリ gem のバージョンを表示する」ということをしています。つまり Linux x86_64 環境であればその環境用のバイナリ gem を、また MacOS X 用であればそれ用のバイナリ gem を確認して出力します。
ただ、バイナリパッケージのない環境も存在します。そういう環境で実行した場合は、下のように ~> を使用したバージョンを表示します。

gem 'libv8', '~> 3.16.14.3'

やっていること

環境の選択

bundle install の際、インストールするべきバージョンを選択しているのは bundler ですが、環境やバイナリパッケージを選択しているのは rubygems です。
rubygems の中では Gem::Platform クラスがこのあたりをコントロールしていますので、少しだけこれを借りています。

rubygems API v1 の利用

はじめは Gem クラスを利用して完全なバージョンを取得しようと思ったのですが、「特定の gem の全バージョンを取得する」ということをできるメソッドが見つからず。
Rubygems API を直接利用することにしたところ、これが非常に簡単!

$ curl http://rubygems.org/api/v1/versions/libv8.yaml
---
- authors: Charles Lowell
  built_at: '2013-12-05T00:00:00Z'
  description: Distributes the V8 JavaScript engine in binary and source forms in
    order to support fast builds of The Ruby Racer
  downloads_count: 79
  number: 3.16.14.3
  summary: Distribution of the V8 JavaScript engine
  platform: x86_64-solaris-2.11
  prerelease: false
  licenses:
  - MIT
  requirements: []
- authors: Charles Lowell
  built_at: '2013-12-04T00:00:00Z'
  description: Distributes the V8 JavaScript engine in binary and source forms in
    order to support fast builds of The Ruby Racer
  downloads_count: 96
...

API については http://github.com/rubygems/rubygems.org を git clone して rake routes とか適当にして確認していましたが、ちゃんと公式ドキュメントが整備されていたようです…。

GET - /api/v1/versions/[GEM NAME].(json|xml|yaml)

Returns an array of gem version details like the below:

$ curl https://rubygems.org/api/v1/versions/coulda.json

[
{
"number" : "0.6.3",
"built_at" : "2010-12-23T05:00:00Z",
"summary" : "Test::Unit-based acceptance testing DSL",
"downloads_count" : 175,
"platform" : "ruby",
"authors" : "Evan David Light",
"description" : "Behaviour Driven Development derived from Cucumber but
as an internal DSL with methods for reuse",
"prerelease" : false,
}
]

http://guides.rubygems.org/rubygems-org-api/#gem_version_methods

Rubygems API は色々と面白く使えそうですね。

おわりに

ということで、 Ruby on Rails を使用するうえで比較的欠かせない libv8 の gem をより短い時間でインストールできるようにするべく、適切なバージョンを探し出してくれるスクリプトをこねこねと作成しました。

今回ざっくり実装したこの機能ですが、あと若干汎用化すると「バイナリパッケージが準備された gem かどうかを判定する」機能となるので、そこまで作ってから gem として公開しようかなあなどともくろんでいます。さすがに今日中は無理でした。

以上、2013年 の Ruby on Rails Calendar 9 日目の記事でした。
…あれ?この記事 Rails 関係あったと言えるのかな? …まあいいか!

*1:このへんの詳細については以前書いた http://d.hatena.ne.jp/suu-g/20121222/1356189597 参照のこと

bundle install を早くする唯四の方法

皆さん、bundler 使用していますか?(はーい!という声)そうですね、とても便利ですね。でも遅い。何より速さが足りない。そう思うことも時折あるのではないでしょうか。bundle install に数分間かかるのはよくある話、ときによっては10分以上も bundle install だけでかかってしまう。しかも遅いときに限って何も表示されない。壊れてる?…と思って放置してごはんを食べて戻ってくるといつの間にか終わってたりする。別に bundler 自体が悪いわけではないですが、不安になります。
「俺の bundler がこんなに遅いわけがない。もっともっと速くなればいいのに」、そう思ったあなたのためにこの記事です。bundle install 時間を短くするただ一つ、いや四つの方法をご紹介します。

rubygems のバージョンを適切にアップデートする

あまり知られてない気がしますが、gem のバージョンによっては bundle install がものすごく低速になることがあります。私が経験したところでは、 2.0.3 では 1.8.25 の 3 倍程度遅くなっていました。

$ gem --version
2.0.3
$ sudo gem update --system

バージョンを確認して 2.0.3 以下の 2.0 系だった場合、アップデートしておきましょう。2.0.7 は速度的には問題なさそうです。

https://rubygems.org ではなく http://rubygems.org を利用する

標準で利用される source 'https://rubygems.org' よりも、 source 'http://rubygems.org' としたほうが微妙に bundle install が早くなります。下の画像はさくらの VPS にて bundle install を試したときの時間を計測したものですが、 http のときは https と比較して1.5倍近く高速化されていますね。

http proxy がはさまれている環境の場合は、さらに早くなる可能性もあります。
ただ、http にすれば当然ながら公開鍵認証による信頼の恩恵は受けられませんし、本家 bundler にて推奨しているのは https://rubygems.org です。このあたりは自己責任でプリーズ。

並列 bundle install を利用する*1

1.4.0.pre.1 より、並列 bundle install が可能になっています。これは爆速です。ぜかましです。本家 bundler の issue によれば、特に RTT が長いときに効果を発揮するとのこと。ちなみに source 行の http/https の違いは並列化でかなり吸収されますので、並列 install を利用する場合はわざわざ source 行を http に書き換える必然性は薄いです。
雑な計測*2ですが、私の環境ではこんな結果が出ました。

$ gem install bundler --version='1.4.0.pre.2'
$ time bundle install --path=.bundle/gems --binstubs=.bundle/bin
real    4m32.956s
$ rm -rf .bundle/gems .bundle/bin
$ time bundle install -j 10 --path=.bundle/gems --binstubs=.bundle/bin
real    0m39.983s

この機能の原作者は @eagletmt 先生。利用する際はありがとうを三唱しましょう。
ただ、正式版ではなく pre の機能*3 ということはお忘れなく。メモリやCPUも使いますし。これも自己責任ですね。

インストール時間を表示させる

bundler は多くの gem を扱うわけなので、そのうちのひとつの gem の install が長くなると、全体としての bundle install 時間も一気に遅くなってしまいます。
具体的には、therubyracer 0.11.0 問題とか、Nokogiri 1.6.0 問題とかですね。
こういった問題に遭遇していたら、install の遅かったやつを探す必要があるわけですが、現状の bundler だとどこが遅いのか、よくわかりません。
ところで MOGOK では bundle install の時間が表示されるのが地味に便利です*4

Bundler installing..
2013-09-08T14:26:02+09:00 console[app2002.22]:   $ bundle install --path=.bundle/gems --binstubs=.bundle/bin --without=test development
2013-09-08T14:26:02+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/...........
2013-09-08T14:26:09+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/..
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing rake (10.1.0)
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing i18n (0.6.5)
...
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing activeresource (3.2.14)
2013-09-08T14:26:16+09:00 console[app2002.22]: Using bundler (1.1.3)
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing json (1.8.0) with native extensions
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing libv8 (3.16.14.3)
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing rack-ssl (1.3.3)
...

これをローカルでも実施できるよう、 bundle install に時刻表示をつけてみます。なんとなくこんな感じで Gemfile にモンキーパッチを書いておけばそれっぽく表示できます。

class << Bundler.ui
  def tell_me (msg, color = nil, newline = nil)
    msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap]
    msg = "[#{Time.now}] " + msg if msg.length > 3
    if newline.nil?
      @shell.say(msg, color)
    else
      @shell.say(msg, color, newline)
    end
  end
end

source 'http://rubygems.org'
gem 'libv8', '~>3.11.8.17'
gem 'therubyracer'

表示がこんな具合↓になるので、ボトルネック探しがはかどりますね。

$ bundle install --path=.bundle/gems --binstubs=.bundle/bin
[2013-09-08 14:38:26 +0900] Fetching gem metadata from http://rubygems.org/..
[2013-09-08 14:38:27 +0900] Installing libv8 (3.11.8.17)
[2013-09-08 14:38:28 +0900] Installing ref (1.0.5)
[2013-09-08 14:38:46 +0900] Installing therubyracer (0.11.4)
[2013-09-08 14:38:46 +0900] Using bundler (1.4.0.pre.2)
[2013-09-08 14:38:46 +0900] Your bundle is complete!
[2013-09-08 14:38:46 +0900] It was installed into ./.bundle/gems

ただ、これは bundler の内部構造に依存したモンキーパッチなので、バージョンによっては使用できなかったり、最悪の場合は機能を壊してしまう可能性があります。これも自己責任。

以上。

これ全部やれば、人によっては bundle install の時間が 1/10 くらいに縮むかもしれません。
システムワイドにインストールするとか、複数のプロジェクトで同じ BUNDLE_PATH を利用する方法もありますが、それらはあまりお勧めできないかなーと思います。bundle install は --path 指定をし、プロジェクトごとに別々の gem を使うようにしましょー。

Have a happy bundle life!

*1:1.4.0.pre.1 〜

*2:Gemfile.lock も .bundle/config も残ってる

*3:pre.2 の段階では並列のオプションが保存されないとか、pre1 では :github の互換性が崩れていたとか、まだ十分に叩かれていないかも知れないとか、画面表示が変わるとか、いろいろ

*4:ステマ

Nginx Proxy の下で GrowthForecast を動かした話

GrowthForecast を動かしたい欲がさきほど突然現れたので、さくらのVPSUbuntuをセットアップしてGrowthForecastを設定してみた。
インストール方法はさておいて、設定するときに注意すべき点が幾つかあったので、それらについて列挙してく。

目的

127.0.0.1:5125 で立てた GrowthForecast を、同ホスト上に立てた Nginx でリバースプロキシし、 http://example.com/growthforecast という URL で見られるようにすること。
サブドメインを使いたくないようなケースね。

想定してるのは、外部に GrowthForecast のグラフを見せ、データの挿入は 127.0.0.1 以外から許さないような用途。

Nginx での基本的な Proxy 設定

とりあえず GET/HEAD だけ許容して 127.0.0.1:5125 に Proxy しとこう。

location /growthforecast {
  if ($request_method !~ ^(GET|HEAD)$) {
    return 403;
  }
  proxy_pass http://127.0.0.1:5125;
}

って思うじゃん。

ダメなんです

ところが、これだとこんな画面になって失敗する。

原因は、ページ内のすべてのリンクが http://127.0.0.1:5125/ を向いてしまっていること。実にロケンロー。
ただ、こんな問題を本家が認識していないはずもなく、 https://github.com/kazeburo/GrowthForecast/issues/3 には対処法が書かれている。

GrowthForecast で行うべき設定

GrowthForecast では、前段に proxy が挟まる場合にはその設定を行う必要がある。と言っても、起動オプションを一つ追加するだけだ。

 $ growthforecast.pl --front-proxy=127.0.0.1

こっちはこれで問題ない。

Nginx で行うべき設定

Nginx 側としては、次の二点を行うことになる。

  • パスの書き換え
  • ホスト名の書き換え
パスの書き換え

Nginx で特に設定を行わない場合、 http://example.com/growthforecast/ にアクセスすると、GrowthForecast 側には /growthforecast/ というパスが伝わる。ここは当然 / であることを期待しているわけなので、 Rewrite が必要となる。

  rewrite ^/growthforecast/(.+) /$1 break;
  rewrite ^/growthforecast / break;

書き換えルールくらい一行で書けそうなものだけど、なぜかうまくいかなかったので二行で無理やり解決。

ホストの通知

GrowthForecast では、HTML中にある各種リンクがすべてフルパスで指定されている。で、プロキシの裏側にいる場合は 127.0.0.1:5125 で立っているもんだから、ページ内のリンクはすべて http://127.0.0.1:5125/ に対するリンクになってしまう。
先ほどの --front-proxy オプションをつけるのは、これを解決させるためだ。
ここで指定されたアドレスからの接続であった場合、先ほどの 127.0.0.1:5125 の代わりに指定されたホスト名を使用するようになる。
つまり、 Nginx 側でも渡すホスト名を指定してやらねばならない。

  proxy_set_header Host $host/growthforecast;

この設定を入れることで、このアドレス問題が解決されるわけ。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
<link rel="stylesheet" href="http://example.com/growthforecast//css/bootstrap.min.css">
<style type='text/css'>
body {

そんな感じ。

何が起きているか?

Nginx からのメッセージを netcat とかで適当にキャプチャすればよくわかる。

$ nc -l 5125
GET /growthforecast HTTP/1.0
Host: 127.0.0.1:5125
...

適当に設定した Nginx では GrowthForecast に対してこういうメッセージが来ていたところ、

$ nc -l 5125
GET / HTTP/1.0
Host: example.com/growthforecast
...

こんな HTTP Request が来るようになるわけ。

まとめ

以上まとめると、次のような感じになる。

Nginx 設定
location /growthforecast {
  if ($request_method !~ ^(GET|HEAD)$) {
    return 403;
  }
  rewrite ^/growthforecast/(.+) /$1 break;
  rewrite ^/growthforecast / break;
  proxy_pass http://127.0.0.1:5125;
  proxy_set_header Host $host/growthforecast;
}
GrowthForecast 起動オプション
 $ growthforecast.pl --front-proxy=127.0.0.1


オッケー☆

知っていればなんてことのない話なのだけど、ブログにまとまっているところを見かけないので、ちょっと残してみることにした。

ただ今回の設定だと URL の途中にダブルスラッシュ入ってかっこわるい。リダイレクトルールの書き方とか、もっといい設定がありそう。

Ruby と exec と fd と

ruby で exec するときにソケットや fd のリークを起こさないために。主に自分用の調査結果のメモ。

結論

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

基本的な話

ruby の exec は基本的には POSIX execve のラッパ。execve 前後では多くのものが保持されないが、ファイルディスクリプタは基本的には残る。

デフォルトでは、ファイルディスクリプタは execve() を行った後でもオープンされたままである。 close-on-exec の印が付いているファイルディスクリプタはクローズされる。

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/execve.2.html

ということで、fork-exec や exec を行いたい場合は fd の扱いに気を付ける必要がある。
また、exec を行う時点で open しっぱなしになっている可能性のある fd に関しては、 close-on-exec のフラグを付けておけば execve 内で close してもらえる。
ruby 1.9.3 以前では、この close-on-exec は自分で設定する必要がある。

f = File.open("/tmp/hoge")
f.close_on_exec = true

close-on-exec は ruby 2.0.0 以降ではデフォルトになっている*1。そのため、 exec による予期せぬ fd リークは起こりにくくなっている。

困るシーン

他の人が使用するフレームワーク系ライブラリの中で exec を使用する場合、そのライブラリユーザが開いた fd やソケットが開きっぱなしになってしまう可能性がある。やばい。宇宙やばい。マルチスレッドならさらにこのような問題が起こりやすくなる。

ユーザの知恵はそんなものを乗り越えられるかというとそんなことはなくて、ならば今すぐ ruby 1.9.3 ユーザ全員の IO::open に close_on_exec = true を授けて見せろと言われてもそうだよそれはできないから、ruby 2.0.0 になってからそうさせてもらうと言うことになった。

では 1.9.3 ではどうすればよいかというと、 Kernel#exec (及び #spawn) に :close_others というオプションがあり、stdin/stdout/stderr 以外の fd をすべて調べて閉じてくれる仕組みがある。しかしこれは単に ruby 処理系にて C で書かれているだけなので、どうやら標準ではなさそうだ。少なくとも execve にはそのようなオプションはない。

f = File.open("/tmp/hoge")
exec("ls /proc/$$/fd", :close_others = true)

1.9.2 1.9.0 以前は close_others の実装がない(1.9.1 より実装された)。そのため、close_others オプションによる fd リーク防止は効果を持たない。close_othersはつけておいても特に害はない。

2.0.0 以降は、 fd ごとに close_on_exec が設定され、exec 時の close を保証してくれる。したがって、意図的に fd を残したい場合を除いては自動的に fd は exec 時に close されることとなり、 :close_others の設定は不要である。とはいえ、close_others を付けていてもそう問題は起きないだろうし、つけておいても問題はない。むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

そういうわけで結論(再掲)は

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

ということになった。まあ 1.9.0 以前って普通は 1.8.7 以前のことだけど。

補足

むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

とさっき書いた。「fd を勝手に閉じられたら困る、という話があるのでは??」と思うかもしれないが…、

本来、Kernel#exec 前後で開いたままにするべき fd は、exec (spawn) のオプションとして明示的に渡しているはずなので、それ以外の fd はすべて閉じるべき。

# たとえば fd 7 番を残したいのであれば
exec("ls -ls", 7 => 7, :close_others => true)

また、close_on_exec は IO::open 時のオプションに設定できないので、ruby 1.9.3 以前では open と close_on_exec 設定とがアトミックな処理にならない。つまり、その間に exec が発生する可能性があり、fd 漏れの起こりうる箇所になる。
その点 :close_others => true なら exec 時に確実に設定できるので、fd 漏れのタイミングは生まれない。多分。

ただ、IO#close_on_exec は POSIX 標準に対するインターフェイスである一方で、:close_others => true は ruby の独自実装。なので、現実的には :close_others を付けるのが正しそうなんだけど、あんまり美しくない感はあって悔しいかもしれない。

そんな感じ。