bin/rails sを実行した時、Railsでは何が起きているのか追ってみた

最近RackSinatraのコードリーディングをしていて、RailsアプリのコードをRackアプリケーションとして読めるようになってきたので、$ bin/rails sをしたら内部では何が起こっているのかを追ってみた。

precondition

ここから本編

$ rails new で作ったばかりのRailsアプリケーションを、$ bin/rails s を実行した時の挙動を見ていく。

# bin/rails

 #!/usr/bin/env ruby
 begin
   load File.expand_path('../spring', __FILE__)
 rescue LoadError => e
   raise unless e.message.include?('spring')
 end
 APP_PATH = File.expand_path('../config/application', __dir__)
 require_relative '../config/boot'
 require 'rails/commands'

APP_PATH../config/application.rbを入れている。 そのあと../config/boot.rbを読み込んで、最後にrails/commandsを読み込んでいる。

#config/application.rb

 require_relative 'boot'
 
 require 'rails/all'
 
 # Require the gems listed in Gemfile, including any gems
 # you've limited to :test, :development, or :production.
 Bundler.require(*Rails.groups)
 
 module RackupResearch
   class Application < Rails::Application
     # Initialize configuration defaults for originally generated Rails version.
     config.load_defaults 6.0
 
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration can go into files in config/initializers
     # -- all .rb files in that directory are automatically loaded after loading
     # the framework and any gems in your application.
   end
 end 

まだこの段階では、このファイルは読み込まれない。 ただ単純にAPP_PATHにファイルパスを入れているだけだ。

次にmodule AppNameの中でclass Application < Rails::Applicationが定義されている。 このAppNamerails new AppNameで決めたやつ。 今回はRackupResearch。 そして、config.load_defaults 6.0で6.0におけるデフォルト設定を読み込んでいる。

これが終わったらrails/commandsをrequireする。

# railties/lib/commands.rb
 # frozen_string_literal: true
 
 require "rails/command"
 
 aliases = {
   "g"  => "generate",
   "d"  => "destroy",
   "c"  => "console",
   "s"  => "server",
   "db" => "dbconsole",
   "r"  => "runner",
   "t"  => "test"
 }
 
 command = ARGV.shift
 command = aliases[command] || command
 
 Rails::Command.invoke command, ARGV

ここでは$ bin/railsに続くコマンドによって処理が変わる。 今はRailsを起動しようとしているのでserverあるいはsが渡されている。

aliasesから一致するエイリアスが見つかったら、それを正式なコマンドに置き換える。要するにRails::Command.invoke "server", ARGVを実行。 ARGVはたぶん空っぽ...でいいのかな。

# railties/lib/command.rb
 # frozen_string_literal: true
 
 require "active_support"
 require "active_support/dependencies/autoload"
 require "active_support/core_ext/enumerable"
 require "active_support/core_ext/object/blank"
 
 require "thor"
 
 module Rails
   module Command
     extend ActiveSupport::Autoload
 
     autoload :Spellchecker
     autoload :Behavior
     autoload :Base
 
     include Behavior
 
     HELP_MAPPINGS = %w(-h -? --help)
 
     class << self
       def hidden_commands # :nodoc:
         @hidden_commands ||= []
       end
 
       def environment # :nodoc:
         ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
       end
 
       # Receives a namespace, arguments and the behavior to invoke the command.
       def invoke(full_namespace, args = [], **config)
         namespace = full_namespace = full_namespace.to_s
 
         if char = namespace =~ /:(\w+)$/
           command_name, namespace = $1, namespace.slice(0, char)
         else
           command_name = namespace
         end
 
         command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
         command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
 
         command = find_by_namespace(namespace, command_name)
         if command && command.all_commands[command_name]
           command.perform(command_name, args, config)
         else
           find_by_namespace("rake").perform(full_namespace, args, config)
         end
       end
 
       # Rails finds namespaces similar to Thor, it only adds one rule:
       #
       # Command names must end with "_command.rb". This is required because Rails
       # looks in load paths and loads the command just before it's going to be used.
       #
       #   find_by_namespace :webrat, :rails, :integration
       #
       # Will search for the following commands:
       #
       #   "rails:webrat", "webrat:integration", "webrat"
       #
       # Notice that "rails:commands:webrat" could be loaded as well, what
       # Rails looks for is the first and last parts of the namespace.
       def find_by_namespace(namespace, command_name = nil) # :nodoc:
         lookups = [ namespace ]
         lookups << "#{namespace}:#{command_name}" if command_name
         lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
 
         lookup(lookups)
 
         namespaces = subclasses.index_by(&:namespace)
         namespaces[(lookups & namespaces.keys).first]
       end
 
       # Returns the root of the Rails engine or app running the command.
       def root
         if defined?(ENGINE_ROOT)
           Pathname.new(ENGINE_ROOT)
         elsif defined?(APP_PATH)
           Pathname.new(File.expand_path("../..", APP_PATH))
         end
       end
 
       def print_commands # :nodoc:
         commands.each { |command| puts("  #{command}") }
       end
 
       private
         COMMANDS_IN_USAGE = %w(generate console server test test:system dbconsole new)
         private_constant :COMMANDS_IN_USAGE
 
         def commands
           lookup!
 
           visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands)
 
           (visible_commands - COMMANDS_IN_USAGE).sort
         end
 
         def command_type # :doc:
           @command_type ||= "command"
         end
 
         def lookup_paths # :doc:
           @lookup_paths ||= %w( rails/commands commands )
         end
 
         def file_lookup_paths # :doc:
           @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ]
         end
     end
   end
 end
 

