diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/unicorn.rb | 120 | ||||
-rw-r--r-- | lib/unicorn/app/exec_cgi.rb | 150 | ||||
-rw-r--r-- | lib/unicorn/configurator.rb | 19 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 2 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 4 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 29 | ||||
-rw-r--r-- | lib/unicorn/launcher.rb | 33 |
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 |