Project Tin Can とは

Project Tin Can は,SCORM 2004 の次の e-Learning を考える Rustici Software の研究プロジェクトです.Project Tin Can は ADL 公認のプロジェクトなので,実質的に SCORM 2004 の後継といってもいいでしょう.本エントリでは,この Project Tin Can の概要を解説します.

Project Tin Can が描く次の e-Learning の形

Project Tin Can では,SCORM 2004 の枠組みにとわわれずに,新しい e-Learning の形態を提案しています.具体的には,従来の WBT にみられる LMS を中心としたコンテンツ配信ではなく,LRS (Learning Record Store) と Web リソース,SNS, ゲーム,シミュレーションなど様々な学習活動との結びつきによる学習経験の提供です.

従来型の LMS ではコンテンツは LMS から配信されましたが,LRS はもはやコンテンツを管理しなくなりました.代わりに,外部の様々な学習活動から学習データ(ログ)を受けとり保管します.

Tin Can API

外部の学習活動が LRS に学習データを送るときに利用するのが Tin Can API です.現在は REST ベースの API 仕様が提案されてるので,それをもとに説明します.Tin Can API が送るメッセージは Activity-Verb-Object の三つ組だそうです.実際には Activity-Verb-Object-with-Result-in-Context 形式もあるので五つ組なのですが,Activity-Verb-Object の三つが強調されます.

このメッセージを JSON として HTTP(S) で LRS に送信します.たとえば,

{
    "actor" : {
      "mbox" : "passingloop@example.com",
      "name" : "passingloop"
    }
    "verb" : "completed",
    "object" : {
       "id" : "http://example.com/course/ruby-1.9.3",
       "definition" : "Introduction to Ruby 1.9.3"
    }
}

となります.

おわりに

LRS と Tin Can API により,コンテンツは LMS で配信する必要がなくなります.これで,LMS に教材をインポートする必要も,また,教材がインポートできる形式である必要もなくなります.すなわち,SNS や ゲーム,シミュレーションといった従来の LMS 型の e-Learning では提供できない学習経験を提供できるようになることを意味します.

Project Tin Can は研究プロジェクトで,今後 SCORM 2004 の次を担う e-Learning 標準に成長するかどうかは現時点では分かりません.しかし,このプロジェクトが成功し,LRS と Tin Can API が広く普及すれば,e-Learning の可能性はさらに広がることでしょう.

AppleScript で iPhone 用に動画を変換する作業を自動化する(ただし,面倒なところは Ruby に任せる)

最近,MacBook Pro にためてきた動画を iPhone でも見られるようにしたくなりました.しかし,iPhone で再生できるのは .m4v 形式なので,この形式に動画を変換する必要があります.幸い,Mac OS X に付属の QuickTime Player 7 を使えば iPhone 用 .m4v を作成できるので,ファイルを一つ一つ開いてマウスでカチカチやっていけば,いずれは全部 .m4v になります.しかし,この方法ではきっと 13 ファイル目くらいで心が折れるので,AppleScript を使って作業を自動化することにしました.

AppleScript とは

AppleScript とは Apple 製のスクリプト言語で,Mac のアプリケーションをスクリプトから操作するために利用されます.AppleScriptMac OS X の一部として提供されるので,面倒なインストールは不要です.AppleScript を使えば面倒な作業を自動化することができます.

AppleScript を勉強してみる

AppleScrip が何か分かったところで AppleScript を勉強してみます.

アプリケーションへのコマンドの送り方

AppleScript でアプリケーションを操作するには,アプリケーションに相当するオブジェクトにコマンドを送ります.アプリケーションにコマンドを送るには,

tell application アプリケーション名
    一つめのコマンド
    二つめのコマンド
    ...
end tell

と,tell 文を使います.iTunes で "My Top Rated" というプレイリストを再生して 5 秒後に停止するスクリプトを例に挙げると,

tell application "iTunes"
     play the playlist named "My Top Rated"
     delay 5
     stop
