about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-03-23 02:03:31 -0700
committerEric Wong <normalperson@yhbt.net>2009-03-23 14:42:41 -0700
commit0b095ea72fb849682a1185a626eef247b5afc1cd (patch)
treeb8f74a3160b3842bbebcf2fe3c5c917ec088e8eb
parent451a1022e16ec6307328125a41244a837c6edcdf (diff)
downloadunicorn-0b095ea72fb849682a1185a626eef247b5afc1cd.tar.gz
This resurrects old code from Mongrel to wrap the Rails
Dispatcher for older versions of Rails.  It seems that
Rails >= 2.2.0 support Rack, but only >=2.3 requires it.

I'd like to support Rails 1.2.x for a while, too.
-rw-r--r--README2
-rwxr-xr-xbin/unicorn_rails42
-rw-r--r--lib/unicorn/app/old_rails.rb23
-rw-r--r--lib/unicorn/app/old_rails/static.rb58
-rw-r--r--lib/unicorn/cgi_wrapper.rb139
5 files changed, 257 insertions, 7 deletions
diff --git a/README b/README
index 0850f2e..b53d7c6 100644
--- a/README
+++ b/README
@@ -90,7 +90,7 @@ of your application or libraries. However, your Rack application may use
 threads internally (and should even be able to continue running threads
 after the request is complete).
 
-=== Rack-enabled versions of Rails (v2.3.2+)
+=== for Rails applications (should work for all 1.2 or later versions)
 
 In RAILS_ROOT, run:
 
diff --git a/bin/unicorn_rails b/bin/unicorn_rails
index 177c109..fae6f4b 100755
--- a/bin/unicorn_rails
+++ b/bin/unicorn_rails
@@ -7,6 +7,7 @@ rails_pid = File.join(Unicorn::HttpServer::DEFAULT_START_CTX[:cwd],
                       "/tmp/pids/unicorn.pid")
 cmd = File.basename($0)
 daemonize = false
+static = true
 listeners = []
 options = { :listeners => listeners }
 host, port = Unicorn::Const::DEFAULT_HOST, 3000
@@ -123,7 +124,22 @@ rails_loader = lambda do ||
   when nil
     lambda do ||
       require 'config/environment'
-      ActionController::Dispatcher.new
+
+      # it seems Rails >=2.2 support Rack, but only >=2.3 requires it
+      old_rails = case ::Rails::VERSION::MAJOR
+      when 0, 1 then true
+      when 2 then Rails::VERSION::MINOR < 3 ? true : false
+      else
+        false
+      end
+
+      if old_rails
+        require 'rack'
+        require 'unicorn/app/old_rails'
+        Unicorn::App::OldRails.new
+      else
+        ActionController::Dispatcher.new
+      end
     end
   when /\.ru$/
     raw = File.open(config, "rb") { |fp| fp.sysread(fp.stat.size) }
@@ -147,12 +163,26 @@ app = lambda do ||
   require 'active_support'
   require 'action_controller'
   ActionController::Base.relative_url_root = map_path if map_path
+  map_path ||= '/'
+  inner_app = inner_app.call
   Rack::Builder.new do
-    use Rails::Rack::LogTailer unless daemonize
-    use Rails::Rack::Debugger if $DEBUG
-    map(map_path || '/') do
-      use Rails::Rack::Static
-      run inner_app.call
+    if inner_app.class.to_s == "Unicorn::App::OldRails"
+      $stderr.puts "LogTailer not available for Rails < 2.3" unless daemonize
+      $stderr.puts "Debugger not available" if $DEBUG
+      map(map_path) do
+        if static
+          require 'unicorn/app/old_rails/static'
+          use Unicorn::App::OldRails::Static
+        end
+        run inner_app
+      end
+    else
+      use Rails::Rack::LogTailer unless daemonize
+      use Rails::Rack::Debugger if $DEBUG
+      map(map_path) do
+        use Rails::Rack::Static if static
+        run inner_app
+      end
     end
   end.to_app
 end