なっが...。 一旦invokeだけ取り出す。

# command.rb
       # Receives a namespace, arguments and the behavior to invoke the command.
       def invoke(full_namespace, args = [], **config)
         namespace = full_namespace = full_namespace.to_s
 
         if char = namespace =~ /:(\w+)$/
           command_name, namespace = $1, namespace.slice(0, char)
         else
           command_name = namespace
         end
 
         command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
         command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
 
         command = find_by_namespace(namespace, command_name)
         if command && command.all_commands[command_name]
           command.perform(command_name, args, config)
         else
           find_by_namespace("rake").perform(full_namespace, args, config)
         end
       end

一致するコマンドがなければUsageを出して、あればそれを実行する。 実行しているのはcommand.perform(command_name, args, config)のところ。

commandfind_by_namespaceで得られるオブジェクトだが、なんだかよく分からない。 serverコマンドで得られるオブジェクトなので、hogehoge_serverと予想してみる。

# command.rb
  
        # Rails finds namespaces similar to Thor, it only adds one rule:
        #
        # Command names must end with "_command.rb". This is required because Rails
        # looks in load paths and loads the command just before it's going to be used.
        #
        #   find_by_namespace :webrat, :rails, :integration
        #
        # Will search for the following commands:
        #
        #   "rails:webrat", "webrat:integration", "webrat"
        #
        # Notice that "rails:commands:webrat" could be loaded as well, what
        # Rails looks for is the first and last parts of the namespace.
        def find_by_namespace(namespace, command_name = nil) # :nodoc:
          lookups = [ namespace ]
          lookups << "#{namespace}:#{command_name}" if command_name
          lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
  
          lookup(lookups)
  
          namespaces = subclasses.index_by(&:namespace)
          namespaces[(lookups & namespaces.keys).first]
        end

コメントアウトにヒントがありそう。

RailsはThorのようにnamespaceを探すが、1つ追加のルールがある。 コマンド名は必ず_command.rbで終わらなければならない。これは、Railsが、実際に利用するコマンドをロードする直前に、ロードするパスを確認するため。

コメントの例ではfind_by_namespace :webrat, :rails, :integrationとある。 この場合、次のコマンドを探しに行く。 "rails:webrat", "webrat:integration", "webrat" 最終的に、"rails:commands:webrat"が読み込まれる。

で、今回の例。今回はcommand_name = "server"となるので、server_command.rbを探しに行けば良い。 ちなみにrailties/lib/rails/commands/server/server_command.rbにファイルがある。

# server_command.rb
 # 長すぎるので省略
 
 module Rails
   module Command
     class ServerCommand < Base # :nodoc:
       def perform
         set_application_directory!
         prepare_restart
 
         Rails::Server.new(server_options).tap do |server|
           # Require application after server sets environment to propagate
           # the --environment option.
           require APP_PATH
           Dir.chdir(Rails.application.root)
 
           if server.serveable?
             print_boot_information(server.server, server.served_url)
             after_stop_callback = -> { say "Exiting" unless options[:daemon] }
             server.start(after_stop_callback)
           else
             say rack_server_suggestion(using)
           end
         end
       end
     end
   end
 end    