end tell

となります.

ファイルの扱い方

いくつかのコマンドは,ファイルを扱うときに絶対パス文字列ではなく「エイリアス」を利用します.絶対パス文字列からエイリアスを作るには,

set fooAlias to POSIX file "/path/to/file"

とします.

AppleScript の勉強をあきらめてみる

と,ここまで AppleScript の基本を勉強したところで,AppleScript の修得をやめます.AppleScript は単にコマンドを発行するだけではなく,制御構造をもったプログラミング言語なのです.しかし,私にとって制御構造を書くのなら Ruby のほうが楽なので,AppleScript はアプリケーション操作のコマンドを発行するためだけに使い,他の部分は Ruby で書きます.

osascript(1)

コマンドラインから AppleScript を実行するには osascript(1) を利用します.Ruby から AppleScript を実行する方法としては RubyCocoa を使う方法もあるのですが,簡単なのでこちらを使います.osascript には標準入力から AppleScript を渡します.

できたプログラム

以上の知識を利用してプログラムを書いてみました.例によって gist ではりつけます.

このプログラムを使うには,コマンドラインから,

$ /usr/bin/ruby movie_importer.rb foo.mpg bar.mp4 ... 

などとします.

おわりに

iPhone 用に動画を変換する作業を AppleScript で自動化しようとしましたが,途中で面倒くさくなって Ruby も使いました.本当は AppleScript を使わなくても RubyCocoa の QTKit 経由で Ruby だけもで同じことができるのですが,お手軽な方法で目的を達成したので,これで終わりとします.

参考

仮説思考とは穴埋め問題思考のことだと理解した

数年前に話題になっていた 仮説思考 BCG流 問題発見・解決の発想法 という本を今更ながらに読みました.この本では問題解決の方法として仮説思考を推奨しています.仮説思考とは,情報が少ない段階から全体像と結論を考える思考スタイルだと書かれていて難しかったのですが,よくよく考えてみると,言っていることは「穴埋め問題思考」だと理解できました.

穴埋め問題思考

「穴埋め問題思考」とは,どういう思考スタイルなのでしょう.ここでは,

財政を圧迫する国民医療費を圧縮するには,どうすればよいのか

という問題例を使って考えることにします.

この問題を解決するためには,まず,国民医療費の支出構造を分析して,...とまず分析から入るのが普通の方法なのですが,穴埋め問題思考ではこうします.

答:
たばこ税を増税すれば喫煙人口が減り,肺ガンや心臓病,脳卒中患者が減るので(= 全体像),
たばこ税を増税して一箱○○円にすれば,税収が△△円減るものの医療費を××円削減できる,結果として財政収支をを□□円改善できる.(= 結論)

と,このような穴埋め問題をつくります.あとは,正解を見つけるために調査するだけです.たばこの価格と税収・医療費との関係をグラフにして収支を一番改善できる価格を求めるだけです.

ここで重要なのは,

  1. 穴埋め問題思考で最初につくる仮説は間違っていてもいい
  2. 穴埋め問題思考で最初につくる仮説は最善のものでなくてもいい
  3. ただし,他に有力な対立仮説がないか検討してみる

ということです.記述式の問題は苦手でも穴埋め問題は得意という人は多いでしょうから,この方法,やってみる価値はあると思います.

しかし,穴埋め問題思考で一番難しいのが「問題をつくる」ところです.はじめに紹介した 仮説思考 BCG流 問題発見・解決の発想法 では,コンサルタントがこの方法で問題解決をしていて,新米コンサルタントは社内のテンプレートを使ったり経験豊富な先輩コンサルタントに指導してもらいながら「問題をつくる」スキルを身につけていくそうです.一般人にはテンプレートはありませんが,先輩コンサルタントの指導の代わりに周りの人との議論は可能です.毎日が問題解決の訓練の場であるコンサルティング会社の環境はうらやましいですが,私も周りの人に協力してもらって問題解決のスキルを伸ばしていきたいと思います.

