diff options
Diffstat (limited to 'lib/unicorn/app')
-rw-r--r-- | lib/unicorn/app/exec_cgi.rb | 156 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails.rb | 29 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails/static.rb | 60 |
3 files changed, 245 insertions, 0 deletions
diff --git a/lib/unicorn/app/exec_cgi.rb b/lib/unicorn/app/exec_cgi.rb new file mode 100644 index 0000000..8f81d78 --- /dev/null +++ b/lib/unicorn/app/exec_cgi.rb @@ -0,0 +1,156 @@ +require 'unicorn' +require 'rack' + +module Unicorn::App + + # This class is highly experimental (even more so than the rest of Unicorn) + # and has never run anything other than cgit. + class ExecCgi + + CHUNK_SIZE = 16384 + PASS_VARS = %w( + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + AUTH_TYPE + PATH_INFO + PATH_TRANSLATED + QUERY_STRING + REMOTE_ADDR + REMOTE_HOST + REMOTE_IDENT + REMOTE_USER + REQUEST_METHOD + SERVER_NAME + SERVER_PORT + SERVER_PROTOCOL + SERVER_SOFTWARE + ).map { |x| x.freeze }.freeze # frozen strings are faster for Hash lookups + + # Intializes the app, example of usage in a config.ru + # map "/cgit" do + # run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi") + # end + def initialize(*args) + @args = args.dup + first = @args[0] or + raise ArgumentError, "need path to executable" + first[0..0] == "/" or @args[0] = ::File.expand_path(first) + File.executable?(@args[0]) or + raise ArgumentError, "#{@args[0]} is not executable" + end + + # Calls the app + def call(env) + out, err = Tempfile.new(''), Tempfile.new('') + out.unlink + err.unlink + inp = force_file_input(env) + inp.sync = out.sync = err.sync = true + pid = fork { run_child(inp, out, err, env) } + inp.close + pid, status = Process.waitpid2(pid) + write_errors(env, err, status) if err.stat.size > 0 + err.close + + return parse_output!(out) if status.success? + out.close + [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] + end + + private + + def run_child(inp, out, err, env) + PASS_VARS.each do |key| + val = env[key] or next + ENV[key] = val + end + ENV['SCRIPT_NAME'] = @args[0] + ENV['GATEWAY_INTERFACE'] = 'CGI/1.1' + env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] } + + a = IO.new(0).reopen(inp) + b = IO.new(1).reopen(out) + c = IO.new(2).reopen(err) + exec(*@args) + end + + # Extracts headers from CGI out, will change the offset of out. + # This returns a standard Rack-compatible return value: + # [ 200, HeadersHash, body ] + def parse_output!(out) + size = out.stat.size + out.sysseek(0) + head = out.sysread(CHUNK_SIZE) + offset = 2 + head, body = head.split(/\n\n/, 2) + if body.nil? + head, body = head.split(/\r\n\r\n/, 2) + offset = 4 + end + offset += head.length + out.instance_variable_set('@unicorn_app_exec_cgi_offset', offset) + size -= offset + + # Allows +out+ to be used as a Rack body. + def out.each + sysseek(@unicorn_app_exec_cgi_offset) + + # don't use a preallocated buffer for sysread since we can't + # guarantee an actual socket is consuming the yielded string + # (or if somebody is pushing to an array for eventual concatenation + begin + yield(sysread(CHUNK_SIZE)) + rescue EOFError + return + end while true + end + + prev = nil + headers = Rack::Utils::HeaderHash.new + head.split(/\r?\n/).each do |line| + case line + when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2 + when /^[ \t]/ then headers[prev] << "\n#{line}" if prev + end + end + headers['Content-Length'] = size.to_s + [ 200, headers, out ] + end + + # ensures rack.input is a file handle that we can redirect stdin to + def force_file_input(env) + inp = env['rack.input'] + if inp.respond_to?(:fileno) && Integer === inp.fileno + inp + elsif inp.size == 0 # inp could be a StringIO or StringIO-like object + ::File.open('/dev/null') + else + tmp = Tempfile.new('') + tmp.unlink + tmp.binmode + + # Rack::Lint::InputWrapper doesn't allow sysread :( + buf = '' + while inp.read(CHUNK_SIZE, buf) + tmp.syswrite(buf) + end + tmp.sysseek(0) + tmp + end + end + + # rack.errors this may not be an IO object, so we couldn't + # just redirect the CGI executable to that earlier. + def write_errors(env, err, status) + err.seek(0) + dst = env['rack.errors'] + pid = status.pid + dst.write("#{pid}: #{@args.inspect} status=#{status} stderr:\n") + err.each_line { |line| dst.write("#{pid}: #{line}") } + dst.flush + end + + end + +end diff --git a/lib/unicorn/app/old_rails.rb b/lib/unicorn/app/old_rails.rb new file mode 100644 index 0000000..9b3a3b1 --- /dev/null +++ b/lib/unicorn/app/old_rails.rb @@ -0,0 +1,29 @@ +# 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) + begin + Dispatcher.dispatch(cgi, + ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, + cgi.body) + rescue Object => e + err = env['rack.errors'] + err.write("#{e} #{e.message}\n") + e.backtrace.each { |line| err.write("#{line}\n") } + end + 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..17c007c --- /dev/null +++ b/lib/unicorn/app/old_rails/static.rb @@ -0,0 +1,60 @@ +# 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 + REQUEST_METHOD = 'REQUEST_METHOD'.freeze + REQUEST_URI = 'REQUEST_URI'.freeze + PATH_INFO = 'PATH_INFO'.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[REQUEST_METHOD]) or return @app.call(env) + + # first try the path as-is + path_info = env[PATH_INFO].chomp("/") + if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") + # File exists as-is so serve it up + env[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[REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/ + path_info << "#$1#{ActionController::Base.page_cache_extension}" + + if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") + env[PATH_INFO] = path_info + return @file_server.call(env) + end + + @app.call(env) # call OldRails + end +end if defined?(Unicorn::App::OldRails) |