diff options
Diffstat (limited to 'lib/unicorn')
-rw-r--r-- | lib/unicorn/app/old_rails.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails/static.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/cgi_wrapper.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/configurator.rb | 24 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 31 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 47 | ||||
-rw-r--r-- | lib/unicorn/http_server.rb | 142 | ||||
-rw-r--r-- | lib/unicorn/launcher.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/oob_gc.rb | 15 | ||||
-rw-r--r-- | lib/unicorn/preread_input.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/select_waiter.rb | 7 | ||||
-rw-r--r-- | lib/unicorn/socket_helper.rb | 37 | ||||
-rw-r--r-- | lib/unicorn/stream_input.rb | 21 | ||||
-rw-r--r-- | lib/unicorn/tee_input.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/tmpio.rb | 11 | ||||
-rw-r--r-- | lib/unicorn/util.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/worker.rb | 11 |
18 files changed, 206 insertions, 148 deletions
diff --git a/lib/unicorn/app/old_rails.rb b/lib/unicorn/app/old_rails.rb index 1e8c41a..54b3e69 100644 --- a/lib/unicorn/app/old_rails.rb +++ b/lib/unicorn/app/old_rails.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: # This code is based on the original Rails handler in Mongrel diff --git a/lib/unicorn/app/old_rails/static.rb b/lib/unicorn/app/old_rails/static.rb index 2257270..cf34e02 100644 --- a/lib/unicorn/app/old_rails/static.rb +++ b/lib/unicorn/app/old_rails/static.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: # This code is based on the original Rails handler in Mongrel # Copyright (c) 2005 Zed A. Shaw diff --git a/lib/unicorn/cgi_wrapper.rb b/lib/unicorn/cgi_wrapper.rb index d9b7fe5..fb43605 100644 --- a/lib/unicorn/cgi_wrapper.rb +++ b/lib/unicorn/cgi_wrapper.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: # This code is based on the original CGIWrapper from Mongrel diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index e8b76f5..3c81596 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -1,13 +1,14 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require 'logger' # Implements a simple DSL for configuring a unicorn server. # -# See https://bogomips.org/unicorn/examples/unicorn.conf.rb and -# https://bogomips.org/unicorn/examples/unicorn.conf.minimal.rb +# See https://yhbt.net/unicorn/examples/unicorn.conf.rb and +# https://yhbt.net/unicorn/examples/unicorn.conf.minimal.rb # example configuration files. An example config file for use with # nginx is also available at -# https://bogomips.org/unicorn/examples/nginx.conf +# https://yhbt.net/unicorn/examples/nginx.conf # # See the link:/TUNING.html document for more information on tuning unicorn. class Unicorn::Configurator @@ -53,6 +54,7 @@ class Unicorn::Configurator server.logger.info("worker=#{worker.nr} ready") }, :pid => nil, + :early_hints => false, :worker_exec => false, :preload_app => false, :check_client_connection => false, @@ -215,7 +217,12 @@ class Unicorn::Configurator set_hook(:before_exec, block_given? ? block : args[0], 1) end - # sets the timeout of worker processes to +seconds+. Workers + # Strongly consider using link:/Application_Timeouts.html instead + # of this misfeature. This misfeature has done decades of damage + # to Ruby since it demotivates the use of fine-grained timeout + # mechanisms. + # + # 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 @@ -276,6 +283,15 @@ class Unicorn::Configurator set_bool(:default_middleware, bool) end + # sets whether to enable the proposed early hints Rack API. + # If enabled, Rails 5.2+ will automatically send a 103 Early Hint + # for all the `javascript_include_tag` and `stylesheet_link_tag` + # in your response. See: https://api.rubyonrails.org/v5.2/classes/ActionDispatch/Request.html#method-i-send_early_hints + # See also https://tools.ietf.org/html/rfc8297 + def early_hints(bool) + set_bool(:early_hints, bool) + end + # 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 diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index 33ab4ac..8032863 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false module Unicorn::Const # :nodoc: # default TCP listen host address (0.0.0.0, all interfaces) diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index bcc1f2d..a48dab7 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: # no stable API here require 'unicorn_http' @@ -61,8 +62,7 @@ class Unicorn::HttpParser # 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) - clear + def read_headers(socket, ai) e = env # From https://www.ietf.org/rfc/rfc3875: @@ -72,17 +72,17 @@ class Unicorn::HttpParser # 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." - e['REMOTE_ADDR'] = socket.kgio_addr + e['REMOTE_ADDR'] = ai.unix? ? '127.0.0.1' : ai.ip_address # short circuit the common case with small GET requests first - socket.kgio_read!(16384, buf) + socket.readpartial(16384, buf) if parse.nil? # 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 - false until add_parse(socket.kgio_read!(16384)) + false until add_parse(socket.readpartial(16384)) end - check_client_connection(socket) if @@check_client_connection + check_client_connection(socket, ai) if @@check_client_connection e['rack.input'] = 0 == content_length ? NULL_IO : @@input_class.new(socket, self) @@ -108,8 +108,8 @@ class Unicorn::HttpParser if Raindrops.const_defined?(:TCP_Info) TCPI = Raindrops::TCP_Info.allocate - def check_client_connection(socket) # :nodoc: - if Unicorn::TCPClient === socket + def check_client_connection(socket, ai) # :nodoc: + if ai.ip? # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state) raise Errno::EPIPE, "client closed connection".freeze, EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state) @@ -153,8 +153,8 @@ class Unicorn::HttpParser # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect. # Not that efficient, but probably still better than doing unnecessary # work after a client gives up. - def check_client_connection(socket) # :nodoc: - if Unicorn::TCPClient === socket && @@tcpi_inspect_ok + def check_client_connection(socket, ai) # :nodoc: + if @@tcpi_inspect_ok && ai.ip? opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect if opt =~ /\bstate=(\S+)/ raise Errno::EPIPE, "client closed connection".freeze, @@ -188,4 +188,15 @@ class Unicorn::HttpParser HTTP_RESPONSE_START.each { |c| socket.write(c) } end end + + # called by ext/unicorn_http/unicorn_http.rl via rb_funcall + def self.is_chunked?(v) # :nodoc: + vals = v.split(/[ \t]*,[ \t]*/).map!(&:downcase) + if vals.pop == 'chunked'.freeze + return true unless vals.include?('chunked'.freeze) + raise Unicorn::HttpParserError, 'double chunked', [] + end + return false unless vals.include?('chunked'.freeze) + raise Unicorn::HttpParserError, 'chunked not last', [] + end end diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index b23e521..3634165 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: # Writes a Rack response to your client using the HTTP/1.1 specification. # You use it by simply doing: @@ -12,6 +13,12 @@ module Unicorn::HttpResponse STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ? Rack::Utils::HTTP_STATUS_CODES : {} + STATUS_WITH_NO_ENTITY_BODY = defined?( + Rack::Utils::STATUS_WITH_NO_ENTITY_BODY) ? + Rack::Utils::STATUS_WITH_NO_ENTITY_BODY : begin + warn 'Rack::Utils::STATUS_WITH_NO_ENTITY_BODY missing' + {} + end # internal API, code will always be common-enough-for-even-old-Rack def err_response(code, response_start_sent) @@ -19,15 +26,28 @@ module Unicorn::HttpResponse "#{code} #{STATUS_CODES[code]}\r\n\r\n" end + def append_header(buf, key, value) + case value + when Array # Rack 3 + value.each { |v| buf << "#{key}: #{v}\r\n" } + when /\n/ # Rack 2 + # avoiding blank, key-only cookies with /\n+/ + value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" } + else + buf << "#{key}: #{value}\r\n" + end + end + # writes the rack_response to socket as an HTTP response def http_response_write(socket, status, headers, body, req = Unicorn::HttpRequest.new) hijack = nil - + do_chunk = false if headers code = status.to_i msg = STATUS_CODES[code] start = req.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze + term = STATUS_WITH_NO_ENTITY_BODY.include?(code) || false buf = "#{start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \ "Date: #{httpdate}\r\n" \ "Connection: close\r\n" @@ -35,25 +55,38 @@ module Unicorn::HttpResponse case key when %r{\A(?:Date|Connection)\z}i next + when %r{\AContent-Length\z}i + append_header(buf, key, value) + term = true + when %r{\ATransfer-Encoding\z}i + append_header(buf, key, value) + term = true if /\bchunked\b/i === value # value may be Array :x when "rack.hijack" # This should only be hit under Rack >= 1.5, as this was an illegal # key in Rack < 1.5 hijack = value else - if value =~ /\n/ - # avoiding blank, key-only cookies with /\n+/ - value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" } - else - buf << "#{key}: #{value}\r\n" - end + append_header(buf, key, value) end end + if !hijack && !term && req.chunkable_response? + do_chunk = true + buf << "Transfer-Encoding: chunked\r\n".freeze + end socket.write(buf << "\r\n".freeze) end if hijack req.hijacked! hijack.call(socket) + elsif do_chunk + begin + body.each do |b| + socket.write("#{b.bytesize.to_s(16)}\r\n", b, "\r\n".freeze) + end + ensure + socket.write("0\r\n\r\n".freeze) + end else body.each { |chunk| socket.write(chunk) } end diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb index 5334fa0..08fbe40 100644 --- a/lib/unicorn/http_server.rb +++ b/lib/unicorn/http_server.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # This is the process manager of Unicorn. This manages worker # processes which in turn handle the I/O and application process. @@ -6,7 +7,7 @@ # forked worker children. # # Users do not need to know the internals of this class, but reading the -# {source}[https://bogomips.org/unicorn.git/tree/lib/unicorn/http_server.rb] +# {source}[https://yhbt.net/unicorn.git/tree/lib/unicorn/http_server.rb] # is education for programmers wishing to learn how unicorn works. # See Unicorn::Configurator for information on how to configure unicorn. class Unicorn::HttpServer @@ -15,7 +16,7 @@ class Unicorn::HttpServer :before_fork, :after_fork, :before_exec, :listener_opts, :preload_app, :orig_app, :config, :ready_pipe, :user, - :default_middleware + :default_middleware, :early_hints attr_writer :after_worker_exit, :after_worker_ready, :worker_exec attr_reader :pid, :logger @@ -69,7 +70,6 @@ class Unicorn::HttpServer # incoming requests on the socket. def initialize(app, options = {}) @app = app - @request = Unicorn::HttpRequest.new @reexec_pid = 0 @default_middleware = true options = options.dup @@ -78,6 +78,7 @@ class Unicorn::HttpServer options[:use_defaults] = true self.config = Unicorn::Configurator.new(options) self.listener_opts = {} + @immortal = [] # immortal inherited sockets from systemd # We use @self_pipe differently in the master and worker processes: # @@ -111,9 +112,7 @@ class Unicorn::HttpServer @worker_data = if worker_data = ENV['UNICORN_WORKER'] worker_data = worker_data.split(',').map!(&:to_i) - worker_data[1] = worker_data.slice!(1..2).map do |i| - Kgio::Pipe.for_fd(i) - end + worker_data[1] = worker_data.slice!(1..2).map { |i| IO.for_fd(i) } worker_data end end @@ -159,6 +158,7 @@ class Unicorn::HttpServer end set_names = listener_names(listeners) dead_names.concat(cur_names - set_names).uniq! + dead_names -= @immortal.map { |io| sock_name(io) } LISTENERS.delete_if do |io| if dead_names.include?(sock_name(io)) @@ -188,7 +188,8 @@ class Unicorn::HttpServer rescue Errno::EEXIST retry end - fp.syswrite("#$$\n") + fp.sync = true + fp.write("#$$\n") File.rename(fp.path, path) fp.close end @@ -241,10 +242,6 @@ class Unicorn::HttpServer tries = opt[:tries] || 5 begin io = bind_listen(address, opt) - unless Kgio::TCPServer === io || Kgio::UNIXServer === io - io.autoclose = false - io = server_cast(io) - end logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}" LISTENERS << io io @@ -387,12 +384,13 @@ class Unicorn::HttpServer # the Ruby itself and not require a separate malloc (on 32-bit MRI 1.9+). # Most reads are only one byte here and uncommon, so it's not worth a # persistent buffer, either: - @self_pipe[0].kgio_tryread(11) + @self_pipe[0].read_nonblock(11, exception: false) end def awaken_master return if $$ != @master_pid - @self_pipe[1].kgio_trywrite('.') # wakeup master process from select + # wakeup master process from select + @self_pipe[1].write_nonblock('.', exception: false) end # reaps all unreaped workers @@ -446,11 +444,6 @@ class Unicorn::HttpServer Dir.chdir(START_CTX[:cwd]) cmd = [ START_CTX[0] ].concat(START_CTX[:argv]) - # avoid leaking FDs we don't know about, but let before_exec - # unset FD_CLOEXEC, if anything else in the app eventually - # relies on FD inheritence. - close_sockets_on_exec(listener_fds) - # exec(command, hash) works in at least 1.9.1+, but will only be # required in 1.9.4/2.0.0 at earliest. cmd << listener_fds @@ -472,29 +465,15 @@ class Unicorn::HttpServer worker_info = [worker.nr, worker.to_io.fileno, worker.master.fileno] env['UNICORN_WORKER'] = worker_info.join(',') - close_sockets_on_exec(listener_fds) - Process.spawn(env, START_CTX[0], *START_CTX[:argv], listener_fds) end def listener_sockets listener_fds = {} - LISTENERS.each do |sock| - sock.close_on_exec = false - listener_fds[sock.fileno] = sock - end + LISTENERS.each { |sock| listener_fds[sock.fileno] = sock } listener_fds end - def close_sockets_on_exec(sockets) - (3..1024).each do |io| - next if sockets.include?(io) - io = IO.for_fd(io) rescue next - io.autoclose = false - io.close_on_exec = true - end - end - # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File def murder_lazy_workers next_sleep = @timeout - 1 @@ -582,16 +561,25 @@ class Unicorn::HttpServer 500 end if code - client.kgio_trywrite(err_response(code, @request.response_start_sent)) + code = err_response(code, @request.response_start_sent) + client.write_nonblock(code, exception: false) end client.close rescue end + def e103_response_write(client, headers) + rss = @request.response_start_sent + buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n" + headers.each { |key, value| append_header(buf, key, value) } + buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze) + client.write(buf) + end + def e100_response_write(client, env) # We use String#freeze to avoid allocations under Ruby 2.1+ # Not many users hit this code path, so it's better to reduce the - # constant table sizes even for 1.9.3-2.0 users who'll hit extra + # constant table sizes even for Ruby 2.0 users who'll hit extra # allocations here. client.write(@request.response_start_sent ? "100 Continue\r\n\r\nHTTP/1.1 ".freeze : @@ -601,8 +589,19 @@ class Unicorn::HttpServer # once a client is accepted, it is processed in its entirety here # in 3 easy steps: read request, call app, write app response - def process_client(client) - status, headers, body = @app.call(env = @request.read(client)) + def process_client(client, ai) + @request = Unicorn::HttpRequest.new + env = @request.read_headers(client, ai) + + if early_hints + env["rack.early_hints"] = lambda do |headers| + e103_response_write(client, headers) + end + end + + env["rack.after_reply"] = [] + + status, headers, body = @app.call(env) begin return if @request.hijacked? @@ -624,6 +623,8 @@ class Unicorn::HttpServer end rescue => e handle_error(client, e) + ensure + env["rack.after_reply"].each(&:call) if env end def nuke_listeners!(readers) @@ -654,7 +655,6 @@ class Unicorn::HttpServer LISTENERS.each { |sock| sock.close_on_exec = true } worker.user(*user) if user.kind_of?(Array) && ! worker.switched - self.timeout /= 2.0 # halve it for select() @config = nil build_app! unless preload_app @after_fork = @listener_opts = @orig_app = nil @@ -668,58 +668,54 @@ class Unicorn::HttpServer logger.info "worker=#{worker_nr} reopening logs..." Unicorn::Util.reopen_logs logger.info "worker=#{worker_nr} done reopening logs" + false rescue => e logger.error(e) rescue nil exit!(77) # EX_NOPERM in sysexits.h end + def prep_readers(readers) + wtr = Unicorn::Waiter.prep_readers(readers) + @timeout *= 500 # to milliseconds for epoll, but halved + wtr + rescue + require_relative 'select_waiter' + @timeout /= 2.0 # halved for IO.select + Unicorn::SelectWaiter.new + end + # runs inside each forked worker, this sits around and waits # for connections and doesn't die until the parent dies (or is # given a INT, QUIT, or TERM signal) def worker_loop(worker) - ppid = @master_pid readers = init_worker_process(worker) - nr = 0 # this becomes negative if we need to reopen logs + waiter = prep_readers(readers) + reopen = false # this only works immediately if the master sent us the signal # (which is the normal case) - trap(:USR1) { nr = -65536 } + trap(:USR1) { reopen = true } ready = readers.dup @after_worker_ready.call(self, worker) begin - nr < 0 and reopen_worker_logs(worker.nr) - nr = 0 + reopen = reopen_worker_logs(worker.nr) if reopen worker.tick = time_now.to_i - tmp = ready.dup - while sock = tmp.shift - # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all, - # but that will return false - if client = sock.kgio_tryaccept - process_client(client) - nr += 1 + while sock = ready.shift + client_ai = sock.accept_nonblock(exception: false) + if client_ai != :wait_readable + process_client(*client_ai) worker.tick = time_now.to_i end - break if nr < 0 + break if reopen end - # make the following bet: if we accepted clients this round, - # we're probably reasonably busy, so avoid calling select() - # and do a speculative non-blocking accept() on ready listeners - # before we sleep again in select(). - unless nr == 0 - tmp = ready.dup - redo - end - - ppid == Process.ppid or return - - # timeout used so we can detect parent death: + # timeout so we can .tick and keep parent from SIGKILL-ing us worker.tick = time_now.to_i - ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0] + waiter.get_readers(ready, readers, @timeout) rescue => e - redo if nr < 0 && readers[0] + redo if reopen && readers[0] Unicorn.log_error(@logger, "listen loop error", e) if readers[0] end while readers[0] end @@ -807,21 +803,21 @@ class Unicorn::HttpServer def inherit_listeners! # inherit sockets from parents, they need to be plain Socket objects - # before they become Kgio::UNIXServer or Kgio::TCPServer inherited = ENV['UNICORN_FD'].to_s.split(',') + immortal = [] # emulate sd_listen_fds() for systemd sd_pid, sd_fds = ENV.values_at('LISTEN_PID', 'LISTEN_FDS') if sd_pid.to_i == $$ # n.b. $$ can never be zero # 3 = SD_LISTEN_FDS_START - inherited.concat((3...(3 + sd_fds.to_i)).to_a) + immortal = (3...(3 + sd_fds.to_i)).to_a + inherited.concat(immortal) end # to ease debugging, we will not unset LISTEN_PID and LISTEN_FDS inherited.map! do |fd| io = Socket.for_fd(fd.to_i) - io.autoclose = false - io = server_cast(io) + @immortal << io if immortal.include?(fd) set_server_sockopt(io, listener_opts[sock_name(io)]) logger.info "inherited addr=#{sock_name(io)} fd=#{io.fileno}" io @@ -830,11 +826,9 @@ class Unicorn::HttpServer config_listeners = config[:listeners].dup LISTENERS.replace(inherited) - # we start out with generic Socket objects that get cast to either - # Kgio::TCPServer or Kgio::UNIXServer objects; but since the Socket - # objects share the same OS-level file descriptor as the higher-level - # *Server objects; we need to prevent Socket objects from being - # garbage-collected + # we only use generic Socket objects for aggregate Socket#accept_nonblock + # return value [ Socket, Addrinfo ]. This allows us to avoid having to + # make getpeername(2) syscalls later on to fill in env['REMOTE_ADDR'] config_listeners -= listener_names if config_listeners.empty? && LISTENERS.empty? config_listeners << Unicorn::Const::DEFAULT_LISTEN diff --git a/lib/unicorn/launcher.rb b/lib/unicorn/launcher.rb index 78e8f39..bd3324e 100644 --- a/lib/unicorn/launcher.rb +++ b/lib/unicorn/launcher.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: $stdout.sync = $stderr.sync = true diff --git a/lib/unicorn/oob_gc.rb b/lib/unicorn/oob_gc.rb index c4741a0..efd9177 100644 --- a/lib/unicorn/oob_gc.rb +++ b/lib/unicorn/oob_gc.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Strongly consider https://github.com/tmm1/gctools if using Ruby 2.1+ # It is built on new APIs in Ruby 2.1, so it is more intelligent than @@ -43,8 +44,8 @@ # use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)} # # Feedback from users of early implementations of this module: -# * https://bogomips.org/unicorn-public/0BFC98E9-072B-47EE-9A70-05478C20141B@lukemelia.com/ -# * https://bogomips.org/unicorn-public/AANLkTilUbgdyDv9W1bi-s_W6kq9sOhWfmuYkKLoKGOLj@mail.gmail.com/ +# * https://yhbt.net/unicorn-public/0BFC98E9-072B-47EE-9A70-05478C20141B@lukemelia.com/ +# * https://yhbt.net/unicorn-public/AANLkTilUbgdyDv9W1bi-s_W6kq9sOhWfmuYkKLoKGOLj@mail.gmail.com/ module Unicorn::OobGC @@ -60,17 +61,17 @@ module Unicorn::OobGC self.const_set :OOBGC_INTERVAL, interval ObjectSpace.each_object(Unicorn::HttpServer) do |s| s.extend(self) - self.const_set :OOBGC_ENV, s.instance_variable_get(:@request).env end app # pretend to be Rack middleware since it was in the past end #:stopdoc: - def process_client(client) - super(client) # Unicorn::HttpServer#process_client - if OOBGC_PATH =~ OOBGC_ENV['PATH_INFO'] && ((@@nr -= 1) <= 0) + def process_client(*args) + super(*args) # Unicorn::HttpServer#process_client + env = instance_variable_get(:@request).env + if OOBGC_PATH =~ env['PATH_INFO'] && ((@@nr -= 1) <= 0) @@nr = OOBGC_INTERVAL - OOBGC_ENV.clear + env.clear disabled = GC.enable GC.start GC.disable if disabled diff --git a/lib/unicorn/preread_input.rb b/lib/unicorn/preread_input.rb index 12eb3e8..c62cc09 100644 --- a/lib/unicorn/preread_input.rb +++ b/lib/unicorn/preread_input.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false module Unicorn # This middleware is used to ensure input is buffered to memory diff --git a/lib/unicorn/select_waiter.rb b/lib/unicorn/select_waiter.rb new file mode 100644 index 0000000..d11ea57 --- /dev/null +++ b/lib/unicorn/select_waiter.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE +class Unicorn::SelectWaiter # :nodoc: + def get_readers(ready, readers, timeout) # :nodoc: + ret = IO.select(readers, nil, nil, timeout) and ready.replace(ret[0]) + end +end diff --git a/lib/unicorn/socket_helper.rb b/lib/unicorn/socket_helper.rb index 8a6f6ee..986932f 100644 --- a/lib/unicorn/socket_helper.rb +++ b/lib/unicorn/socket_helper.rb @@ -1,20 +1,9 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :enddoc: require 'socket' module Unicorn - - # Instead of using a generic Kgio::Socket for everything, - # tag TCP sockets so we can use TCP_INFO under Linux without - # incurring extra syscalls for Unix domain sockets. - # TODO: remove these when we remove kgio - TCPClient = Class.new(Kgio::Socket) # :nodoc: - class TCPSrv < Kgio::TCPServer # :nodoc: - def kgio_tryaccept # :nodoc: - super(TCPClient) - end - end - module SocketHelper # internal interface @@ -91,7 +80,7 @@ module Unicorn def set_server_sockopt(sock, opt) opt = DEFAULTS.merge(opt || {}) - TCPSocket === sock and set_tcp_sockopt(sock, opt) + set_tcp_sockopt(sock, opt) if sock.local_address.ip? rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf) if rcvbuf || sndbuf @@ -135,7 +124,9 @@ module Unicorn end old_umask = File.umask(opt[:umask] || 0) begin - Kgio::UNIXServer.new(address) + s = Socket.new(:UNIX, :STREAM) + s.bind(Socket.sockaddr_un(address)) + s ensure File.umask(old_umask) end @@ -163,8 +154,7 @@ module Unicorn sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1) end sock.bind(Socket.pack_sockaddr_in(port, addr)) - sock.autoclose = false - TCPSrv.for_fd(sock.fileno) + sock end # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6 @@ -180,10 +170,6 @@ module Unicorn def sock_name(sock) case sock when String then sock - when UNIXServer - Socket.unpack_sockaddr_un(sock.getsockname) - when TCPServer - tcp_name(sock) when Socket begin tcp_name(sock) @@ -196,16 +182,5 @@ module Unicorn end module_function :sock_name - - # casts a given Socket to be a TCPServer or UNIXServer - def server_cast(sock) - begin - Socket.unpack_sockaddr_in(sock.getsockname) - TCPSrv.for_fd(sock.fileno) - rescue ArgumentError - Kgio::UNIXServer.for_fd(sock.fileno) - end - end - end # module SocketHelper end # module Unicorn diff --git a/lib/unicorn/stream_input.rb b/lib/unicorn/stream_input.rb index 41d28a0..23a9976 100644 --- a/lib/unicorn/stream_input.rb +++ b/lib/unicorn/stream_input.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # When processing uploads, unicorn may expose a StreamInput object under # "rack.input" of the Rack environment when @@ -49,8 +50,7 @@ class Unicorn::StreamInput to_read = length - @rbuf.size rv.replace(@rbuf.slice!(0, @rbuf.size)) until to_read == 0 || eof? || (rv.size > 0 && @chunked) - @socket.kgio_read(to_read, @buf) or eof! - filter_body(@rbuf, @buf) + filter_body(@rbuf, @socket.readpartial(to_read, @buf)) rv << @rbuf to_read -= @rbuf.size end @@ -61,6 +61,8 @@ class Unicorn::StreamInput read_all(rv) end rv + rescue EOFError + return eof! end # :call-seq: @@ -83,9 +85,10 @@ class Unicorn::StreamInput begin @rbuf.sub!(re, '') and return $1 return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof? - @socket.kgio_read(@@io_chunk_size, @buf) or eof! - filter_body(once = '', @buf) + filter_body(once = '', @socket.readpartial(@@io_chunk_size, @buf)) @rbuf << once + rescue EOFError + return eof! end while true end @@ -107,14 +110,15 @@ private def eof? if @parser.body_eof? while @chunked && ! @parser.parse - once = @socket.kgio_read(@@io_chunk_size) or eof! - @buf << once + @buf << @socket.readpartial(@@io_chunk_size) end @socket = nil true else false end + rescue EOFError + return eof! end def filter_body(dst, src) @@ -127,10 +131,11 @@ private dst.replace(@rbuf) @socket or return until eof? - @socket.kgio_read(@@io_chunk_size, @buf) or eof! - filter_body(@rbuf, @buf) + filter_body(@rbuf, @socket.readpartial(@@io_chunk_size, @buf)) dst << @rbuf end + rescue EOFError + return eof! ensure @rbuf.clear end diff --git a/lib/unicorn/tee_input.rb b/lib/unicorn/tee_input.rb index 2ccc2d9..b3c6535 100644 --- a/lib/unicorn/tee_input.rb +++ b/lib/unicorn/tee_input.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Acts like tee(1) on an input input to provide a input-like stream # while providing rewindable semantics through a File/StringIO backing diff --git a/lib/unicorn/tmpio.rb b/lib/unicorn/tmpio.rb index db88ed3..deecd80 100644 --- a/lib/unicorn/tmpio.rb +++ b/lib/unicorn/tmpio.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # :stopdoc: require 'tmpdir' @@ -11,12 +12,18 @@ class Unicorn::TmpIO < File # immediately, switched to binary mode, and userspace output # buffering is disabled def self.new + path = nil + + # workaround File#path being tainted: + # https://bugs.ruby-lang.org/issues/14485 fp = begin - super("#{Dir::tmpdir}/#{rand}", RDWR|CREAT|EXCL, 0600) + path = "#{Dir::tmpdir}/#{rand}" + super(path, RDWR|CREAT|EXCL, 0600) rescue Errno::EEXIST retry end - unlink(fp.path) + + unlink(path) fp.binmode fp.sync = true fp diff --git a/lib/unicorn/util.rb b/lib/unicorn/util.rb index b826de4..f28d929 100644 --- a/lib/unicorn/util.rb +++ b/lib/unicorn/util.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require 'fcntl' module Unicorn::Util # :nodoc: diff --git a/lib/unicorn/worker.rb b/lib/unicorn/worker.rb index 5ddf379..d2445d5 100644 --- a/lib/unicorn/worker.rb +++ b/lib/unicorn/worker.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require "raindrops" # This class and its members can be considered a stable interface @@ -65,15 +66,15 @@ class Unicorn::Worker end # writing and reading 4 bytes on a pipe is atomic on all POSIX platforms # Do not care in the odd case the buffer is full, here. - @master.kgio_trywrite([signum].pack('l')) + @master.write_nonblock([signum].pack('l'), exception: false) rescue Errno::EPIPE # worker will be reaped soon end # this only runs when the Rack app.call is not running - # act like a listener - def kgio_tryaccept # :nodoc: - case buf = @to_io.kgio_tryread(4) + # act like Socket#accept_nonblock(exception: false) + def accept_nonblock(*_unused) # :nodoc: + case buf = @to_io.read_nonblock(4, exception: false) when String # unpack the buffer and trigger the signal handler signum = buf.unpack('l') @@ -82,7 +83,7 @@ class Unicorn::Worker when nil # EOF: master died, but we are at a safe place to exit fake_sig(:QUIT) when :wait_readable # keep waiting - return false + return :wait_readable end while true # loop, as multiple signals may be sent end |