すがブロ

sugamasaoのhatenablogだよ

Rackの起動の流れを追う

Rackの動きについてコードを追ってみたよ

Rackのバージョンは1.4.1です。
Rackの動きの前に、まずはざっくりRackで動かすっていうのを確認してみよう。

rack up!

まずは適当にRackの設定ファイル兼クラスを定義する

require 'rack'
require 'pp'

class Sample
  def call(env)
    pp env
    [200, {"Content-Type" => "text/plain"}, ["rack up!\n"]]
  end
end

run Sample.new

これを rack up する

sugamasao@GRAM% rackup sample.ru
[2012-02-13 23:35:44] INFO  WEBrick 1.3.1
[2012-02-13 23:35:44] INFO  ruby 1.9.3 (2011-11-30) [x86_64-darwin10.8.0]
[2012-02-13 23:35:44] INFO  WEBrick::HTTPServer#start: pid=1660 port=9292

起動した。
アクセスしてみよう。

sugamasao@GRAM% curl http://localhost:9292
rack up!

文字列が返ってきた。callメソッドが受け取る引数となってるenvは何が出てるのかな?

{"GATEWAY_INTERFACE"=>"CGI/1.1",
 "PATH_INFO"=>"/",
 "QUERY_STRING"=>"",
 "REMOTE_ADDR"=>"127.0.0.1",
 "REMOTE_HOST"=>"localhost",
 "REQUEST_METHOD"=>"GET",
 "REQUEST_URI"=>"http://localhost:9292/",
 "SCRIPT_NAME"=>"",
 "SERVER_NAME"=>"localhost",
 "SERVER_PORT"=>"9292",
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2011-11-30)",
 "HTTP_USER_AGENT"=>
  "curl/7.21.2 (x86_64-apple-darwin10.5.0) libcurl/7.21.2 OpenSSL/1.0.0g zlib/1.2.5 libidn/1.22",
 "HTTP_HOST"=>"localhost:9292",
 "HTTP_ACCEPT"=>"*/*",
 "rack.version"=>[1, 1],
 "rack.input"=>
  #<Rack::Lint::InputWrapper:0x000001022bd178
   @input=#<StringIO:0x000001022b8f10>>,
 "rack.errors"=>
  #<Rack::Lint::ErrorWrapper:0x000001022bd100 @error=#<IO:<STDERR>>>,
 "rack.multithread"=>true,
 "rack.multiprocess"=>false,
 "rack.run_once"=>false,
 "rack.url_scheme"=>"http",
 "HTTP_VERSION"=>"HTTP/1.1",
 "REQUEST_PATH"=>"/"}
127.0.0.1 - - [13/Feb/2012 23:36:13] "GET / HTTP/1.1" 200 - 0.0051

なるほど、いわゆる環境変数が渡ってきているようだ。

それでは本題

この一連の流れはどのように動いているのか?

rackup コマンドから追っていこう

rackup ってコマンドを使ってるくらいだから、コマンドを探していくと良いに違いない。とりあえずrackのgemsの場所へ移動してみよう。どこかわからない人は gem envコマンドを使ってGEM PATH: のパスへ移動してみると良いよ。
gemsディレクトリ内にrackのgemのディレクトリがあるので移動する。当然binディレクトリがあるので中身を見てみよう。

#!/usr/bin/env ruby

require "rack"
Rack::Server.start

おお、これしかないのか。じゃあServer#startを見れば良いんだな?
serverは lib/rack/server.rb か。
開いてみると、イキナリ Server クラス内にさらに Option クラスが出てくる。OptParser使ってるくらいだから引数解析用の内部クラスなんだろう、と予想を立てて読み飛ばす。というか、rdefs*1したほうが読みやすそうな気がするな。

sugamasao@GRAM% rdefs lib/rack/server.rb
module Rack
  class Server
    class Options
      def parse!(args)
      def handler_opts(options)
    def self.start(options = nil)
    attr_writer :options
    def initialize(options = nil)
    def options
    def default_options
    def app
    def self.logging_middleware
    def self.middleware
    def middleware
    def start &blk
    def server
    private
      def parse_options(args)
      def opt_parser
      def build_app(app)
      def wrapped_app
      def daemonize_app
      def write_pid