ソルトもストレッチングも両方必要

ある条件のもとでパスワードを守るには、ソルトもストレッチングも両方必要です.本当は怖いパスワードの話 (1/4):ハッシュとソルト、ストレッチングを正しく理解する - @IT がその理由の良い解説になっているのですが,4 ページに分かれていて長いので Q&A 形式でまとめてみました.
ここからの話の前提として,システムに侵入され root 権限を取られたときのことを考えています.

ソルトとストレッチングが必要な理由

Q. パスワードを暗号化でなくハッシュで保存するのはなぜか?

A. 暗号化されたパスワードは復号鍵で容易に復元できるから

パスワードを暗号化しただけでは,「root 権限の奪取」即「パスワードの流出」となってしまいます.技術的な注意を怠っていたとか過失があるとか非難されても仕方がないレベルです.

Q. 単純なハッシュではなくソルトやストレッチングを使うのはなぜか?

A. ユーザが「弱い」パスワードを使うから

ユーザが「強い」パスワード,たとえば,128 文字以上の大文字小文字数字記号が適度に混ざった無意味な文字列を確実に使ってくれるのであれば,実はソルトもストレッチングも不要です.一方,ユーザが弱いパスワードを使っている場合,パスワードをハッシュで保存しても「レインボーテーブル」や「総当たり」などの攻撃で元のパスワードを復元できてしまいます.

Q. ソルトは何のために使うのか?

A. 弱いパスワードを強くするため.

いくらパスワードをハッシュしても,パスワードが短かければ「レインボーテーブル」などを使って容易に復元できてしまいます.また,いくらパスワードが長くても辞書に載っているような単語を使っていると「辞書攻撃」で容易に復元できてしまします.このような「弱い」パスワードにソルトを連結してからハッシュすることで,「強い」パスワードと同等の強度を得ます.

Q. ストレッチングは何のために使うのか?

A. 総当たり攻撃に耐える時間を稼ぐため

いくら強いパスワードを使っても,総当たり攻撃でいつかはパスワードを復元されてしまいます.最近の CPU/GPU 性能向上により,総当たり攻撃は現実的な時間で完了する可能性があります.高速に計算できるハッシュ関数を使っていると総当たり攻撃にかかる時間が短かくなるので,計算に時間のかかるハッシュ関数を使って攻撃者に嫌がらせをしようというのがストレッチングの考え方です.うまくいくと,パスワードが復元される前に攻撃者の寿命が尽きます.すなわち,総当たり攻撃が現実的な時間で実行できなくなるということです.

おわりに

この話をまとめると,

  • root 権限取られたときに
  • 暗号化だとパスワードが流出するからハッシュする.
  • ユーザが「弱い」パスワードを使うから
    • ソルトを使って「強く」して
    • ストレッチングで総当たり攻撃に耐える時間を稼ぐ

となります.元記事の,

攻撃者によってすべての情報が盗まれるという前提に立つと、パスワードを安全に守る現状のベストプラクティスは「ソルト+ハッシュ+ストレッチング」です。

本当は怖いパスワードの話 (3/4):ハッシュとソルト、ストレッチングを正しく理解する - @IT

はその通りだと思います.

これを聞いてもソルト+ストレッチングを採用できない人は,きっと,パスワードを安全に守れない人なので別のベストプラクティスを採用しましょう.すなわち「パスワードを保存するのをあきらめて,OAuth などで外部の ID サービスを利用する」です.

Missing Parts

元記事では,

  • ソルトの長さはどれくらいが適当か
  • ストレッチングの回数はどれくらいが適当か(あるいは,key derivation 関数の構成法)

は触れられていません(例はある).これらについて,全ての環境で「これでいい」という数値はなく,状況に合わせて総合的に必要があると考えているからだと推測します.もちろん,基準とか考え方とかを提示することはできるのでしょうが,そこはきっとコンサルタントの飯の種なんだといます.

7つの言語7つの世界 第6章 Erlang セルフスタディやってみた

