bin/rails sを実行した時、Railsでは何が起きているのか追ってみた
最近Rack
とSinatra
のコードリーディングをしていて、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
が定義されている。
このAppName
はrails 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)
のところ。
command
はfind_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.rb
をrequire
している。
# 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/all
でRailsの全てのモジュールをrequireしている。節操ない。
そしてclass Application < Rails::Application
が定義される。
application.rb
はここまで。次はRails::Server < Rack::Server
のstart
メソッドを見てみよう。
# 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
に繋げている。
create_tmp_directories
は名前の通りtempディレクトリtmp/pids
とかを作っている。
で、最終的にsuper()
を呼び出している。
これはrack/rack
を読む必要がある。
# 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_app
にtouch
する処理が書かれている。
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.ru
。
Railsアプリを生成すると、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, path
のcfgfile
がconfig.ru
をFile.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::Application
のinitialize!
が呼ばれる。
(割とこの辺から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
。
アプリケーションの実行かと思いきや、ここのrun
はbuilder.rb
のdef 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_app
のbuild_app app
に入ってくるapp
とは、builder.rb
で.to_app
されたconfig.ru
のrun
に渡されたアプリケーションオブジェクトであった。
つまり、以下のようなイメージになる。(実際はオプションの設定とかあるので、厳密には同じにはならない)
# 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
の数だけ、渡されてきたapp
をMiddleware.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
に登録されるらしい。
結果として、Puma
オブジェクトを得ることができる。
server.run wrapped_app, options, &blk
に戻る。
最終的に、このserver
はRack::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
あたりを読んでいきたい。