なるほど。self.startが目的のメソッドのようだな。見に行ってみよう。

136     def self.start(options = nil)
137       new(options).start
138     end

ふーん、ここのnewってのは自身(Rack::Server)をインスタンス化してるんだな。つまり、Rack::Server.new(options).startってわけだ。そして、インスタンスのstartを呼んでいると。

ちょっとコンストラクタを見てみるか。

174     def initialize(options = nil)
175       @options = options
176       @app = options[:app] if options && options[:app]
177     end
178 
179     def options
180       @options ||= parse_options(ARGV)
181     end

ついでにコンストラクタ内で呼んでる options メソッドも抜粋した。parse_optionsってのもメソッドで、この中でOptionsクラスを使ったりしてARGVの解析やRACK_ENVの設定とかを行なっているようだ。
ということは、ここで引数のsample.ruを読んでいるのか?

とはいえ、Optionsクラスは普通にopt.onで引数のkeyとvalueの組み合わせがほとんどなんだよなぁ……っと、一番最後にここがあやしいな。もし、argsがparse!された後に残っていたら……つまりそれは-s ナンチャラみたいなキーワード付きの引数じゃなくて、単独の引数ってことになるから、つまりsample.ruということではないだろうか?(ここでprintデバッグしたらそのとおりだったw)

92         options[:config] = args.last if args.last                                      
93         options 

なるほど、引数で渡した sample.ru は options[:config] に入っているのだな。
さて、それではさっきはチラ見ですましておいた引数の処理を改めて見てみよう。options[:config]を絶対パスにしてるなぁ。こいつはいつ評価されるのだろうか?

273       def parse_options(args)
274         options = default_options
275 
276         # Don't evaluate CGI ISINDEX parameters.
277         # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
278         args.clear if ENV.include?("REQUEST_METHOD")
279 
280         options.merge! opt_parser.parse!(args)
281         options[:config] = ::File.expand_path(options[:config])
282         ENV["RACK_ENV"] = options[:environment]
283         options
284       end

ここでoptionの解析は終わりだし、インスタンスメソッドのstartの方に進んだほうが良さそうだ。

229     def start &blk
(省略)
249 
250       # Touch the wrapped app, so that the config.ru is loaded before
251       # daemonization (i.e. before chdir, etc).
252       wrapped_app
253 
254       daemonize_app if options[:daemonize]
255       write_pid if options[:pid]
256 
257       trap(:INT) do
258         if server.respond_to?(:shutdown)
259           server.shutdown
260         else
261           exit
262         end
263       end
264 
265       server.run wrapped_app, options, &blk
266     end

前半はdebugフラグがどうとかだから関係ないな。daemonとかpidとか、SIGNALの受け取ってる所とかもそんなに重要ではないだろう。そうすると、wrapped_appが怪しい。最後にserver.runでも使ってるし。
一体何をしてるんだってばよ?
っていうか、コメントに書いてあるけど、wrapped_appを呼んでおくことで daemonizeする前に config.ru (つまりきっと今回はsample.ruのことだろう)をloadしてるってことか?

300       def wrapped_app
301         @wrapped_app ||= build_app app
302       end

build_app メソッドを呼んでるらしい。そんで、引数にappを使ってるけどこのappもまたメソッドである。うごご。

194     def app
195       @app ||= begin
196         if !::File.exist? options[:config]
197           abort "configuration #{options[:config]} not found"
198         end
199 
200         app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
201         self.options.merge! options
202         app
203       end
204     end