diff --git a/lib/unicorn/app/old_rails.rb b/lib/unicorn/app/old_rails.rb
new file mode 100644
index 0000000..bb9577a
--- /dev/null
+++ b/lib/unicorn/app/old_rails.rb
@@ -0,0 +1,23 @@
+# This code is based on the original Rails handler in Mongrel
+# Copyright (c) 2005 Zed A. Shaw
+# Copyright (c) 2009 Eric Wong
+# You can redistribute it and/or modify it under the same terms as Ruby.
+# Additional work donated by contributors.  See CONTRIBUTORS for more info.
+require 'unicorn/cgi_wrapper'
+require 'dispatcher'
+
+module Unicorn; module App; end; end
+
+# Implements a handler that can run Rails.
+class Unicorn::App::OldRails
+
+  def call(env)
+    cgi = Unicorn::CGIWrapper.new(env)
+    Dispatcher.dispatch(cgi,
+        ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS,
+        cgi.body)
+    cgi.out  # finalize the response
+    cgi.rack_response
+  end
+
+end
diff --git a/lib/unicorn/app/old_rails/static.rb b/lib/unicorn/app/old_rails/static.rb
new file mode 100644
index 0000000..c9366d2
--- /dev/null
+++ b/lib/unicorn/app/old_rails/static.rb
@@ -0,0 +1,58 @@
+# This code is based on the original Rails handler in Mongrel
+# Copyright (c) 2005 Zed A. Shaw
+# Copyright (c) 2009 Eric Wong
+# You can redistribute it and/or modify it under the same terms as Ruby.
+
+require 'rack/file'
+
+# Static file handler for Rails < 2.3.  This handler is only provided
+# as a convenience for developers.  Performance-minded deployments should
+# use nginx (or similar) for serving static files.
+#
+# This supports page caching directly and will try to resolve a
+# request in the following order:
+#
+# * If the requested exact PATH_INFO exists as a file then serve it.
+# * If it exists at PATH_INFO+rest_operator+".html" exists
+#   then serve that.
+#
+# This means that if you are using page caching it will actually work
+# with Unicorn and you should see a decent speed boost (but not as
+# fast as if you use a static server like nginx).
+class Unicorn::App::OldRails::Static
+  FILE_METHODS = { 'GET' => true, 'HEAD' => true }.freeze
+
+  def initialize(app)
+    @app = app
+    @root = "#{::RAILS_ROOT}/public"
+    @file_server = ::Rack::File.new(@root)
+  end
+
+  def call(env)
+    # short circuit this ASAP if serving non-file methods
+    FILE_METHODS.include?(env[Unicorn::Const::REQUEST_METHOD]) or
+      return @app.call(env)
+
+    # first try the path as-is
+    path_info = env[Unicorn::Const::PATH_INFO].chomp("/")
+    if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
+      # File exists as-is so serve it up
+      env[Unicorn::Const::PATH_INFO] = path_info
+      return @file_server.call(env)
+    end
+
+    # then try the cached version:
+
+    # grab the semi-colon REST operator used by old versions of Rails
+    # this is the reason we didn't just copy the new Rails::Rack::Static
+    env[Unicorn::Const::REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/
+    path_info << "#$1#{ActionController::Base.page_cache_extension}"
+
+    if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
+      env[Unicorn::Const::PATH_INFO] = path_info
+      return @file_server.call(env)
+    end
+
+    @app.call(env) # call OldRails
+  end
+end if defined?(Unicorn::App::OldRails)
diff --git a/lib/unicorn/cgi_wrapper.rb b/lib/unicorn/cgi_wrapper.rb
new file mode 100644
index 0000000..816b0a0
--- /dev/null
+++ b/lib/unicorn/cgi_wrapper.rb
@@ -0,0 +1,139 @@
+# This code is based on the original CGIWrapper from Mongrel
+# Copyright (c) 2005 Zed A. Shaw
+# Copyright (c) 2009 Eric Wong
+# You can redistribute it and/or modify it under the same terms as Ruby.
+#
+# Additional work donated by contributors.  See CONTRIBUTORS for more info.
+
+require 'cgi'
+
+module Unicorn; end
+
+# The beginning of a complete wrapper around Unicorn's internal HTTP
+# processing system but maintaining the original Ruby CGI module.  Use
+# this only as a crutch to get existing CGI based systems working.  It
+# should handle everything, but please notify us if you see special
+# warnings.  This work is still very alpha so we need testers to help
+# work out the various corner cases.
+class Unicorn::CGIWrapper < ::CGI
+  undef_method :env_table
+  attr_reader :env_table
+  attr_reader :body
+
+  # these are stripped out of any keys passed to CGIWrapper.header function
+  NPH = 'nph'.freeze # Completely ignored, Unicorn outputs the date regardless
+  CONNECTION = 'connection'.freeze # Completely ignored. Why is CGI doing this?
+  CHARSET = 'charset'.freeze # this gets appended to Content-Type
+  COOKIE = 'cookie'.freeze # maps (Hash,Array,String) to "Set-Cookie" headers
+  STATUS = 'status'.freeze # stored as @status
+
+  # some of these are common strings, but this is the only module
+  # using them and the reason they're not in Unicorn::Const
+  SET_COOKIE = 'Set-Cookie'.freeze
+  CONTENT_TYPE = 'Content-Type'.freeze
+  CONTENT_LENGTH = 'Content-Length'.freeze # this is NOT Const::CONTENT_LENGTH
+  RACK_INPUT = 'rack.input'.freeze
+  RACK_ERRORS = 'rack.errors'.freeze
+
+  # this maps CGI header names to HTTP header names
+  HEADER_MAP = {
+    'type' => CONTENT_TYPE,
+    'server' => 'Server'.freeze,
+    'language' => 'Content-Language'.freeze,
+    'expires' => 'Expires'.freeze,
+    'length' => CONTENT_LENGTH,
+  }.freeze
+
+  # Takes an a Rackable environment, plus any additional CGI.new
+  # arguments These are used internally to create a wrapper around the
+  # real CGI while maintaining Rack/Unicorn's view of the world.  This
+  # this will NOT deal well with large responses that take up a lot of
+  # memory, but neither does the CGI nor the original CGIWrapper from
+  # Mongrel...
+  def initialize(rack_env, *args)
+    @env_table = rack_env
+    @status = 200
+    @head = { :cookies => [] }
+    @body = StringIO.new
+    super(*args)
+  end
+
+  # finalizes the response in a way Rack applications would expect
+  def rack_response
+    cookies = @head.delete(:cookies)
+    cookies.empty? or @head[SET_COOKIE] = cookies.join("\n")
+    @head[CONTENT_LENGTH] ||= @body.size
+
+    [ @status, @head, [ @body.string ] ]
+  end
+
+  # The header is typically called to send back the header.  In our case we
+  # collect it into a hash for later usage.  This can be called multiple
+  # times to set different cookies.
+  def header(options = "text/html")
+    # if they pass in a string then just write the Content-Type
+    if String === options
+      @head[CONTENT_TYPE] ||= options
+    else
+      HEADER_MAP.each_pair do |from, to|
+        from = options.delete(from) or next
+        @head[to] = from
+      end
+
+      @head[CONTENT_TYPE] ||= "text/html"
+      if charset = options.delete(CHARSET)
+        @head[CONTENT_TYPE] << "; charset=#{charset}"
+      end
+
+      # lots of ways to set cookies
+      if cookie = options.delete(COOKIE)
+        cookies = @head[:cookies]
+        case cookie
+        when Array
+          cookie.each { |c| cookies << c.to_s }
+        when Hash
+          cookie.each_value { |c| cookies << c.to_s }
+        else
+          cookies << cookie.to_s
+        end
+      end
+      @status ||= (status = options.delete(STATUS))
+      # drop the keys we don't want anymore
+      options.delete(NPH)
+      options.delete(CONNECTION)
+
+      # finally, set the rest of the headers as-is
+      options.each_pair { |k,v| @head[k] = v }
+    end
+
+    # doing this fakes out the cgi library to think the headers are empty
+    # we then do the real headers in the out function call later
+    ""
+  end
+
+  # The dumb thing is people can call header or this or both and in
+  # any order.  So, we just reuse header and then finalize the
+  # HttpResponse the right way.  This will have no effect if called
+  # the second time if the first "outputted" anything.
+  def out(options = "text/html")
+    header(options)
+    @body.size == 0 or return
+    @body << yield
+  end
+
+  # Used to wrap the normal stdinput variable used inside CGI.
+  def stdinput
+    @env_table[RACK_INPUT]
+  end
+
+  # The stdoutput should be completely bypassed but we'll drop a
+  # warning just in case
+  def stdoutput
+    err = @env_table[RACK_ERRORS]
+    err.puts "WARNING: Your program is doing something not expected."
+    err.puts "Please tell Eric that stdoutput was used and what software " \
+             "you are running.  Thanks."
+    @body
+  end
+
+end