bundle exec rails sを追った

tamuraです。

Railsわからなすぎなので追ってみました。 バージョンは5.2あたりだった気がします。

目標は Rails.application って何?がわかること。

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'

rails/commands.rb

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

引数は s なので実行コマンドはこうなる。

Rails::Command.invoke "server", []

rails/command.rb

invoke

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

namespace = "server" command_name = "server"

なのでこうなる。

command = find_by_namespace("server", "server")

find_by_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

lookups は最終的にこうなる。

["server", "server:server", "rails:server", "rails:server:server"]

rails/command/behavir.rb

lookup

def lookup(namespaces)
  paths = namespaces_to_paths(namespaces)

  paths.each do |raw_path|
    lookup_paths.each do |base|
      path = "#{base}/#{raw_path}_#{command_type}"

      begin
        require path
        return
      rescue LoadError => e
        raise unless e.message =~ /#{Regexp.escape(path)}$/
      rescue Exception => e
        warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
      end
    end
  end
end

paths["server, "server/server", "rails/server", "rails/server/server"] lookup_paths%( rails/commands commands ) command_type"command"

なので

  • 1回目 : require "rails/commands/server_command" => 失敗
  • 2回目 : require "commands/server_command" => 失敗
  • 3回目 : require "rails/commands/server/server_command" => 成功!

rails/commands.rb

find_by_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

subclassesRails::Command::Base.inherited で設定している

rails/command/base.rb

inherited

def inherited(base) #:nodoc:
  super

  if base.name && base.name !~ /Base$/
    Rails::Command.subclasses << base
  end
end

ServerCommand が読み込まれたとき、そのClasssubclassesに入れ込んでいる。

rails/commands.rb

find_by_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

index_by(&:namespace)namespace

rails/command/base.rb

def namespace(name = nil)
  if name
    super
  else
    @namespace ||= super.chomp("_command").sub(/:command:/, ":")
  end
end

とあって、Rails::Command::ServerCommandThorを継承しているので、

rails:command:server_command chomp -> rails:command:server sub -> rails:server

となる。 なので以下の様になる。

namespaces = {'rails:server': Rails::Command::ServerCommand}
namespaces[(lookups & namespaces.keys).first] => Rails::Command::ServerCommand

find_by_namespace終了。

rails/command.rb

invoke

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

command = Rails::Command::ServerCommand

command.all_commands["server"]"server"=>#<struct Thor::Command name="server" が返ってくるのでiftrueになる。

Rails::Command::ServerCommand.perform("server", [], nil)

これはインスタンスメソッドではなくクラスメソッド。

rails/command/base.rb

perform

def perform(command, args, config) # :nodoc:
  if Rails::Command::HELP_MAPPINGS.include?(args.first)
    command, args = "help", []
  end

  dispatch(command, args.dup, nil, config)
end

dispatchThor のメソッド。

thor.rb

dispatch

def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength
  meth ||= retrieve_command_name(given_args)
  command = all_commands[normalize_command_name(meth)]

  if !command && config[:invoked_via_subcommand]
    # We're a subcommand and our first argument didn't match any of our
    # commands. So we put it back and call our default command.
    given_args.unshift(meth)
    command = all_commands[normalize_command_name(default_command)]
  end

  if command
    args, opts = Thor::Options.split(given_args)
    if stop_on_unknown_option?(command) && !args.empty?
      # given_args starts with a non-option, so we treat everything as
      # ordinary arguments
      args.concat opts
      opts.clear
    end
  else
    args = given_args
    opts = nil
    command = dynamic_command_class.new(meth)
  end

  opts = given_opts || opts || []
  config[:current_command] = command
  config[:command_options] = command.options

  instance = new(args, opts, config)
  yield instance if block_given?
  args = instance.args
  trailing = args[Range.new(arguments.size, -1)]
  instance.invoke_command(command, trailing || [])
end

いろいろやっているけど、

instance = new(args, opts, config)

Rails::Command::ServerCommandをインスタンス化している。

instance.invoke_command(command, trailing || [])

ここでインスタンスメソッドを実行している。

(command#<struct Thor::Command name="server", description="", long_description=nil, usage="", options={}, ancestor_name=nil>)

thor/invocation.rb

invoke_command

def invoke_command(command, *args) #:nodoc:
  current = @_invocations[self.class]

  unless current.include?(command.name)
    current << command.name
    command.run(self, *args)
  end
end

command#<struct Thor::Command name="server", description="", long_description=nil, usage="", options={}, ancestor_name=nil>

thor/command.rb

run

def run(instance, args = [])
  arity = nil

  if private_method?(instance)
    instance.class.handle_no_command_error(name)
  elsif public_method?(instance)
    arity = instance.method(name).arity
    instance.__send__(name, *args)
  elsif local_method?(instance, :method_missing)
    instance.__send__(:method_missing, name.to_sym, *args)
  else
    instance.class.handle_no_command_error(name)
  end
rescue ArgumentError => e
  handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
rescue NoMethodError => e
  handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (raise e)
end

public_method?trueになるのでinstance.__send__(name, *args)を実行する。

rails/commands/server/server_commands.rb

perform

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)
    server.start
  end
end

ここにきてサーバが起動される。

  • set_application_directory!
    • ディレクトリを移動する
  • prepare_restart
    • pidファイルを削除する

initialize

def initialize(options = nil)
  @default_options = options || {}
  super(@default_options)
  set_environment
end

引数のoptionsServerCommand.server_optionsで定義済み。

set_environmentは以下の通り。

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

perform

  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)
    server.start
  end

require APP_PATHAPP_PATHconfig/application.rbへのパス。 (ここのconfig/application.rbは自分のRailsアプリケーションのやつ)

config/application.rb

module MyApp
  class Application < Rails::Application
    config.load_defaults 5.2
  end
end

こうなっていて、rails/application.rbでこう定義されていて、

module Rails
  class Application < Engine
    class << self
      def inherited(base)
        Rails.app_class = base'
        ...snip...
      end
    end
  end
end

rails.rbでこう定義されているので、

module Rails
  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
  end
end

Rails.applicationRails::Application を継承した自分のアプリのApplicationということがわかった。

不明点

  • Rails::Command::ServerCommand"server"っていうコマンドはいつ追加されたの?
  • instance.__send__("server", *args) でなんでいきなり Rails::Command::ServerCommandperformが実行されるの?
comments powered by Disqus