options[:config]にsample.ruのパスが入ってるので、ようやくファイルのロードが行われるようだ。
lib/rack/builder.rbを開いてみよう。ちょっと疲れてきた。ここがparse_fileか。

 32     def self.parse_file(config, opts = Server::Options.new)
 33       options = {}
 34       if config =~ /\.ru$/
 35         cfgfile = ::File.read(config)
 36         if cfgfile[/^#\\(.*)/] && opts
 37           options = opts.parse! $1.split(/\s+/)
 38         end
 39         cfgfile.sub!(/^__END__\n.*\Z/m, '')
 40         app = eval "Rack::Builder.new {\n" + cfgfile + "\n}.to_app",
 41           TOPLEVEL_BINDING, config
 42       else
 43         require config
 44         app = Object.const_get(::File.basename(config, '.rb').capitalize)
 45       end
 46       return app, options
 47     end

おおまかに、拡張子が.ruであれば eval でRack::Builder.newの引数として食わしてappとするようだ。そうじゃなければ普通にrequireして、ファイル名からクラス名を推測してappとすると*2
心が折れそうだけど、がんばってRack::Builder.newを見てみようか。

 49     def initialize(default_app = nil,&block)
 50       @use, @map, @run = [], nil, default_app
 51       instance_eval(&block) if block_given?
 52     end

これ、このコードを見るまで分からなかったんだけど、evalで与えてる引数ってデフォルト引数になってるdefault_appの部分を省略してブロックだけ渡してるんだ。つまり、default_appはnilで、blockの方にsample.ruの中身が渡ってきてるってこと。とすると、さらに実際はinstance_evalが実行されているってことだ。instance_evalってのはinstance_evalメソッドのあるスコープでそのblockを実行するっていうニュアンスだと思っているので、つまりこのinitializeメソッド内でsample.ruの中身が実行されていると思えば良いだろう。
そうして、sample.ruを思い返すと、runメソッドで、クラスをnewしたインスタンスを渡しているわけだ。クラスをnewするのは普通なので置いておいて、runメソッドだ。このrunメソッドはinstance_evalから推測するにきっとこのRack:Builderクラスのrunメソッドを使っているのだ。

 99     def run(app)
100       @run = app
101     end

あった。これだ。つまり@runにはsample.rn内で定義したSample.newのインスタンスが入っているのだ!!
んで、ちょっともどって最初のevalに戻ると、newした後に to_app をしている。to_appとは?

127     def to_app
128       app = @map ? generate_map(@run, @map) : @run
129       fail "missing run or map statement" unless app
130       @use.reverse.inject(app) { |a,e| e[a] }
131     end

うーむ、とうやらルーティングとかの設定をし始めるのだろうか?まぁ初回のこのタイミングに限って言えば@mapはnilだし、@useは空の配列なので、injectの結果 app ……つまり @run が返るだけなんだけども。
ちょっと疲れてきたのでgenerate_mapとかは置いておく。さぁ、もう少しでゴールが見えてきそうだ。
さて、再度server.rbへ戻ってこよう。

194     def app
195       @app ||= begin
196         if !::File.exist? options[:config]
197           abort "configuration #{options[:config]} not found"
198         end
199 
200         app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
201         self.options.merge! options
202         app
203       end
204     end

こうしてみると、このappにはsample.ru内に書いてあるSampleクラスのインスタンスが入っているというのがわかる。
よしよし、さあ、続きを見よう。

290       def build_app(app)
291         middleware[options[:environment]].reverse_each do |middleware|
292           middleware = middleware.call(self) if middleware.respond_to?(:call)
293           next unless middleware
294           klass = middleware.shift
295           app = klass.new(app, *middleware)
296         end
297         app
298       end

build_appに行くと突然middlewareとか言うのがでてくる。これを追うと、インスタンスメソッドのmiddlewareからさらにクラスメソッドのmiddlewareに移る。

212     def self.middleware
213       @middleware ||= begin
214         m = Hash.new {|h,k| h[k] = []}
215         m["deployment"].concat [
216           [Rack::ContentLength],
217           [Rack::Chunked],
218           logging_middleware
219         ]
220         m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]]
221         m
222       end
223     end

ここでは、ContentLengthとか、予めcallメソッドを持ったRackミドルウェア群の中でデフォルトでRackが積んでくれるものを登録しているようだ。

build_appに戻ってみると、その積まれたミドルウェアをcallするか、newしていく。newしている場合(今回がそうだ)は、初回は引数を、二回目以降はそのnewしたオブジェクトをさらに引数として次のミドルウェアに載せて行っている。つまり、登録されたミドルウェアとアプリケーションをコンストラクタの引数でドンドン積み上げているというか、押し込んでいるのだ。
そうして最終的に作成されたミドルウェアがappとしてreturnされるのであった。

