`rails s`読んだ

2015-01-10#rails

rails sでRailsサーバーが起動するまでに何が起きているのかを紐解くことでRailsとは何なのかを理解していきたい。今回読んでいくソースコードのコミットは2d9b9fb5b5f6015e66d3ad5cb96bc1ba117fd626だ。

TL;DR

コマンドの実行

まずbin/railsを見る。bin/railsはrails newを実行したときに生成されるのだが、このひな形はrailties/lib/rails/generators/rails/app/templates/bin/railsにある。

# bin/rails

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__ )
require_relative '../config/boot'
require 'rails/commands'

config/boot.rbとrails/commands.rbを見る。

# config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__ )

require 'bundler/setup' # Set up gems listed in the Gemfile.
# railties/lib/rails/commands.rb

ARGV << '--help' if ARGV.empty?

aliases = {
  "g" => "generate",
  "d" => "destroy",
  "c" => "console",
  "s" => "server",
  "db" => "dbconsole",
  "r" => "runner"
}

command = ARGV.shift
command = aliases[command] || command

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)

rails/commands/commands_tasksを見る。

# railsties/lib/rails/commands/commands_tasks.rb

module Rails
  class CommandsTasks
    # ...

    def initialize(argv)
      @argv = argv
    end

    def run_command!(command)
      command = parse_command(command)
      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        write_error_message(command)
      end
    end

    # ...
  end
end
# railsties/lib/rails/commands/commands_tasks.rb

module Rails
  class CommandsTasks
    # ...

    def server
      set_application_directory!
      require_command!("server")

      Rails::Server.new.tap do |server|
        require APP_PATH
        Dir.chdir(Rails.application.root)
        server.start
      end
    end

    # ...
  end
end

設定の読み込み

# config/application.rb

require File.expand_path('../boot', __FILE__ )

require 'rails/all'

Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    config.active_record.raise_in_transactional_callbacks = true
  end
end

rails/all.rbを見る。

# railties/lib/rails/all.rb

require "rails"

%w(
  active_record
  action_controller
  action_view
  action_mailer
  active_job
  rails/test_unit
  sprockets
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end

rails.rbを見る。

# railsties/lib/rails.rb

module Rails
  # ...

  class << self
    # ...
  end
end

rails/all.rbとrails.rbについて見たので、config/applicationに戻る。

# config/application.rb

# ...

Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    config.active_record.raise_in_transactional_callbacks = true
  end
end
# railties/lib/rails.rb

module Rails
  class << self
    # ...

    def env
      @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
    end

    # ...

    def groups(*groups)
      hash = groups.extract_options!
      env = Rails.env
      groups.unshift(:default, env)
      groups.concat ENV["RAILS_GROUPS"].to_s.split(",")
      groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) }
      groups.compact!
      groups.uniq!
      groups
    end

    # ...
  end
end

config/application.rbに戻る。

# config/application.rb

# ...

module SampleApp
  class Application < Rails::Application
    config.active_record.raise_in_transactional_callbacks = true
  end
end
# railties/lib/rails/application.rb

module Rails
  class Application < Engine
    class << self
      def inherited(base)
        super
        Rails.app_class = base
        add_lib_to_load_path!(find_root(base.called_from))
      end
    end
  end
end

サーバーの起動

サーバー起動前にどういった設定を読み込んでいるか見たので、サーバーの起動について詳細に見ていく。

# railsties/lib/rails/commands/commands_tasks.rb

module Rails
  class CommandsTasks
    # ...

    def server
      set_application_directory!
      require_command!("server")

      Rails::Server.new.tap do |server|
        require APP_PATH
        Dir.chdir(Rails.application.root)
        server.start
      end
    end

    # ...
  end
end

Rails::Serverはrails/commands/server.rbで定義されているので見る。

# railsties/lib/rails/commands/server

module Rails
  class Server < ::Rack::Server
    # ...

    def initialize(*)
      super
      set_environment
    end

    # ...

    def start
      print_boot_information
      trap(:INT) { exit }
      create_tmp_directories
      log_to_stdout if options[:log_stdout]

      super
    ensure
      puts 'Exiting' unless @options && options[:daemonize]
    end

    # ...
  end
end

config.ruを見る。

# config.ru

require ::File.expand_path('../config/environment', __FILE__ )
run Rails.application

とりあえずconfig/environment.rbを見る。

# config/environment.rb

require File.expand_path('../application', __FILE__ )
Rails.application.initialize!

Rails.application.initialize!について見ていくため、まずはRails.applicationの定義を見る。

