railsの起動プロセスを追う その1
はじめに
運良くruby/rails を使うweb系企業に転職して気がつけばもう1年。 色々あったけど、やっぱり私はrubyが好きです(by キキ)
しばらくブログを放置していましたが、せっかくインプットしたことをアウトプットしないのは勿体無いなぁと思ったので、
今回から何回かrails s
してから実際サーバが立ち上がるまでどういう風にrailsのコードが実行されるのか雑にソースを追ってみようと思っています。
ゴールとしては Rails.application
を rackに突っ込んで、アプリケーションサーバが起動出来るところまで追えればいいかなと考えています。
言うは簡単ですが、これをやるのは色々と周辺知識(rackとか、rackとか、rackとか)が必要になるので、そこも合わせて読んでいけるといいなと考えています。
ただ本当に公私共々忙しいので、更新速度は亀みたいになりそうです。。。もしこのブログを読んでくれる人がいたなら申し訳ないです。
追記:以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。
rails s事始め
railsコマンド
まずは作ったrailsアプリの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以前はとりあえず飛ばすとして、次の関心事はrequire_relative '../config/boot'
ですが、これもbundlerのrequireだったりキャッシュのロードだったりで起動には直接関係ないです。
ということでrequire 'rails/commands'
を見て行きます。
コマンド処理への道のり
キッカー
- https://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/commands.rb
ここでは省略形で呼ばれたコマンドを正しい形に戻した上で、requireしたタイミングでCommandのクラスメソッドであるinvokeを呼んでいるだけになります。
一応補足すると、
bin/rails s
を指定するとcommand
はserver
になります。
Rails::Command.invoke command, ARGV
コマンドを実行するためのクラスの特定
https://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/command.rb#L32
Command.invoke
で注目すべきはfind_by_namespace
で指定されたコマンドのサブクラスのクラス名を持ってきている部分と、
そのクラス名を使ってperform
メソッドを呼んでいるところです。
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
とりあえず、find_by_namespace
を見ていみるとnamespace
とcommand_name
を使ってlookups
というところに文字列を入れていて、
これを lookup
というメソッドに渡して、何故かsubclasses
メソッドからnamespaces
というのを引いています。
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
パット見分からないのですが、 lookup(lookups)
では一致するコマンドのrb をrequir
していて、serverの場合はhttps://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/commands/server/server_command.rbをrequireしています。ServerCommand
クラスは Command
のサブクラスなので、
subclasses
で引いて来れるんですね。よく出来ています。
namespaces
と言っているのは複数の候補が見つかった場合にそれを格納するので複数形になっていますが、とりあえずは気にしなくて良さそうです。
今回はserver
を指定しているのでRails::Command::ServerCommand
が返る形になります(はず。たぶん。)
performの闇
lib/rails/commands.rb
に戻って、find_by_namespace
で取得したクラスに対して、以下でperform
メソッドを呼んでいます。
https://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/command.rb#L46
こっからが鬼のようにややこしいのですが、perform
メソッドは特異クラスのメソッドになります。つまり Rails::Command::ServerCommand
の特異クラスの perform
メソッドが呼ばれるのですが、Rails::Command::ServerCommand
の定義を見ると perform
メソッドはインスタンスメソッドの定義しかないのです。
つまりどっかに特異クラスの perform
メソッドが定義されているはずで、探してみるとsuperクラスである Rails::Command::Base
に定義があります。
rails/base.rb at 5-2-stable · rails/rails · GitHub
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
これがRails::Command::ServerCommand
のインスタンス化をしているのと思いきや、このメソッドは dispatch
メソッドを呼び出して終わります。
では dispatch
メソッドは何処に?という話になると思うのですが、これは継承元のThor
の dispatch
メソッドでした。
このメソッドでは自分自身、つまりRails::Command::ServerCommand
をnew
してインスタンスを作成しています。
thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub
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)]
retrieve_command_name
では given_args
の最初の値を持ってきています。こいつにはserver
が入っているはずなので、meth
の内容はserver
という文字列になります。
thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub
# Retrieve the command name from given args. def retrieve_command_name(args) #:nodoc: meth = args.first.to_s unless args.empty? args.shift if meth && (map[meth] || meth !~ /^\-/) end
話をdispatch
に戻して続きを見ていきます。
all_commands
は 下で解説する method_add
で呼ばれる created_command
内で生成されていて meth
と一致するThor::Command
のインスタンスを返します。
今回の場合は、meth.name
が server
になるようなインスタンスを返す形になります。
次に後半を見ていきます。 thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub
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 || [])
!command && config[:invoked_via_subcommand]
の部分と if command 〜 else
の部分はコマンドが見つからなかった場合の処理と、
オプションが見つからなかった場合の処理なので、今回は関係ありません。
instance = new(args, opts, config)
で new
しているのは自分自身、つまりRails::Command::ServerCommand
です。
今回はblock
は取っていないので、yield instance if block_given?
は関係ありません。
args[Range.new(arguments.size, -1)]
は serverコマンドにオプションを渡したときに必要になってくるようですが、
ちょっと今回は置いておきます(もう考えることが手一杯でめんどくさい。。。)
その後invoke_command
メソッドを呼び出して何かしらしているのですが、ここからがまた魔境です。
invoke_command
はcommand
を run
しているだけです。
このとき渡しているself
は Rails::Command::ServerCommand
のインスタンスですね。
thor/invocation.rb at v0.20.0 · erikhuda/thor · GitHub
def invoke_command(command, *args) #:nodoc: current = @_invocations[self.class] unless current.include?(command.name) current << command.name command.run(self, *args) end end
run
の中身はとりあえず public_method
の場合を見ればいいんですが、この処理ではinstance
のname
メソッドを実行しています。
thor/command.rb at v0.20.0 · erikhuda/thor · GitHub
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
このname
は 後段で出てくるmethod_added
中で呼ばれる create_command
で生成されています。
thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub
def create_command(meth) #:nodoc: @usage ||= nil @desc ||= nil @long_desc ||= nil @hide ||= nil if @usage && @desc base_class = @hide ? Thor::HiddenCommand : Thor::Command commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options) @usage, @desc, @long_desc, @method_options, @hide = nil true elsif all_commands[meth] || meth == "method_missing"
このときname
には server
という文字列が入ってきます。
thor/command.rb at v0.20.0 · erikhuda/thor · GitHub
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/ def initialize(name, description, long_description, usage, options = nil) super(name.to_s, description, long_description, usage, options || {}) end
となると、self
が Rails::Command::ServerCommand
のインスタンスなので、 server
メソッドがRails::Command::ServerCommand
の何処かに定義してあるはずなんです。
はずなんですが。。。どこを見ても見当たらないんですね。
serverとperformの関係
ここまで来ると目だけで追うのは難しい気がしてきたので、binding.pry
して、server
メソッドのsource_location
を見てみたのですが、
何故かRails::Command::ServerCommand
の perform
メソッドを指しているではありませんか。
perform
メソッドの自体は ServerCommand
クラスの下記部分になります。
rails/server_command.rb at 5-2-stable · rails/rails · GitHub
名前が違う物を指すということは、考えられることはalias_method
しているのかなと思い調べてみると、railties/lib/rails/command/base.rb
で怪しげな処理を見つけました。
alias_method command_name, meth
の部分が怪しげです。
rails/base.rb at 5-2-stable · rails/rails · GitHub
def create_command(meth) if meth == "perform" alias_method command_name, meth else # Prevent exception about command without usage. # Some commands define their documentation differently. @usage ||= "" @desc ||= "" super end end
なんですが、今度はこのメソッドを呼び出しているところがなかなか見つからないんですね。 怪しいところをくまなく探すと、今度はThorの方に怪しげな箇所を見つけました。
method_added
というのはメソッドが定義された際に呼ばれるrubyのコールバックです。
つまり、def hogehoge
が読み込まれた時点でコールバックとして呼ばれます。
meth
には定義されたメソッドの名前が入ります。
thor/base.rb at v0.20.0 · erikhuda/thor · GitHub
# Fire this callback whenever a method is added. Added methods are # tracked as commands by invoking the create_command method. def method_added(meth) meth = meth.to_s if meth == "initialize" initialize_added return end # Return if it's not a public instance method return unless public_method_defined?(meth.to_sym) @no_commands ||= false return if @no_commands || !create_command(meth) is_thor_reserved_word?(meth, :command) Thor::Base.register_klass_file(self) end
ここで、meth
に perform
が入ってきた場合のことを考えると、create_command
ではalias_method command_name, meth
が実行されます。
このときcommand_name
はserver
になっています。
rails/base.rb at 5-2-stable · rails/rails · GitHub
# Return command name without namespaces. # # Rails::Command::TestCommand.command_name # => 'test' def command_name @command_name ||= begin if command = name.to_s.split("::").last command.chomp!("Command") command.underscore end end end
つまり、perform
メソッドは server
メソッドとしても存在することになります。
これで、thor/command.rb
のinstance.__send__(name, *args)
で、name
が server
の際にRails::Command::ServerCommand#perform
が呼ばれることの謎が解けました。
継承が重なると定義を探すのが大変ですね。 とりあえず切りがいいので今回はここまでです。
まとめ
rails
コマンド自体の処理は薄いrails s
した後の処理はCommand.invoke
内で行っている- コマンドを目的のクラス(Rails::Command::ServerCommand)にdispatchする処理はrails以外にthorも複雑に絡んでいる。
aliasメソッドを使って、
performメソッドを
server` メソッドとして呼び出せるようにしている。
次回はperformメソッドから見て行きます。