about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/unicorn.rb120
-rw-r--r--lib/unicorn/app/exec_cgi.rb150
-rw-r--r--lib/unicorn/configurator.rb19
-rw-r--r--lib/unicorn/const.rb2
-rw-r--r--lib/unicorn/http_request.rb4
-rw-r--r--lib/unicorn/http_response.rb29
-rw-r--r--lib/unicorn/launcher.rb33
7 files changed, 284 insertions, 73 deletions
diff --git a/lib/unicorn.rb b/lib/unicorn.rb
index d442f63..2f86de2 100644
--- a/lib/unicorn.rb
+++ b/lib/unicorn.rb
@@ -23,7 +23,6 @@ module Unicorn
   # forked worker children.
   class HttpServer
     attr_reader :logger
-    include Process
     include ::Unicorn::SocketHelper
 
     DEFAULT_START_CTX = {
@@ -53,7 +52,7 @@ module Unicorn
       @start_ctx = DEFAULT_START_CTX.dup
       @start_ctx.merge!(start_ctx) if start_ctx
       @app = app
-      @mode = :idle
+      @sig_queue = []
       @master_pid = $$
       @workers = Hash.new
       @io_purgatory = [] # prevents IO objects in here from being GC-ed
@@ -160,33 +159,45 @@ module Unicorn
       # are trapped.  See trap_deferred
       @rd_sig, @wr_sig = IO.pipe unless (@rd_sig && @wr_sig)
       @rd_sig.nonblock = @wr_sig.nonblock = true
+      mode = nil
+      respawn = true
 
-      reset_master
+      QUEUE_SIGS.each { |sig| trap_deferred(sig) }
+      trap('CHLD') { |sig_nr| awaken_master }
       $0 = "unicorn master"
-      logger.info "master process ready" # test relies on this message
+      logger.info "master process ready" # test_exec.rb relies on this message
       begin
         loop do
           reap_all_workers
-          case @mode
-          when :idle
+          case (mode = @sig_queue.shift)
+          when nil
             murder_lazy_workers
-            spawn_missing_workers
+            spawn_missing_workers if respawn
+            master_sleep
           when 'QUIT' # graceful shutdown
             break
           when 'TERM', 'INT' # immediate shutdown
             stop(false)
             break
-          when 'USR1' # user-defined (probably something like log reopening)
-            kill_each_worker('USR1')
+          when 'USR1' # rotate logs
+            logger.info "master rotating logs..."
             Unicorn::Util.reopen_logs
-            reset_master
+            logger.info "master done rotating logs"
+            kill_each_worker('USR1')
           when 'USR2' # exec binary, stay alive in case something went wrong
             reexec
-            reset_master
+          when 'WINCH'
+            if Process.ppid == 1 || Process.getpgrp != $$
+              respawn = false
+              logger.info "gracefully stopping all workers"
+              kill_each_worker('QUIT')
+            else
+              logger.info "SIGWINCH ignored because we're not daemonized"
+            end
           when 'HUP'
+            respawn = true
             if @config.config_file
               load_config!
-              reset_master
               redo # immediate reaping since we may have QUIT workers
             else # exec binary and exit if there's no config file
               logger.info "config_file not present, reexecuting binary"
@@ -194,19 +205,7 @@ module Unicorn
               break
             end
           else
-            logger.error "master process in unknown mode: #{@mode}, resetting"
-            reset_master
-          end
-          reap_all_workers
-
-          ready = begin
-            IO.select([@rd_sig], nil, nil, 1) or next
-          rescue Errno::EINTR # next
-          end
-          ready[0] && ready[0][0] or next
-          begin # just consume the pipe when we're awakened, @mode is set
-            loop { @rd_sig.sysread(Const::CHUNK_SIZE) }
-          rescue Errno::EAGAIN, Errno::EINTR # next
+            logger.error "master process in unknown mode: #{mode}"
           end
         end
       rescue Errno::EINTR
@@ -214,7 +213,6 @@ module Unicorn
       rescue Object => e
         logger.error "Unhandled master loop exception #{e.inspect}."
         logger.error e.backtrace.join("\n")
-        reset_master
         retry
       end
       stop # gracefully shutdown all workers on our way out
@@ -241,48 +239,57 @@ module Unicorn
     private
 
     # list of signals we care about and trap in master.
-    TRAP_SIGS = %w(QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
+    QUEUE_SIGS =
+      %w(WINCH QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
 
     # defer a signal for later processing in #join (master process)
     def trap_deferred(signal)
       trap(signal) do |sig_nr|
-        # we only handle/defer one signal at a time and ignore all others
-        # until we're ready again.  Queueing signals can lead to more bugs,
-        # and simplicity is the most important thing
-        TRAP_SIGS.each { |sig| trap(sig, 'IGNORE') }
-        if Symbol === @mode
-          @mode = signal
-          begin
-            @wr_sig.syswrite('.') # wakeup master process from IO.select
-          rescue Errno::EAGAIN
-          rescue Errno::EINTR
-            retry
-          end
+        if @sig_queue.size < 5
+          @sig_queue << signal
+          awaken_master
+        else
+          logger.error "ignoring SIG#{signal}, queue=#{@sig_queue.inspect}"
         end
       end
     end
 
+    # wait for a signal hander to wake us up and then consume the pipe
+    # Wake up every second anyways to run murder_lazy_workers
+    def master_sleep
+      begin
+        ready = IO.select([@rd_sig], nil, nil, 1)
+        ready && ready[0] && ready[0][0] or return
+        loop { @rd_sig.sysread(Const::CHUNK_SIZE) }
+      rescue Errno::EAGAIN, Errno::EINTR
+      end
+    end
 
-    def reset_master
-      @mode = :idle
-      TRAP_SIGS.each { |sig| trap_deferred(sig) }
+    def awaken_master
+      begin
+        @wr_sig.syswrite('.') # wakeup master process from IO.select
+      rescue Errno::EAGAIN # pipe is full, master should wake up anyways
+      rescue Errno::EINTR
+        retry
+      end
     end
 
     # reaps all unreaped workers
     def reap_all_workers
       begin
         loop do
-          pid = waitpid(-1, WNOHANG) or break
+          pid, status = Process.waitpid2(-1, Process::WNOHANG)
+          pid or break
           if @reexec_pid == pid
-            logger.error "reaped exec()-ed PID:#{pid} status=#{$?.exitstatus}"
+            logger.error "reaped #{status.inspect} exec()-ed"
             @reexec_pid = 0
             self.pid = @pid.chomp('.oldbin') if @pid
+            $0 = "unicorn master"
           else
             worker = @workers.delete(pid)
             worker.tempfile.close rescue nil
-            logger.info "reaped PID:#{pid} " \
-                        "worker=#{worker.nr rescue 'unknown'} " \
-                        "status=#{$?.exitstatus}"
+            logger.info "reaped #{status.inspect} " \
+                        "worker=#{worker.nr rescue 'unknown'}"
           end
         end
       rescue Errno::ECHILD
@@ -330,6 +337,7 @@ module Unicorn
         @before_exec.call(self) if @before_exec
         exec(*cmd)
       end
+      $0 = "unicorn master (old)"
     end
 
     # forcibly terminate all workers that haven't checked in in @timeout
@@ -352,6 +360,13 @@ module Unicorn
       return if @workers.size == @worker_processes
       (0...@worker_processes).each do |worker_nr|
         @workers.values.include?(worker_nr) and next
+        begin
+          Dir.chdir(@start_ctx[:cwd])
+        rescue Errno::ENOENT => err
+          logger.fatal "#{err.inspect} (#{@start_ctx[:cwd]})"
+          @sig_queue << 'QUIT' # forcibly emulate SIGQUIT
+          return
+        end
         tempfile = Tempfile.new('') # as short as possible to save dir space
         tempfile.unlink # don't allow other processes to find or see it
         tempfile.sync = true
@@ -389,7 +404,8 @@ module Unicorn
     # by the user.
     def init_worker_process(worker)
       build_app! unless @preload_app
-      TRAP_SIGS.each { |sig| trap(sig, 'IGNORE') }
+      @sig_queue.clear
+      QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
       trap('CHLD', 'DEFAULT')
       trap('USR1') do
         @logger.info "worker=#{worker.nr} rotating logs..."
@@ -403,7 +419,7 @@ module Unicorn
       @workers.values.each { |other| other.tempfile.close rescue nil }
       @workers.clear
       @start_ctx.clear
-      @mode = @start_ctx = @workers = @rd_sig = @wr_sig = nil
+      @start_ctx = @workers = @rd_sig = @wr_sig = nil
       @listeners.each { |sock| set_cloexec(sock) }
       ENV.delete('UNICORN_FD')
       @after_fork.call(self, worker.nr) if @after_fork
@@ -426,7 +442,7 @@ module Unicorn
         @listeners.each { |sock| sock.close rescue nil } # break IO.select
       end
 
-      while alive && @master_pid == ppid
+      while alive && @master_pid == Process.ppid
         # we're a goner in @timeout seconds anyways if tempfile.chmod
         # breaks, so don't trap the exception.  Using fchmod() since
         # futimes() is not available in base Ruby and I very strongly
@@ -492,7 +508,7 @@ module Unicorn
     # is no longer running.
     def kill_worker(signal, pid)
       begin
-        kill(signal, pid)
+        Process.kill(signal, pid)
       rescue Errno::ESRCH
         worker = @workers.delete(pid) and worker.tempfile.close rescue nil
       end
@@ -514,7 +530,7 @@ module Unicorn
     def valid_pid?(path)
       if File.exist?(path) && (pid = File.read(path).to_i) > 1
         begin
-          kill(0, pid)
+          Process.kill(0, pid)
           return pid
         rescue Errno::ESRCH
         end
diff --git a/lib/unicorn/app/exec_cgi.rb b/lib/unicorn/app/exec_cgi.rb
new file mode 100644
index 0000000..f5e7db9
--- /dev/null
+++ b/lib/unicorn/app/exec_cgi.rb
@@ -0,0 +1,150 @@
+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] }
+
+      IO.new(0).reopen(inp)
+      IO.new(1).reopen(out)
+      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)
+        begin
+          loop { yield(sysread(CHUNK_SIZE)) }
+        rescue EOFError
+        end
+      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 :(
+        while buf = inp.read(CHUNK_SIZE)
+          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/configurator.rb b/lib/unicorn/configurator.rb
index dd9ae3b..b4713c5 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -173,13 +173,14 @@ module Unicorn
     # worker processes.  For per-worker listeners, see the after_fork example
     def listeners(addresses)
       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)
       @set[:listeners] = [] unless Array === @set[:listeners]
