diff options
Diffstat (limited to 'lib/unicorn')
-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 | ||||
-rw-r--r-- | lib/unicorn/cgi_wrapper.rb | 149 | ||||
-rw-r--r-- | lib/unicorn/configurator.rb | 159 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 96 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 220 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 68 | ||||
-rw-r--r-- | lib/unicorn/launcher.rb | 33 | ||||
-rw-r--r-- | lib/unicorn/socket.rb | 142 | ||||
-rw-r--r-- | lib/unicorn/socket_helper.rb | 90 | ||||
-rw-r--r-- | lib/unicorn/util.rb | 15 |
12 files changed, 780 insertions, 437 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) diff --git a/lib/unicorn/cgi_wrapper.rb b/lib/unicorn/cgi_wrapper.rb new file mode 100644 index 0000000..bc622ea --- /dev/null +++ b/lib/unicorn/cgi_wrapper.rb @@ -0,0 +1,149 @@ +# 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 + Status = 'Status'.freeze # code + human-readable text, Rails sets this + + # 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 = { + 'status' => Status, + '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 = nil + @head = {} + @headv = Hash.new { |hash,key| hash[key] = [] } + @body = StringIO.new + super(*args) + end + + # finalizes the response in a way Rack applications would expect + def rack_response + # @head[CONTENT_LENGTH] ||= @body.size + @headv[SET_COOKIE] += @output_cookies if @output_cookies + @headv.each_pair do |key,value| + @head[key] ||= value.join("\n") unless value.empty? + end + + # Capitalized "Status:", with human-readable status code (e.g. "200 OK") + parseable_status = @head.delete(Status) + @status ||= parseable_status.split(/ /)[0].to_i rescue 500 + + [ @status || 500, @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.to_s + 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) + set_cookies = @headv[SET_COOKIE] + case cookie + when Array + cookie.each { |c| set_cookies << c.to_s } + when Hash + cookie.each_value { |c| set_cookies << c.to_s } + else + set_cookies << cookie.to_s + end + end + @status ||= options.delete(STATUS) # all lower-case + + # drop the keys we don't want anymore + options.delete(NPH) + options.delete(CONNECTION) + + # finally, set the rest of the headers as-is, allowing duplicates + options.each_pair { |k,v| @headv[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 if block_given? + 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 diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index dd9ae3b..a432f64 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -1,5 +1,4 @@ -require 'unicorn/socket' -require 'unicorn/const' +require 'socket' require 'logger' module Unicorn @@ -8,42 +7,40 @@ module Unicorn # # Example (when used with the unicorn config file): # worker_processes 4 - # listeners %w(0.0.0.0:9292 /tmp/my_app.sock) + # listen '/tmp/my_app.sock', :backlog => 1 + # listen '0.0.0.0:9292' # timeout 10 # pid "/tmp/my_app.pid" - # after_fork do |server,worker_nr| - # server.listen("127.0.0.1:#{9293 + worker_nr}") rescue nil + # after_fork do |server,worker| + # server.listen("127.0.0.1:#{9293 + worker.nr}") rescue nil # end class Configurator - include ::Unicorn::SocketHelper - # The default logger writes its output to $stderr DEFAULT_LOGGER = Logger.new($stderr) unless defined?(DEFAULT_LOGGER) # Default settings for Unicorn DEFAULTS = { :timeout => 60, - :listeners => [ Const::DEFAULT_LISTEN ], + :listeners => [], :logger => DEFAULT_LOGGER, :worker_processes => 1, - :after_fork => lambda { |server, worker_nr| - server.logger.info("worker=#{worker_nr} spawned pid=#{$$}") + :after_fork => lambda { |server, worker| + server.logger.info("worker=#{worker.nr} spawned pid=#{$$}") # per-process listener ports for debugging/admin: # "rescue nil" statement is needed because USR2 will # cause the master process to reexecute itself and the # per-worker ports can be taken, necessitating another # HUP after QUIT-ing the original master: - # server.listen("127.0.0.1:#{8081 + worker_nr}") rescue nil + # server.listen("127.0.0.1:#{8081 + worker.nr}") rescue nil }, - :before_fork => lambda { |server, worker_nr| - server.logger.info("worker=#{worker_nr} spawning...") + :before_fork => lambda { |server, worker| + server.logger.info("worker=#{worker.nr} spawning...") }, :before_exec => lambda { |server| server.logger.info("forked child re-executing...") }, :pid => nil, - :backlog => 1024, :preload_app => false, :stderr_path => nil, :stdout_path => nil, @@ -83,23 +80,6 @@ module Unicorn @set[key] end - # Changes the listen() syscall backlog to +nr+ for yet-to-be-created - # sockets. Due to limitations of the OS, this cannot affect - # existing listener sockets in any way, sockets must be completely - # closed and rebound (inherited sockets preserve their existing - # backlog setting). Some operating systems allow negative values - # here to specify the maximum allowable value. See the listen(2) - # syscall documentation of your OS for the exact semantics of this. - # - # If you are running unicorn on multiple machines, lowering this number - # can help your load balancer detect when a machine is overloaded - # and give requests to a different machine. - def backlog(nr) - Integer === nr or raise ArgumentError, - "not an integer: backlog=#{nr.inspect}" - @set[:backlog] = nr - end - # sets object to the +new+ Logger-like object. The new logger-like # object must respond to the following methods: # +debug+, +info+, +warn+, +error+, +fatal+, +close+ @@ -116,23 +96,37 @@ module Unicorn # the worker after forking. The following is an example hook which adds # a per-process listener to every worker: # - # after_fork do |server,worker_nr| + # after_fork do |server,worker| # # per-process listener ports for debugging/admin: # # "rescue nil" statement is needed because USR2 will # # cause the master process to reexecute itself and the # # per-worker ports can be taken, necessitating another # # HUP after QUIT-ing the original master: - # server.listen("127.0.0.1:#{9293 + worker_nr}") rescue nil + # server.listen("127.0.0.1:#{9293 + worker.nr}") rescue nil + # + # # drop permissions to "www-data" in the worker + # # generally there's no reason to start Unicorn as a priviledged user + # # as it is not recommended to expose Unicorn to public clients. + # uid, gid = Process.euid, Process.egid + # user, group = 'www-data', 'www-data' + # target_uid = Etc.getpwnam(user).uid + # target_gid = Etc.getgrnam(group).gid + # worker.tempfile.chown(target_uid, target_gid) + # if uid != target_uid || gid != target_gid + # Process.initgroups(user, target_gid) + # Process::GID.change_privilege(target_gid) + # Process::UID.change_privilege(target_uid) + # end # end - def after_fork(&block) - set_hook(:after_fork, block) + def after_fork(*args, &block) + set_hook(:after_fork, block_given? ? block : args[0]) end # sets before_fork got be a given Proc object. This Proc # object will be called by the master process before forking # each worker. - def before_fork(&block) - set_hook(:before_fork, block) + def before_fork(*args, &block) + set_hook(:before_fork, block_given? ? block : args[0]) end # sets the before_exec hook to a given Proc object. This @@ -141,20 +135,22 @@ module Unicorn # for freeing certain OS resources that you do NOT wish to # share with the reexeced child process. # There is no corresponding after_exec hook (for obvious reasons). - def before_exec(&block) - set_hook(:before_exec, block, 1) + def before_exec(*args, &block) + set_hook(:before_exec, block_given? ? block : args[0], 1) end # sets the timeout of worker processes to +seconds+. Workers # handling the request/app.call/response cycle taking longer than # this time period will be forcibly killed (via SIGKILL). This # timeout is enforced by the master process itself and not subject - # to the scheduling limitations by the worker process. + # to the scheduling limitations by the worker process. Due the + # low-complexity, low-overhead implementation, timeouts of less + # than 3.0 seconds can be considered inaccurate and unsafe. def timeout(seconds) Numeric === seconds or raise ArgumentError, "not numeric: timeout=#{seconds.inspect}" - seconds > 0 or raise ArgumentError, - "not positive: timeout=#{seconds.inspect}" + seconds >= 3 or raise ArgumentError, + "too low: timeout=#{seconds.inspect}" @set[:timeout] = seconds end @@ -171,13 +167,59 @@ module Unicorn # sets listeners to the given +addresses+, replacing or augmenting the # current set. This is for the global listener pool shared by all # worker processes. For per-worker listeners, see the after_fork example - def listeners(addresses) + # This is for internal API use only, do not use it in your Unicorn + # config file. Use listen instead. + def listeners(addresses) # :nodoc: Array === addresses or addresses = Array(addresses) + addresses.map! { |addr| expand_addr(addr) } @set[:listeners] = addresses end - # adds an +address+ to the existing listener set - def listen(address) + # adds an +address+ to the existing listener set. + # + # The following options may be specified (but are generally not needed): + # + # +backlog+: this is the backlog of the listen() syscall. + # + # Some operating systems allow negative values here to specify the + # maximum allowable value. In most cases, this number is only + # recommendation and there are other OS-specific tunables and + # variables that can affect this number. See the listen(2) + # syscall documentation of your OS for the exact semantics of + # this. + # + # If you are running unicorn on multiple machines, lowering this number + # can help your load balancer detect when a machine is overloaded + # and give requests to a different machine. + # + # Default: 1024 + # + # +rcvbuf+, +sndbuf+: maximum send and receive buffer sizes of sockets + # + # These correspond to the SO_RCVBUF and SO_SNDBUF settings which + # can be set via the setsockopt(2) syscall. Some kernels + # (e.g. Linux 2.4+) have intelligent auto-tuning mechanisms and + # there is no need (and it is sometimes detrimental) to specify them. + # + # See the socket API documentation of your operating system + # to determine the exact semantics of these settings and + # other operating system-specific knobs where they can be + # specified. + # + # Defaults: operating system defaults + def listen(address, opt = { :backlog => 1024 }) + address = expand_addr(address) + if String === address + Hash === @set[:listener_opts] or + @set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} } + [ :backlog, :sndbuf, :rcvbuf ].each do |key| + value = opt[key] or next + Integer === value or + raise ArgumentError, "not an integer: #{key}=#{value.inspect}" + end + @set[:listener_opts][address].merge!(opt) + end + @set[:listeners] = [] unless Array === @set[:listeners] @set[:listeners] << address end @@ -194,6 +236,10 @@ module Unicorn # properly close/reopen sockets. Files opened for logging do not # have to be reopened as (unbuffered-in-userspace) files opened with # the File::APPEND flag are written to atomically on UNIX. + # + # In addition to reloading the unicorn-specific config settings, + # SIGHUP will reload application code in the working + # directory/symlink when workers are gracefully restarted. def preload_app(bool) case bool when TrueClass, FalseClass @@ -249,5 +295,28 @@ module Unicorn @set[var] = my_proc end + # expands "unix:path/to/foo" to a socket relative to the current path + # expands pathnames of sockets if relative to "~" or "~username" + # expands "*:port and ":port" to "0.0.0.0:port" + def expand_addr(address) #:nodoc + return "0.0.0.0:#{address}" if Integer === address + return address unless String === address + + case address + when %r{\Aunix:(.*)\z} + File.expand_path($1) + when %r{\A~} + File.expand_path(address) + when %r{\A(?:\*:)?(\d+)\z} + "0.0.0.0:#$1" + when %r{\A(.*):(\d+)\z} + # canonicalize the name + packed = Socket.pack_sockaddr_in($2.to_i, $1) + Socket.unpack_sockaddr_in(packed).reverse!.join(':') + else + address + end + end + end end diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index 46398e5..241c52e 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -1,89 +1,19 @@ -module Unicorn +require 'rack/utils' - # Every standard HTTP code mapped to the appropriate message. These are - # used so frequently that they are placed directly in Unicorn for easy - # access rather than Unicorn::Const itself. - HTTP_STATUS_CODES = { - 100 => 'Continue', - 101 => 'Switching Protocols', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Moved Temporarily', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported' - } +module Unicorn # Frequently used constants when constructing requests or responses. Many times # the constant just refers to a string with the same contents. Using these constants # gave about a 3% to 10% performance improvement over using the strings directly. # Symbols did not really improve things much compared to constants. - # - # While Unicorn does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT, - # REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or - # too taxing on performance. module Const - DATE="Date".freeze - - # This is the part of the path after the SCRIPT_NAME. - PATH_INFO="PATH_INFO".freeze - - # Request body - HTTP_BODY="HTTP_BODY".freeze - - # This is the initial part that your handler is identified as by URIClassifier. - SCRIPT_NAME="SCRIPT_NAME".freeze - - # The original URI requested by the client. Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME. - REQUEST_URI='REQUEST_URI'.freeze - REQUEST_PATH='REQUEST_PATH'.freeze - - UNICORN_VERSION="0.1.0".freeze - - UNICORN_TMP_BASE="unicorn".freeze + UNICORN_VERSION="0.7.0".freeze DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address DEFAULT_PORT = "8080".freeze # default TCP listen port DEFAULT_LISTEN = "#{DEFAULT_HOST}:#{DEFAULT_PORT}".freeze - # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff. - ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Unicorn #{UNICORN_VERSION}\r\n\r\nNOT FOUND".freeze - - CONTENT_LENGTH="CONTENT_LENGTH".freeze - - # A common header for indicating the server is too busy. Not used yet. - ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze - # The basic max request size we'll try to read. CHUNK_SIZE=(16 * 1024) @@ -94,23 +24,15 @@ module Unicorn # Maximum request body size before it is moved out of memory and into a tempfile for reading. MAX_BODY=MAX_HEADER + # common errors we'll send back + ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze + ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze + # A frozen format for this is about 15% faster - CONTENT_TYPE = "Content-Type".freeze - LAST_MODIFIED = "Last-Modified".freeze - ETAG = "ETag".freeze - REQUEST_METHOD="REQUEST_METHOD".freeze - GET="GET".freeze - HEAD="HEAD".freeze - # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32) - ETAG_FORMAT="\"%x-%x-%x\"".freeze - LINE_END="\r\n".freeze + CONTENT_LENGTH="CONTENT_LENGTH".freeze REMOTE_ADDR="REMOTE_ADDR".freeze HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze - HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze - HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze - REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze - HOST = "HOST".freeze - CONNECTION = "Connection".freeze + RACK_INPUT="rack.input".freeze end end diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index ce0e408..368305f 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -1,5 +1,4 @@ require 'tempfile' -require 'uri' require 'stringio' # compiled extension @@ -13,165 +12,140 @@ module Unicorn # class HttpRequest + # default parameters we merge into the request env for Rack handlers + DEFAULTS = { + "rack.errors" => $stderr, + "rack.multiprocess" => true, + "rack.multithread" => false, + "rack.run_once" => false, + "rack.version" => [1, 0].freeze, + "SCRIPT_NAME" => "".freeze, + + # this is not in the Rack spec, but some apps may rely on it + "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze + } + + # Optimize for the common case where there's no request body + # (GET/HEAD) requests. + NULL_IO = StringIO.new + LOCALHOST = '127.0.0.1'.freeze + + # Being explicitly single-threaded, we have certain advantages in + # not having to worry about variables being clobbered :) + BUFFER = ' ' * Const::CHUNK_SIZE # initial size, may grow + PARSER = HttpParser.new + PARAMS = Hash.new + def initialize(logger) @logger = logger - @body = nil - @buffer = ' ' * Const::CHUNK_SIZE # initial size, may grow - @parser = HttpParser.new - @params = Hash.new - end - - def reset - @parser.reset - @params.clear - @body.close rescue nil - @body = nil end - # # Does the majority of the IO processing. It has been written in - # Ruby using about 7 different IO processing strategies and no - # matter how it's done the performance just does not improve. It is - # currently carefully constructed to make sure that it gets the best - # possible performance, but anyone who thinks they can make it - # faster is more than welcome to take a crack at it. + # Ruby using about 8 different IO processing strategies. + # + # It is currently carefully constructed to make sure that it gets + # the best possible performance for the common case: GET requests + # that are fully complete after a single read(2) + # + # Anyone who thinks they can make it faster is more than welcome to + # take a crack at it. # # returns an environment hash suitable for Rack if successful # This does minimal exception trapping and it is up to the caller # to handle any socket errors (e.g. user aborted upload). def read(socket) - data = String.new(read_socket(socket)) - nparsed = 0 - - # Assumption: nparsed will always be less since data will get - # filled with more after each parsing. If it doesn't get more - # then there was a problem with the read operation on the client - # socket. Effect is to stop processing when the socket can't - # fill the buffer for further parsing. - while nparsed < data.length - nparsed = @parser.execute(@params, data, nparsed) - - if @parser.finished? - # From http://www.ietf.org/rfc/rfc3875: - # "Script authors should be aware that the REMOTE_ADDR and - # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) - # may not identify the ultimate source of the request. They - # identify the client for the immediate request to the server; - # that client may be a proxy, gateway, or other intermediary - # acting on behalf of the actual source client." - @params[Const::REMOTE_ADDR] = socket.unicorn_peeraddr - - handle_body(socket) and return rack_env # success! - return nil # fail - else - # Parser is not done, queue up more data to read and continue - # parsing - data << read_socket(socket) - if data.length >= Const::MAX_HEADER - raise HttpParserError.new("HEADER is longer than allowed, " \ - "aborting client early.") - end - end + # reset the parser + unless NULL_IO == (input = PARAMS[Const::RACK_INPUT]) # unlikely + input.close rescue nil + input.close! rescue nil end - nil # XXX bug? + PARAMS.clear + PARSER.reset + + # From http://www.ietf.org/rfc/rfc3875: + # "Script authors should be aware that the REMOTE_ADDR and + # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) + # may not identify the ultimate source of the request. They + # identify the client for the immediate request to the server; + # that client may be a proxy, gateway, or other intermediary + # acting on behalf of the actual source client." + PARAMS[Const::REMOTE_ADDR] = + TCPSocket === socket ? socket.peeraddr.last : LOCALHOST + + # short circuit the common case with small GET requests first + PARSER.execute(PARAMS, socket.readpartial(Const::CHUNK_SIZE, BUFFER)) and + return handle_body(socket) + + data = BUFFER.dup # socket.readpartial will clobber BUFFER + + # Parser is not done, queue up more data to read and continue parsing + # an Exception thrown from the PARSER will throw us out of the loop + begin + data << socket.readpartial(Const::CHUNK_SIZE, BUFFER) + PARSER.execute(PARAMS, data) and return handle_body(socket) + end while true rescue HttpParserError => e @logger.error "HTTP parse error, malformed request " \ - "(#{@params[Const::HTTP_X_FORWARDED_FOR] || - socket.unicorn_peeraddr}): #{e.inspect}" + "(#{PARAMS[Const::HTTP_X_FORWARDED_FOR] || + PARAMS[Const::REMOTE_ADDR]}): #{e.inspect}" @logger.error "REQUEST DATA: #{data.inspect}\n---\n" \ - "PARAMS: #{@params.inspect}\n---\n" - socket.closed? or socket.close rescue nil - nil + "PARAMS: #{PARAMS.inspect}\n---\n" + raise e end private # Handles dealing with the rest of the request - # returns true if successful, false if not + # returns a Rack environment if successful, raises an exception if not def handle_body(socket) - http_body = @params[Const::HTTP_BODY] - content_length = @params[Const::CONTENT_LENGTH].to_i - remain = content_length - http_body.length + http_body = PARAMS.delete(:http_body) + content_length = PARAMS[Const::CONTENT_LENGTH].to_i - # must read more data to complete body - if remain < Const::MAX_BODY - # small body, just use that - @body = StringIO.new(http_body) - else # huge body, put it in a tempfile - @body = Tempfile.new(Const::UNICORN_TMP_BASE) - @body.binmode - @body.sync = true - @body.syswrite(http_body) + if content_length == 0 # short circuit the common case + PARAMS[Const::RACK_INPUT] = NULL_IO.closed? ? NULL_IO.reopen : NULL_IO + return PARAMS.update(DEFAULTS) end + # must read more data to complete body + remain = content_length - http_body.length + + body = PARAMS[Const::RACK_INPUT] = (remain < Const::MAX_BODY) ? + StringIO.new : Tempfile.new('unicorn') + + body.binmode + body.write(http_body) + # Some clients (like FF1.0) report 0 for body and then send a body. # This will probably truncate them but at least the request goes through # usually. - if remain > 0 - read_body(socket, remain) or return false # fail! - end - @body.rewind - @body.sysseek(0) if @body.respond_to?(:sysseek) + read_body(socket, remain, body) if remain > 0 + body.rewind # in case read_body overread because the client tried to pipeline # another request, we'll truncate it. Again, we don't do pipelining # or keepalive - @body.truncate(content_length) - true + body.truncate(content_length) + PARAMS.update(DEFAULTS) end - # Returns an environment which is rackable: - # http://rack.rubyforge.org/doc/files/SPEC.html - # Based on Rack's old Mongrel handler. - def rack_env - # It might be a dumbass full host request header - @params[Const::REQUEST_PATH] ||= - URI.parse(@params[Const::REQUEST_URI]).path - raise "No REQUEST PATH" unless @params[Const::REQUEST_PATH] - - @params["QUERY_STRING"] ||= '' - @params.delete "HTTP_CONTENT_TYPE" - @params.delete "HTTP_CONTENT_LENGTH" - @params.update({ "rack.version" => [0,1], - "rack.input" => @body, - "rack.errors" => $stderr, - "rack.multithread" => false, - "rack.multiprocess" => true, - "rack.run_once" => false, - "rack.url_scheme" => "http", - Const::PATH_INFO => @params[Const::REQUEST_PATH], - Const::SCRIPT_NAME => "", - }) - end - - # Does the heavy lifting of properly reading the larger body requests in - # small chunks. It expects @body to be an IO object, socket to be valid, - # It also expects any initial part of the body that has been read to be in - # the @body already. It will return true if successful and false if not. - def read_body(socket, remain) - while remain > 0 - # writes always write the requested amount on a POSIX filesystem - remain -= @body.syswrite(read_socket(socket)) - end - true # success! + # Does the heavy lifting of properly reading the larger body + # requests in small chunks. It expects PARAMS['rack.input'] to be + # an IO object, socket to be valid, It also expects any initial part + # of the body that has been read to be in the PARAMS['rack.input'] + # already. It will return true if successful and false if not. + def read_body(socket, remain, body) + begin + # write always writes the requested amount on a POSIX filesystem + remain -= body.write(socket.readpartial(Const::CHUNK_SIZE, BUFFER)) + end while remain > 0 rescue Object => e - logger.error "Error reading HTTP body: #{e.inspect}" - socket.closed? or socket.close rescue nil + @logger.error "Error reading HTTP body: #{e.inspect}" # Any errors means we should delete the file, including if the file # is dumped. Truncate it ASAP to help avoid page flushes to disk. - @body.truncate(0) rescue nil + body.truncate(0) rescue nil reset - false - end - - # read(2) on "slow" devices like sockets can be interrupted by signals - def read_socket(socket) - begin - socket.sysread(Const::CHUNK_SIZE, @buffer) - rescue Errno::EINTR - retry - end + raise e end end diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index 7bbb940..15df3f6 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -21,52 +21,50 @@ module Unicorn class HttpResponse - # headers we allow duplicates for - ALLOWED_DUPLICATES = { - 'Set-Cookie' => true, - 'Set-Cookie2' => true, - 'Warning' => true, - 'WWW-Authenticate' => true, - }.freeze + # Every standard HTTP code mapped to the appropriate message. + CODES = Rack::Utils::HTTP_STATUS_CODES.inject({}) { |hash,(code,msg)| + hash[code] = "#{code} #{msg}" + hash + } + + # Rack does not set/require a Date: header. We always override the + # Connection: and Date: headers no matter what (if anything) our + # Rack application sent us. + SKIP = { 'connection' => true, 'date' => true, 'status' => true }.freeze + EMPTY = ''.freeze # :nodoc + OUT = [] # :nodoc # writes the rack_response to socket as an HTTP response def self.write(socket, rack_response) status, headers, body = rack_response + status = CODES[status.to_i] + OUT.clear - # Rack does not set/require Date, but don't worry about Content-Length - # since Rack applications that conform to Rack::Lint enforce that - out = [ "#{Const::DATE}: #{Time.now.httpdate}" ] - sent = { Const::CONNECTION => true, Const::DATE => true } - + # Don't bother enforcing duplicate supression, it's a Hash most of + # the time anyways so just hope our app knows what it's doing headers.each do |key, value| - if ! sent[key] || ALLOWED_DUPLICATES[key] - sent[key] = true - out << "#{key}: #{value}" + next if SKIP.include?(key.downcase) + if value =~ /\n/ + value.split(/\n/).each { |v| OUT << "#{key}: #{v}\r\n" } + else + OUT << "#{key}: #{value}\r\n" end end - socket_write(socket, - "HTTP/1.1 #{status} #{HTTP_STATUS_CODES[status]}\r\n" \ + # Rack should enforce Content-Length or chunked transfer encoding, + # so don't worry or care about them. + # Date is required by HTTP/1.1 as long as our clock can be trusted. + # Some broken clients require a "Status" header so we accomodate them + socket.write("HTTP/1.1 #{status}\r\n" \ + "Date: #{Time.now.httpdate}\r\n" \ + "Status: #{status}\r\n" \ "Connection: close\r\n" \ - "#{out.join("\r\n")}\r\n\r\n") - body.each { |chunk| socket_write(socket, chunk) } + "#{OUT.join(EMPTY)}\r\n") + body.each { |chunk| socket.write(chunk) } + socket.close # flushes and uncorks the socket immediately + ensure + body.respond_to?(:close) and body.close rescue nil end - private - - # write(2) can return short on slow devices like sockets as well - # as fail with EINTR if a signal was caught. - def self.socket_write(socket, buffer) - loop do - begin - written = socket.syswrite(buffer) - return written if written == buffer.length - buffer = buffer[written..-1] - rescue Errno::EINTR - retry - end - end - end - end end diff --git a/lib/unicorn/launcher.rb b/lib/unicorn/launcher.rb new file mode 100644 index 0000000..8c96059 --- /dev/null +++ b/lib/unicorn/launcher.rb @@ -0,0 +1,33 @@ +$stdin.sync = $stdout.sync = $stderr.sync = true +require 'unicorn' + +class Unicorn::Launcher + + # We don't do a lot of standard daemonization stuff: + # * umask is whatever was set by the parent process at startup + # and can be set in config.ru and config_file, so making it + # 0000 and potentially exposing sensitive log data can be bad + # policy. + # * don't bother to chdir("/") here since unicorn is designed to + # run inside APP_ROOT. Unicorn will also re-chdir() to + # the directory it was started in when being re-executed + # to pickup code changes if the original deployment directory + # is a symlink or otherwise got replaced. + def self.daemonize! + $stdin.reopen("/dev/null") + + # We only start a new process group if we're not being reexecuted + # and inheriting file descriptors from our parent + unless ENV['UNICORN_FD'] + exit if fork + Process.setsid + exit if fork + + # $stderr/$stderr can/will be redirected separately in the Unicorn config + $stdout.reopen("/dev/null", "a") + $stderr.reopen("/dev/null", "a") + end + $stdin.sync = $stdout.sync = $stderr.sync = true + end + +end diff --git a/lib/unicorn/socket.rb b/lib/unicorn/socket.rb deleted file mode 100644 index 4913261..0000000 --- a/lib/unicorn/socket.rb +++ /dev/null @@ -1,142 +0,0 @@ -require 'fcntl' -require 'socket' -require 'io/nonblock' - -# non-portable Socket code goes here: -class Socket - module Constants - # configure platform-specific options (only tested on Linux 2.6 so far) - case RUBY_PLATFORM - when /linux/ - # from /usr/include/linux/tcp.h - TCP_DEFER_ACCEPT = 9 unless defined?(TCP_DEFER_ACCEPT) - TCP_CORK = 3 unless defined?(TCP_CORK) - when /freebsd(([1-4]\..{1,2})|5\.[0-4])/ - when /freebsd/ - # Use the HTTP accept filter if available. - # The struct made by pack() is defined in /usr/include/sys/socket.h - # as accept_filter_arg - unless `/sbin/sysctl -nq net.inet.accf.http`.empty? - unless defined?(SO_ACCEPTFILTER_HTTPREADY) - SO_ACCEPTFILTER_HTTPREADY = ['httpready',nil].pack('a16a240').freeze - end - - end - end - end -end - -class UNIXSocket - UNICORN_PEERADDR = '127.0.0.1'.freeze - def unicorn_peeraddr - UNICORN_PEERADDR - end -end - -class TCPSocket - def unicorn_peeraddr - peeraddr.last - end -end - -module Unicorn - module SocketHelper - include Socket::Constants - - def set_client_sockopt(sock) - sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) if defined?(TCP_NODELAY) - sock.setsockopt(SOL_TCP, TCP_CORK, 1) if defined?(TCP_CORK) - end - - def set_cloexec(io) - io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined?(Fcntl::FD_CLOEXEC) - end - - def set_server_sockopt(sock) - if defined?(TCP_DEFER_ACCEPT) - sock.setsockopt(SOL_TCP, TCP_DEFER_ACCEPT, 1) rescue nil - end - if defined?(SO_ACCEPTFILTER_HTTPREADY) - sock.setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, - SO_ACCEPTFILTER_HTTPREADY) rescue nil - end - end - - def destroy_safely(io) - if io.respond_to?(:path) && File.stat(io.path).ino == io.stat.ino - File.unlink(io.path) rescue nil - end - io.close rescue nil - end - - # creates a new server, socket. address may be a HOST:PORT or - # an absolute path to a UNIX socket. address can even be a Socket - # object in which case it is immediately returned - def bind_listen(address = '0.0.0.0:8080', backlog = 1024) - return address unless String === address - - domain, bind_addr = if address[0..0] == "/" - if File.exist?(address) - if File.socket?(address) - if self.respond_to?(:logger) - logger.info "unlinking existing socket=#{address}" - end - File.unlink(address) - else - raise ArgumentError, - "socket=#{address} specified but it is not a socket!" - end - end - [ AF_UNIX, Socket.pack_sockaddr_un(address) ] - elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/ - [ AF_INET, Socket.pack_sockaddr_in($2.to_i, $1) ] - else - raise ArgumentError, "Don't know how to bind: #{address}" - end - - sock = Socket.new(domain, SOCK_STREAM, 0) - sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) if defined?(SO_REUSEADDR) - begin - sock.bind(bind_addr) - rescue Errno::EADDRINUSE - sock.close rescue nil - return nil - end - sock.listen(backlog) - set_server_sockopt(sock) if domain == AF_INET - sock - end - - # Returns the configuration name of a socket as a string. sock may - # be a string value, in which case it is returned as-is - # Warning: TCP sockets may not always return the name given to it. - def sock_name(sock) - case sock - when String then sock - when UNIXServer - Socket.unpack_sockaddr_un(sock.getsockname) - when TCPServer - Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') - when Socket - begin - Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') - rescue ArgumentError - Socket.unpack_sockaddr_un(sock.getsockname) - end - else - raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}" - end - end - - # casts a given Socket to be a TCPServer or UNIXServer - def server_cast(sock) - begin - Socket.unpack_sockaddr_in(sock.getsockname) - TCPServer.for_fd(sock.fileno) - rescue ArgumentError - UNIXServer.for_fd(sock.fileno) - end - end - - end # module SocketHelper -end # module Unicorn diff --git a/lib/unicorn/socket_helper.rb b/lib/unicorn/socket_helper.rb new file mode 100644 index 0000000..850ad85 --- /dev/null +++ b/lib/unicorn/socket_helper.rb @@ -0,0 +1,90 @@ +require 'socket' + +module Unicorn + module SocketHelper + include Socket::Constants + + def set_server_sockopt(sock, opt) + opt ||= {} + if opt[:rcvbuf] || opt[:sndbuf] + log_buffer_sizes(sock, "before: ") + sock.setsockopt(SOL_SOCKET, SO_RCVBUF, opt[:rcvbuf]) if opt[:rcvbuf] + sock.setsockopt(SOL_SOCKET, SO_SNDBUF, opt[:sndbuf]) if opt[:sndbuf] + log_buffer_sizes(sock, " after: ") + end + sock.listen(opt[:backlog] || 1024) + end + + def log_buffer_sizes(sock, pfx = '') + respond_to?(:logger) or return + rcvbuf = sock.getsockopt(SOL_SOCKET, SO_RCVBUF).unpack('i') + sndbuf = sock.getsockopt(SOL_SOCKET, SO_SNDBUF).unpack('i') + logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}" + end + + # creates a new server, socket. address may be a HOST:PORT or + # an absolute path to a UNIX socket. address can even be a Socket + # object in which case it is immediately returned + def bind_listen(address = '0.0.0.0:8080', opt = { :backlog => 1024 }) + return address unless String === address + + sock = if address[0..0] == "/" + if File.exist?(address) + if File.socket?(address) + if self.respond_to?(:logger) + logger.info "unlinking existing socket=#{address}" + end + File.unlink(address) + else + raise ArgumentError, + "socket=#{address} specified but it is not a socket!" + end + end + old_umask = File.umask(0) + begin + UNIXServer.new(address) + ensure + File.umask(old_umask) + end + elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/ + TCPServer.new($1, $2.to_i) + else + raise ArgumentError, "Don't know how to bind: #{address}" + end + set_server_sockopt(sock, opt) + sock + end + + # Returns the configuration name of a socket as a string. sock may + # be a string value, in which case it is returned as-is + # Warning: TCP sockets may not always return the name given to it. + def sock_name(sock) + case sock + when String then sock + when UNIXServer + Socket.unpack_sockaddr_un(sock.getsockname) + when TCPServer + Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') + when Socket + begin + Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') + rescue ArgumentError + Socket.unpack_sockaddr_un(sock.getsockname) + end + else + raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}" + end + end + + # casts a given Socket to be a TCPServer or UNIXServer + def server_cast(sock) + begin + Socket.unpack_sockaddr_in(sock.getsockname) + TCPServer.for_fd(sock.fileno) + rescue ArgumentError + UNIXServer.for_fd(sock.fileno) + end + end + + end # module SocketHelper +end # module Unicorn diff --git a/lib/unicorn/util.rb b/lib/unicorn/util.rb index 0400fd0..2d3f827 100644 --- a/lib/unicorn/util.rb +++ b/lib/unicorn/util.rb @@ -4,6 +4,8 @@ module Unicorn class Util class << self + APPEND_FLAGS = File::WRONLY | File::APPEND + # this reopens logs that have been rotated (using logrotate(8) or # similar). It is recommended that you install # A +File+ object is considered for reopening if it is: @@ -17,17 +19,20 @@ module Unicorn ObjectSpace.each_object(File) do |fp| next if fp.closed? next unless (fp.sync && fp.path[0..0] == "/") - - flags = fp.fcntl(Fcntl::F_GETFL) - open_flags = File::WRONLY | File::APPEND - next unless (flags & open_flags) == open_flags + next unless (fp.fcntl(Fcntl::F_GETFL) & APPEND_FLAGS) == APPEND_FLAGS begin a, b = fp.stat, File.stat(fp.path) next if a.ino == b.ino && a.dev == b.dev rescue Errno::ENOENT end - fp.reopen(fp.path, "a") + + open_arg = 'a' + if fp.respond_to?(:external_encoding) && enc = fp.external_encoding + open_arg << ":#{enc.to_s}" + enc = fp.internal_encoding and open_arg << ":#{enc.to_s}" + end + fp.reopen(fp.path, open_arg) fp.sync = true nr += 1 end # each_object |