Rackのuseとrunを紐解く

require "rack"

# rack middleware
class HogeMidWare
  def initialize(app)
    puts "init hoge"
    @app = app
  end

  def call(env)
    puts "hoge"
    @app.call(env)
  end
end

# rack middleware
class FooMidWare
  def initialize(app)
    puts "init foo"
    @app = app
  end

  def call(env)
    puts "foo"
    @app.call(env)
  end
end

# rack application
class HomApp
  def call(env)
    puts "hom"
    [200, {"Content-Type" => "text/plain"}, ["hom"]]
  end
end


use HogeMidWare
use FooMidWare

run HomApp.new

このようなconfig.ruがあったときにrackupコマンドを打つと、"init foo"、"init hoge"の順番に出力され、 curl http://localhost:9292/とかでアクセスすると、"hoge"、"foo"、"hom"の順番に出力される。

つまり上のコードの最後のuseとrunの部分は実質的に

run HogeMidWare.new( FooMidWare.new( HomApp.new ) )

と書いたのと同じである。
Rackの使い方についてはRack解説 - Rackの構造とRack DSLがわかりやすい。
ではどういう仕組みでこうなっているのかコードを見ていこう。(ちなみに今回読んだRackのバージョンは1.5)

まず始めに、rackupコマンドを見てみる。

#!/usr/bin/env ruby

require "rack"
Rack::Server.start

すると、こんな感じで、Rack::Serverの特異メソッド(クラスメソッド)startを呼び出していることがわかる。 今回はこのstartを起点に一連の処理の流れを見ていく。

ちなみに、メソッド呼び出しの流れはだいたい以下のようになっている。

Rack::Server::start@server.rb:146
  Rack::Server#initialize@server.rb:184
  Rack::Server#start@server.rb:247
    Rack::Server#options@server.rb:189
      Rack::Server#parse_options@server.rb:308
        Rack::Server#default_options@server.rb:193
    Rack::Server#wraped_app@server.rb:337
      Rack::Server#app@server.rb:207
        Rack::Server#build_app_and_options_from_config@server.rb:294
          Rack::Server#opt_parser@server.rb:323
          Rack::Builder::parse_file@builder.rb:32
            Rack::Builder::new_from_string@builder.rb:48
              Rack::Builder#initialize@builder.rb:53
                Rack::Builder#use@builder.rb:81
                Rack::Builder#run@builder.rb:103
              Rack::Builder#to_app@builder.rb:144
      Rack::Server#build_app@server.rb:326
        Rack::Server#middleware@servre.rb:243
          Rack::Server::middleware@server.rb:238
            Rack::Server::default_middleware_by_environment@server.rb:218
              Rack::Server::logging_middleware@server.rb:212
    Rack::Server#server@server.rb:289
      Rack::Handler::default@handler.rb:46
        Rack::Handler::pick@handler.rb:34
          Rack::Handler::get@handler.rb:11
    Rack::Handler::WEBrick::run@handler/webrick.rb:25

順番に見ていこう。
まずは、Rack::Server::start。
これはserver.rb(lib/rack/server.rb)の146行目にある。

    def self.start(options = nil)
      new(options).start
    end

そこで、Rack::Server#initializeを見てみよう。 これは、server.rbの184行目にある。

    def initialize(options = nil)
      @options = options
      @app = options[:app] if options && options[:app]
    end
  • インスタンス変数にオプションを設定している何の変哲もないコード。

続いて、Rack::Server#startはservre.rbの247行目。

    def start &blk
      if options[:warn]
        $-w = true
      end

      if includes = options[:include]
        $LOAD_PATH.unshift(*includes)
      end

      if library = options[:require]
        require library
      end

      if options[:debug]
        $DEBUG = true
        require 'pp'
        p options[:server]
        pp wrapped_app
        pp app
      end

      check_pid! if options[:pid]

      # Touch the wrapped app, so that the config.ru is loaded before
      # daemonization (i.e. before chdir, etc).
      wrapped_app

      daemonize_app if options[:daemonize]

      write_pid if options[:pid]

      trap(:INT) do
        if server.respond_to?(:shutdown)
          server.shutdown
        else
          exit
        end
      end
  • 割りと色々やってるように見えるメソッドだが、大事なのはoptionsメソッド(これメソッドですよ!)と途中のコメントの下に出てくるwrapped_appメソッド、そしてシグナルハンドラを設定している辺りで出てくるserverメソッド

この3つのメソッドを見ていこう。(wrapped_appがとても長い。)

Rack::Server#optionsはserver.rbの189行目。

    def options
      @options ||= parse_options(ARGV)
    end

Rack::Server#parse_optionsはserver.rbの308行目。

      def parse_options(args)
        options = default_options

        # Don't evaluate CGI ISINDEX parameters.
        # http://www.meb.uni-bonn.de/docs/cgi/cl.html
        args.clear if ENV.include?(REQUEST_METHOD)

        options.merge! opt_parser.parse!(args)
        options[:config] = ::File.expand_path(options[:config])
        ENV["RACK_ENV"] = options[:environment]
        options
      end
  • なんや色々やっててごちゃごちゃしてるけど、結局はoptionsをいじくりまわしてるだけ。

Rack::Server#default_optionsだけ見ておこう。
これはserver.rbの189行目。

    def default_options
      environment  = ENV['RACK_ENV'] || 'development'
      default_host = environment == 'development' ? 'localhost' : '0.0.0.0'

      {
        :environment => environment,
        :pid         => nil,
        :Port        => 9292,
        :Host        => default_host,
        :AccessLog   => [],
        :config      => "config.ru"
      }
    end
  • 引数なしで起動した場合のデフォルト値がこれ。