-      @set[:listeners] << address
+      @set[:listeners] << expand_addr(address)
     end
 
     # sets the +path+ for the PID file of the unicorn master process
@@ -194,6 +195,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 +254,17 @@ module Unicorn
       @set[var] = my_proc
     end
 
+    # 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 address unless String === address
+      if address[0..0] == '~'
+        return File.expand_path(address)
+      elsif address =~ %r{\A\*?:(\d+)\z}
+        return "0.0.0.0:#$1"
+      end
+      address
+    end
+
   end
 end
diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb
index 46398e5..8f9e978 100644
--- a/lib/unicorn/const.rb
+++ b/lib/unicorn/const.rb
@@ -68,7 +68,7 @@ module Unicorn
     REQUEST_URI='REQUEST_URI'.freeze
     REQUEST_PATH='REQUEST_PATH'.freeze
     
-    UNICORN_VERSION="0.1.0".freeze
+    UNICORN_VERSION="0.2.2".freeze
 
     UNICORN_TMP_BASE="unicorn".freeze
 
diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb
index ce0e408..411c56c 100644
--- a/lib/unicorn/http_request.rb
+++ b/lib/unicorn/http_request.rb
@@ -130,8 +130,6 @@ module Unicorn
       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,
@@ -155,7 +153,7 @@ module Unicorn
       end
       true # success!
     rescue Object => e
