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あたりを読んでいきたい。