7つの言語 7つの世界 第6章 Erlang のセルフスタディやってみました。私は Erlang 未経験者だったので最適解ではありませんが、私の解答例を書いておきます。

1日目

探してみよう

Erlang のオフィシャルサイト

Erlang Programming Language

Erlang の関数ライブラリの正式ドキュメント

Erlang -- Erlang Reference Manual

Erlang の OTP ライブラリのドキュメント

Erlang/OTP 22.0

試してみよう

再帰を用いて文字列を構成する単語の数を返す関数を書け.

単語はスペース区切りとし、スペースの数を数えて最後に + 1 したものを出力する関数 word_count/1 を書きます。例によって、コードは gist で貼りつけます。

Emacs から M-x erlang-shell で実行してみました。

Erlang R14B03 (erts-5.8.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.4  (abort with ^G)
1> c(word_count).
./word_count.erl:5: Warning: variable 'Anything' is unused
{ok,word_count}
2> word_count:word_count("Erlang").
1
3> word_count:word_count("Hello Erlang.").
2
4> 

再帰を用いて 10 まで数える関数を書け.

実行結果:

Erlang R14B03 (erts-5.8.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.4  (abort with ^G)
1> c(count).
{ok,count}
2> count:count(10).
10
3> 

ちゃんと 10 まで数えているはずです。

{error, Message}または success という形式の入力が与えられたとき,マッチングを用いて,“success” または “error: message”のどちらかを出力する関数を 書け.

実行結果:

Erlang R14B03 (erts-5.8.4) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.4  (abort with ^G)
1> c(judge).
{ok,judge}
2> judge:judge(success).
"success"
3> judge:judge({error, "I like Java."}).
"error: I like Java."
4> 

2日目

試してみよう

[{erlang, "a functional language"}, {ruby, "an OO language"}] の ようなキーワード値タプルのリストを考える.リストとキーワードを受け取り,そ のキーワードに対応する値を返す関数を書け.

[{item quantity price}, ...] という形式の買い物リストを考える.このと き,[{item total_price}, ...] という形式の items のリストを構築するリス ト内包表記を書け.ただし,total_price は,quantity×price とする.

Eshell V5.8.4  (abort with ^G)
1> List = [ { sugar, 3, 2.75 }, { solt, 1, 0.75 }, { egg, 24, 0.10} ].
[{sugar,3,2.75},{solt,1,0.75},{egg,24,0.1}]
2> [ { Item, Quantity * Price } || { Item, Quantity, Price } <- List ].
[{sugar,8.25},{solt,0.75},{egg,2.4000000000000004}]
3> 

長さ 9 のリストまたはタプルとして表現された三目並べのボードを読み込むプロ グラムを書け.勝者が確定している場合はその勝者(x または o のどちらか)を, これ以上指し手がない場合は cat を,勝負がついていない場合は no_winner を返 すようにせよ.

ボードを長さ 9 のリストで、空いているマスを u で表現します。x か o が一列に並んでいれば勝者を、そうでなれば空いているマスがあるかどうかを調べて cat か no_winner を返します。

実行例

Eshell V5.8.4  (abort with ^G)
1> c(ticktacktoe).
{ok,ticktacktoe}
2> ticktacktoe:board([x, x, u, u, x, o, u, o, u]).
cat
3> ticktacktoe:board([x, x, o, o, o, x, x, o, o]).
no_winner
4> ticktacktoe:board([x, x, o, u, x, o, u, x, u]).
x
5> 

3日目

3日目のセルフスタディをやる前に プログラミングErlang を読みました。この本を調べれば簡単です。

探してみよう

プロセスが死んだときに,そのプロセスを再起動する OTP サービス

supervisor を使えそうです → http://erlang.shibu.jp/design_principles/supervisor.html

簡単な OTP サーバを構築するためのドキュメント

OTPデザイン原則 — Erlang User's Guide v5.8.1 documentation が参考になります。

試してみよう

translate_service を監視し,死んだら再起動するようにせよ.

OTP の supervisor と gen_server を使います。オリジナルの translate_service.erl は gen_server を使って translator_server.erl として書き直しました。supervisor を使って translate_supervisor.erl を書きました。

実行例

Eshell V5.8.4  (abort with ^G)
1> c(translate_server).
{ok,translate_server}
2> c(translate_supervisor).
{ok,translate_supervisor}
3> translate_server:translate("casa").
** exception exit: {noproc,
                       {gen_server,call,
                           [translate_server,{trans,"casa"},1000]}}
     in function  gen_server:call/3
4> translate_supervisor:start().
translate_server starting
true
5> translate_server:translate("casa").
"white"
6> translate_server:translate("blanca").
"house"
7> translate_server:translate(undefined).
translate_server stopping
translate_server starting

=ERROR REPORT==== 6-Oct-2011::15:00:43 ===
** Generic server translate_server terminating 
** Last message in was {trans,undefined}
** When Server state == 2
** Reason for termination == 
** {{case_clause,undefined},
    [{translate_server,lookup,1},
     {translate_server,handle_call,3},
     {gen_server,handle_msg,5},
     {proc_lib,init_p_do_apply,3}]}
** exception exit: {{{case_clause,undefined},
                     [{translate_server,lookup,1},
                      {translate_server,handle_call,3},
                      {gen_server,handle_msg,5},
                      {proc_lib,init_p_do_apply,3}]},
                    {gen_server,call,
                                [translate_server,{trans,undefined},1000]}}
     in function  gen_server:call/3
8> translate_server:translate("casa").
"white"
9> translate_server:translate("blanca").
"house"
10> 

Doctor プロセスが死んだとき,自分自身で再起動するようにしてみよ.

doctor.erl を immortal_doctor.erl として書き直しました。immortal_doctor は poisoning で死んでも再起動します。

実行例

Eshell V5.8.4  (abort with ^G)
1> c(roulette).
{ok,roulette}
2> c(immortal_doctor).
{ok,immortal_doctor}
3> Doc = spawn(fun immortal_doctor:loop/0).
<0.43.0>
4> Doc ! new.
Creating and monitoring process.
new
5> revolver ! 1.
click
1
6> revolver ! 2.
click
2
7> revolver ! 3.
bang.
3
The shooter <0.45.0> died with reason {roulette,die,at,{15,2,47}}. Restarting. 
Creating and monitoring process.
8> revolver ! 1.
click
1
9> exit(Doc, poisoning).
<0.31.0> poisoned the immortal with reason poisoning.true
 Restarting. 
Creating and monitoring process.
10> revolver ! 1.
click
1
11> 
ボーナス問題

メッセージのログをファイルに記録する基本的な OTP サーバを作成せよ.

OTP に SASL という便利な仕組みがあるので、それを使います。試してみようで作った translate_server.erl と translate_supervisor.erl を SASL でエラーログを吐くように修正します。といっても io:format を error_logger:error_msg に変更しただけです。

さらに、SASL の設定ファイル elog.config を用意します。

あとは、Erlang を起動するときにブート引数で SASL を指定します。

% erl -boot start_sasl -config elog

これで、ログが /var/tmp/elog に書き出されます。

7つの言語7つの世界 第5章 Scala セルフスタディやってみた

7つの言語 7つの世界 の第5章 Scala の宿題をやってみました。私が Scala に触るのはこれが初めてです。

インストール

id:passingloopMacPorts から Homebrew にのりかえました

% brew install scala

2.9.1 がイストールされました。

1日目

探してみよう

ScalaAPI ドキュメント

Scala Standard Library 2.12.8

JavaScala の比較

他の言語との比較もできる http://wota.jp/ac/?date=20100426#p01

var と val を比較した説明

いろいろありますが 関数型Scala(4):クロージャ - Mario Gleichmann - Digital Romanticism がおすすめです。

試してみよう

X,O,および空白文字で埋まった三目並べボードを見て,勝者,引き分け,勝者 未定のいずれかの判定を下すゲームを書け.必要ならクラスを使うこと.

ホームページ移転のお知らせ - Yahoo!ジオシティーズ の解法を参考にしてみました。元の C 言語プログラミングをそのまま Scala で書いたので、Scala っぽいプログラムではなくなったのが残念です。例によって gist を貼り付けます。

盤面を

0 1 2
3 4 5
6 7 8

の一次元配列 List[Char] で表現しています。実行結果は、

% scala tictactoe.scala 
O

で O の勝ちです。

2日目

探してみよう

Scala ファイルの使い方の説明

Google 先生に聞いても、これといった日本語の説明が見つからなかったので、図書館にあった本(で貸し出しされていなかったもの)を 2 冊借りてきて探してみました。

.scala ファイルはスクリプトでもコンパイルしても使える。スクリプトとして使うのなら、

$ scala Foo.scala

で、コンパイルするなら、

$ scalac Foo.scala
$ scala Foo

で実行できる。

クロージャはコードブロックとどこが違うのか

これは Google 先生に教えてもらったものを引用して答えとします。

クロージャのカッコいいところ、其の壱。クロージャはコードブロックである。かつ、使われる環境と結合できること。これが正式なクロージャの形であり、関数ポインタやら内部クラスやらその他もろもろのクロージャっぽいものと一線を画す機能です。

http://capsctrl.que.jp/kdmsnr/wiki/bliki/?Closure
試してみよう

2 日目のセルフスタディから var 禁止になりました (p.127)

foldLeft を用いて,文字列リストの合計長を計算せよ.

val words = List("lions", "tigers", "bears")
println(words.foldLeft(0)((sum, word) => sum + word.length))

実行結果は、

% scala total_length.scala 
16

不敬語である Shoot と Darn を Pucky と Beans で置き換えるメソッドを持つ Censor トレイトを書け.マップを用いて不敬語とその代替語のペアを格納すること.

文書 document を List[String] で表現するとして、

getOrElse を使ってシンプルに書けました。

不敬語とその代替語をファイルから読み込むようにしてみよ.

各行がスペース区切りで不敬語と代替語のペアになっているファイルを dictionary.txt という名前で用意して、

この dictionary.txt ファイルを censor するときに読みこむことにします。

関数 dictionary の戻り型 List[String,String] を指定しないとエラーになりました。Scala型推論できなかったようです。Scala 静的型付けなので、推論できない型は指定する必要があるようです。

3 日目

ページサイズ計算プログラムで,リンク先ごとに新しいアクターを作成しなかった場合,アプリケーションのパフォーマンスはどうなるだろうか.

ページサイズ計算アプリケーションに,ページ内のリンク数を数えるメッセージ を追加してみよ.

2 つまとめてやってみました。本文で紹介されている code/scala/sizer.scala へのパッチを gist で貼り付けます。

実行してみると、

% scala sizer.scala
Sequential run:
Size for http://www.amazon.com/: (103007,168)
Size for http://www.twitter.com/: (46019,20)
Size for http://www.google.com/: (11689,19)
Size for http://www.cnn.com/: (86324,194)
Method took 4.871293 seconds.
Concurrent run
Size for http://www.google.com/: (11686,19)
Size for http://www.amazon.com/: (105275,165)
Size for http://www.twitter.com/: (46021,20)
Size for http://www.cnn.com/: (86324,194)
Method took 1.82726 seconds.
Single actor run
Size for http://www.amazon.com/: (103046,169)
Size for http://www.twitter.com/: (46020,20)
Size for http://www.google.com/: (11674,19)
Size for http://www.cnn.com/: (86322,194)
Method took 3.628382 seconds.

Sequential run より Single actor run のほうが早いという結果になりました。

Pow をインストールするとシステムに何をされるのか確認する

Pow37signals 謹製の Rack サーバです。Rails 開発で愛用している人も多いのではないでしょうか。Pow をインストールすると、

  • (1) http://my_app_name.dev/ にアクセスすると
  • (2) Pow が Rack アプリケーションを自動的に起動して
  • (3) ~/.pow/my_app_name にある Rack アプリケーションを利用できる

のでとても便利です。しかも、Pow のインストールは

$ curl get.pow.cx | sh

だけで完結するので簡単です。しかし、魔法のような (1)-(3) を見ていると、システムに何か複雑な操作をしているのではないかと不安になります。そこで、インストールスクリプトを順に追って、Pow をインストールするとシステムに何をされるのか確認することにします。この記事を書いている時点のインストールスクリプトget.pow.cx · GitHub に保存しています。以下、このインストールスクリプトに沿って解説をすすめていきます。

インストールスクリプトは何をするか


それでは、順に見ていくことにしましょう。

1-37 行目

まず、目につくのが Pow のロゴ AA です(右図)。これがほとんどの人に気づかれないのはもったいないと思います。このロゴがシステムに何かするわけではありません。しかし、こういう見えないところで頑張るのは私の好みなので紹介した次第です。

41-59行目

Pow は Max OS X 10.6 以降でないと動作しないので、実行環境を確認しています。まだシステムには何もしていません。ここで、Pow のインストール先ディレクトリと Pow 本体のパスを変数に設定しています。

  • POW_ROOT="$HOME/Library/Application Support/Pow"
  • POW_BIN="$POW_ROOT/Current/bin/pow"
64行目: インストールディレクトリの作成
mkdir -p "$POW_ROOT/Hosts" "$POW_ROOT/Versions"

インストール先ディレクトリとして、$POW_ROOT/Hosts と $POW_ROOT/Versions を作成しています。$POW_ROOT="$HOME/Library/Application Support/Pow" であることは先に説明しました。

69-71行目: 既にインストールしてある Pow を削除
cd "$POW_ROOT/Versions"
rm -rf "$POW_ROOT/Versions/$VERSION"

以降の作業は $POW_ROOT/Versions で行われます。

75行目: Pow 本体をダウンロード
curl -s http://get.pow.cx/versions/$VERSION.tar.gz | tar xzf -

$POW_ROOT/Versions に展開されます。

80-82行目: ダウンロードしたバージョンを Current としてシンボリックリンクを作成
cd "$POW_ROOT"
rm -f Current
ln -s Versions/$VERSION Current

最新のバージョンに Current という名前にシンボリックリンクを張るのは Mac OS X の流儀ですね。といっても、私はこの流儀が書かれているドキュメントを見たことがありません。もし知っている人がいたら誰か教えてください。

87-88行目: ホームディレクトリに $POW_ROOT/Hosts へのシンボリックリンクを ~/.pow として作成
cd "$HOME"
[[ -a .pow ]] || ln -s "$POW_ROOT/Hosts" .pow
93行目: pow 起動スクリプトの設定
echo "*** Installing local configuration files..."
"$POW_BIN" --install-local

pow --install-local を実行して、pow 起動スクリプトを設定しています。pow --install-local が何をするかは pow/command.js at master · basecamp/pow · GitHub から installer.coffee を追いかけると分かります。簡単に言うと、/Users/passoingloop/Library/LaunchAgents/cx.pow.powd.plist *1 を作成して、ユーザのログイン時に

$SHELL --login -c "'/Users/passingloop/Library/Application Support/Pow/Versions/0.3.2/bin/pow'"

が実行されるようにしているだけです。

99行目: root 権限が必要かチェック
"$POW_BIN" --install-system --dry-run >/dev/null && NEEDS_ROOT=0 || NEEDS_ROOT=1

root 権限が必要なら NEEDS_ROOT=1, 不要なら NEEDS_ROOT=0 を設定します。root 権限が必要かどうかをチェックするこのテクニックは、他のインストーラを作成するときにも使えそうです。

104-108行目: ポートフォワーディング設定
if [ $NEEDS_ROOT -eq 1 ]; then
  echo "*** Installing system configuration files as root..."
  sudo "$POW_BIN" --install-system
  sudo launchctl load -Fw /Library/LaunchDaemons/cx.pow.firewall.plist 2>/dev/null
fi

を実行して、起動時に 80 番ポートへのアクセスを 20559 番に転送するよう設定します。pow --install-system が何をするかは、--install-local と同じように pow/command.js at master · basecamp/pow · GitHub から installer.coffee を追いかけると分かります。簡単に言うと、/etc/resolver/dev と /Library/LaunchDaemons/cx.pow.firewall.plist を作成して、後者を sudo launchctl load -Fw します。sudo launchctl load -Fw ... は起動時に PID=1 の launchd から cx.pow.firewall.plist を実行する設定です。cx.pow.firewall.plist には実際に launchd が実行するコマンド、

ipfw add fwd 127.0.0.1,20559 tcp from any to me dst-port 80 in && sysctl -w net.inet.ip.forwarding=1

が書かれています。/etc/resolver/dev には *.dev の名前解決をローカルホストの 20560 番ポートで動いている DNS サーバで解決する設定が書かれています。

113-115行目: Pow の起動
echo "*** Starting the Pow server..."
launchctl unload "$HOME/Library/LaunchAgents/cx.pow.powd.plist" 2>/dev/null || true
launchctl load -Fw "$HOME/Library/LaunchAgents/cx.pow.powd.plist" 2>/dev/null

インストールした Pow をいよいよ起動します。既に起動している Pow があれば先に停止します。

以降

Pow が正しく起動したかどうかの確認です。

インストールスクリプトがシステムに何をしたか

これまで確認したことをまとめると、インストールスクリプトは、

  • インストールディレクトリを作成する
    • $HOME/Library/Application Support/Pow
  • インストールディレクトリに Pow 本体をダウンロードして展開
  • ホームディレクトリにシンボリックリンク ~/.pow を作成
    • 参照先は $HOME/Library/Application Support/Pow/Hosts
  • /Users/passoingloop/Library/LaunchAgents/cx.pow.powd.plist を作成
    • このファイルはログイン時に読み込まれ実行される → ログイン時に Pow が起動
  • /etc/resolver/dev を作成
    • *.dev なホスト名はローカルホストの 20560 番ポートで解決
  • /Library/LaunchDaemon/cx.pow.firefall.plist を作成
  • launchctl を実行して ↑ をロード。起動時に実行されるようにする
    • 起動時に 80 番ポートへのアクセスを 20559 番ポートに転送する設定
  • Pow を起動

となります。

書き出してみると、Pow の魔法の秘密に近づけるような気がします。つまり、Pow は 20559 番ポートで HTTP サーバとして、20560 番ポートで DNS サーバとして起動します。my_app_name.dev という ホスト名は Pow DNS サーバで 127.0.0.1 に名前解決されます。ローカルホストの 80 番ポートへのアクセスは、インストールスクリプトが設定したポートフォワーディングにより 20559 番ポートへと転送され、Pow HTTP サーバがリクエストを受け取ります。あとは、バーチャルホストを使って Pow が各 Rack アプリケーションにリクエストを振り分けるだけです。このとき、Rack サーバが起動していなければ、Pow が起動してくれるというわけです。

おわりに

本エントリでは、Pow をインストールするといシステムにどのような影響があるかを、インストールスクリプトを順に追って確認してみました。また、確認することで Pow の魔法の一端を垣間見ることができました。Pow をイストールしてもシステムに害は無いようなので、どんどん curl get.pow.cx | sh しましょう。

ただし、私は一点だけ気になるところがありました。それはポートフォワーディングのため実行する ipfw コマンドで、from any となっている点です。FW を適切に運用している環境では問題ないのでしょうが、私の場合、そういう環境ばかりというわけにはいかず気持ち悪いので、from any を from me と変更したスクリプトを使っていることを最後につけ加えます。

*1:passingloop は各自のユーザ名で読み換えてください