Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: Sinatraの`ruby app.rb`からWeb Serverが起動するまでのコードを読んでみた。

今まで暗黙的に使ってたRailsのコードをちゃんと読もうと思ったのですが、最初からRailsのコードを読んでいくと深みにはまりそうだったので、まずはシンプルなsinatraの実装を読んでみようということで、Sinatra(ver 2.0.5)のWeb Serverが起動するまでのコードを読んでみました🎩

sinatrarb.com

※ちなみにSinatraの由来は、アメリカの歌手「フランク・シナトラ」が由来みたいですね🇺🇸

Sinatraアプリの実行

今回は下記のようなファイルを作成し、

# app.rb
require 'sinatra'

get '/' do
  'Hello world!'
end

実行した際に下記のような起動メッセージが表示されるまでを見ていきます👀

$ ruby myapp.rb 
[2019-07-27 17:36:15] INFO  WEBrick 1.4.2
[2019-07-27 17:36:15] INFO  ruby 2.6.1 (2019-01-30) [x86_64-darwin17]
== Sinatra (v2.0.5) has taken the stage on 4567 for development with backup from WEBrick 

Sinatraのメインライブラリの読み込み

まずはapp.rbの最初に記載されたrequire 'sinatra'が実行され、sinatraが読み込まれます。

https://github.com/sinatra/sinatra/blob/master/lib/sinatra.rb

sinatraの中では、'sinatra/main'が読み込まれて、enable :inline_templatesが実行されます。

require 'sinatra/main'
enable :inline_templates

enableメソッドが呼ばれるとset(:inline_templates, true)が実行され、singleton_classにメソッドが定義されます。 https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1256

def enable(*opts)
  opts.each { |key| set(key, true) }
end

set(:inline_templates, true)はなんか、ゴニョゴニョして最後はdefine_singletonで、singleton_classinline_templatesへのsetterとgetterが定義される。 https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1220

def set
  # ︙ なんかオプション値のErrorチェックしてraiseとかする処理
  setter = proc { |val| set option, val, true }
  getter = proc { value }
  # ︙ なんかProcとかHashを渡された時を考慮してgetter、setterを調整する処理
  define_singleton("#{option}=", setter)
  define_singleton(option, getter)
  define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
  self
end

# singleton_classにメソッドを定義する
def define_singleton(name, content = Proc.new)
  singleton_class.class_eval do
    undef_method(name) if method_defined? name
    String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
  end
end

sinatra/mainを読み込むと、Sinatra::Applicationが定義されます。 定義時にsingleton_classへの実行ファイル及び実行判定のメソッドの定義と、実行時のオプションのパース、そしてWebServerの起動処理が実行されます。 https://github.com/sinatra/sinatra/blob/master/lib/sinatra/main.rb

require 'sinatra/base'

module Sinatra
  class Application < Base
    # app_fileで実行ファイルのパス(app.rb)にアクセスできるようにsingleton_classにメソッドを定義
    set :app_file, caller_files.first || $0
    # runでapp_fileの値と引数で渡ってきたファイル名を絶対パスで比較するようにsingleton_classにメソッドを定義
    # sinatraでは、実行中のファイルと引数で渡されたファイルが等しい場合に実行(ruby app.rb)とみなしている?
    set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }


    # オブション周りの設定
    if run? && ARGV.any?
      require 'optparse'
      OptionParser.new { |op|
        op.on('-p port',   'set the port (default is 4567)')                { |val| set :port, Integer(val) }
        op.on('-o addr',   "set the host (default is #{bind})")             { |val| set :bind, val }
        op.on('-e env',    'set the environment (default is development)')  { |val| set :environment, val.to_sym }
        op.on('-s server', 'specify rack server/handler (default is thin)') { |val| set :server, val }
        op.on('-q',        'turn on quiet mode (default is off)')           {       set :quiet, true }
        op.on('-x',        'turn on the mutex lock (default is off)')       {       set :lock, true }
      }.parse!(ARGV.dup)
    end
  end

  # 例外が発生してない、かつ実行の場合に、インタプリタ終了時(app.rbの実行完了後)にrun!を実行
  # https://docs.ruby-lang.org/ja/latest/method/Kernel/m/at_exit.html
  at_exit { Application.run! if $!.nil? && Application.run? }
end

ここまでがsinatraのメインモジュールの読み込み処理です📦

Web Serverの起動処理

実際のServer起動処理を見ていきます👀
まずrun!では、RackHandlerのオブジェクトを取得し、設定を反映後Web Serverを起動します。

https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1448

def run!(options = {}, &block)
  return if running?
  set options
  # RackHandlerのオブジェクトを取得(WebServerの本体?)してRename
  handler         = detect_rack_handler
  handler_name    = handler.name.gsub(/.*::/, '')
  # 設定の反映
  server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
  server_settings.merge!(:Port => port, :Host => bind)

  # Web Serverの起動
  begin
    start_server(handler, server_settings, handler_name, &block)
  rescue Errno::EADDRINUSE
    $stderr.puts "== Someone is already performing on port #{port}!"
    raise
  ensure
    quit!
  end
end

実際のWeb Serverの起動処理は、handler.runでWeb Serverを起動し、標準出力に、== Sinatra (v2.0.5) has taken the stage on 4567 for development with backup from WEBrickが表示しています🎉

https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1518

# Starts the server by running the Rack Handler.
def start_server(handler, server_settings, handler_name)
  # Ensure we initialize middleware before startup, to match standard Rack
  # behavior, by ensuring an instance exists:
  prototype
  # Run the instance we created:
  handler.run(self, server_settings) do |server|
    # 終了 or cgiじゃなければ(?)開始のメッセージを表示
    # suppress_messages?の意味がよく分からなかった。。。
    unless suppress_messages?
      $stderr.puts "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
    end

    setup_traps
    set :running_server, server
    set :handler_name,   handler_name
    server.threaded = settings.threaded if server.respond_to? :threaded=

    yield server if block_given?
  end
end

とりあえず、このような流れでWeb Serverが起動しているようですね!

おわりに

今回はSinatraの実行からWeb Serverを起動するまでを見てみました。 実際のWebServerの起動についてはもっと手順が必要だと思っていたのですが、意外とシンプルでしたね💦 ※Rack::Handler.getでWebServerのオブジェクトを取得してRack::Handler.runする感じ。

Rackにかなりラップされているっぽい、かつRailsを扱っていてもRackはよく聞くので、こちらもちゃんと理解したいですね😥

github.com

参考

yktwww.hatenablog.com