290       def build_app(app)
291         middleware[options[:environment]].reverse_each do |middleware|
292           middleware = middleware.call(self) if middleware.respond_to?(:call)
293           next unless middleware
294           klass = middleware.shift
295           app = klass.new(app, *middleware)
296         end
297         app
298       end

build_appで返ってきたオブジェクトはそのままwrapped_appに返る。これでようやく先にすすめるぞ。
で、startメソッドの最後の行に到達するのであった。

265       server.run wrapped_app, options, &blk

wrapped_appはさっきの通り、積み上がったミドルウェアだ。optionsは引数やらなので気にしなくて良いだろう。blkも今回のstartメソッドの呼び出しでは使っていないので無い。つまり、あとはserver.runを見れば答えがわかるはずである。
で、serverって何よ?

268     def server
269       @_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
270     end

Rack::Handlerらしいよ。
options[:server]は引数で指定できるみたいだけど、特に指定していないのでnilが入っている。ので、実際はRack::Handler.defaultが呼ばれているはずである。
lib/rack/handler.rb の default メソッドを見てみよう。

 29     def self.default(options = {})
 30       # Guess.
 31       if ENV.include?("PHP_FCGI_CHILDREN")
 32         # We already speak FastCGI
 33         options.delete :File
 34         options.delete :Port
 35     
 36         Rack::Handler::FastCGI
 37       elsif ENV.include?("REQUEST_METHOD")
 38         Rack::Handler::CGI
 39       else
 40         begin
 41           Rack::Handler::Thin
 42         rescue LoadError
 43           Rack::Handler::WEBrick
 44         end
 45       end
 46     end 

handlerを明示しない場合には環境に合わせて何を呼ぶか決めているわけだ。今回のサンプルではwebricが起動していたので、一番最後のrescueまで落ちた、ということなんだろう。
で、そのhandlerのrunメソッドとは?
lib/rack/handler/webrick.rb の run メソッドを見てみよう。

  8       def self.run(app, options={})
  9         options[:BindAddress] = options.delete(:Host) if options[:Host]
 10         @server = ::WEBrick::HTTPServer.new(options)
 11         @server.mount "/", Rack::Handler::WEBrick, app
 12         yield @server  if block_given?
 13         @server.start
 14       end

最終的にはwebrickのmountでURLをマッピングして、自身のクラス(Rack::Handler::WEBrick)と、ミドルウェア群を渡している、ということなのだな。
というわけでmountを調べると

第一引数がディレクトリ(この場合"/"だ)、第二引数がサーブレット、第三引数がオプションということらしい。
で、ドキュメントによると、第二引数であるサーブレットのオブジェクトを生成して、serviceメソッドをコールするらしい。そしてオブジェクトを生成する際に、引数として第三引数のオブジェクトを渡してくれるそうだ。
そうすると、実際にHTTPリクエストが来た時の流れはRack::Handler::WEBrickコンストラクタとserviceメソッドを見れば良いわけだ。
コンストラクタはsuperクラスを呼ぶのとoptionで渡ってきたミドルウェアオブジェクトを確保をしているだけ。
じゃあ、serviceメソッドは、というと

 33       def service(req, res)
(省略)
 59         status, headers, body = @app.call(env)
 60         begin
(省略)
 71           body.each { |part|
 72             res.body << part
 73           }
 74         ensure
 75           body.close  if body.respond_to? :close
 76         end
 77       end

というわけで、無事にRackミドルウェアのcallにたどり着いた。ミドルウェア間はひたすらcallで自分の上に乗っているミドルウェアを呼ぶので、最終的にはSampleクラスのインスタンスにたどり着くという寸法である。

これで長年の run Sample.newの謎がとけてすっきりした。
いやー、しかし長かった。。。

*1:インストールしていない人は gem install rdefsするんだ

*2:const_getとかを実際に使ってるコード初めてみたよ