次はRack::Server#wrapped_appを見ていく。
これはserver.rbの337行目。

      def wrapped_app
        @wrapped_app ||= build_app app
      end
  • 単純に見えるが、右辺で2つのメソッドbuild_appとappを呼び出してる。

ココらへんからちょっとややこしくなってくる。

まずはRack::Server#appから見ていこう。
場所はserver.rbの207行目。

    def app
      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
    end
  • 他のメソッドに丸投げしている。今回はoptions[:builder]がnilだとして、build_app_and_options_from_configを見ていこう。

Rack::Server#build_app_and_options_from_configはserver.rbの294行目。

      def build_app_and_options_from_config
        if !::File.exist? options[:config]
          abort "configuration #{options[:config]} not found"
        end

        app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
        self.options.merge! options
        app
      end
  • options[:config]にはRackの設定ファイルのファイル名が入っている。それがなかった場合エラーで終了する。
  • あった場合、Rack::Builderのparse_fileメソッドにファイル名を渡している。
  • opt_parserはあんまり気にしなくてもいい。

Rack::Builder::parse_fileを見ていこう。
これはbuilder.rbの32行目にある。

    def self.parse_file(config, opts = Server::Options.new)
      options = {}
      if config =~ /\.ru$/
        cfgfile = ::File.read(config)
        if cfgfile[/^#\\(.*)/] && opts
          options = opts.parse! $1.split(/\s+/)
        end
        cfgfile.sub!(/^__END__\n.*\Z/m, '')
        app = new_from_string cfgfile, config
      else
        require config
        app = Object.const_get(::File.basename(config, '.rb').capitalize)
      end
      return app, options
    end
  • ここでは引数に渡したファイル名のファイルの中身を読み込んで、余分な箇所をなくして、new_from_stringに引数として渡している。

Rack::Builder::new_from_stringはbuilder.rbの48行目にある。

    def self.new_from_string(builder_script, file="(rackup)")
      eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
        TOPLEVEL_BINDING, file, 0
    end
  • 設定ファイルから読み込んだ内容をevalに渡し、評価するというとってもトリッキーなことをしている。

ここらへんがハイライト。

Rack::Builder::newを呼んでいるので、Rack::Builder#initializeを見ていこう。
これはbuilder.rbの53行目。

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

instance_evalを使っているのはどういうことか。
一番最初に出てきたconfig.ruを読み込んだとすると、上のnew_from_string関数中のbuilder_scriptは(クラス定義の部分を除いて)

builder_script = <<EOS
use HogeMidWare
use FooMidWare

run HomApp.new
EOS

となっているはずだ、これがnew_from_string中でevalされてブロックに渡り、initializeでinstance_evalされているということは、実質的にinitializeは以下のような処理を行っていることになる。

    def initialize(defalt_app = nil)
      @use, @map, @run, @warmup = [], nil, default_app, nil
      use HogeMidWare
      use FooMidWare
      run HomApp.new
    end

インスタンスメソッドのuseとrunを呼び出している。

ということで、Rack::Builder#useとRack::Builder#runをみていこう。

Rack::Builder#useはbuilder.rbの81行目にある。

    def use(middleware, *args, &block)
      if @map
        mapping, @map = @map, nil
        @use << proc { |app| generate_map app, mapping }
      end
      @use << proc { |app| middleware.new(app, *args, &block) }
    end

今回は@mapがnilなのでメソッド中の最後の行だけみると、appを受け取ってappをコンストラクタの引数としてオブジェクトを初期化するProcオブジェクトが@useという配列にpushされている。

続いて、Rack::Builder#runはbuilder.rbの103行目にある。

    def run(app)
      @run = app
    end
  • @runにappをセットするだけの単純なメソッド

ここで、Rack::Builder::new_from_stringに戻って、ここで出てくるRack::Builder#to_appを見ていく。 Rack::Builder#to_appはbuilder.rbの144行目。

    def to_app
      app = @map ? generate_map(@run, @map) : @run
      fail "missing run or map statement" unless app
      app = @use.reverse.inject(app) { |a,e| e[a] }
      @warmup.call(app) if @warmup
      app
    end
  • 今回は@mapがnilなのでappには@runが代入される。
  • メソッド中の3行目では@useに入っていたProcオブジェクトをcallして順々にオブジェクトを組み上げていっている。とてもきれいなinjectの使い方だ。一番最初に示した、run HogeMidWare.new( FooMidWare.new( HomApp.new ) )はまさにここで行われている。

Rack::Server#wraped_appに戻ろう。
ここまで、wraped_app中のappを見てきたのであった。
次はRack::Server#build_appを見ていこう。
これはserver.rbの326行目だ。

      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end
  • 詳細は省くけれど、このメソッドメソッドappで頑張って組み上げてきたオブジェクトappを核にしてデフォルトで使われるRackミドルウェアを取り付けていってる。イメージとしては。

そして、Rack::Server#startに戻って、次はシグナルハンドラ中で使われてるメソッドRack::Server#server。
これはserver.rbの289行目にある。

    def server
      @_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
    end
  • ハンドラ(ウェブサーバーの実体?)の取得している。

そしてこのハンドラの特異メソッドrunを呼び出すところからサーバーは動き出す。

こんど(いつ?)はこのハンドラのrunメソッドに迫っていこう。