# Load the given file as a rackup file, treating the
# contents as if specified inside a Rack::Builder block.
#
# Treats the first comment at the beginning of a line
# that starts with a backslash as options similar to
# options passed on a rackup command line.
#
# Ignores content in the file after +__END__+, so that
# use of +__END__+ will not result in a syntax error.
#
# Example config.ru file:
#
# $ cat config.ru
#
# #\ -p 9393
#
# use Rack::ContentLength
# require './app.rb'
# run App
def self . load_file ( path , opts = Server :: Options . new )
options = {}
cfgfile = :: File . read ( path )
cfgfile . slice! ( /\A #{ UTF_8_BOM } / ) if cfgfile . encoding == Encoding :: UTF_8
if cfgfile [ /^#\\(.*)/ ] && opts
warn "Parsing options from the first comment line is deprecated!"
options = opts . parse! $1 . split ( /\s+/ )
end
cfgfile . sub! ( /^__END__\n.*\Z/m , '' )
app = new_from_string cfgfile , path
return app , options
end
Looks like Builder.new_from_string
is the key for getting to bottom of this. Let's look it up.
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self . new_from_string ( builder_script , file = "(rackup)" )
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding , builder =
TOPLEVEL_BINDING .
eval ( 'Rack::Builder.new.instance_eval { [binding, self] }' )
eval builder_script , binding , file
builder . to_app
end
Wow, look at that! This is where the magic happens. Let's dive into it.
Double eval magic
This code makes Rack::Builder
instance a top level context and evaluates
our script within that context. This provides the run
method to our config.ru
script, which is
actually defined in Rack::Builder#run
[1 ] .
TOPLEVEL_BINDING
is, simply, a binding of the top-level context[2 ] .
Why do we need it here? Couldn't we simply write:
def self . new_from_string ( builder_script , file = "(rackup)" )
bind , builder = Rack :: Builder . new . instance_eval { [ binding , self ] }
eval builder_script , bind , file
builder . to_app
end
I was confused by this, so I've asked the author, Benoit Daloze, to explain it and he did . Not having
TOPLEVEL_BINDING
would give the Rack
namespace to constants defined in our config.ru
:
# config.ru
p Builder # would print out Rack::Builder
That's why we need to evaluate within TOPLEVEL_BINDING
.
A simpler, and a previously used version of this magic is:
def self . new_from_string ( builder_script , file = "(rackup)" )
eval "Rack::Builder.new { \n " + builder_script + " \n }.to_app" ,
TOPLEVEL_BINDING , file , 0
end
However, this means that magic comments from config.ru
would be ignored, since they are not present at the beginning of the eval
-ed string. We would have to check for them and add them at the beginning of the string like this:
def self . new_from_string ( builder_script , file = "(rackup)" )
eval "# frozen_string_literal: true \n Rack::Builder.new { \n " +
builder_script + " \n }.to_app" ,
TOPLEVEL_BINDING , file , 0
end
Benoit's solution to this is much more elegant.
Going deeper
Calling run
in our config.ru
script, means that our journey continues to Rack::Builder#run
:
# 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
Since there are no other method calls to follow, it looks like we've come the end. Let's continue looking at branches that we've ignored previously.
The last thing we ignored was build_app
in wrapped_app
[↗ ] :
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
Inspecting middleware
further leads us to:
def middleware
default_middleware_by_environment
end
def default_middleware_by_environment
m = Hash . new { | h , k | h [ k ] = [] }
m [ "deployment" ] = [
[ Rack :: ContentLength ] ,
logging_middleware ,
[ Rack :: TempfileReaper ]
]
m [ "development" ] = [
[ Rack :: ContentLength ] ,
logging_middleware ,
[ Rack :: ShowExceptions ] ,
[ Rack :: Lint ] ,
[ Rack :: TempfileReaper ]
]
m
end
wrapped_app
is going to become a chain of middleware that's going to look like this:
Rack::ContentLength ⟶
Rack::CommonLogger ⟶
Rack::ShowExceptions ⟶
Rack::Lint ⟶
Rack::TempfileReaper ⟶
our app
The next thing we ignored is server.run
[↗ ] .
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
The default Rack handler is Webrick, so in our case, server.run
is actually Rack::Handler::Webrick.run
:
def self . run ( app , ** options )
environment = ENV [ 'RACK_ENV' ] || 'development'
default_host = environment == 'development' ? 'localhost' : nil
if ! options [ :BindAddress ] || options [ :Host ]
options [ :BindAddress ] = options . delete ( :Host ) || default_host
end
options [ :Port ] ||= 8080
if options [ :SSLEnable ]
require 'webrick/https'
end
@server = :: WEBrick :: HTTPServer . new ( options )
@server . mount "/" , Rack :: Handler :: WEBrick , app
yield @server if block_given?
@server . start
end
This simply starts the Webrick server and passes our middleware to it as app
.
And that's it! This article hopefully demystified what happens behind the curtain when you run rackup
and you hopefully better understand how it works now.
Notes and examples
Here's the simplified version of what Rack::Builder.new_from_string
is doing:
class Context
def test
puts "testing"
end
end
bind = Context . new . instance_eval { binding }
eval "test" , bind
The first argument for eval
is our
simplified config.ru
. Running this will print out "testing"
.
I'm just kidding. It's not "simply". Let me explain what the hell I am talking about. Ruby documentation says that Binding
"encapsulates the execution context at some particular place in the code and retain this context for future use". TOPLEVEL_BINDING
is simply Binding
of the top level-context. Here's an example:
foo = 42
def print_foo
print foo # undefined local variable or method `foo' for main:Object
end
begin ; print_foo ; rescue => ex ; puts ex . message ; end
def print_foo_with_top_level_binding
print TOPLEVEL_BINDING . eval ( "foo" ) # 42
end
print_foo_with_top_level_binding