さて、ここでやっとserverコマンドが始まる。 重要な行はRails::Server.new(server_options).tap do |server|だろうか。 Rails::Serverオブジェクトを生成して、tap blockと続けている。

Object#tapはちょっと不思議なメソッドで、レシーバ自身を返す。 主な使い方は"HoGe".upcase.tap { |o| puts o }みたいにして、副作用なしにoの中身を見たりできる。

つまり、最終的にRails::Server.newオブジェクト自体が、performの結果で返却される。 じゃあブロックってなんのためにあるの?という疑問が出てくる。

Rails::Server.new(server_options).tap do |server|で渡されるブロックの引数serverは、レシーバ自身を指す。 つまり、ここでは生成したばかりのRails::Serverオブジェクトだ。 そして、そのオブジェクトに対してserver.startを実行している。

つまり、server.startを実行しながら、尚且つそのServerオブジェクト自体を返している。

...と思ったんだけど、別に返却先がないので、単純に作ったそばから実行したいとかなのかな。

さて、このRails::Serverはどこにあるのだろうか? 答えは同じserver_command.rbファイル内にある。

ファイルの最初の方で、すでに定義されている。

# server_command.rb
 # frozen_string_literal: true
 
 require "fileutils"
 require "action_dispatch"
 require "rails"
 require "active_support/deprecation"
 require "active_support/core_ext/string/filters"
 require "rails/dev_caching"
 
 module Rails
   class Server < ::Rack::Server
     class Options
       def parse!(args)
         Rails::Command::ServerCommand.new([], args).server_options
       end
     end
 
     def initialize(options = nil)
       @default_options = options || {}
       super(@default_options)
       set_environment
     end
 
     def opt_parser
       Options.new
     end
 
     def set_environment
       ENV["RAILS_ENV"] ||= options[:environment]
     end
 
     def start(after_stop_callback = nil)
       trap(:INT) { exit }
       create_tmp_directories
       setup_dev_caching
       log_to_stdout if options[:log_stdout]
 
       super()
     ensure
       after_stop_callback.call if after_stop_callback
     end
 
     def serveable? # :nodoc:
       server
       true
     rescue LoadError, NameError
       false
     end
 
     def middleware
       Hash.new([])
     end
 
     def default_options
       super.merge(@default_options)
     end
 
     def served_url
       "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma?
     end
 
     private
       def setup_dev_caching
         if options[:environment] == "development"
           Rails::DevCaching.enable_by_argument(options[:caching])
         end
       end
 
       def create_tmp_directories
         %w(cache pids sockets).each do |dir_to_make|
           FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
         end
       end
 
       def log_to_stdout
         wrapped_app # touch the app so the logger is set up
 
         console = ActiveSupport::Logger.new(STDOUT)
         console.formatter = Rails.logger.formatter
         console.level = Rails.logger.level
 
         unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
           Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
         end
       end
 
       def use_puma?
         server.to_s == "Rack::Handler::Puma"
       end
   end

ちょっと長いけど、class Server < ::Rack::Serverに注目。 このServerオブジェクトは、::Rack::Serverを継承している!

Rails::server.new(server_options).tap do |server|と書いているので、まずはServerクラスのinitializeが走る。

# server_command.rb
     def initialize(options = nil)
       @default_options = options || {}
       super(@default_options)
       set_environment
     end

set_environmentが呼ばれている。

server_command.rb
     def set_environment
       ENV["RAILS_ENV"] ||= options[:environment]
     end

たった1行だけだが、実は裏でたくさんのことをしている。怖い。 このoptionsはハッシュだが、これ自体がRack::Serverのメソッド。

# lib/rack/server.rb
     def options
       merged_options = @use_default_options ? default_options.merge(@options) : @options
       merged_options.reject { |k, v| @ignore_options.include?(k) }
     end

オプションによって挙動はちょっと変わるが、ここではdevelopmentの時のデフォルトの場合を見てみよう。

# lib/rack/server.rb
     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

という設定値らしい。ちなみにrackupで特に何も指定せずRackアプリケーションを立ち上げた時のport番号は9292だ。

