SlackTsuribariというgemを作った
SlackTsuribariというSlackのIncoming Webhookを使う為のラッパーgemを作りました。
目的
- 自分の勉強用(Github Actionとか使いたかった)
- SlackのIncoming Webhook ラッパーのgemの代表格であるslack-notifierが長らく更新されずにいたので、メンテ状態にあるgemがほしかった
使い方
とりあえずslackのIncoming Webhookを使う為の最低限の機能だけを持ったgemにしました。 使うにはまずHookを用意して、そのHookを使ってメッセージを投げるような感じになります。
とりあえずテキストのメッセージを投げるだけなら、SlackのIncoming Webhookを設定したあとに以下だけでメッセージをslackにpost出来ます。
hook = SlackTsuribari::Hook.config('Incoming WebhookのURL') SlackTsuribari::Angler.easy_throw!(hook, 'test message')
投稿されたイメージ
色のバーを横に付けたい等、もう少しカスタマイズしたメッセージをpostする場合は、 SlackのIncoming Webhookのメッセージ添付ファイルのフォーマットに沿って、attachments属性を設定する形にします。 attachmentに何を指定できるかはSlackのリファレンス が参考になると思います。
hook = SlackTsuribari::Hook.config('Incoming WebhookのURL') payload = { attachments: [ fallback: 'test message', text: 'test message', color: '#00FF00', ] } SlackTsuribari::Angler.throw!(hook, payload)
投稿されたイメージ
Array#lengthと引数・ブロックなしArray#countの違い
自分はArrayで使える#lengthと#countの違いを生まれてこの方意識した事がなかったのですが、配列の長さを数えるだけであれば、lengthの方がcountより早いという話を聞いたので、自分なりに根拠を調べて見ました。
調査
そもそも両者のドキュメントを比べてみると、lengthは純粋に長さを求めるだけなのに対して、countは引数やブロックに数える対象の条件を加えることが出来ます。
※ MRIの場合countはArray用にメソッドがあるのですが、るりま的にはEnumerableがそれを代理しているようだったので、一旦はそちらのリンクを貼っています。
この時点ですでに何か違うのかなという気はするのですが、今回は「lengthの方がcountより早い」の根拠が知りたいのでもう少し深掘りしてみます。
まずlengthの方を見ていきます。
https://ruby-doc.org/core-2.6.3/Array.html#method-i-length
こちらの方はRARRAY_LEN
というマクロに配列を渡して、その長さを返しています。
static VALUE rb_ary_length(VALUE ary) { long len = RARRAY_LEN(ary); return LONG2NUM(len); }
ここで気になるのは RARRAY_LEN
マクロの実装なので、それを追ってみます。
#define RARRAY_LEN(a) rb_array_len(a)
static inline long rb_array_len(VALUE a) { return (RBASIC(a)->flags & RARRAY_EMBED_FLAG) ? RARRAY_EMBED_LEN(a) : RARRAY(a)->as.heap.len; }
RARRAY_EMBED_LENは短い配列のときだけ使用する物らしいので一旦読み飛ばすと、RARRAY(a)->as.heap.len
をRARRAY_LEN
は返しているようです。
RARRAY(a)->as.heap.len
は構造体メンバの静的な値を返しているだけな為、O(1)で配列の長さを返せるようになっています。
次にcountを見ていきます。
https://ruby-doc.org/core-2.6.3/Array.html#method-i-count
委細は省くのですが、countが引数なし、ブロックなしで呼ばれる場合の動作はreturn LONG2NUM(RARRAY_LEN(ary)
になる為、lengthと変わらないように見えます。
違うところと言えば、rb_check_arity
と rb_block_given_p
を呼び出しているところ位なので、これのオーバーヘッド分lengthが有利になるかなという気はしました。
static VALUE rb_ary_count(int argc, VALUE *argv, VALUE ary) { long i, n = 0; if (rb_check_arity(argc, 0, 1) == 0) { VALUE v; if (!rb_block_given_p()) return LONG2NUM(RARRAY_LEN(ary)); for (i = 0; i < RARRAY_LEN(ary); i++) { v = RARRAY_AREF(ary, i); if (RTEST(rb_yield(v))) n++; } } else { VALUE obj = argv[0]; if (rb_block_given_p()) { rb_warn("given block not used"); } for (i = 0; i < RARRAY_LEN(ary); i++) { if (rb_equal(RARRAY_AREF(ary, i), obj)) n++; } } return LONG2NUM(n); }
手元のPCで検証してみると、わずかではありますがlengthの方が早かったです。
検証ソース
start = Time.now (0...10_000_000).each do a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] a.length end finish = Time.now puts "length:#{finish - start}" start = Time.now (0...10_000_000).each do a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] a.count end finish = Time.now puts "count:#{finish - start}"
結果
ruby test.rb length:1.01813 count:1.258869
Array#include?の速度
rubyを書いているとよくArray#include?
を使う事が多いのですが、これのパフォーマンスが気になったの調査して見ました。
パフォーマンスの調査としては最悪のケースを考えたいので、要素が引っかからない場合(線形探索と仮定した場合、Arrayのすべての要素を舐める必要がある)を対象とします。
def make_array_number_of(n) array = [] n.times do |i| array << "hoge#{i}" end array end def include_performance(array) a=Time.now pp array.include?("gaga") b=Time.now pp b-a end [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000].each do |n| pp "#{n}s array" make_array_number_of(n).tap do |array| include_performance(array) end end
結果
someone $ ruby performance.rb "1s array" false 2.7e-05 "10s array" false 3.1e-05 "100s array" false 3.7e-05 "1000s array" false 9.3e-05 "10000s array" false 0.000857 "100000s array" false 0.001764 "1000000s array" false 0.016368 "10000000s array" false 0.167093 "100000000s array" false 1.78809
要素が増えると線形的に探索時間が増加している(要素が10倍になると、探索時間も10倍)になるように見えます。
つまりArrayに対する include?
はArrayの要素が増えて来ると、それだけボトルネックになるようです。
ちなみにHashを使うと、要素に対してO(1)でアクセスする事が出来るようになります。
def make_array_number_of(n) array = [] n.times do |i| array << "hoge#{i}" end array end def include_performance(array) a=Time.now pp array.include?("gaga") b=Time.now pp b-a end def make_hash_number_of(n) hash = {} n.times do |i| hash["hoge#{i}"] = true end hash end def hash_performance(hash) a=Time.now pp hash.include?("gaga") b=Time.now pp b-a end [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000].each do |n| pp "#{n}s array" make_array_number_of(n).tap do |array| include_performance(array) end make_hash_number_of(n).tap do |hash| hash_performance(hash) end end
結果
someone $ ruby performance.rb "1s array" false 2.7e-05 false 2.1e-05 "10s array" false 3.1e-05 false 2.9e-05 "100s array" false 6.6e-05 false 3.6e-05 "1000s array" false 6.8e-05 false 0.000114 "10000s array" false 0.000376 false 9.1e-05 "100000s array" false 0.006702 false 7.7e-05 "1000000s array" false 0.01912 false 0.000101 "10000000s array" false 0.171654 false 7.1e-05 "100000000s array" false 1.888531 false 0.000158
こちらについてはHashの要素数がどれだけ増えても、探索時間に変化はありませんでした。 つまり配列の要素にするよりはハッシュのキーにした方が早くアクセス出来るということになります。
おまけ
Cのソースを見てみると、同じ include?
でもかなり実装に差異があることが分かります。
Array
/* * call-seq: * ary.include?(object) -> true or false * * Returns +true+ if the given +object+ is present in +self+ (that is, if any * element <code>==</code> +object+), otherwise returns +false+. * * a = [ "a", "b", "c" ] * a.include?("b") #=> true * a.include?("z") #=> false */ VALUE rb_ary_includes(VALUE ary, VALUE item) { long i; VALUE e; for (i=0; i<RARRAY_LEN(ary); i++) { e = RARRAY_AREF(ary, i); if (rb_equal(e, item)) { return Qtrue; } } return Qfalse; }
Hash
/* * call-seq: * hsh.has_key?(key) -> true or false * hsh.include?(key) -> true or false * hsh.key?(key) -> true or false * hsh.member?(key) -> true or false * * Returns <code>true</code> if the given key is present in <i>hsh</i>. * * h = { "a" => 100, "b" => 200 } * h.has_key?("a") #=> true * h.has_key?("z") #=> false * * Note that <code>include?</code> and <code>member?</code> do not test member * equality using <code>==</code> as do other Enumerables. * * See also Enumerable#include? */ MJIT_FUNC_EXPORTED VALUE rb_hash_has_key(VALUE hash, VALUE key) { if (RHASH_AR_TABLE_P(hash) && ar_lookup(hash, key, 0)) { return Qtrue; } else if (RHASH_ST_TABLE_P(hash) && st_lookup(RHASH_ST_TABLE(hash), key, 0)) { return Qtrue; } return Qfalse; }
static int ar_lookup(VALUE hash, st_data_t key, st_data_t *value) { st_hash_t hash_value = do_hash(key); unsigned bin = find_entry(hash, hash_value, key); if (bin == RHASH_AR_TABLE_MAX_BOUND) { return 0; } else { HASH_ASSERT(bin < RHASH_AR_TABLE_MAX_BOUND); if (value != NULL) { *value = RHASH_AR_TABLE_REF(hash, bin)->record; } return 1; } }
static inline st_hash_t do_hash(st_data_t key) { st_hash_t hash = (st_hash_t)(*objhash.hash)(key); return (RESERVED_HASH_VAL == hash) ? RESERVED_HASH_SUBSTITUTION_VAL : hash; }
ハッシュの仕組みが分かっていないと難しいですね。(st_hash_t)(*objhash.hash)(key)
はおそらく hash[key]
の事でしょう。
正直これ以上追うのはしんどかったので追っていないのですが、仕組みとしては「Rubyソースコード完全解説」に記載されているシステム上で使用しているハッシュテーブルと同じ仕組みを使っているという認識です。
詳しくは下記のリンク先をご覧下さい。
railsの起動プロセスを追う その4
はじめに
railsの起動までを見ていこうその4ということで、今回は rails new
で生成される config.ru
から見ていこうと思います。
以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。
※この起動プロセスについてはrails公式のドキュメントがあります。詳しくはこちらをご覧下さい。
run
前回までで、下記の require_relative 'config/environment'
の解析を終えたので、今回は run Rails.application
から見ていきます。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
Rails.applicationの部分はその3で調査した下記コードから Rails::Application
を継承したクラスのインスタンスが入ります。
rails/rails.rb at v5.2.1 · rails/rails · GitHub
def application @application ||= (app_class.instance if app_class) end
rails/application.rb at v5.2.1 · rails/rails · GitHub
def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) ActiveSupport.run_load_hooks(:before_configuration, base) end
./config/application.rb
module TestRails class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2
run
自体は引数に渡ってきた Rails::application
を @app
にセットするだけです。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
# Takes an argument that is an object that responds to #call and returns a Rack response. # The simplest form of this is a lambda object: # # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] } # # However this could also be a class: # # class Heartbeat # def self.call(env) # [200, { "Content-Type" => "text/plain" }, ["OK"]] # end # end # # run Heartbeat def run(app) @run = app end
new_from_string の評価結果
これでやっと、 new_from_string
内の Rack::Builder.new {\n" + builder_script + "\n}}
が何を返してくるのかが分かりました。
※builder_scriptはconfig.ruの内容と等価です。
次の関心事は、Rack::Builder.new {\n" + builder_script + "\n}.to_app
が何をしているかです。
to_app
の実体は railties/lib/rails/application.rb
にあります。
この処理では自分自身、つまりRails::Application
を継承したクラスのインスタンスが返されます。
rails/application.rb at v5.2.1 · rails/rails · GitHub
def to_app #:nodoc: self end
よって、new_from_string
はRails::Application
を継承したクラスのインスタンスが返ることになります。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
def self.new_from_string(builder_script, file="(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end
ということで wrapped_app
の app
が返す値はself.parse_file -> build_app_and_options_from_config -> app->app から、Rails::Application
を継承したクラスのインスタンスが返ります。
再びwrapped_app
ここまでで、 wrapped_app
内の app
が何を返すかが分かりました。
次に、 build_app
が何をしているのかを見ていきます。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def wrapped_app @wrapped_app ||= build_app app end
build_appの内容は以下のようになっています。
rack/server.rb at 2.0.5 · rack/rack · GitHub
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
何やらいろいろと処理をしていますが、 middleware
は継承先のクラスで以下のように定義されていて、このメソッド自体は 引数のappの内容をそのまま返します。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def middleware Hash.new([]) end
これで再びstartメソッドの処理を追うことに戻れます。
再びstart
以下にstartのソースを再掲します。
rack/server.rb at 2.0.5 · rack/rack · GitHub
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). wrapped_app 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
ここまでで、wrapped_app
が行っていることは判明しています。
次の daemonize_app
は デーモン化をしているだけなので、特に読むところはありません。
その次の write_pid
はサーバが二重起動しないようにする為のpidが入ったロックファイルを作成するという物で、コード的には読むところはあるのですが、趣旨と外れるので飛ばします。
trap(:INT)
のブロックもその3で説明した Ctrl-Cのときの動作を定義する物なので飛ばします。
最後の server.run wrapped_app, options, &blk
は、今まで待ちに待ったサーバを起動する部分になります。
サーバの起動。
serverについては、rack側には以下の定義があります。
一般的には、@_server
に起動するサーバの実体が入ってくるようにします。
rack/server.rb at 2.0.5 · rack/rack · GitHub
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
例えば、Pumaの場合 Rack::Handler.default
に Rack::Handler::Puma
が入ってきます。
Puma側では下記のように self.run
メソッドが定義されていて、Pumaが起動されるようになっています。
puma/puma.rb at master · puma/puma · GitHub
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
これでやっと、一連のrailsの起動プロセスを追うことが出来ました。
まとめ
- 起動の際に使うアプリケーションは
Rails::Application
を継承したクラスのインスタンスを使っている。 - 起動するサーバはアプリケーションサーバ側で runメソッドを定義してある物を実行する。
railsの起動プロセスを追う その3
はじめに
また間が空いてしまいましたが、 railsの起動までを見ていこう第3段ということで、今回は rails/all.rb
から見ていこうと思います。
以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。
※今回書いていて今更気がついたのですが、この起動プロセスについてはrails公式のドキュメントがありました。詳しくはこちらをご覧下さい。
rails.rbの読み込みと、app_classへの値のセット
以下が前回最後に読んだコードになります。
rails/all.rb at v5.2.1 · rails/rails · GitHub
# frozen_string_literal: true require "rails" %w( active_record/railtie active_storage/engine action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie action_cable/engine rails/test_unit/railtie sprockets/railtie ).each do |railtie| begin require railtie rescue LoadError end end
ここの最初で require "rails"
で読み込んでいるファイルは railties/lib/rails.rb
になります。
rails.rbで起動に関わる重要な部分は def application
の部分です。
rails/rails.rb at v5.2.1 · rails/rails · GitHub
def application @application ||= (app_class.instance if app_class) end
この application
の実体は app_class
のインスタンスですが、これは Rails::Application
を継承した時点でセットされます。
rails/application.rb at v5.2.1 · rails/rails · GitHub
def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) ActiveSupport.run_load_hooks(:before_configuration, base) end
つまり、その2で紹介した ./config/application.rb
の下記部分が読み込まれた時点でセットされます。
上記のdef inherited
の base
には下記の Applicaiton
が渡ってくる事になります。
module TestRails class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2
その後 all.rb
では各モジュールのrailtieをrequireしていますが、これ自体は各モジュールの初期化の意味合いが強いので、今回は飛ばします(sprockets/railtieは飛ばしていいものか悩みましたが、話がややこしくなるだけなので今回は一旦飛ばします)
再びperform
ここまでで、サーバを起動する為の準備が完全に整ったので、再びperformに戻っていきます。 rails/server_command.rb at v5.2.1 · rails/rails · GitHub
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
前段までの話で、require APP_PATH
が実行されたときに何が起こるのかについては理解することが出来ました。
Dir.chdir(Rails.application.root)
は ./config/application.rb
内の Application
が持つ root
にカレントディレクトリを移すというのをやっています。
(おそらくですが、server.startする際にカレントディレクトリが重要になる局面があるのかなと勝手に推測しています)
実体は下記になります。
rails/rails.rb at v5.2.1 · rails/rails · GitHub
# Returns a Pathname object of the current Rails project, # otherwise it returns +nil+ if there is no project: # # Rails.root # # => #<Pathname:/Users/someuser/some/path/project> def root application && application.config.root end
ここでやっと、サーバ起動の本丸である server.start
に入っていきます。
serverの起動
server.start
の実体は、Rails::Server
クラスのインスタンスメソッド、start
です。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def start print_boot_information trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super ensure # The '-h' option calls exit before @options is set. # If we call 'options' with it unset, we get double help banners. puts "Exiting" unless @options && options[:daemonize] end
trap(:INT) { exit }
は INT= SIGINTをアプリケーションが受け取ったときに、ブロック内のコールバックである exit を実行するという物です。
SIGINTというのは割り込みの事で、Ctrl-CとかをするとOSから発生するシグナルの事です。
その他の部分はメソッド名を見れば何しているかが大体分かるので割愛します。
このメソッドは super をしていますが、 そもそも Rails::Server
クラスは Rack::Server
を継承している物なため、このsuperの先は Rackになります。
Rackのstart
Rack側のstartメソッドは以下になります。
rack/server.rb at 2.0.5 · rack/rack · GitHub
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). wrapped_app 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
wrapped_app
の部分より前は基本的にオプションの制御なので読み飛ばして問題ないです。
wrapped_app
は下記のように定義されています。重要なのはここで build_app
に渡している app
です。
このappは最終的に起動するサーバに渡される railsアプリケーション自身になります。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def wrapped_app @wrapped_app ||= build_app app end
appの行方
appの定義は Rack::Server
を継承している Rails::Server
側でオーバーライドされているので、一旦そちらの定義を見てみます。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def app @app ||= begin app = super if app.is_a?(Class) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. Please change `run #{app}` to `run Rails.application` in config.ru. MSG end app.respond_to?(:to_app) ? app.to_app : app end end
ぱっと見ると、 app = super
で継承元の def app
の結果を app
に入れているように見えます。
といことで、継承元である Rack::Server
を見てみると以下のような形になっています。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end
builderオプションが立っている場合とそうでない場合とで動作が違いますが、今回は立っていない方 = build_app_and_options_from_config
が呼ばれるパターンであると仮定します(これはrackの設定ファイルであるconfig.ruをrailsが持っている為で、build_app_and_options_from_config
は config.ru
から設定を読み込む形になっているからです)
build_app_and_options_from_config
の定義は以下のようになっています。
rack/server.rb at 2.0.5 · rack/rack · GitHub
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
重要なのは app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
の部分です。このメソッドが返す app
が最終的に起動するサーバに渡される railsアプリケーション自身になります。
Rack::Builder.parse_file
は以下で定義されています。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
def self.parse_file(config, opts = Server::Options.new) options = {} if config =~ /\.ru$/ cfgfile = ::File.read(config) if cfgfile[/^#\\(.*)/] && opts options = opts.parse! $1.split(/\s+/) end cfgfile.sub!(/^__END__\n.*\Z/m, '') app = new_from_string cfgfile, config else require config app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join('')) end return app, options end
railsの場合、ここで引数のconfigに渡ってくるのは、基本的に作成したアプリケーションのrootディレクトリにある config.ru
です。
なので、if config =~ /\.ru$/
の中の処理が実行されます。
この処理の中では cfgfile = ::File.read(config)
で config.ru
の中身を読み込んで、cfgfile.sub!(/^__END__\n.*\Z/m, '')
で最終行の改行を消した後に
app = new_from_string cfgfile, config
を呼んで app
を取得しています。
eval
new_from_string
は第一引数の内容を Rack::Builder.new
のブロックに渡した上で、to_app
メソッドを呼ぶ事でappを取得するメソッドです。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
def self.new_from_string(builder_script, file="(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end
なかなかトリッキーなことをしています。
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
は一旦後回しにして、evalの第2〜4の引数の内容を見ていきます。
第2引数の TOPLEVEL_BINDING
は evalの評価を TOPLEVEL_BINDING
で示したコンテキストで実行するという物です。これを指定しないと現在のコンテキストでevalが評価されるので、意図的にこれを指定している物と思われます。
第3引数のfileはスタックトレースに使われるファイル名で、今回は config.ru
です。
第4引数の0は評価される "Rack::Builder.new {\n" + builder_script + "\n}.to_app"
が何行目に書かれていると仮定するかという数字で、指定するとスタックトレース(だけではないですが)で以下のように表示が変わります。
irb(main):001:0> eval "raise",TOPLEVEL_BINDING, 'hoge.rb', 0 RuntimeError: from hoge.rb:in `<main>' from (irb):2:in `eval' from (irb):2 from /Users/hoge/.rbenv/versions/2.3.1/bin/irb:11:in `<main>' irb(main):002:0> eval "raise",TOPLEVEL_BINDING, 'hoge.rb', 5 RuntimeError: from hoge.rb:5:in `<main>' from (irb):3:in `eval' from (irb):3 from /Users/hoge/.rbenv/versions/2.3.1/bin/irb:11:in `<main>' irb(main):003:0>
ここまでで、evalの第2〜4の引数が何の意味を持つかが分かりました。
次は後回しにしていた eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
の中身を見ていきます。
evalで評価する物
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
では builder_script
を Rack::Builder.new
のブロックに展開した上で、to_app
した内容を最終的にメソッドの戻り値としています。
builder_script
は config.ru
の内容その物です。
ここまで config.ru
の内容をスルーしてきましたが、具体的なイメージがないとコードを追うのが難しくなってきたので、rails new
で新しく作成したrailsアプリケーションの config.ru
を見てみます。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
上記より、 Rack::Builder.new {\n" + builder_script + "\n}.to_app"
は以下と等価になります。
Rack::Builder.new { # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application }.to_app
Rack::Builder
Rack::Builderではブロックで渡された内容を評価します。 rack/builder.rb at 2.0.5 · rack/rack · GitHub
def initialize(default_app = nil, &block) @use, @map, @run, @warmup = [], nil, default_app, nil instance_eval(&block) if block_given? end
instance_eval
の部分はDSLを解析する際によく用いられる書き方です。今回の場合だと config.ru
のDSLを解析する為に、 Rack::Builder
クラスに対してブロックで渡された物を評価させるというやり方を取っています。
config.ruの解析
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'
で読み込んでいるファイルもrails.new
して作られる物です。内容は以下のような形になっています。
# Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize!
require_relative 'application'
で読み込んでいる application
は「rails.rbの読み込みと、app_classへの値のセット」で説明した application.rbですので説明は省きます。
次のRails.application.initialize!
の中身は以下の通りです。
rails/application.rb at v5.2.1 · rails/rails · GitHub
# 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ですが、これは Applicaton が継承しているEngineクラスが継承しているRailtieがincludeしているInitializableモジュールのメソッドです。 rails/application.rb at v5.2.1 · rails/rails · GitHub
class Application < Engine
rails/engine.rb at v5.2.1 · rails/rails · GitHub
class Engine < Railtie
rails/railtie.rb at v5.2.1 · rails/rails · GitHub
class Railtie autoload :Configuration, "rails/railtie/configuration" include Initializable
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
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
処理を見ていくと、run_initializers
は一回以上実行されないようにクラスインスタンス変数 @run
を持っています。
最初はこれが定義されていないので、initializers
に対してtsort
した物を順にrunしていく形をしています。
initializers
の定義は以下の通りです。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers @initializers ||= self.class.initializers_for(self) end
initializers_for
の処理の詳細は後でもう一度追うのですが、今はinitializers_chain
をmapしているところだけを掘っていきます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
initializers_chain
では、自身の継承チェーンを一番古い祖先から initializers
特異メソッドがあるかどうか = Initializable
が使えるクラスなのかどうなのかを判定し、
ある場合は 各クラスのクラスインスタンス変数 initializers
が定義されていればそれを、なければ空のCollection
クラスのインスタンスを Collection
クラスのインスタンスに追加していくということをしています。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_chain initializers = Collection.new ancestors.reverse_each do |klass| next unless klass.respond_to?(:initializers) initializers = initializers + klass.initializers end initializers end
どういうことかと言うと、例えば、engine.rb
は以下のように initializer
クラスマクロを使った初期化処理の登録を行っています。
rails/engine.rb at v5.2.1 · rails/rails · GitHub
initializer :load_config_initializers do config.paths["config/initializers"].existent.sort.each do |initializer| load_config_initializer(initializer) end end
この initializer
クラスマクロはクラスインスタンス変数である initializers
に Initializer
クラスのインスタンスを追加しています。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
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
クラスインスタンス変数である initializers
は特異メソッドである initializers
が呼ばれた際に値が入っていればその値をそのまま使われ、そうでなければ空のCollectionを返します。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers @initializers ||= Collection.new end
話を initializers_chain
を呼んでいる initializers_for
メソッドに戻します。
このメソッドで追わないといけない部分はi.bind(binding)
の部分です。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
この処理を読む上で重要なのは i
が Initializer
クラスのインスタンスであるということです。
Initializer
クラスはインスタンスメソッドとして、bind
を持っているので、下記の処理が呼ばれることになります。
@context
は特異メソッドである initializer
がInitializer
クラスのインスタンスを作成した際はnilが渡るので、基本的にはbindメソッドに渡されたcontextに沿ってInitializer
クラスのインスタンスが作り直されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def bind(context) return self if @context Initializer.new(@name, context, @options, &block) end
ここまで読むと、run_initializers
まで戻ってこれます。
tsort_each
は rubyのトポジカルソートのライブラリです(今回はちゃんと調べる気がないので、一旦飛ばします。ごめんなさい)
initializer.belongs_to?(group)
はとりあえず必ず true
になる(groupにはdefaultしか渡ってこない上に、今回の場合だとinitializerのgroupがdefault以外になることはないと思われる)ので、initializer.run(*args)
が実行されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
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(*args)
の定義は下記になります。@context
はこの段階では self
です。
この処理でinitializer
クラスマクロでブロックに渡された初期化処理の内容が実行されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def run(*args) @context.instance_exec(*args, &block) end
これでやっとconfig.ru
の解析をする部分まで戻ってきました。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
だいぶ長くなってしまったので、次回は run Rails.application
の部分から読んでいきます。
まとめ
- サーバを起動する直前で
config/application.rb
の内容を読み込んで各モジュールの初期化及び、@application
に自分自身を登録している。 - サーバの起動では、SIGINTをトラップ出来るようにしている。
- サーバの起動のほとんどは Rackの
start
メソッドが担当している。 - 'config/environment.rb' を読み込んだタイミングで、
initializer
クラスマクロで定義された初期化を行っている。
railsの起動プロセスを追う その2
はじめに
だいぶ間が空いてしまいましたが、
railsの起動までを見ていこう第2段ということで、今回はserver_command.rb
のperform
から見ていこうと思います。
以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。
Serverの起動
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
perform
の主目的はサーバの起動です。つまりこの解析が終わるとrailsの起動は完了することになります。
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
は大したことはしていないので飛ばして、Rails::Server.new
から見ていきます。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def initialize(options = nil) @default_options = options || {} super(@default_options) set_environment end
このメソッド自体はほとんど何もしていないので、super
先を見ていきます。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def initialize(options = nil) @ignore_options = [] if options @use_default_options = false @options = options @app = options[:app] if options[:app] else argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV @use_default_options = true @options = parse_options(argv) end end
これはインスタンスを返しているだけのようです。
ということで、server_command.rb
の方に戻ります。
次に注目すべきはtap
の中の以下の部分です。
require APP_PATH Dir.chdir(Rails.application.root) server.start
APP_PATH
はどこに定義さているのでしょうか?
実を言うと、追ってきたソースの中に一度出てきています。
記憶のいい方は覚えているかもしれないですが(自分は記憶が悪い方なので、一生懸命探しましたが。。。)、
一番最初の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は File.expand_path('../config/application', __dir__)
になります。
./config/application.rb
はrails new
したときに作成されるファイルで、中身は以下のような物になります。
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 TestRails class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2 # 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/all.rb
はrailsを構成している各モジュールの初期化モジュールをrequire
しています。
rails/all.rb at v5.2.1 · rails/rails · GitHub
require "rails" %w( active_record/railtie active_storage/engine action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie action_cable/engine rails/test_unit/railtie sprockets/railtie ).each do |railtie| begin require railtie rescue LoadError end end
あんまり一気にやろうとすると分からなくなってくるので今日はここまで。
ruby本体をコンパイルしてみる
久々の更新です。
今日は、rubyの本体のコードをちゃんとローカルに持ってきてコンパイル出来る状態にしてみました。 (本当は別の目的があったんですが、すんなりコンパイルが通ったのでやり方だけメモしておこうという算段。。。) 基本はGithubのruby/rubyのReadmeを見れば事足りますが、一部見ても難しいところがあったので、そのメモを残して置こうと思います。
前提
- 環境はMac(Mojave)です。
- Command Line Toolsを入れておく必要があります(なくても出来るかもですが、難易度上がりそう)
rubyのソースをgitからcloneする
$ git clone git@github.com:ruby/ruby.git
$ cd ruby
./configureを作る
環境に合わせてconfigureを作成してくれるautoconfを利用してこれを元 configureを作ります。
$ autoconf
./configureを実行する
makeを実行する前段として、環境による動作の違いやコンパイルの可否をチェックしてくれるconfigureスクリプトを流します。
Readmeだと ./configure
だけですが、自分の環境ではOpenSSL系とReadline系の組み込みライブラリが使えなかったので、OpenSSLとReadlineのインクルードファイル(C言語の.hが拡張子のファイル)の場所を指定して上げる形にしました(OpenSSLとReadlineについてはHomebrewでインストールしているので、そのインストールパスを指定しています)
--prefixにはrubyをインストールパスを設定します。複数バージョンインストールしたいので、ruby/trunkというディレクトリ以下にインストールすることにしました。
$ ./configure --prefix=$HOME/ruby/trunk --with-opt-dir=$(brew --prefix openssl):$(brew --prefix readline)
with_opt_dirを指定するとその下にパスからincludeとlibのパスを探してそれぞれLDFLAGS/CPPFLAGSにセットされるようです。
if test "${with_opt_dir+set}" = set; then : withval=$with_opt_dir; val=`echo "$PATH_SEPARATOR$withval" | sed "s|$PATH_SEPARATOR\([^$PATH_SEPARATOR]*\)| -I\1/include|g;s/^ //"` CPPFLAGS="$CPPFLAGS $val" val=`echo "$PATH_SEPARATOR$withval" | sed "s|$PATH_SEPARATOR\([^$PATH_SEPARATOR]*\)| -L\1/lib${rpathflag:+ $rpathflag\\\\1/lib}|g;s/^ //"` LDFLAGS="$LDFLAGS $val" LDFLAGS_OPTDIR="$val" OPT_DIR="$withval"
PATH_SEPARATORは「:」です。
# The user is always right. if test "${PATH_SEPARATOR+set}" != set; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || PATH_SEPARATOR=';' } fi
makeする
makeしてコンパイルします
$ make
make checkする
$ make check
拡張gemのビルド
minitest だったりrakeだったりをビルドします
$ make update-gems && make extract-gems
インストール
$ make install
確認
インストール先のディレクトリに移動してrubyのバージョンが正しいことを確認します。
$ cd $HOME/ruby/trunk/bin $ ./ruby --version
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メソッドから見て行きます。
rubyソースコードリーディング 1日目
特に理由はないが、読める所からrubyのソースを読もうと思う。 パット見途中で挫折しそうだが、何も無しに何処まで読めるか試してみるのもありかなと。
とりあえずはv2_5_0_rc1のmain.cから
int main(int argc, char **argv) { #ifdef RUBY_DEBUG_ENV ruby_set_debug_option(getenv("RUBY_DEBUG")); #endif
RUBY_DEBUG_ENVが有効ならruby_set_debug_optionを呼ぶ。
RUBY_DEBUG_ENVはRUBY_DEVELが有効であれば定義される。
#if RUBY_DEVEL && !defined RUBY_DEBUG_ENV # define RUBY_DEBUG_ENV 1 #endif
RUBY_DEVELはconfigure.acで定義されているか、こっちはm4マクロなので読み解くのがしんどい。 恐らく、RUBY_PATCHLEVELに入っている物のうち、-*に一致する場合はRUBY_DEVELがyesに、そうでない場合はnoが入るということだと思うが、 なんでそういうことをしているかはよく分からない。
AS_CASE([$RUBY_PATCHLEVEL], [-*], [RUBY_DEVEL=yes], [RUBY_DEVEL=no])
戻って、ruby_set_debug_optionはdebug.cで定義されていて、中でruby_each_wordsを呼んでいる。
void ruby_set_debug_option(const char *str) { ruby_each_words(str, set_debug_option, 0); }
set_debug_optionはdebug.c上でstatic voidとして定義されているので第二引数は関数ポインタと思われる(こう言う書き方が出来るは知らなかった)
static void set_debug_option(const char *str, int len, void *arg) { if (!ruby_env_debug_option(str, len, arg)) { fprintf(stderr, "unexpected debug option: %.*s\n", len, str); } }
とりあえず今日はここまで。