# railties/lib/rails.rb

module Rails
  class << self
    def application
      @application ||= (app_class.instance if app_class)
    end
  end
end

Rails::Applicationを見る。

# railties/lib/rails/application.rb

module Rails
  class Application < Engine
    class << self
      def instance
        super.run_load_hooks!
      end
    end

    def run_load_hooks!
      return self if @ran_load_hooks
      @ran_load_hooks = true
      ActiveSupport.run_load_hooks(:before_configuration, self)

      @initial_variable_values.each do |variable_name, value|
        if INITIAL_VARIABLES.include?(variable_name)
          instance_variable_set("@#{variable_name}", value)
        end
      end

      instance_eval(&@block) if @block
      self
    end
  end
end

initializerの実行

config/environment.rbに戻る。

# config/environment.rb

require File.expand_path('../application', __FILE__ )
Rails.application.initialize!

Rails::Applicationを見る。

# railties/lib/rails/application.rb

module Rails
  class Application
    def initialize!(group=:default) #:nodoc:
      raise "Application has been already initialized." if @initialized
      run_initializers(group, self)
      @initialized = true
      self
    end
  end
end
# railties/lib/rails/initializable.rb

module Rails
  module Initializable
    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
  end
end
# railties/lib/rails/application.rb

module Rails
  class Application
    def initializers
      Bootstrap.initializers_for(self) +
      railties_initializers(super) +
      Finisher.initializers_for(self)
    end
  end
end
# railties/lib/rails/initializable.rb

def initializers_for(binding)
  Collection.new(initializers_chain.map { |i| i.bind(binding) })
end
# railties/lib/rails/initializable.rb

class Collection < Array
  include TSort

  alias :tsort_each_node :each
  def tsort_each_child(initializer, &block)
    select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block)
  end

  def +(other)
    Collection.new(to_a + other.to_a)
  end
end

Initializable.initializers_forに戻る。

# railties/lib/rails/initializable.rb

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

initializers_chainの要素はおそらくInitializerインスタンスだと思われるので、Initializer#bindを見る。

# railties/lib/rails/Initializable.rb

class Initializer
  def bind(context)
    return self if @context
    Initializer.new(@name, context, @options, &block)
  end
end

Rails::Application#initializersに戻る。

# railties/lib/rails/application.rb

module Rails
  class Application
    def initializers
      Bootstrap.initializers_for(self) +
      railties_initializers(super) +
      Finisher.initializers_for(self)
    end
  end
end

#railties_initializersがまだ残っているので見る。

# railties/lib/rails/application.rb

module Rails
  class Application
    def ordered_railties
      @ordered_railties ||= begin
        order = config.railties_order.map do |railtie|
          # ...
        end

        all = (railties - order)
        all.push(self) unless (all + order).include?(self)
        order.push(:all) unless order.include?(:all)

        index = order.index(:all)
        order[index] = all
        order
      end
    end

    def railties_initializers(current)
      initializers = []
      ordered_railties.reverse.flatten.each do |r|
        if r == self
          initializers += current
        else
          initializers += r.initializers
        end
      end
      initializers
    end
  end
end

#railtiesRails::Engineから継承されたメソッドなので見る。

# railties/lib/rails/engine.rb

def railties
  @railties ||= Railties.new
end

Rails::Engine::Railtiesを見る。

# railties/lib/rails/engine/railties.rb

def initialize
  @_all ||= ::Rails::Railtie.subclasses.map(&:instance) +
    ::Rails::Engine.subclasses.map(&:instance)
end

railties_initializersに戻る。

# railties/lib/rails/application.rb

def railties_initializers(current)
  initializers = []
  ordered_railties.reverse.flatten.each do |r|
    if r == self
      initializers += current
    else
      initializers += r.initializers
    end
  end
  initializers
end

Rails::Application#initializersについて見たので、#run_initializersに戻る。

# 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

Initializer#runを見る。

# railties/lib/rails/initializable.rb

class Initializer
  def run(*args)
    @context.instance_exec(*args, &block)
  end
end

続いて、実行されるInitializerがどのように初期化されて登録されているのか見ていく。これはRails::Initializable::ClassMethods.initializerによって行われる。

# railties/lib/rails/initializable.rb

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

Railsアプリケーションの起動

initializersの実行について一通り眺めたのでこれでconfig/environment.rbを読んだことになる。config.ruに戻る。

# config.ru

require ::File.expand_path('../config/environment', __FILE__ )
run Rails.application

参考