diff options
30 files changed, 197 insertions, 1089 deletions
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 0030ca4..493fe52 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -DEF_VER = "v4.9.0" +DEF_VER = "v5.0.0.GIT" CONSTANT = "Unicorn::Const::UNICORN_VERSION" RVF = "lib/unicorn/version.rb" GVF = "GIT-VERSION-FILE" diff --git a/GNUmakefile b/GNUmakefile index 7cf1023..2995a6b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -86,10 +86,14 @@ test-unit: $(wildcard test/unit/test_*.rb) $(slow_tests): $(test_prefix)/.stamp @$(MAKE) $(shell $(awk_slow) $@) +# ensure we can require just the HTTP parser without the rest of unicorn +test-require: $(ext)/unicorn_http.$(DLEXT) + $(RUBY) --disable-gems -I$(ext) -runicorn_http -e Unicorn + test-integration: $(test_prefix)/.stamp $(MAKE) -C t -check: test test-integration +check: test-require test test-integration test-all: check TEST_OPTS = -v @@ -17,9 +17,12 @@ See Unicorn::Configurator for details on the config file format. \Unicorn is NOT for serving slow clients, that is the job of nginx. * worker_processes should be *at* *least* the number of CPU cores on - a dedicated server. If your application has occasionally slow - responses that are /not/ CPU-intensive, you may increase this to - workaround those inefficiencies. + a dedicated server (unless you do not have enough memory). + If your application has occasionally slow responses that are /not/ + CPU-intensive, you may increase this to workaround those inefficiencies. + +* Under Ruby 2.2 or later, Etc.nprocessors may be used to determine + the number of CPU cores present. * worker_processes may be increased for Unicorn::OobGC users to provide more consistent response times. diff --git a/bin/unicorn b/bin/unicorn index c272e43..3c5e5cb 100755 --- a/bin/unicorn +++ b/bin/unicorn @@ -29,7 +29,7 @@ op = OptionParser.new("", 24, ' ') do |opts| opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path| - $LOAD_PATH.unshift(*path.split(/:/)) + $LOAD_PATH.unshift(*path.split(':')) end opts.on("-r", "--require LIBRARY", diff --git a/bin/unicorn_rails b/bin/unicorn_rails index b080846..ea4f822 100755 --- a/bin/unicorn_rails +++ b/bin/unicorn_rails @@ -30,7 +30,7 @@ op = OptionParser.new("", 24, ' ') do |opts| opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path| - $LOAD_PATH.unshift(*path.split(/:/)) + $LOAD_PATH.unshift(*path.split(':')) end opts.on("-r", "--require LIBRARY", diff --git a/examples/git.ru b/examples/git.ru deleted file mode 100644 index 59a31c9..0000000 --- a/examples/git.ru +++ /dev/null @@ -1,13 +0,0 @@ -#\-E none - -# See http://thread.gmane.org/gmane.comp.web.curl.general/10473/raw on -# how to setup git for this. A better version of the above patch was -# accepted and committed on June 15, 2009, so you can pull the latest -# curl CVS snapshot to try this out. -require 'unicorn/app/inetd' - -use Rack::Lint -use Rack::Chunked # important! -run Unicorn::App::Inetd.new( - *%w(git daemon --verbose --inetd --export-all --base-path=/home/ew/unicorn) -) diff --git a/ext/unicorn_http/httpdate.c b/ext/unicorn_http/httpdate.c index bf54fdd..0a1045f 100644 --- a/ext/unicorn_http/httpdate.c +++ b/ext/unicorn_http/httpdate.c @@ -66,7 +66,7 @@ static VALUE httpdate(VALUE self) void init_unicorn_httpdate(void) { - VALUE mod = rb_const_get(rb_cObject, rb_intern("Unicorn")); + VALUE mod = rb_define_module("Unicorn"); mod = rb_define_module_under(mod, "HttpResponse"); buf = rb_str_new(0, buf_capa - 1); diff --git a/ext/unicorn_http/unicorn_http.rl b/ext/unicorn_http/unicorn_http.rl index 6293033..4254540 100644 --- a/ext/unicorn_http/unicorn_http.rl +++ b/ext/unicorn_http/unicorn_http.rl @@ -29,82 +29,27 @@ void init_unicorn_httpdate(void); /* all of these flags need to be set for keepalive to be supported */ #define UH_FL_KEEPALIVE (UH_FL_KAVERSION | UH_FL_REQEOF | UH_FL_HASHEADER) -/* - * whether or not to trust X-Forwarded-Proto and X-Forwarded-SSL when - * setting rack.url_scheme - */ -static VALUE trust_x_forward = Qtrue; - -static unsigned long keepalive_requests = 100; /* same as nginx */ - -/* - * Returns the maximum number of keepalive requests a client may make - * before the parser refuses to continue. - */ -static VALUE ka_req(VALUE self) -{ - return ULONG2NUM(keepalive_requests); -} - -/* - * Sets the maximum number of keepalive requests a client may make. - * A special value of +nil+ causes this to be the maximum value - * possible (this is architecture-dependent). - */ -static VALUE set_ka_req(VALUE self, VALUE val) -{ - keepalive_requests = NIL_P(val) ? ULONG_MAX : NUM2ULONG(val); - - return ka_req(self); -} - -/* - * Sets whether or not the parser will trust X-Forwarded-Proto and - * X-Forwarded-SSL headers and set "rack.url_scheme" to "https" accordingly. - * Rainbows!/Zbatery installations facing untrusted clients directly - * should set this to +false+ - */ -static VALUE set_xftrust(VALUE self, VALUE val) -{ - if (Qtrue == val || Qfalse == val) - trust_x_forward = val; - else - rb_raise(rb_eTypeError, "must be true or false"); - - return val; -} - -/* - * returns whether or not the parser will trust X-Forwarded-Proto and - * X-Forwarded-SSL headers and set "rack.url_scheme" to "https" accordingly - */ -static VALUE xftrust(VALUE self) -{ - return trust_x_forward; -} - -static size_t MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */ +static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */ /* this is only intended for use with Rainbows! */ static VALUE set_maxhdrlen(VALUE self, VALUE len) { - return SIZET2NUM(MAX_HEADER_LEN = NUM2SIZET(len)); + return UINT2NUM(MAX_HEADER_LEN = NUM2UINT(len)); } /* keep this small for Rainbows! since every client has one */ struct http_parser { int cs; /* Ragel internal state */ unsigned int flags; - unsigned long nr_requests; - size_t mark; - size_t offset; + unsigned int mark; + unsigned int offset; union { /* these 2 fields don't nest */ - size_t field; - size_t query; + unsigned int field; + unsigned int query; } start; union { - size_t field_len; /* only used during header processing */ - size_t dest_offset; /* only used during body processing */ + unsigned int field_len; /* only used during header processing */ + unsigned int dest_offset; /* only used during body processing */ } s; VALUE buf; VALUE env; @@ -124,13 +69,25 @@ static void parser_raise(VALUE klass, const char *msg) VALUE exc = rb_exc_new2(klass, msg); VALUE bt = rb_ary_new(); - rb_funcall(exc, id_set_backtrace, 1, bt); - rb_exc_raise(exc); + rb_funcall(exc, id_set_backtrace, 1, bt); + rb_exc_raise(exc); +} + +static inline unsigned int ulong2uint(unsigned long n) +{ + unsigned int i = (unsigned int)n; + + if (sizeof(unsigned int) != sizeof(unsigned long)) { + if ((unsigned long)i != n) { + rb_raise(rb_eRangeError, "too large to be 32-bit uint: %lu", n); + } + } + return i; } #define REMAINING (unsigned long)(pe - p) -#define LEN(AT, FPC) (FPC - buffer - hp->AT) -#define MARK(M,FPC) (hp->M = (FPC) - buffer) +#define LEN(AT, FPC) (ulong2uint(FPC - buffer) - hp->AT) +#define MARK(M,FPC) (hp->M = ulong2uint((FPC) - buffer)) #define PTR_TO(F) (buffer + hp->F) #define STR_NEW(M,FPC) rb_str_new(PTR_TO(M), LEN(M, FPC)) #define STRIPPED_STR_NEW(M,FPC) stripped_str_new(PTR_TO(M), LEN(M, FPC)) @@ -466,7 +423,7 @@ http_parser_execute(struct http_parser *hp, char *buffer, size_t len) post_exec: /* "_out:" also goes here */ if (hp->cs != http_parser_error) hp->cs = cs; - hp->offset = p - buffer; + hp->offset = ulong2uint(p - buffer); assert(p <= pe && "buffer overflow after parsing execute"); assert(hp->offset <= len && "offset longer than length"); @@ -491,26 +448,29 @@ static void set_url_scheme(VALUE env, VALUE *server_port) VALUE scheme = rb_hash_aref(env, g_rack_url_scheme); if (NIL_P(scheme)) { - if (trust_x_forward == Qfalse) { - scheme = g_http; + /* + * would anybody be horribly opposed to removing the X-Forwarded-SSL + * and X-Forwarded-Proto handling from this parser? We've had it + * forever and nobody has said anything against it, either. + * Anyways, please send comments to our public mailing list: + * unicorn-public@bogomips.org (no HTML mail, no subscription necessary) + */ + scheme = rb_hash_aref(env, g_http_x_forwarded_ssl); + if (!NIL_P(scheme) && STR_CSTR_EQ(scheme, "on")) { + *server_port = g_port_443; + scheme = g_https; } else { - scheme = rb_hash_aref(env, g_http_x_forwarded_ssl); - if (!NIL_P(scheme) && STR_CSTR_EQ(scheme, "on")) { - *server_port = g_port_443; - scheme = g_https; + scheme = rb_hash_aref(env, g_http_x_forwarded_proto); + if (NIL_P(scheme)) { + scheme = g_http; } else { - scheme = rb_hash_aref(env, g_http_x_forwarded_proto); - if (NIL_P(scheme)) { - scheme = g_http; + long len = RSTRING_LEN(scheme); + if (len >= 5 && !memcmp(RSTRING_PTR(scheme), "https", 5)) { + if (len != 5) + scheme = g_https; + *server_port = g_port_443; } else { - long len = RSTRING_LEN(scheme); - if (len >= 5 && !memcmp(RSTRING_PTR(scheme), "https", 5)) { - if (len != 5) - scheme = g_https; - *server_port = g_port_443; - } else { - scheme = g_http; - } + scheme = g_http; } } } @@ -608,7 +568,6 @@ static VALUE HttpParser_init(VALUE self) http_parser_init(hp); hp->buf = rb_str_new(NULL, 0); hp->env = rb_hash_new(); - hp->nr_requests = keepalive_requests; return self; } @@ -631,55 +590,6 @@ static VALUE HttpParser_clear(VALUE self) return self; } -/** - * call-seq: - * parser.dechunk! => parser - * - * Resets the parser to a state suitable for dechunking response bodies - * - */ -static VALUE HttpParser_dechunk_bang(VALUE self) -{ - struct http_parser *hp = data_get(self); - - http_parser_init(hp); - - /* - * we don't care about trailers in dechunk-only mode, - * but if we did we'd set UH_FL_HASTRAILER and clear hp->env - */ - if (0) { - rb_funcall(hp->env, id_clear, 0); - hp->flags = UH_FL_HASTRAILER; - } - - hp->flags |= UH_FL_HASBODY | UH_FL_INBODY | UH_FL_CHUNKED; - hp->cs = http_parser_en_ChunkedBody; - - return self; -} - -/** - * call-seq: - * parser.reset => nil - * - * Resets the parser to it's initial state so that you can reuse it - * rather than making new ones. - * - * This method is deprecated and to be removed in Unicorn 4.x - */ -static VALUE HttpParser_reset(VALUE self) -{ - static int warned; - - if (!warned) { - rb_warn("Unicorn::HttpParser#reset is deprecated; " - "use Unicorn::HttpParser#clear instead"); - } - HttpParser_clear(self); - return Qnil; -} - static void advance_str(VALUE str, off_t nr) { long len = RSTRING_LEN(str); @@ -842,15 +752,13 @@ static VALUE HttpParser_keepalive(VALUE self) * parser.next? => true or false * * Exactly like HttpParser#keepalive?, except it will reset the internal - * parser state on next parse if it returns true. It will also respect - * the maximum *keepalive_requests* value and return false if that is - * reached. + * parser state on next parse if it returns true. */ static VALUE HttpParser_next(VALUE self) { struct http_parser *hp = data_get(self); - if ((HP_FL_ALL(hp, KEEPALIVE)) && (hp->nr_requests-- != 0)) { + if (HP_FL_ALL(hp, KEEPALIVE)) { HP_FL_SET(hp, TO_CLEAR); return Qtrue; } @@ -969,7 +877,7 @@ void Init_unicorn_http(void) { VALUE mUnicorn, cHttpParser; - mUnicorn = rb_const_get(rb_cObject, rb_intern("Unicorn")); + mUnicorn = rb_define_module("Unicorn"); cHttpParser = rb_define_class_under(mUnicorn, "HttpParser", rb_cObject); eHttpParserError = rb_define_class_under(mUnicorn, "HttpParserError", rb_eIOError); @@ -982,8 +890,6 @@ void Init_unicorn_http(void) rb_define_alloc_func(cHttpParser, HttpParser_alloc); rb_define_method(cHttpParser, "initialize", HttpParser_init, 0); rb_define_method(cHttpParser, "clear", HttpParser_clear, 0); - rb_define_method(cHttpParser, "reset", HttpParser_reset, 0); - rb_define_method(cHttpParser, "dechunk!", HttpParser_dechunk_bang, 0); rb_define_method(cHttpParser, "parse", HttpParser_parse, 0); rb_define_method(cHttpParser, "add_parse", HttpParser_add_parse, 1); rb_define_method(cHttpParser, "headers", HttpParser_headers, 2); @@ -1012,14 +918,6 @@ void Init_unicorn_http(void) */ rb_define_const(cHttpParser, "LENGTH_MAX", OFFT2NUM(UH_OFF_T_MAX)); - /* default value for keepalive_requests */ - rb_define_const(cHttpParser, "KEEPALIVE_REQUESTS_DEFAULT", - ULONG2NUM(keepalive_requests)); - - rb_define_singleton_method(cHttpParser, "keepalive_requests", ka_req, 0); - rb_define_singleton_method(cHttpParser, "keepalive_requests=", set_ka_req, 1); - rb_define_singleton_method(cHttpParser, "trust_x_forwarded=", set_xftrust, 1); - rb_define_singleton_method(cHttpParser, "trust_x_forwarded?", xftrust, 0); rb_define_singleton_method(cHttpParser, "max_header_len=", set_maxhdrlen, 1); init_common_fields(); diff --git a/lib/unicorn.rb b/lib/unicorn.rb index 358748f..9fdcb8e 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -1,5 +1,4 @@ # -*- encoding: binary -*- -require 'fcntl' require 'etc' require 'stringio' require 'rack' @@ -22,8 +21,7 @@ module Unicorn # since there is nothing in the application stack that is responsible # for client shutdowns/disconnects. This exception is visible to Rack # applications unless PrereadInput middleware is loaded. - class ClientShutdown < EOFError - end + ClientShutdown = Class.new(EOFError) # :stopdoc: @@ -102,19 +100,13 @@ module Unicorn # remove this when we only support Ruby >= 2.0 def self.pipe # :nodoc: - Kgio::Pipe.new.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) } + Kgio::Pipe.new.each { |io| io.close_on_exec = true } end # :startdoc: end # :enddoc: -require 'unicorn/const' -require 'unicorn/socket_helper' -require 'unicorn/stream_input' -require 'unicorn/tee_input' -require 'unicorn/http_request' -require 'unicorn/configurator' -require 'unicorn/tmpio' -require 'unicorn/util' -require 'unicorn/http_response' -require 'unicorn/worker' -require 'unicorn/http_server' + +%w(const socket_helper stream_input tee_input http_request configurator + tmpio util http_response worker http_server).each do |s| + require_relative "unicorn/#{s}" +end diff --git a/lib/unicorn/app/exec_cgi.rb b/lib/unicorn/app/exec_cgi.rb deleted file mode 100644 index 232b681..0000000 --- a/lib/unicorn/app/exec_cgi.rb +++ /dev/null @@ -1,154 +0,0 @@ -# -*- encoding: binary -*- -# :enddoc: -require 'unicorn' - -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 < Struct.new(:args) - - 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 } # frozen strings are faster for Hash assignments - - class Body < Unicorn::TmpIO - def body_offset=(n) - sysseek(@body_offset = n) - end - - def each - sysseek @body_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 - break - end while true - end - end - - # 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) - self.args = args - first = args[0] or - raise ArgumentError, "need path to executable" - first[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 = Body.new, Unicorn::TmpIO.new - inp = force_file_input(env) - 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] } - - $stdin.reopen(inp) - $stdout.reopen(out) - $stderr.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.body_offset = offset - size -= offset - 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 - status = headers.delete("Status") || 200 - headers['Content-Length'] = size.to_s - [ status, 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'] - # inp could be a StringIO or StringIO-like object - if inp.respond_to?(:size) && inp.size == 0 - ::File.open('/dev/null', 'rb') - else - tmp = Unicorn::TmpIO.new - - buf = inp.read(CHUNK_SIZE) - begin - tmp.syswrite(buf) - end while inp.read(CHUNK_SIZE, buf) - 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/inetd.rb b/lib/unicorn/app/inetd.rb deleted file mode 100644 index 13b6624..0000000 --- a/lib/unicorn/app/inetd.rb +++ /dev/null @@ -1,109 +0,0 @@ -# -*- encoding: binary -*- -# :enddoc: -# Copyright (c) 2009 Eric Wong -# You can redistribute it and/or modify it under the same terms as Ruby 1.8 or -# the GPLv2+ (GPLv3+ preferred) - -# this class *must* be used with Rack::Chunked -module Unicorn::App - class Inetd < Struct.new(:cmd) - - class CatBody < Struct.new(:errors, :err_rd, :out_rd, :pid_map) - def initialize(env, cmd) - self.errors = env['rack.errors'] - in_rd, in_wr = IO.pipe - self.err_rd, err_wr = IO.pipe - self.out_rd, out_wr = IO.pipe - - cmd_pid = fork { - inp, out, err = (0..2).map { |i| IO.new(i) } - inp.reopen(in_rd) - out.reopen(out_wr) - err.reopen(err_wr) - [ in_rd, in_wr, err_rd, err_wr, out_rd, out_wr ].each { |i| i.close } - exec(*cmd) - } - [ in_rd, err_wr, out_wr ].each { |io| io.close } - [ in_wr, err_rd, out_rd ].each { |io| io.binmode } - in_wr.sync = true - - # Unfortunately, input here must be processed inside a seperate - # thread/process using blocking I/O since env['rack.input'] is not - # IO.select-able and attempting to make it so would trip Rack::Lint - inp_pid = fork { - input = env['rack.input'] - [ err_rd, out_rd ].each { |io| io.close } - - # this is dependent on input.read having readpartial semantics: - buf = input.read(16384) - begin - in_wr.write(buf) - end while input.read(16384, buf) - } - in_wr.close - self.pid_map = { - inp_pid => 'input streamer', - cmd_pid => cmd.inspect, - } - end - - def each - begin - rd, = IO.select([err_rd, out_rd]) - rd && rd.first or next - - if rd.include?(err_rd) - begin - errors.write(err_rd.read_nonblock(16384)) - rescue Errno::EINTR - rescue Errno::EAGAIN - break - end while true - end - - rd.include?(out_rd) or next - - begin - yield out_rd.read_nonblock(16384) - rescue Errno::EINTR - rescue Errno::EAGAIN - break - end while true - rescue EOFError,Errno::EPIPE,Errno::EBADF,Errno::EINVAL - break - end while true - - self - end - - def close - pid_map.each { |pid, str| - begin - pid, status = Process.waitpid2(pid) - status.success? or - errors.write("#{str}: #{status.inspect} (PID:#{pid})\n") - rescue Errno::ECHILD - errors.write("Failed to reap #{str} (PID:#{pid})\n") - end - } - out_rd.close - err_rd.close - end - - end - - def initialize(*cmd) - self.cmd = cmd - end - - def call(env) - /\A100-continue\z/i =~ env[Unicorn::Const::HTTP_EXPECT] and - return [ 100, {} , [] ] - - [ 200, { 'Content-Type' => 'application/octet-stream' }, - CatBody.new(env, cmd) ] - end - - end - -end diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index 0658c81..32e49c1 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -1,6 +1,5 @@ # -*- encoding: binary -*- require 'logger' -require 'unicorn/ssl_configurator' # Implements a simple DSL for configuring a \Unicorn server. # @@ -13,7 +12,6 @@ require 'unicorn/ssl_configurator' # See the link:/TUNING.html document for more information on tuning unicorn. class Unicorn::Configurator include Unicorn - include Unicorn::SSLConfigurator # :stopdoc: attr_accessor :set, :config_file, :after_reload @@ -48,7 +46,6 @@ class Unicorn::Configurator :check_client_connection => false, :rewindable_input => true, # for Rack 2.x: (Rack::VERSION[0] <= 1), :client_body_buffer_size => Unicorn::Const::MAX_BODY, - :trust_x_forwarded => true, } #:startdoc: @@ -556,18 +553,6 @@ class Unicorn::Configurator set[:user] = [ user, group ] end - # Sets whether or not the parser will trust X-Forwarded-Proto and - # X-Forwarded-SSL headers and set "rack.url_scheme" to "https" accordingly. - # Rainbows!/Zbatery installations facing untrusted clients directly - # should set this to +false+. This is +true+ by default as Unicorn - # is designed to only sit behind trusted nginx proxies. - # - # This has never been publically documented and is subject to removal - # in future releases. - def trust_x_forwarded(bool) # :nodoc: - set_bool(:trust_x_forwarded, bool) - 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" @@ -601,7 +586,7 @@ private def canonicalize_tcp(addr, port) packed = Socket.pack_sockaddr_in(port, addr) port, addr = Socket.unpack_sockaddr_in(packed) - /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}" + addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}" end def set_path(var, path) #:nodoc: @@ -657,7 +642,7 @@ private raise ArgumentError, "rackup file (#{ru}) not readable" # it could be a .rb file, too, we don't parse those manually - ru =~ /\.ru\z/ or return + ru.end_with?('.ru') or return /^#\\(.*)/ =~ File.read(ru) or return RACKUP[:optparse].parse!($1.split(/\s+/)) diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index 51d7394..33ab4ac 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -1,12 +1,6 @@ # -*- encoding: binary -*- -# :enddoc: -# 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. -module Unicorn::Const +module Unicorn::Const # :nodoc: # default TCP listen host address (0.0.0.0, all interfaces) DEFAULT_HOST = "0.0.0.0" @@ -23,22 +17,5 @@ module Unicorn::Const # temporary file for reading (112 kilobytes). This is the default # value of client_body_buffer_size. MAX_BODY = 1024 * 112 - - # :stopdoc: - # common errors we'll send back - # (N.B. these are not used by unicorn, but we won't drop them until - # unicorn 5.x to avoid breaking Rainbows!). - ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n" - ERROR_414_RESPONSE = "HTTP/1.1 414 Request-URI Too Long\r\n\r\n" - ERROR_413_RESPONSE = "HTTP/1.1 413 Request Entity Too Large\r\n\r\n" - ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n" - - EXPECT_100_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n" - EXPECT_100_RESPONSE_SUFFIXED = "100 Continue\r\n\r\nHTTP/1.1 " - - HTTP_RESPONSE_START = ['HTTP', '/1.1 '] - HTTP_EXPECT = "HTTP_EXPECT" - - # :startdoc: end -require 'unicorn/version' +require_relative 'version' diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index 6b20431..9888430 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -26,8 +26,11 @@ class Unicorn::HttpParser # :stopdoc: # A frozen format for this is about 15% faster + # Drop these frozen strings when Ruby 2.2 becomes more prevalent, + # 2.2+ optimizes hash assignments when used with literal string keys REMOTE_ADDR = 'REMOTE_ADDR'.freeze RACK_INPUT = 'rack.input'.freeze + HTTP_RESPONSE_START = [ 'HTTP', '/1.1 '] @@input_class = Unicorn::TeeInput @@check_client_connection = false @@ -86,7 +89,7 @@ class Unicorn::HttpParser # detect if the socket is valid by writing a partial response: if @@check_client_connection && headers? @response_start_sent = true - Unicorn::Const::HTTP_RESPONSE_START.each { |c| socket.write(c) } + HTTP_RESPONSE_START.each { |c| socket.write(c) } end e[RACK_INPUT] = 0 == content_length ? diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index 083951c..cc027c5 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -24,14 +24,12 @@ module Unicorn::HttpResponse # writes the rack_response to socket as an HTTP response def http_response_write(socket, status, headers, body, response_start_sent=false) - status = CODES[status.to_i] || status hijack = nil http_response_start = response_start_sent ? '' : 'HTTP/1.1 ' if headers - buf = "#{http_response_start}#{status}\r\n" \ + buf = "#{http_response_start}#{CODES[status.to_i] || status}\r\n" \ "Date: #{httpdate}\r\n" \ - "Status: #{status}\r\n" \ "Connection: close\r\n" headers.each do |key, value| case key diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb index 329c5bf..82747b8 100644 --- a/lib/unicorn/http_server.rb +++ b/lib/unicorn/http_server.rb @@ -1,5 +1,4 @@ # -*- encoding: binary -*- -require "unicorn/ssl_server" # This is the process manager of Unicorn. This manages worker # processes which in turn handle the I/O and application process. @@ -21,10 +20,6 @@ class Unicorn::HttpServer attr_reader :pid, :logger include Unicorn::SocketHelper include Unicorn::HttpResponse - include Unicorn::SSLServer - - # backwards compatibility with 1.x - Worker = Unicorn::Worker # all bound listener sockets LISTENERS = [] @@ -32,23 +27,6 @@ class Unicorn::HttpServer # listeners we have yet to bind NEW_LISTENERS = [] - # This hash maps PIDs to Workers - WORKERS = {} - - # We use SELF_PIPE differently in the master and worker processes: - # - # * The master process never closes or reinitializes this once - # initialized. Signal handlers in the master process will write to - # it to wake up the master from IO.select in exactly the same manner - # djb describes in http://cr.yp.to/docs/selfpipe.html - # - # * The workers immediately close the pipe they inherit. See the - # Unicorn::Worker class for the pipe workers use. - SELF_PIPE = [] - - # signal queue used for self-piping - SIG_QUEUE = [] - # list of signals we care about and trap in master. QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ] @@ -71,7 +49,7 @@ class Unicorn::HttpServer # # Unicorn::HttpServer::START_CTX[0] = "/home/bofh/2.2.0/bin/unicorn" START_CTX = { - :argv => ARGV.map { |arg| arg.dup }, + :argv => ARGV.map(&:dup), 0 => $0.dup, } # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano @@ -100,6 +78,19 @@ class Unicorn::HttpServer self.config = Unicorn::Configurator.new(options) self.listener_opts = {} + # We use @self_pipe differently in the master and worker processes: + # + # * The master process never closes or reinitializes this once + # initialized. Signal handlers in the master process will write to + # it to wake up the master from IO.select in exactly the same manner + # djb describes in http://cr.yp.to/docs/selfpipe.html + # + # * The workers immediately close the pipe they inherit. See the + # Unicorn::Worker class for the pipe workers use. + @self_pipe = [] + @workers = {} # hash maps PIDs to Workers + @sig_queue = [] # signal queue used for self-piping + # we try inheriting listeners first, so we bind them later. # we don't write the pid file until we've bound listeners in case # unicorn was started twice by mistake. Even though our #pid= method @@ -119,13 +110,13 @@ class Unicorn::HttpServer inherit_listeners! # this pipe is used to wake us up from select(2) in #join when signals # are trapped. See trap_deferred. - SELF_PIPE.replace(Unicorn.pipe) + @self_pipe.replace(Unicorn.pipe) @master_pid = $$ # setup signal handlers before writing pid file in case people get # trigger happy and send signals as soon as the pid file exists. # Note that signals don't actually get handled until the #join method - QUEUE_SIGS.each { |sig| trap(sig) { SIG_QUEUE << sig; awaken_master } } + QUEUE_SIGS.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } } trap(:CHLD) { awaken_master } # write pid early for Mongrel compatibility if we're not inheriting sockets @@ -158,9 +149,6 @@ class Unicorn::HttpServer LISTENERS.delete_if do |io| if dead_names.include?(sock_name(io)) - IO_PURGATORY.delete_if do |pio| - pio.fileno == io.fileno && (pio.close rescue nil).nil? # true - end (io.close rescue nil).nil? # true else set_server_sockopt(io, listener_opts[sock_name(io)]) @@ -198,7 +186,7 @@ class Unicorn::HttpServer if path if x = valid_pid?(path) return path if pid && path == pid && x == $$ - if x == reexec_pid && pid =~ /\.oldbin\z/ + if x == reexec_pid && pid.end_with?('.oldbin') logger.warn("will not set pid=#{path} while reexec-ed "\ "child is running PID:#{x}") return @@ -241,7 +229,7 @@ class Unicorn::HttpServer begin io = bind_listen(address, opt) unless Kgio::TCPServer === io || Kgio::UNIXServer === io - prevent_autoclose(io) + io.autoclose = false io = server_cast(io) end logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}" @@ -267,7 +255,7 @@ class Unicorn::HttpServer # is signalling us too often. def join respawn = true - last_check = Time.now + last_check = time_now proc_name 'master' logger.info "master process ready" # test_exec.rb relies on this message @@ -281,11 +269,11 @@ class Unicorn::HttpServer end begin reap_all_workers - case SIG_QUEUE.shift + case @sig_queue.shift when nil # avoid murdering workers after our master process (or the # machine) comes out of suspend/hibernation - if (last_check + @timeout) >= (last_check = Time.now) + if (last_check + @timeout) >= (last_check = time_now) sleep_time = murder_lazy_workers else sleep_time = @timeout/2.0 + 1 @@ -339,8 +327,8 @@ class Unicorn::HttpServer # Terminates all workers, but does not exit master process def stop(graceful = true) self.listeners = [] - limit = Time.now + timeout - until WORKERS.empty? || Time.now > limit + limit = time_now + timeout + until @workers.empty? || time_now > limit if graceful soft_kill_each_worker(:QUIT) else @@ -369,14 +357,6 @@ class Unicorn::HttpServer Unicorn::TeeInput.client_body_buffer_size = bytes end - def trust_x_forwarded - Unicorn::HttpParser.trust_x_forwarded? - end - - def trust_x_forwarded=(bool) - Unicorn::HttpParser.trust_x_forwarded = bool - end - def check_client_connection Unicorn::HttpRequest.check_client_connection end @@ -389,17 +369,17 @@ class Unicorn::HttpServer # wait for a signal hander to wake us up and then consume the pipe def master_sleep(sec) + @self_pipe[0].kgio_wait_readable(sec) or return # 11 bytes is the maximum string length which can be embedded within # 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: - IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return - SELF_PIPE[0].kgio_tryread(11) + @self_pipe[0].kgio_tryread(11) end def awaken_master return if $$ != @master_pid - SELF_PIPE[1].kgio_trywrite('.') # wakeup master process from select + @self_pipe[1].kgio_trywrite('.') # wakeup master process from select end # reaps all unreaped workers @@ -413,7 +393,7 @@ class Unicorn::HttpServer self.pid = pid.chomp('.oldbin') if pid proc_name 'master' else - worker = WORKERS.delete(wpid) and worker.close rescue nil + worker = @workers.delete(wpid) and worker.close rescue nil m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" status.success? ? logger.info(m) : logger.error(m) end @@ -451,10 +431,7 @@ class Unicorn::HttpServer self.reexec_pid = fork do listener_fds = {} LISTENERS.each do |sock| - # IO#close_on_exec= will be available on any future version of - # Ruby that sets FD_CLOEXEC by default on new file descriptors - # ref: http://redmine.ruby-lang.org/issues/5041 - sock.close_on_exec = false if sock.respond_to?(:close_on_exec=) + sock.close_on_exec = false listener_fds[sock.fileno] = sock end ENV['UNICORN_FD'] = listener_fds.keys.join(',') @@ -467,13 +444,13 @@ class Unicorn::HttpServer (3..1024).each do |io| next if listener_fds.include?(io) io = IO.for_fd(io) rescue next - prevent_autoclose(io) - io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + io.autoclose = false + io.close_on_exec = true end # 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 if RUBY_VERSION >= "1.9.1" + cmd << listener_fds logger.info "executing #{cmd.inspect} (in #{Dir.pwd})" before_exec.call(self) exec(*cmd) @@ -484,8 +461,8 @@ class Unicorn::HttpServer # 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 - now = Time.now.to_i - WORKERS.dup.each_pair do |wpid, worker| + now = time_now.to_i + @workers.dup.each_pair do |wpid, worker| tick = worker.tick 0 == tick and next # skip workers that haven't processed any clients diff = now - tick @@ -503,7 +480,7 @@ class Unicorn::HttpServer end def after_fork_internal - SELF_PIPE.each { |io| io.close }.clear # this is master-only, now + @self_pipe.each(&:close).clear # this is master-only, now @ready_pipe.close if @ready_pipe Unicorn::Configurator::RACKUP.clear @ready_pipe = @init_listeners = @before_exec = @before_fork = nil @@ -518,11 +495,11 @@ class Unicorn::HttpServer def spawn_missing_workers worker_nr = -1 until (worker_nr += 1) == @worker_processes - WORKERS.value?(worker_nr) and next - worker = Worker.new(worker_nr) + @workers.value?(worker_nr) and next + worker = Unicorn::Worker.new(worker_nr) before_fork.call(self, worker) if pid = fork - WORKERS[pid] = worker + @workers[pid] = worker worker.atfork_parent else after_fork_internal @@ -536,9 +513,9 @@ class Unicorn::HttpServer end def maintain_worker_count - (off = WORKERS.size - worker_processes) == 0 and return + (off = @workers.size - worker_processes) == 0 and return off < 0 and return spawn_missing_workers - WORKERS.each_value { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) } + @workers.each_value { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) } end # if we get any error, try to write something back to the client @@ -566,12 +543,15 @@ class Unicorn::HttpServer rescue end - def expect_100_response - if @request.response_start_sent - Unicorn::Const::EXPECT_100_RESPONSE_SUFFIXED - else - Unicorn::Const::EXPECT_100_RESPONSE - 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 + # allocations here. + client.write(@request.response_start_sent ? + "100 Continue\r\n\r\nHTTP/1.1 ".freeze : + "HTTP/1.1 100 Continue\r\n\r\n".freeze) + env.delete('HTTP_EXPECT'.freeze) end # once a client is accepted, it is processed in its entirety here @@ -581,8 +561,7 @@ class Unicorn::HttpServer return if @request.hijacked? if 100 == status.to_i - client.write(expect_100_response) - env.delete(Unicorn::Const::HTTP_EXPECT) + e100_response_write(client, env) status, headers, body = @app.call(env) return if @request.hijacked? end @@ -615,22 +594,21 @@ class Unicorn::HttpServer worker.atfork_child # we'll re-trap :QUIT later for graceful shutdown iff we accept clients EXIT_SIGS.each { |sig| trap(sig) { exit!(0) } } - exit!(0) if (SIG_QUEUE & EXIT_SIGS)[0] + exit!(0) if (@sig_queue & EXIT_SIGS)[0] WORKER_QUEUE_SIGS.each { |sig| trap(sig, nil) } trap(:CHLD, 'DEFAULT') - SIG_QUEUE.clear + @sig_queue.clear proc_name "worker[#{worker.nr}]" START_CTX.clear - WORKERS.clear + @workers.clear after_fork.call(self, worker) # can drop perms and create listeners - LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) } + 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 - ssl_enable! @after_fork = @listener_opts = @orig_app = nil readers = LISTENERS.dup readers << worker @@ -665,7 +643,7 @@ class Unicorn::HttpServer begin nr < 0 and reopen_worker_logs(worker.nr) nr = 0 - worker.tick = Time.now.to_i + worker.tick = time_now.to_i tmp = ready.dup while sock = tmp.shift # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all, @@ -673,7 +651,7 @@ class Unicorn::HttpServer if client = sock.kgio_tryaccept process_client(client) nr += 1 - worker.tick = Time.now.to_i + worker.tick = time_now.to_i end break if nr < 0 end @@ -690,7 +668,7 @@ class Unicorn::HttpServer ppid == Process.ppid or return # timeout used so we can detect parent death: - worker.tick = Time.now.to_i + worker.tick = time_now.to_i ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0] rescue => e redo if nr < 0 && readers[0] @@ -702,17 +680,17 @@ class Unicorn::HttpServer # is no longer running. def kill_worker(signal, wpid) Process.kill(signal, wpid) - rescue Errno::ESRCH - worker = WORKERS.delete(wpid) and worker.close rescue nil + rescue Errno::ESRCH + worker = @workers.delete(wpid) and worker.close rescue nil end # delivers a signal to each worker def kill_each_worker(signal) - WORKERS.keys.each { |wpid| kill_worker(signal, wpid) } + @workers.keys.each { |wpid| kill_worker(signal, wpid) } end def soft_kill_each_worker(signal) - WORKERS.each_value { |worker| worker.soft_kill(signal) } + @workers.each_value { |worker| worker.soft_kill(signal) } end # unlinks a PID file at given +path+ if it contains the current PID @@ -782,10 +760,10 @@ 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(/,/).map do |fd| + inherited = ENV['UNICORN_FD'].to_s.split(',').map do |fd| io = Socket.for_fd(fd.to_i) set_server_sockopt(io, listener_opts[sock_name(io)]) - prevent_autoclose(io) + io.autoclose = false logger.info "inherited addr=#{sock_name(io)} fd=#{fd}" server_cast(io) end @@ -814,4 +792,17 @@ class Unicorn::HttpServer raise ArgumentError, "no listeners" if LISTENERS.empty? NEW_LISTENERS.clear end + + # try to use the monotonic clock in Ruby >= 2.1, it is immune to clock + # offset adjustments and generates less garbage (Float vs Time object) + begin + Process.clock_gettime(Process::CLOCK_MONOTONIC) + def time_now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + rescue NameError, NoMethodError + def time_now # Ruby <= 2.0 + Time.now + end + end end diff --git a/lib/unicorn/socket_helper.rb b/lib/unicorn/socket_helper.rb index 820b778..812ac53 100644 --- a/lib/unicorn/socket_helper.rb +++ b/lib/unicorn/socket_helper.rb @@ -4,12 +4,6 @@ require 'socket' module Unicorn module SocketHelper - # :stopdoc: - include Socket::Constants - - # prevents IO objects in here from being GC-ed - # kill this when we drop 1.8 support - IO_PURGATORY = [] # internal interface, only used by Rainbows!/Zbatery DEFAULTS = { @@ -22,7 +16,7 @@ module Unicorn :tcp_defer_accept => 1, # FreeBSD, we need to override this to 'dataready' if we - # eventually get HTTPS support + # eventually support non-HTTP/1.x :accept_filter => 'httpready', # same default value as Mongrel @@ -32,76 +26,47 @@ module Unicorn :tcp_nopush => nil, :tcp_nodelay => true, } - #:startdoc: # 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) - - # do not send out partial frames (Linux) - TCP_CORK = 3 unless defined?(TCP_CORK) - - # Linux got SO_REUSEPORT in 3.9, BSDs have had it for ages - unless defined?(SO_REUSEPORT) - if RUBY_PLATFORM =~ /(?:alpha|mips|parisc|sparc)/ - SO_REUSEPORT = 0x0200 # untested - else - SO_REUSEPORT = 15 # only tested on x86_64 and i686 - end - end - when /freebsd/ - # do not send out partial frames (FreeBSD) - TCP_NOPUSH = 4 unless defined?(TCP_NOPUSH) - - def accf_arg(af_name) - [ af_name, nil ].pack('a16a240') - end if defined?(SO_ACCEPTFILTER) - end - - def prevent_autoclose(io) - if io.respond_to?(:autoclose=) - io.autoclose = false - else - IO_PURGATORY << io - end - end + def accf_arg(af_name) + [ af_name, nil ].pack('a16a240') + end if RUBY_PLATFORM =~ /freebsd/ && Socket.const_defined?(:SO_ACCEPTFILTER) def set_tcp_sockopt(sock, opt) # just in case, even LANs can break sometimes. Linux sysadmins # can lower net.ipv4.tcp_keepalive_* sysctl knobs to very low values. - sock.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) if defined?(SO_KEEPALIVE) + Socket.const_defined?(:SO_KEEPALIVE) and + sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1) - if defined?(TCP_NODELAY) + if Socket.const_defined?(:TCP_NODELAY) val = opt[:tcp_nodelay] - val = DEFAULTS[:tcp_nodelay] if nil == val - sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, val ? 1 : 0) + val = DEFAULTS[:tcp_nodelay] if val.nil? + sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, val ? 1 : 0) end val = opt[:tcp_nopush] unless val.nil? - if defined?(TCP_CORK) # Linux - sock.setsockopt(IPPROTO_TCP, TCP_CORK, val) - elsif defined?(TCP_NOPUSH) # TCP_NOPUSH is lightly tested (FreeBSD) - sock.setsockopt(IPPROTO_TCP, TCP_NOPUSH, val) + if Socket.const_defined?(:TCP_CORK) # Linux + sock.setsockopt(:IPPROTO_TCP, :TCP_CORK, val) + elsif Socket.const_defined?(:TCP_NOPUSH) # FreeBSD + sock.setsockopt(:IPPROTO_TCP, :TCP_NOPUSH, val) end end - # No good reason to ever have deferred accepts off - # (except maybe benchmarking) - if defined?(TCP_DEFER_ACCEPT) + # No good reason to ever have deferred accepts off in single-threaded + # servers (except maybe benchmarking) + if Socket.const_defined?(:TCP_DEFER_ACCEPT) # this differs from nginx, since nginx doesn't allow us to # configure the the timeout... seconds = opt[:tcp_defer_accept] seconds = DEFAULTS[:tcp_defer_accept] if [true,nil].include?(seconds) seconds = 0 unless seconds # nil/false means disable this - sock.setsockopt(SOL_TCP, TCP_DEFER_ACCEPT, seconds) + sock.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, seconds) elsif respond_to?(:accf_arg) name = opt[:accept_filter] - name = DEFAULTS[:accept_filter] if nil == name + name = DEFAULTS[:accept_filter] if name.nil? begin - sock.setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, accf_arg(name)) + sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, accf_arg(name)) rescue => e logger.error("#{sock_name(sock)} " \ "failed to set accept_filter=#{name} (#{e.inspect})") @@ -114,10 +79,11 @@ module Unicorn TCPSocket === sock and set_tcp_sockopt(sock, opt) - if opt[:rcvbuf] || opt[:sndbuf] + rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf) + if rcvbuf || 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] + sock.setsockopt(:SOL_SOCKET, :SO_RCVBUF, rcvbuf) if rcvbuf + sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf log_buffer_sizes(sock, " after: ") end sock.listen(opt[:backlog]) @@ -126,8 +92,8 @@ module Unicorn end def log_buffer_sizes(sock, pfx = '') - rcvbuf = sock.getsockopt(SOL_SOCKET, SO_RCVBUF).unpack('i') - sndbuf = sock.getsockopt(SOL_SOCKET, SO_SNDBUF).unpack('i') + rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int + sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}" end @@ -172,25 +138,25 @@ module Unicorn def new_tcp_server(addr, port, opt) # n.b. we set FD_CLOEXEC in the workers - sock = Socket.new(opt[:ipv6] ? AF_INET6 : AF_INET, SOCK_STREAM, 0) + sock = Socket.new(opt[:ipv6] ? :AF_INET6 : :AF_INET, :SOCK_STREAM) if opt.key?(:ipv6only) - defined?(IPV6_V6ONLY) or + Socket.const_defined?(:IPV6_V6ONLY) or abort "Socket::IPV6_V6ONLY not defined, upgrade Ruby and/or your OS" - sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0) + sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0) end - sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - if defined?(SO_REUSEPORT) && opt[:reuseport] - sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) + sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1) + if Socket.const_defined?(:SO_REUSEPORT) && opt[:reuseport] + sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1) end sock.bind(Socket.pack_sockaddr_in(port, addr)) - prevent_autoclose(sock) + sock.autoclose = false Kgio::TCPServer.for_fd(sock.fileno) end # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6 def tcp_name(sock) port, addr = Socket.unpack_sockaddr_in(sock.getsockname) - /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}" + addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}" end module_function :tcp_name diff --git a/lib/unicorn/ssl_client.rb b/lib/unicorn/ssl_client.rb deleted file mode 100644 index a8c79e3..0000000 --- a/lib/unicorn/ssl_client.rb +++ /dev/null @@ -1,11 +0,0 @@ -# -*- encoding: binary -*- -# :stopdoc: -class Unicorn::SSLClient < Kgio::SSL - alias write kgio_write - alias close kgio_close - - # this is no-op for now, to be fixed in kgio-monkey if people care - # about SSL support... - def shutdown(how = nil) - end -end diff --git a/lib/unicorn/ssl_configurator.rb b/lib/unicorn/ssl_configurator.rb deleted file mode 100644 index 34f09ec..0000000 --- a/lib/unicorn/ssl_configurator.rb +++ /dev/null @@ -1,104 +0,0 @@ -# -*- encoding: binary -*- -# :stopdoc: -# This module is included in Unicorn::Configurator -# :startdoc: -# -module Unicorn::SSLConfigurator - def ssl(&block) - ssl_require! - before = @set[:listeners].dup - opts = @set[:ssl_opts] = {} - yield - (@set[:listeners] - before).each do |address| - (@set[:listener_opts][address] ||= {})[:ssl_opts] = opts - end - ensure - @set.delete(:ssl_opts) - end - - def ssl_certificate(file) - ssl_set(:ssl_certificate, file) - end - - def ssl_certificate_key(file) - ssl_set(:ssl_certificate_key, file) - end - - def ssl_client_certificate(file) - ssl_set(:ssl_client_certificate, file) - end - - def ssl_dhparam(file) - ssl_set(:ssl_dhparam, file) - end - - def ssl_ciphers(openssl_cipherlist_spec) - ssl_set(:ssl_ciphers, openssl_cipherlist_spec) - end - - def ssl_crl(file) - ssl_set(:ssl_crl, file) - end - - def ssl_prefer_server_ciphers(bool) - ssl_set(:ssl_prefer_server_ciphers, check_bool(bool)) - end - - def ssl_protocols(list) - ssl_set(:ssl_protocols, list) - end - - def ssl_verify_client(on_off_optional) - ssl_set(:ssl_verify_client, on_off_optional) - end - - def ssl_session_timeout(seconds) - ssl_set(:ssl_session_timeout, seconds) - end - - def ssl_verify_depth(depth) - ssl_set(:ssl_verify_depth, depth) - end - - # Allows specifying an engine for OpenSSL to use. We have not been - # able to successfully test this feature due to a lack of hardware, - # Reports of success or patches to unicorn-public@bogomips.org is - # greatly appreciated. - def ssl_engine(engine) - ssl_warn_global(:ssl_engine) - ssl_require! - OpenSSL::Engine.load - OpenSSL::Engine.by_id(engine) - @set[:ssl_engine] = engine - end - - def ssl_compression(bool) - # OpenSSL uses the SSL_OP_NO_COMPRESSION flag, Flipper follows suit - # with :ssl_no_compression, but we negate it to avoid exposing double - # negatives to the user. - ssl_set(:ssl_no_compression, check_bool(:ssl_compression, ! bool)) - end - -private - - def ssl_warn_global(func) # :nodoc: - Hash === @set[:ssl_opts] or return - warn("`#{func}' affects all SSL contexts in this process, " \ - "not just this block") - end - - def ssl_set(key, value) # :nodoc: - cur = @set[:ssl_opts] - Hash === cur or - raise ArgumentError, "#{key} must be called inside an `ssl' block" - cur[key] = value - end - - def ssl_require! # :nodoc: - require "flipper" - require "unicorn/ssl_client" - rescue LoadError - warn "install 'kgio-monkey' for SSL support" - raise - end -end diff --git a/lib/unicorn/ssl_server.rb b/lib/unicorn/ssl_server.rb deleted file mode 100644 index c00c3ae..0000000 --- a/lib/unicorn/ssl_server.rb +++ /dev/null @@ -1,42 +0,0 @@ -# -*- encoding: binary -*- -# :stopdoc: -# this module is meant to be included in Unicorn::HttpServer -# It is an implementation detail and NOT meant for users. -module Unicorn::SSLServer - attr_accessor :ssl_engine - - def ssl_enable! - sni_hostnames = rack_sni_hostnames(@app) - seen = {} # we map a single SSLContext to multiple listeners - listener_ctx = {} - @listener_opts.each do |address, address_opts| - ssl_opts = address_opts[:ssl_opts] or next - listener_ctx[address] = seen[ssl_opts.object_id] ||= begin - unless sni_hostnames.empty? - ssl_opts = ssl_opts.dup - ssl_opts[:sni_hostnames] = sni_hostnames - end - ctx = Flipper.ssl_context(ssl_opts) - # FIXME: make configurable - ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF - ctx - end - end - Unicorn::HttpServer::LISTENERS.each do |listener| - ctx = listener_ctx[sock_name(listener)] or next - listener.extend(Kgio::SSLServer) - listener.ssl_ctx = ctx - listener.kgio_ssl_class = Unicorn::SSLClient - end - end - - # ugh, this depends on Rack internals... - def rack_sni_hostnames(rack_app) # :nodoc: - hostnames = {} - if Rack::URLMap === rack_app - mapping = rack_app.instance_variable_get(:@mapping) - mapping.each { |hostname,_,_,_| hostnames[hostname] = true } - end - hostnames.keys - end -end diff --git a/lib/unicorn/tmpio.rb b/lib/unicorn/tmpio.rb index dcdf9da..db88ed3 100644 --- a/lib/unicorn/tmpio.rb +++ b/lib/unicorn/tmpio.rb @@ -22,11 +22,6 @@ class Unicorn::TmpIO < File fp end - # for easier env["rack.input"] compatibility with Rack <= 1.1 - def size - stat.size - end unless File.method_defined?(:size) - # pretend we're Tempfile for Rack::TempfileReaper alias close! close end diff --git a/lib/unicorn/util.rb b/lib/unicorn/util.rb index 94c4e37..c7784bd 100644 --- a/lib/unicorn/util.rb +++ b/lib/unicorn/util.rb @@ -1,5 +1,6 @@ # -*- encoding: binary -*- +require 'fcntl' module Unicorn::Util # :stopdoc: diff --git a/lib/unicorn/worker.rb b/lib/unicorn/worker.rb index e74a1c9..b3f8afe 100644 --- a/lib/unicorn/worker.rb +++ b/lib/unicorn/worker.rb @@ -11,7 +11,6 @@ require "raindrops" class Unicorn::Worker # :stopdoc: attr_accessor :nr, :switched - attr_writer :tmp attr_reader :to_io # IO.select-compatible PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE @@ -23,7 +22,7 @@ class Unicorn::Worker @offset = nr % PER_DROP @raindrop[@offset] = 0 @nr = nr - @tmp = @switched = false + @switched = false @to_io, @master = Unicorn.pipe end @@ -101,18 +100,8 @@ class Unicorn::Worker @raindrop[@offset] end - # only exists for compatibility - def tmp # :nodoc: - @tmp ||= begin - tmp = Unicorn::TmpIO.new - tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) - tmp - end - end - # called in both the master (reaping worker) and worker (SIGQUIT handler) def close # :nodoc: - @tmp.close if @tmp @master.close if @master @to_io.close if @to_io end @@ -141,7 +130,6 @@ class Unicorn::Worker uid = Etc.getpwnam(user).uid gid = Etc.getgrnam(group).gid if group Unicorn::Util.chown_logs(uid, gid) - @tmp.chown(uid, gid) if @tmp if gid && Process.egid != gid Process.initgroups(user, gid) Process::GID.change_privilege(gid) diff --git a/t/t0016-trust-x-forwarded-false.sh b/t/t0016-trust-x-forwarded-false.sh deleted file mode 100755 index 3163690..0000000 --- a/t/t0016-trust-x-forwarded-false.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -. ./test-lib.sh -t_plan 5 "trust_x_forwarded=false configuration test" - -t_begin "setup and start" && { - unicorn_setup - echo "trust_x_forwarded false" >> $unicorn_config - unicorn -D -c $unicorn_config env.ru - unicorn_wait_start -} - -t_begin "spoofed request with X-Forwarded-Proto does not trigger" && { - curl -H 'X-Forwarded-Proto: https' http://$listen/ | \ - grep -F '"rack.url_scheme"=>"http"' -} - -t_begin "spoofed request with X-Forwarded-SSL does not trigger" && { - curl -H 'X-Forwarded-SSL: on' http://$listen/ | \ - grep -F '"rack.url_scheme"=>"http"' -} - -t_begin "killing succeeds" && { - kill $unicorn_pid -} - -t_begin "check stderr has no errors" && { - check_stderr -} - -t_done diff --git a/t/t0017-trust-x-forwarded-true.sh b/t/t0017-trust-x-forwarded-true.sh deleted file mode 100755 index 11103c5..0000000 --- a/t/t0017-trust-x-forwarded-true.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -. ./test-lib.sh -t_plan 5 "trust_x_forwarded=true configuration test" - -t_begin "setup and start" && { - unicorn_setup - echo "trust_x_forwarded true " >> $unicorn_config - unicorn -D -c $unicorn_config env.ru - unicorn_wait_start -} - -t_begin "spoofed request with X-Forwarded-Proto sets 'https'" && { - curl -H 'X-Forwarded-Proto: https' http://$listen/ | \ - grep -F '"rack.url_scheme"=>"https"' -} - -t_begin "spoofed request with X-Forwarded-SSL sets 'https'" && { - curl -H 'X-Forwarded-SSL: on' http://$listen/ | \ - grep -F '"rack.url_scheme"=>"https"' -} - -t_begin "killing succeeds" && { - kill $unicorn_pid -} - -t_begin "check stderr has no errors" && { - check_stderr -} - -t_done diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb index 4f13c9a..efd82e1 100644 --- a/test/unit/test_http_parser_ng.rb +++ b/test/unit/test_http_parser_ng.rb @@ -8,10 +8,15 @@ include Unicorn class HttpParserNgTest < Test::Unit::TestCase def setup - HttpParser.keepalive_requests = HttpParser::KEEPALIVE_REQUESTS_DEFAULT @parser = HttpParser.new end + def test_parser_max_len + assert_raises(RangeError) do + HttpParser.max_header_len = 0xffffffff + 1 + end + end + def test_next_clear r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" @parser.buf << r @@ -29,25 +34,6 @@ class HttpParserNgTest < Test::Unit::TestCase assert_equal false, @parser.response_start_sent end - def test_keepalive_requests_default_constant - assert_kind_of Integer, HttpParser::KEEPALIVE_REQUESTS_DEFAULT - assert HttpParser::KEEPALIVE_REQUESTS_DEFAULT >= 0 - end - - def test_keepalive_requests_setting - HttpParser.keepalive_requests = 0 - assert_equal 0, HttpParser.keepalive_requests - HttpParser.keepalive_requests = nil - assert HttpParser.keepalive_requests >= 0xffffffff - HttpParser.keepalive_requests = 1 - assert_equal 1, HttpParser.keepalive_requests - HttpParser.keepalive_requests = 666 - assert_equal 666, HttpParser.keepalive_requests - - assert_raises(TypeError) { HttpParser.keepalive_requests = "666" } - assert_raises(TypeError) { HttpParser.keepalive_requests = [] } - end - def test_connection_TE @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: TE\r\n" @parser.buf << "TE: trailers\r\n\r\n" @@ -71,41 +57,11 @@ class HttpParserNgTest < Test::Unit::TestCase "REQUEST_METHOD" => "GET", "QUERY_STRING" => "" }.freeze - HttpParser::KEEPALIVE_REQUESTS_DEFAULT.times do |nr| + 100.times do |nr| @parser.buf << req assert_equal expect, @parser.parse assert @parser.next? end - @parser.buf << req - assert_equal expect, @parser.parse - assert ! @parser.next? - end - - def test_fewer_keepalive_requests_with_next? - HttpParser.keepalive_requests = 5 - @parser = HttpParser.new - req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".freeze - expect = { - "SERVER_NAME" => "example.com", - "HTTP_HOST" => "example.com", - "rack.url_scheme" => "http", - "REQUEST_PATH" => "/", - "SERVER_PROTOCOL" => "HTTP/1.1", - "PATH_INFO" => "/", - "HTTP_VERSION" => "HTTP/1.1", - "REQUEST_URI" => "/", - "SERVER_PORT" => "80", - "REQUEST_METHOD" => "GET", - "QUERY_STRING" => "" - }.freeze - 5.times do |nr| - @parser.buf << req - assert_equal expect, @parser.parse - assert @parser.next? - end - @parser.buf << req - assert_equal expect, @parser.parse - assert ! @parser.next? end def test_default_keepalive_is_off @@ -663,69 +619,4 @@ class HttpParserNgTest < Test::Unit::TestCase assert_equal expect, env2 assert_equal "", @parser.buf end - - def test_keepalive_requests_disabled - req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".freeze - expect = { - "SERVER_NAME" => "example.com", - "HTTP_HOST" => "example.com", - "rack.url_scheme" => "http", - "REQUEST_PATH" => "/", - "SERVER_PROTOCOL" => "HTTP/1.1", - "PATH_INFO" => "/", - "HTTP_VERSION" => "HTTP/1.1", - "REQUEST_URI" => "/", - "SERVER_PORT" => "80", - "REQUEST_METHOD" => "GET", - "QUERY_STRING" => "" - }.freeze - HttpParser.keepalive_requests = 0 - @parser = HttpParser.new - @parser.buf << req - assert_equal expect, @parser.parse - assert ! @parser.next? - end - - def test_chunk_only - tmp = "" - assert_equal @parser, @parser.dechunk! - assert_nil @parser.filter_body(tmp, "6\r\n") - assert_equal "", tmp - assert_nil @parser.filter_body(tmp, "abcdef") - assert_equal "abcdef", tmp - assert_nil @parser.filter_body(tmp, "\r\n") - assert_equal "", tmp - src = "0\r\n\r\n" - assert_equal src.object_id, @parser.filter_body(tmp, src).object_id - assert_equal "", tmp - end - - def test_chunk_only_bad_align - tmp = "" - assert_equal @parser, @parser.dechunk! - assert_nil @parser.filter_body(tmp, "6\r\na") - assert_equal "a", tmp - assert_nil @parser.filter_body(tmp, "bcde") - assert_equal "bcde", tmp - assert_nil @parser.filter_body(tmp, "f\r") - assert_equal "f", tmp - src = "\n0\r\n\r\n" - assert_equal src.object_id, @parser.filter_body(tmp, src).object_id - assert_equal "", tmp - end - - def test_chunk_only_reset_ok - tmp = "" - assert_equal @parser, @parser.dechunk! - src = "1\r\na\r\n0\r\n\r\n" - assert_nil @parser.filter_body(tmp, src) - assert_equal "a", tmp - assert_equal src.object_id, @parser.filter_body(tmp, src).object_id - - assert_equal @parser, @parser.dechunk! - src = "0\r\n\r\n" - assert_equal src.object_id, @parser.filter_body(tmp, src).object_id - assert_equal "", tmp - assert_equal src, @parser.filter_body(tmp, src) - end end diff --git a/test/unit/test_http_parser_xftrust.rb b/test/unit/test_http_parser_xftrust.rb deleted file mode 100644 index 8d0cc37..0000000 --- a/test/unit/test_http_parser_xftrust.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -*- encoding: binary -*- -require './test/test_helper' - -include Unicorn - -class HttpParserXFTrustTest < Test::Unit::TestCase - def setup - assert HttpParser.trust_x_forwarded? - end - - def test_xf_trust_false_xfp - HttpParser.trust_x_forwarded = false - parser = HttpParser.new - parser.buf << "GET / HTTP/1.1\r\nHost: foo:\r\n" \ - "X-Forwarded-Proto: https\r\n\r\n" - env = parser.parse - assert_kind_of Hash, env - assert_equal 'foo', env['SERVER_NAME'] - assert_equal '80', env['SERVER_PORT'] - assert_equal 'http', env['rack.url_scheme'] - end - - def test_xf_trust_false_xfs - HttpParser.trust_x_forwarded = false - parser = HttpParser.new - parser.buf << "GET / HTTP/1.1\r\nHost: foo:\r\n" \ - "X-Forwarded-SSL: on\r\n\r\n" - env = parser.parse - assert_kind_of Hash, env - assert_equal 'foo', env['SERVER_NAME'] - assert_equal '80', env['SERVER_PORT'] - assert_equal 'http', env['rack.url_scheme'] - end - - def teardown - HttpParser.trust_x_forwarded = true - end -end diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index 10b247b..d0f0c79 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -38,7 +38,6 @@ class ResponseTest < Test::Unit::TestCase http_response_write(out,'200', {}, []) assert ! out.closed? assert out.length > 0, "output didn't have data" - assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/).size end def test_response_200 @@ -71,7 +70,6 @@ class ResponseTest < Test::Unit::TestCase out = StringIO.new http_response_write(out,200, {"X-Whatever" => "stuff"}, []) assert ! out.closed? - assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size end def test_body_closed @@ -91,9 +89,5 @@ class ResponseTest < Test::Unit::TestCase assert ! out.closed? headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/) assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0]) - status = headers.grep(/\AStatus:/i).first - assert status - assert_equal "Status: 666 I AM THE BEAST", status end - end diff --git a/test/unit/test_sni_hostnames.rb b/test/unit/test_sni_hostnames.rb deleted file mode 100644 index 457afee..0000000 --- a/test/unit/test_sni_hostnames.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -*- encoding: binary -*- -require "test/unit" -require "unicorn" - -# this tests an implementation detail, it may change so this test -# can be removed later. -class TestSniHostnames < Test::Unit::TestCase - include Unicorn::SSLServer - - def setup - GC.start - end - - def teardown - GC.start - end - - def test_host_name_detect_one - app = Rack::Builder.new do - map "http://sni1.example.com/" do - use Rack::ContentLength - use Rack::ContentType, "text/plain" - run lambda { |env| [ 200, {}, [] ] } - end - end.to_app - hostnames = rack_sni_hostnames(app) - assert hostnames.include?("sni1.example.com") - end - - def test_host_name_detect_multiple - app = Rack::Builder.new do - map "http://sni2.example.com/" do - use Rack::ContentLength - use Rack::ContentType, "text/plain" - run lambda { |env| [ 200, {}, [] ] } - end - map "http://sni3.example.com/" do - use Rack::ContentLength - use Rack::ContentType, "text/plain" - run lambda { |env| [ 200, {}, [] ] } - end - end.to_app - hostnames = rack_sni_hostnames(app) - assert hostnames.include?("sni2.example.com") - assert hostnames.include?("sni3.example.com") - end -end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb index 09d65af..7526e82 100644 --- a/test/unit/test_socket_helper.rb +++ b/test/unit/test_socket_helper.rb @@ -189,7 +189,7 @@ class TestSocketHelper < Test::Unit::TestCase port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :reuseport => true) - cur = sock.getsockopt(Socket::SOL_SOCKET, SO_REUSEPORT).unpack('i')[0] + cur = sock.getsockopt(:SOL_SOCKET, :SO_REUSEPORT).int assert_operator cur, :>, 0 rescue Errno::ENOPROTOOPT # kernel does not support SO_REUSEPORT (older Linux) |