diff --git a/.rubocop.yml b/.rubocop.yml index cfa17fa7bd..ca4bdf59dd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,6 @@ AllCops: DisabledByDefault: true - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.2 DisplayCopNames: true StyleGuideCopsOnly: false Exclude: diff --git a/.travis.yml b/.travis.yml index db49344976..776dab16c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ before_install: r_eng="$(ruby -e 'STDOUT.write RUBY_ENGINE')"; rv="$(ruby -e 'STDOUT.write RUBY_VERSION')"; if [ "$r_eng" == "ruby" ]; then - if [ "$rv" \< "2.6" ]; then gem update --system --no-document --conservative + if [ "$rv" \< "2.3" ]; then gem update --system 2.7.9 --no-document + elif [ "$rv" \< "2.6" ]; then gem update --system --no-document --conservative fi fi if [ "$TRAVIS_OS_NAME" == "osx" ]; then @@ -24,6 +25,9 @@ script: - bundle exec rake rvm: + - 2.2.10 + - 2.3.8 + - 2.4.6 - 2.5.5 - 2.6.3 - ruby-head @@ -31,8 +35,14 @@ rvm: matrix: fast_finish: true include: + - rvm: 2.2 + dist: trusty + env: NOTES="Trusty OpenSSL 1.0.1" - rvm: ruby-head env: RUBYOPT="--jit" + - rvm: 2.4.6 + os: osx + osx_image: xcode10.2 - rvm: 2.5.5 os: osx osx_image: xcode10.2 diff --git a/History.md b/History.md index 6cebc16a4f..2a9416fc57 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,17 @@ ## Master -* x features +x features + +* 2 bugfixes + * Socket removed after reload (#1829) + * Add extconf tests for DTLS_method & TLS_server_method, use in minissl.rb. (#1832) +* 1 feature + * Add log_formatter configuration + + +## 4.0.0 / 2019-06-25 + +* 9 features * Add support for disabling TLSv1.0 (#1562) * Request body read time metric (#1569) * Add out_of_band hook (#1648) @@ -9,9 +20,9 @@ * Add option to suppress SignalException on SIGTERM (#1690) * Allow mutual TLS CA to be set using `ssl_bind` DSL (#1689) * Reactor now uses nio4r instead of `select` (#1728) - * Minimum Ruby version now >= 2.5 (#1813) - * Add log_formatter configuration -* x bugfixes + * Add status to pumactl with pidfile (#1824) + +* 9 bugfixes * Do not accept new requests on shutdown (#1685, #1808) * Fix 3 corner cases when request body is chunked (#1508) * Change pid existence check's condition branches (#1650) diff --git a/Rakefile b/Rakefile index 58873a657f..f56dce4a4f 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ require "rake/testtask" require "rake/extensiontask" require "rake/javaextensiontask" require "rubocop/rake_task" -require 'puma/detect' +require_relative 'lib/puma/detect' require 'rubygems/package_task' require 'bundler/gem_tasks' diff --git a/ext/puma_http11/extconf.rb b/ext/puma_http11/extconf.rb index 59c54f9c4b..ee6723061d 100644 --- a/ext/puma_http11/extconf.rb +++ b/ext/puma_http11/extconf.rb @@ -9,6 +9,14 @@ %w'ssl ssleay32'.find {|ssl| have_library(ssl, 'SSL_CTX_new')} have_header "openssl/bio.h" + + # below is yes for 1.0.2 & later + have_func "DTLS_method" , "openssl/ssl.h" + + # below are yes for 1.1.0 & later, may need to check func rather than macro + # with versions after 1.1.1 + have_func "TLS_server_method" , "openssl/ssl.h" + have_macro "SSL_CTX_set_min_proto_version", "openssl/ssl.h" end end diff --git a/ext/puma_http11/mini_ssl.c b/ext/puma_http11/mini_ssl.c index 120ab2275f..607f6ea8f3 100644 --- a/ext/puma_http11/mini_ssl.c +++ b/ext/puma_http11/mini_ssl.c @@ -168,8 +168,11 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) { ID sym_no_tlsv1 = rb_intern("no_tlsv1"); VALUE no_tlsv1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1, 0); - +#ifdef HAVE_TLS_SERVER_METHOD + ctx = SSL_CTX_new(TLS_server_method()); +#else ctx = SSL_CTX_new(SSLv23_server_method()); +#endif conn->ctx = ctx; SSL_CTX_use_certificate_chain_file(ctx, RSTRING_PTR(cert)); @@ -232,8 +235,11 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) { VALUE engine_init_client(VALUE klass) { VALUE obj; ms_conn* conn = engine_alloc(klass, &obj); - +#ifdef HAVE_DTLS_METHOD conn->ctx = SSL_CTX_new(DTLS_method()); +#else + conn->ctx = SSL_CTX_new(DTLSv1_method()); +#endif conn->ssl = SSL_new(conn->ctx); SSL_set_app_data(conn->ssl, NULL); SSL_set_verify(conn->ssl, SSL_VERIFY_NONE, NULL); diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index 34278cb223..ec8d0a5531 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -53,7 +53,8 @@ def close @unix_paths.each do |i| # Errno::ENOENT is intermittently raised begin - File.unlink i + unix_socket = UNIXSocket.new i + unix_socket.close rescue Errno::ENOENT end end @@ -63,7 +64,7 @@ def import_from_env remove = [] ENV.each do |k,v| - if /PUMA_INHERIT_\d+/.match?(k) + if k =~ /PUMA_INHERIT_\d+/ fd, url = v.split(":", 2) @inherited_fds[url] = fd.to_i remove << k @@ -75,7 +76,7 @@ def import_from_env key = [ :unix, Socket.unpack_sockaddr_un(sock.getsockname) ] rescue ArgumentError port, addr = Socket.unpack_sockaddr_in(sock.getsockname) - if /\:/.match?(addr) + if addr =~ /\:/ addr = "[#{addr}]" end key = [ :tcp, addr, port ] diff --git a/lib/puma/client.rb b/lib/puma/client.rb index 5084e3254e..0aa4425e5a 100644 --- a/lib/puma/client.rb +++ b/lib/puma/client.rb @@ -27,9 +27,10 @@ class ConnectionError < RuntimeError; end # For example a web request from a browser or from CURL. This # # An instance of `Puma::Client` can be used as if it were an IO object - # for example it is passed into `IO.select` inside of the `Puma::Reactor`. - # This is accomplished by the `to_io` method which gets called on any - # non-IO objects being used with the IO api such as `IO.select. + # by the reactor, that's because the latter is expected to call `#to_io` + # on any non-IO objects it polls. For example nio4r internally calls + # `IO::try_convert` (which may call `#to_io`) when a new socket is + # registered. # # Instances of this class are responsible for knowing if # the header and body are fully buffered via the `try_to_finish` method. diff --git a/lib/puma/configuration.rb b/lib/puma/configuration.rb index 877e3e0ed4..900a0105ee 100644 --- a/lib/puma/configuration.rb +++ b/lib/puma/configuration.rb @@ -348,7 +348,7 @@ def self.random_token end if bytes - token = +"" + token = "".dup bytes.each_byte { |b| token << b.to_s(16) } else token = (0..count).to_a.map { rand(255).to_s(16) }.join diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 396ca09176..15b9b7e169 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -100,8 +100,8 @@ class UnsupportedOption < RuntimeError # too taxing on performance. module Const - PUMA_VERSION = VERSION = "3.12.1".freeze - CODE_NAME = "Llamas in Pajamas".freeze + PUMA_VERSION = VERSION = "4.0.0".freeze + CODE_NAME = "4 Fast 4 Furious".freeze PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze FAST_TRACK_KA_TIMEOUT = 0.2 diff --git a/lib/puma/control_cli.rb b/lib/puma/control_cli.rb index dc6fdce398..853b3da817 100644 --- a/lib/puma/control_cli.rb +++ b/lib/puma/control_cli.rb @@ -206,6 +206,16 @@ def send_signal when "phased-restart" Process.kill "SIGUSR1", @pid + when "status" + begin + Process.kill 0, @pid + puts "Puma is started" + rescue Errno::ESRCH + raise "Puma is not running" + end + + return + else return end diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb index 1d242d36d3..fffe30bc40 100644 --- a/lib/puma/launcher.rb +++ b/lib/puma/launcher.rb @@ -254,7 +254,7 @@ def restart! argv = restart_args Dir.chdir(@restart_dir) - argv += [redirects] if RUBY_VERSION >= '1.9' + argv += [redirects] Kernel.exec(*argv) end end @@ -283,7 +283,7 @@ def prune_bundler wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild")) args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv # Ruby 2.0+ defaults to true which breaks socket activation - args += [{:close_others => false}] if RUBY_VERSION >= '2.0' + args += [{:close_others => false}] Kernel.exec(*args) end end diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index f4ec92ee83..0d419cfd0c 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -20,10 +20,11 @@ module Puma # # ## Reactor Flow # - # A request comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance. - # The reactor stores the request in an array and calls `IO.select` on the array in a loop. + # A connection comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance, + # which stores it in an array and waits for any of the connections to be ready for reading. # - # When the request is written to by the client then the `IO.select` will "wake up" and + # The waiting/wake up is performed with nio4r, which will use the apropriate backend (libev, Java NIO or + # just plain IO#select). The call to `NIO::Selector#select` will "wake up" and # return the references to any objects that caused it to "wake". The reactor # then loops through each of these request objects, and sees if they're complete. If they # have a full header and body then the reactor passes the request to a thread pool. @@ -69,19 +70,18 @@ def initialize(server, app_pool) private - # Until a request is added via the `add` method this method will internally # loop, waiting on the `sockets` array objects. The only object in this # array at first is the `@ready` IO object, which is the read end of a pipe # connected to `@trigger` object. When `@trigger` is written to, then the loop - # will break on `IO.select` and return an array. + # will break on `NIO::Selector#select` and return an array. # # ## When a request is added: # # When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array. # Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`. # - # When that happens, the internal loop stops blocking at `IO.select` and returns a reference + # When that happens, the internal loop stops blocking at `NIO::Selector#select` and returns a reference # to whatever "woke" it up. On the very first loop, the only thing in `sockets` is `@ready`. # When `@trigger` is written-to, the loop "wakes" and the `ready` # variable returns an array of arrays that looks like `[[#], [], []]` where the @@ -97,7 +97,7 @@ def initialize(server, app_pool) # to the `@ready` IO object. For example: `[#, #]`. # # Since the `Puma::Client` in this example has data that has not been read yet, - # the `IO.select` is immediately able to "wake" and read from the `Puma::Client`. At this point the + # the `NIO::Selector#select` is immediately able to "wake" and read from the `Puma::Client`. At this point the # `ready` output looks like this: `[[#], [], []]`. # # Each element in the first entry is iterated over. The `Puma::Client` object is not @@ -109,12 +109,12 @@ def initialize(server, app_pool) # # If the request body is not present then nothing will happen, and the loop will iterate # again. When the client sends more data to the socket the `Puma::Client` object will - # wake up the `IO.select` and it can again be checked to see if it's ready to be + # wake up the `NIO::Selector#select` and it can again be checked to see if it's ready to be # passed to the thread pool. # # ## Time Out Case # - # In addition to being woken via a write to one of the sockets the `IO.select` will + # In addition to being woken via a write to one of the sockets the `NIO::Selector#select` will # periodically "time out" of the sleep. One of the functions of this is to check for # any requests that have "timed out". At the end of the loop it's checked to see if # the first element in the `@timeout` array has exceed its allowed time. If so, @@ -124,7 +124,7 @@ def initialize(server, app_pool) # # This behavior loops until all the objects that have timed out have been removed. # - # Once all the timeouts have been processed, the next duration of the `IO.select` sleep + # Once all the timeouts have been processed, the next duration of the `NIO::Selector#select` sleep # will be set to be equal to the amount of time it will take for the next timeout to occur. # This calculation happens in `calculate_sleep`. def run_internal @@ -320,7 +320,7 @@ def run_in_thread end end - # The `calculate_sleep` sets the value that the `IO.select` will + # The `calculate_sleep` sets the value that the `NIO::Selector#select` will # sleep for in the main reactor loop when no sockets are being written to. # # The values kept in `@timeouts` are sorted so that the first timeout @@ -351,18 +351,18 @@ def calculate_sleep # object. # # The main body of the reactor loop is in `run_internal` and it - # will sleep on `IO.select`. When a new connection is added to the + # will sleep on `NIO::Selector#select`. When a new connection is added to the # reactor it cannot be added directly to the `sockets` array, because - # the `IO.select` will not be watching for it yet. + # the `NIO::Selector#select` will not be watching for it yet. # - # Instead what needs to happen is that `IO.select` needs to be woken up, + # Instead what needs to happen is that `NIO::Selector#select` needs to be woken up, # the contents of `@input` added to the `sockets` array, and then - # another call to `IO.select` needs to happen. Since the `Puma::Client` + # another call to `NIO::Selector#select` needs to happen. Since the `Puma::Client` # object can be read immediately, it does not block, but instead returns # right away. # # This behavior is accomplished by writing to `@trigger` which wakes up - # the `IO.select` and then there is logic to detect the value of `*`, + # the `NIO::Selector#select` and then there is logic to detect the value of `*`, # pull the contents from `@input` and add them to the sockets array. # # If the object passed in has a timeout value in `timeout_at` then diff --git a/lib/puma/server.rb b/lib/puma/server.rb index 150458ec74..ac1e28674a 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -105,7 +105,7 @@ def tcp_mode! # On Linux, use TCP_CORK to better control how the TCP stack # packetizes our stream. This improves both latency and throughput. # - if RUBY_PLATFORM.match?(/linux/) + if RUBY_PLATFORM =~ /linux/ UNPACK_TCP_STATE_FROM_TCP_INFO = "C".freeze # 6 == Socket::IPPROTO_TCP diff --git a/puma.gemspec b/puma.gemspec index 49b1ae3cfb..b11ffec044 100644 --- a/puma.gemspec +++ b/puma.gemspec @@ -17,8 +17,5 @@ Gem::Specification.new do |s| %w[History.md LICENSE README.md] s.homepage = "http://puma.io" s.license = "BSD-3-Clause" - - # We will guarantee to support the last 2 major releases, - # and may choose to support further back as we see fit. - s.required_ruby_version = Gem::Requirement.new(">= 2.5") + s.required_ruby_version = Gem::Requirement.new(">= 2.2") end diff --git a/test/shell/t1.rb b/test/shell/t1.rb index 907a238e8c..25c523e426 100644 --- a/test/shell/t1.rb +++ b/test/shell/t1.rb @@ -11,7 +11,7 @@ File.unlink "t1-stdout" if File.file? "t1-stdout" File.unlink "t1-pid" if File.file? "t1-pid" -if %r!GET / HTTP/1\.1!.match?(log) +if log =~ %r!GET / HTTP/1\.1! exit 0 else exit 1 diff --git a/test/shell/t2.rb b/test/shell/t2.rb index d48fc856cf..e173d9eb6f 100644 --- a/test/shell/t2.rb +++ b/test/shell/t2.rb @@ -2,6 +2,8 @@ sleep 5 system "curl http://localhost:10103/" +out=`ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb status` + system "ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb stop" sleep 1 @@ -10,7 +12,7 @@ File.unlink "t2-stdout" if File.file? "t2-stdout" -if log =~ %r(GET / HTTP/1\.1) && !File.file?("t2-pid") +if log =~ %r(GET / HTTP/1\.1) && !File.file?("t2-pid") && out == "Puma is started\n" exit 0 else exit 1 diff --git a/test/test_integration.rb b/test/test_integration.rb index 147d8c63a9..78f4f01d8f 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -186,7 +186,7 @@ def test_phased_restart_via_pumactl until done @events.stdout.rewind log = @events.stdout.readlines.join("") - if /- Worker \d \(pid: \d+\) booted, phase: 1/.match?(log) + if log =~ /- Worker \d \(pid: \d+\) booted, phase: 1/ assert_match(/TERM sent/, log) assert_match(/- Worker \d \(pid: \d+\) booted, phase: 1/, log) done = true @@ -197,6 +197,7 @@ def test_phased_restart_via_pumactl ccli.run assert_kind_of Thread, t.join, "server didn't stop" + assert File.exist? @bind_path end def test_kill_unknown_via_pumactl diff --git a/test/test_persistent.rb b/test/test_persistent.rb index 53539075ed..06ec90aa71 100644 --- a/test/test_persistent.rb +++ b/test/test_persistent.rb @@ -37,7 +37,7 @@ def teardown end def lines(count, s=@client) - str = +"" + str = "".dup Timeout.timeout(5) do count.times { str << s.gets } end diff --git a/test/test_pumactl.rb b/test/test_pumactl.rb index e03c140610..db3c38e64e 100644 --- a/test/test_pumactl.rb +++ b/test/test_pumactl.rb @@ -39,7 +39,7 @@ def test_control_no_token assert_equal 'none', control_cli.instance_variable_get("@control_auth_token") end - def test_control_url + def test_control_url_and_status host = "127.0.0.1" port = find_open_port url = "tcp://#{host}:#{port}/" @@ -64,11 +64,18 @@ def test_control_url assert_match "200 OK", body assert_match "embedded app", body - shutdown_cmd = Puma::ControlCLI.new(opts + ["halt"]) - shutdown_cmd.run + status_cmd = Puma::ControlCLI.new(opts + ["status"]) + out, _ = capture_subprocess_io do + status_cmd.run + end + assert_match "Puma is started\n", out - # TODO: assert something about the stop command + shutdown_cmd = Puma::ControlCLI.new(opts + ["halt"]) + out, _ = capture_subprocess_io do + shutdown_cmd.run + end + assert_match "Command halt sent success\n", out - t.join + assert_kind_of Thread, t.join, "server didn't stop" end end diff --git a/win_gem_test/puma.ps1 b/win_gem_test/puma.ps1 index 71ad0480a5..60a6275353 100644 --- a/win_gem_test/puma.ps1 +++ b/win_gem_test/puma.ps1 @@ -13,7 +13,7 @@ Make-Const repo_name 'puma' Make-Const url_repo 'https://github.com/puma/puma.git' #———————————————————————————————————————————————————————————————— lowest ruby version -Make-Const ruby_vers_low 25 +Make-Const ruby_vers_low 22 # null = don't compile; false = compile, ignore test (allow failure); # true = compile & test Make-Const trunk $false ; Make-Const trunk_x64 $false