From 0b095ea72fb849682a1185a626eef247b5afc1cd Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 23 Mar 2009 02:03:31 -0700 Subject: unicorn_rails: support non-Rack versions of Rails 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. --- README | 2 +- bin/unicorn_rails | 42 +++++++++-- lib/unicorn/app/old_rails.rb | 23 ++++++ lib/unicorn/app/old_rails/static.rb | 58 +++++++++++++++ lib/unicorn/cgi_wrapper.rb | 139 ++++++++++++++++++++++++++++++++++++ 5 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 lib/unicorn/app/old_rails.rb create mode 100644 lib/unicorn/app/old_rails/static.rb create mode 100644 lib/unicorn/cgi_wrapper.rb 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 -- cgit v1.2.3-24-ge0c7