3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Sinatraを支えるRuby記法 - 起動編

Last updated at Posted at 2019-05-22

はじめに

前回はリクエストに対する処理(getで書いたブロック)がどう登録されるのかを見てきました。今回は起動編、つまり、

ruby myapp.rb

とした場合に何が起こっているのかを見ていきます。

あらためてmyapp.rbを見てみましょう。

myapp.rb
require 'sinatra'

get '/' do
  'Hello world!'
end

myapp.rbにはサーバを起動するコードは書かれていません。が、先の通りにmyapp.rbを実行するとサーバが起動します。今回はこのからくりを探るのが目的です。

Sinatra::Application(sinatra/main.rbの方)

sinatra/main.rbに書かれているコードのうち前回無視した方を見てみます。

sinatra/main.rbより抜粋
module Sinatra
  class Application < Base

    # we assume that the first file that requires 'sinatra' is the
    # app_file. all other path related options are calculated based
    # on this path by default.
    set :app_file, caller_files.first || $0

    set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }

    # オプション処理
  end

  at_exit { Application.run! if $!.nil? && Application.run? }
end

setやcaller_files等はSinatra::Baseに書かれていますが大体予想通りのものなので省略します。
上記のコードで注目すべきは下から2行目、at_exitです。リファレンスにあるようにこのメソッドで登録したブロックはインタプリタが終了するときに実行されます。これが「サーバ起動を書いてないのにサーバが起動するからくり」です。

Sinatra::Base.run!

以上終わり、となるわけはもちろんなく、Application.run!の中に入っていきます。前回も見たようにApplicationにはあまり処理は書かれておらず、run!メソッドはBaseクラスの方に書かれています。

sinatra/base.rbより抜粋
      # Run the Sinatra app as a self-hosted server using
      # Thin, Puma, Mongrel, or WEBrick (in that order). If given a block, will call
      # with the constructed handler once we have taken the stage.
      def run!(options = {}, &block)
        return if running?
        set options
        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)

        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

detect_rack_handlerと関連コード

detect_rack_handlerはrun!のコメントに書かれているようなことが行われているわけですがRubyに慣れてないとわかりにくいと思うので説明します。

sinatra/base.rbより抜粋
      def detect_rack_handler
        servers = Array(server)
        servers.each do |server_name|
          begin
            return Rack::Handler.get(server_name.to_s)
          rescue LoadError, NameError
          end
        end
        fail "Server handler (#{servers.join(',')}) not found."
      end

「で、serverはどうなってるの?」と探すと以下のようになっています。

sinatra/base.rbより抜粋
    set :server, %w[HTTP webrick]

    if ruby_engine == 'macruby'
      server.unshift 'control_tower'
    else
      server.unshift 'reel'
      server.unshift 'puma'
      server.unshift 'mongrel'  if ruby_engine.nil?
      server.unshift 'thin'     if ruby_engine != 'jruby'
      server.unshift 'trinidad' if ruby_engine == 'jruby'
    end

run!のコメントと違ってるようなw
それはともかく、このsetやif文がどこに書かれているかというと、SinatraモジュールのBaseクラス直下です。
Rubyは(Pythonもですが)「クラス定義に普通の実行文が書ける。それらの実行文はクラスが定義される際に実行される(そもそもメソッド定義なども実行文である)」という特徴があります。ここら辺がJavaなどとは大きく異なる点です。

start_server

サーバの起動はstart_serverメソッドで行われます。
ここで重要なのはRackハンドラに渡しているのはApplicationクラスオブジェクト1という点です。つまり、リクエストが届いてハンドラから呼び出される場合、Applicationクラスのクラスメソッドのcallが呼び出されることになります。call後の流れについては詳しくは処理編で説明します。

sinatra/base.rbより抜粋
      # 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|
          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

prototypeメソッド

start_serverでは初めにprototypeというメソッドが呼び出されています。起動編の最後にこのメソッドについて見てみましょう。

sinatra/base.rbより抜粋
      # The prototype instance used to process requests.
      def prototype
        @prototype ||= new
      end

prototypeはクラスメソッドなので、newとはつまり、「Application.new」ということです。

「つまり、ここでApplicationクラスのインスタンスが作られてるんだな」と普通は考えると思いますが、話はもう少し複雑です(複雑でなければ取り上げません)
以下のコードを見てください。

sinatra/base.rbより抜粋
      # Create a new instance without middleware in front of it.
      alias new! new unless method_defined? :new!

      # Create a new instance of the class fronted by its middleware
      # pipeline. The object is guaranteed to respond to #call but may not be
      # an instance of the class new was called on.
      def new(*args, &bk)
        instance = new!(*args, &bk)
        Wrapper.new(build(instance).to_app, instance)
      end

なんじゃこりゃ( ゚д゚)
そう、なんとRubyはインスタンスを作るnewメソッドを差し替えることができるのです。つまり以下のことが行われています。

  • 本来のnew→new!にエイリアスすることで保存
  • newを再定義

buildメソッドではコメントにあるようにミドルウェアの初期化が行われ、それがWrapperインスタンスにくるまれます。まあそこら辺については本題ではないので次のリクエスト処理編では無視して読み進めていきます。

ここまでのまとめ

以上ここまでサーバ起動について見てきました。大事なのはat_exitです。他にも「自分で書くことはないかもしれないけどライブラリ読む際には出てくるかもしれない記法」がいくつかありました。

  • Sinatraでサーバ起動コードを書かなくてもサーバが起動するのはat_exitにサーバ起動が書かれているから。
  • Rubyではクラス定義直下に実行文が書ける。うまく利用すると環境に応じた処理を記述することなどができる。
  • クラスメソッド中で単に「new」と書かれている場合、「そのクラスのインスタンスを生成する(self.new。selfはクラスオブジェクト)」という意味である。
  • Rubyではnewメソッドを再定義することが可能である(本来のnewはaliasを使うことで保存できる)

処理編に続く。

  1. 書かれているのはBaseクラスですが普通にアプリを動かす際にはBaseクラスを継承したApplicationクラスがselfです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?