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の謎がとけてすっきりした。
いやー、しかし長かった。。。