initializeが終わったので、次はrequire APP_PATHをみる必要がある。

APP_PATHは、実はかなり冒頭に出てきた。

APP_PATH../config/application.rbを入れている

ここでやっと、config/application.rbrequireしている。

# config/application.rb
 require_relative 'boot'
 
 require 'rails/all'
 
 # Require the gems listed in Gemfile, including any gems
 # you've limited to :test, :development, or :production.
 Bundler.require(*Rails.groups)
 
 module RackupResearch
   class Application < Rails::Application
     # Initialize configuration defaults for originally generated Rails version.
     config.load_defaults 6.0
 
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration can go into files in config/initializers
     # -- all .rb files in that directory are automatically loaded after loading
     # the framework and any gems in your application.
   end
 end

require 'rails/allRailsの全てのモジュールをrequireしている。節操ない。 そしてclass Application < Rails::Applicationが定義される。

application.rbはここまで。次はRails::Server < Rack::Serverstartメソッドを見てみよう。

# server_command.rb
      def start(after_stop_callback = nil)
        trap(:INT) { exit }
        create_tmp_directories
        setup_dev_caching
        log_to_stdout if options[:log_stdout]
  
        super()
      ensure
        after_stop_callback.call if after_stop_callback
      end

trap(:INT)はSIGINTをトラップして、Kernel#exitに繋げている。

docs.ruby-lang.org

create_tmp_directoriesは名前の通りtempディレクトtmp/pidsとかを作っている。

で、最終的にsuper()を呼び出している。 これはrack/rackを読む必要がある。

github.com

# lib/rack/server.rb
     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).
       handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
         wrapped_app
       end
 
       daemonize_app if options[:daemonize]
 
       write_pid if options[:pid]
 
       trap(:INT) do
         if server.respond_to?(:shutdown)
           server.shutdown
         else
           exit
         end
       end
 
       server.run wrapped_app, options, &blk
     end

さて、ここではserver.runが行われる前に、wrapped_apptouchする処理が書かれている。 touchっていうのがちょっとフワッとしたニュアンスだけど、wrapped_appはメソッドなのでそれを呼び出しているということを指していると思う。 呼び出すまで@wrapped_appは空っぽなので、touchをするとそこにwrapped_appの実体が格納される。

# lib/rack/server.rb
 def wrapped_app
   @wrapped_app ||= build_app app
 end
 
 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
 
 def app
   @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
 end
 
 private
 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)
   @options.merge!(options) { |key, old, new| old }
   app
 end
       
 def build_app_from_string
   Rack::Builder.new_from_string(self.options[:builder])
 end

options[:config]はデフォルトでconfig.ruRailsアプリを生成すると、config.ruは自動生成される。

# config.ru
 # This file is used by Rack-based servers to start the application.
 
 require_relative 'config/environment'
 
 run Rails.application

app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) ここでは、Rack::Builder.parse_fileで、config.ruファイルの中身を取り出して評価している。