-      logger.error "Error reading HTTP body: #{e.inspect}"
+      @logger.error "Error reading HTTP body: #{e.inspect}"
       socket.closed? or socket.close rescue nil
 
       # Any errors means we should delete the file, including if the file
diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb
index 7bbb940..c8aa3f9 100644
--- a/lib/unicorn/http_response.rb
+++ b/lib/unicorn/http_response.rb
@@ -21,35 +21,32 @@ module Unicorn
 
   class HttpResponse
 
-    # headers we allow duplicates for
-    ALLOWED_DUPLICATES = {
-      'Set-Cookie' => true,
-      'Set-Cookie2' => true,
-      'Warning' => true,
-      'WWW-Authenticate' => true,
-    }.freeze
+    # 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 }.freeze
 
     # writes the rack_response to socket as an HTTP response
     def self.write(socket, rack_response)
       status, headers, body = rack_response
+      out = [ "Date: #{Time.now.httpdate}" ]
 
-      # 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}"
-        end
+        next if SKIP.include?(key.downcase)
+        value.split(/\n/).each { |v| out << "#{key}: #{v}" }
       end
 
+      # Rack should enforce Content-Length or chunked transfer encoding,
+      # so don't worry or care about them.
       socket_write(socket,
                    "HTTP/1.1 #{status} #{HTTP_STATUS_CODES[status]}\r\n" \
                    "Connection: close\r\n" \
                    "#{out.join("\r\n")}\r\n\r\n")
       body.each { |chunk| socket_write(socket, chunk) }
+      ensure
+        body.respond_to?(:close) and body.close rescue nil
     end
 
     private
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