猫の魔法

主にruby系の技術メモを記載

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')

投稿されたイメージ

f:id:nekomaho:20200329225953p:plain:w300
slackへの投稿

色のバーを横に付けたい等、もう少しカスタマイズしたメッセージを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)

投稿されたイメージ

f:id:nekomaho:20200329231438p:plain:w300
slackへの投稿

Array#lengthと引数・ブロックなしArray#countの違い

自分はArrayで使える#lengthと#countの違いを生まれてこの方意識した事がなかったのですが、配列の長さを数えるだけであれば、lengthの方がcountより早いという話を聞いたので、自分なりに根拠を調べて見ました。

調査

そもそも両者のドキュメントを比べてみると、lengthは純粋に長さを求めるだけなのに対して、countは引数やブロックに数える対象の条件を加えることが出来ます。

MRIの場合countはArray用にメソッドがあるのですが、るりま的にはEnumerableがそれを代理しているようだったので、一旦はそちらのリンクを貼っています。

docs.ruby-lang.org

docs.ruby-lang.org

この時点ですでに何か違うのかなという気はするのですが、今回は「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.lenRARRAY_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_arityrb_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ソースコード完全解説」に記載されているシステム上で使用しているハッシュテーブルと同じ仕組みを使っているという認識です。 詳しくは下記のリンク先をご覧下さい。

i.loveruby.net

railsの起動プロセスを追う その4

はじめに

railsの起動までを見ていこうその4ということで、今回は rails new で生成される config.ru から見ていこうと思います。 以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。

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_stringRails::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_appapp が返す値は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.defaultRack::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公式のドキュメントがありました。詳しくはこちらをご覧下さい。

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 inheritedbase には下記の 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_configconfig.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_scriptRack::Builder.new のブロックに展開した上で、to_app した内容を最終的にメソッドの戻り値としています。 builder_scriptconfig.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.ruDSLを解析する為に、 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 クラスマクロはクラスインスタンス変数である initializersInitializer クラスのインスタンスを追加しています。 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

この処理を読む上で重要なのは iInitializer クラスのインスタンスであるということです。 Initializer クラスはインスタンスメソッドとして、bindを持っているので、下記の処理が呼ばれることになります。 @context は特異メソッドである initializerInitializer クラスのインスタンスを作成した際は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_eachrubyのトポジカルソートのライブラリです(今回はちゃんと調べる気がないので、一旦飛ばします。ごめんなさい) 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.rbperformから見ていこうと思います。 以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。

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.rbrails 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.rbrailsを構成している各モジュールの初期化モジュールを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調査系まとめ - 猫の魔法

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'を見て行きます。

コマンド処理への道のり

キッカー

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 を見ていみるとnamespacecommand_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 メソッドは何処に?という話になると思うのですが、これは継承元のThordispatch メソッドでした。 このメソッドでは自分自身、つまりRails::Command::ServerCommandnewしてインスタンスを作成しています。 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.nameserver になるようなインスタンスを返す形になります。

次に後半を見ていきます。 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_commandcommandrun しているだけです。 このとき渡しているselfRails::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 の場合を見ればいいんですが、この処理ではinstancename メソッドを実行しています。

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

となると、selfRails::Command::ServerCommandインスタンスなので、 server メソッドがRails::Command::ServerCommandの何処かに定義してあるはずなんです。 はずなんですが。。。どこを見ても見当たらないんですね。

serverとperformの関係

ここまで来ると目だけで追うのは難しい気がしてきたので、binding.pry して、server メソッドのsource_location を見てみたのですが、 何故かRails::Command::ServerCommandperform メソッドを指しているではありませんか。 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

ここで、methperformが入ってきた場合のことを考えると、create_command ではalias_method command_name, meth が実行されます。 このときcommand_nameserver になっています。

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.rbinstance.__send__(name, *args) で、nameserver の際に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);
    }
}

とりあえず今日はここまで。