# lib/rack/builder.rb
   class Builder
     UTF_8_BOM = '\xef\xbb\xbf'
 
    def self.parse_file(config, opts = Server::Options.new)
      if config.end_with?('.ru')
        return self.load_file(config, opts)
      else
        require config
        app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
        return app, {}
      end
    end
    
    def self.load_file(path, opts = Server::Options.new)
      options = {}
      cfgfile = ::File.read(path)
      cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8
      if cfgfile[/^#\\(.*)/] && opts
        options = opts.parse! $1.split(/\s+/)
      end
      cfgfile.sub!(/^__END__\n.*\Z/m, '')
      app = new_from_string cfgfile, path
      return app, options
    end
    
    def self.new_from_string(builder_script, file = "(rackup)")
      eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
        TOPLEVEL_BINDING, file, 0
    end
    
    def initialize(default_app = nil, &block)
      @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
      instance_eval(&block) if block_given?
    end
    
    def self.app(default_app = nil, &block)
      self.new(default_app, &block).to_app
    end

最終的に、Builder#initializeまで処理が渡っていく。

app = new_from_string cfgfile, pathcfgfileconfig.ruFile.readした結果、pathがファイルパス。

new_from_stringではconfig.ruの中身を文字列としてevalに渡して、その場で評価している。 評価の仕方は eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 と、かなり豪快。 Builder#initializeに処理が移る。

initializeではconfig.ruの中身がブロックとして渡されている。 instance_eval (&block)と書かれているので、現在のコンテキストでconfig.ruが実行される。

# config.ru
 # This file is used by Rack-based servers to start the application.
 
 require_relative 'config/environment'
 
 run Rails.application

まずはrequire_relative 'config/environment'

# config/enviromennt.rb
 # Load the Rails application.
 require_relative 'application'
 
 # Initialize the Rails application.
 Rails.application.initialize!

まずconfig/application.rbがrequireされる。 次に、読み込まれたclass Application < Rails::Applicationinitialize!が呼ばれる。 (割とこの辺からRails Guidesと一致しない感じになってる気がする)

# railties/lib/rails/application.rb
     # Initialize the application passing the given group. By default, the
     # group is :default
     def initialize!(group = :default) #:nodoc:
       raise "Application has been already initialized." if @initialized
       run_initializers(group, self)
       @initialized = true
       self
     end

run_initializersはちょっと複雑だけど、様々なinitializerを呼び出す。

# railties/lib/rails/initializable.rb
     def run_initializers(group = :default, *args)
       return if instance_variable_defined?(:@ran)
       initializers.tsort_each do |initializer|
         initializer.run(*args) if initializer.belongs_to?(group)
       end
       @ran = true
     end
 
     def initializers
       @initializers ||= self.class.initializers_for(self)
     end
 
     module ClassMethods
       def initializers
         @initializers ||= Collection.new
       end
 
       def initializers_chain
         initializers = Collection.new
         ancestors.reverse_each do |klass|
           next unless klass.respond_to?(:initializers)
           initializers = initializers + klass.initializers
         end
         initializers
       end
 
       def initializers_for(binding)
         Collection.new(initializers_chain.map { |i| i.bind(binding) })
       end
 
       def initializer(name, opts = {}, &blk)
         raise ArgumentError, "A block must be passed when defining an initializer" unless blk
         opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
         initializers << Initializer.new(name, nil, opts, &blk)
       end
     end

このinitializationsが終わると、次はrun Rails.application。 アプリケーションの実行かと思いきや、ここのrunbuilder.rbdef runのこと。 Rails.applicationはアプリケーションオブジェクトなんだけど、呼ばれているのはrailties/lib/rails.rbのapplicationメソッド。

# railties/lib/rails.rb
 module Rails
   extend ActiveSupport::Autoload
 
   autoload :Info
   autoload :InfoController
   autoload :MailersController
   autoload :WelcomeController
 
   class << self
     @application = @app_class = nil
 
     attr_writer :application
     attr_accessor :app_class, :cache, :logger
     def application
       @application ||= (app_class.instance if app_class)
     end

app_classにはすでにAppName::Applicationが定義されている。 多分config/application.rbをrequireしたときにはもうなってるかも。

で、何をしているかといえば、

# lib/rack/builder.rb
     def run(app)
       @run = app
     end

これだけ。

やっとRack::Serverに処理が返ってくる。長すぎる。

# lib/rack/server.rb
 def wrapped_app
   @wrapped_app ||= build_app app
 end
 
 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
 
 def app
   @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
 end
 
 private
 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)
   # **ここがめちゃくちゃ長かった!!**
   @options.merge!(options) { |key, old, new| old }
   app
 end
       
 def build_app_from_string
   Rack::Builder.new_from_string(self.options[:builder])
 end

で、思い出してもらいたいのがRack::Builder.parse_file 結構ざっくり飛ばして説明するけど、Rack::Builderにはto_appというメソッドが定義されている。

# lib/rack/builder.rb
     def self.new_from_string(builder_script, file = "(rackup)")
       eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
         TOPLEVEL_BINDING, file, 0
     end
     
     def to_app
       app = @map ? generate_map(@run, @map) : @run
       fail "missing run or map statement" unless app
       app.freeze if @freeze_app
       app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
       @warmup.call(app) if @warmup
       app
     end

config.ruの中身をブロックに取って初期化したBuilderオブジェクトを.to_appで変換している。

@runはさっき定義したばっかり。中身はApp。 変数名から予想するに、現在起動中のRackアプリケーションの存在を示すのに使っているみたい。

これで、やっとserver.rbに処理が返ってくる。

結局、wrapped_appbuild_app appに入ってくるappとは、builder.rb.to_appされたconfig.rurunに渡されたアプリケーションオブジェクトであった。 つまり、以下のようなイメージになる。(実際はオプションの設定とかあるので、厳密には同じにはならない)

# server.rb
 def wrapped_app
    @wrapped_app ||= build_app Rails.application
 end
 
  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

build_appは、middlewareの数だけ、渡されてきたappMiddleware.new(app, *args)でラップしていく。 最終的に、ミルフィーユのようなRackアプリケーションが完成する。どの層も、call(env)が定義されており、さらに次の層へのインターフェイスとしてinitializeで渡される@appを持っているので、何らかのリクエストがきたら、処理をして次のappへ渡すことができる。

# lib/rack/server.rb
     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).
       handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
         wrapped_app
       end
       
       ## ここまで終わった
 
       daemonize_app if options[:daemonize]
 
       write_pid if options[:pid]
 
       trap(:INT) do
         if server.respond_to?(:shutdown)
           server.shutdown
         else
           exit
         end
       end
 
       server.run wrapped_app, options, &blk
     end    

残りの処理を見ていこう。 次はdemonize_appだが、$ rails server -dと、-dオプションを指定したときにtrueが入る。 子プロセスを生成してそちらで処理を継続し、スクリプトの実行は終了させて制御端末に返す、というのをやっている。

options[:pid]にはtmp/pids/server.pidというファイルパスが格納されている。 Railsは起動時にここにプロセス番号を書きにいく

trap(:INT)はSIGINTをフックするもの 通常だとスクリプトの実行は停止されてしまうが、それをフックして安全にサーバをシャットダウンする処理が書かれている。

最後に、server.run wrapped_app, options, &blkが評価される。 さて、ここでserverは何者かと疑問に思うかもしれないが、これはメソッドで定義されている。

# server.rb
     def server
       @_server ||= Rack::Handler.get(options[:server])
 
       unless @_server
         @_server = Rack::Handler.default
 
         # We already speak FastCGI
         @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
       end
 
       @_server
     end

Rack::Handler.getというメソッドを呼んでサーバオブジェクトを受け取っている。 引数のoptions[:server]は、現行のRailsであれば"puma"が入っているはず。

# lib/rack/handler.rb
   module Handler
     def self.get(server)
       return unless server
       server = server.to_s
 
       unless @handlers.include? server
         load_error = try_require('rack/handler', server)
       end
 
       if klass = @handlers[server]
         klass.split("::").inject(Object) { |o, x| o.const_get(x) }
       else
         const_get(server, false)
       end
 
     rescue NameError => name_error
       raise load_error || name_error
     end

getメソッドでは、渡されたserverの名前を使って、@handlerから一致するオブジェクトを探している。 実はこの時点で"puma"では一致するオブジェクトを探せないので、try_requireでロードしてこようとする。

# handler.rb
     def self.try_require(prefix, const_name)
       file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
         gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
 
       require(::File.join(prefix, file))
       nil
     rescue LoadError => error
       error
     end

ここでは、rack/handler/pumaという文字列でrequireを行なっている。 どうやらrequire 'rack/handler/pumaした時点でPumaが@handlersに登録されるらしい。

github.com

結果として、Pumaオブジェクトを得ることができる。

server.run wrapped_app, options, &blkに戻る。 最終的に、このserverRack::Handler::Pumaのことで、このモジュールに対して.runを実行している。

# lib/rack/handler/puma.rb
       def self.run(app, options = {})
         conf   = self.config(app, options)
 
         events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
 
         launcher = ::Puma::Launcher.new(conf, :events => events)
 
         yield launcher if block_given?
         begin
           launcher.run
         rescue Interrupt
           puts "* Gracefully stopping, waiting for requests to finish"
           launcher.stop
           puts "* Goodbye!"
         end
       end

launcherはPumaを起動してくれる君。 こいつにlauncher.runを投げて、Pumaを起動している。

これでPumaが起動して、PumaはconfigにRails.applicationを持っているので、リクエストがきたらそちらに処理を丸投げする。

めでたしめでたし。

おわりに

initializers周りのコードをほとんど追わなかったが、この部分でかなりの初期化を行なっているようだったので、次はActionDispatchあたりを読んでいきたい。