diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index 6749d16a..1b9517b9 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -58,34 +58,32 @@ module Puma ios.map { |io| io.addr[1] }.uniq end - def import_from_env(env_hash) - remove = [] - remove += create_inherited_fds(env_hash) - env_hash.each do |k,v| - if k == 'LISTEN_FDS' && ENV['LISTEN_PID'].to_i == $$ - # systemd socket activation. - # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4. - # LISTEN_PID = PID of the service process, aka us - # see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html + def create_inherited_fds(env_hash) + env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v| + fd, url = v.split(":", 2) + @inherited_fds[url] = fd.to_i + end.keys # pass keys back for removal + end - number_of_sockets_to_listen_on = v.to_i - number_of_sockets_to_listen_on.times do |index| - fd = index + 3 # 3 is the magic number you add to follow the SA protocol - sock = TCPServer.for_fd(fd) # TODO: change to BasicSocket? - key = begin # Try to parse as a path - [:unix, Socket.unpack_sockaddr_un(sock.getsockname)] - rescue ArgumentError # Try to parse as a port/ip - port, addr = Socket.unpack_sockaddr_in(sock.getsockname) - addr = "[#{addr}]" if addr =~ /\:/ - [:tcp, addr, port] - end - @activated_sockets[key] = sock - @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS" - end - remove << k << 'LISTEN_PID' + # systemd socket activation. + # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4. + # LISTEN_PID = PID of the service process, aka us + # see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html + def create_activated_fds(env_hash) + return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$ + env_hash['LISTEN_FDS'].to_i.times do |index| + sock = TCPServer.for_fd(socket_activation_fd(index)) + key = begin # Try to parse as a path + [:unix, Socket.unpack_sockaddr_un(sock.getsockname)] + rescue ArgumentError # Try to parse as a port/ip + port, addr = Socket.unpack_sockaddr_in(sock.getsockname) + addr = "[#{addr}]" if addr =~ /\:/ + [:tcp, addr, port] end + @activated_sockets[key] = sock + @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS" end - remove + ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV end def parse(binds, logger, log_msg = 'Listening') @@ -382,14 +380,8 @@ module Puma end.map { |addrinfo| addrinfo.ip_address }.uniq end - # def create_activated_sockets(env_hash) - # end - - def create_inherited_fds(env_hash) - env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v| - fd, url = v.split(":", 2) - @inherited_fds[url] = fd.to_i - end.keys # pass keys back for removal + def socket_activation_fd(int) + int + 3 # 3 is the magic number you add to follow the SA protocol end end end diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb index 99fe1db1..876df022 100644 --- a/lib/puma/launcher.rb +++ b/lib/puma/launcher.rb @@ -48,8 +48,8 @@ module Puma @config = conf @binder = Binder.new(@events) - env_to_remove = @binder.import_from_env(ENV) - env_to_remove.each { |k| ENV.delete k } + @binder.create_inherited_fds(ENV).each { |k| ENV.delete k } + @binder.create_activated_fds(ENV).each { |k| ENV.delete k } @environment = conf.environment diff --git a/test/test_binder.rb b/test/test_binder.rb index 02e4c2e7..3cc2b10d 100644 --- a/test/test_binder.rb +++ b/test/test_binder.rb @@ -250,7 +250,7 @@ class TestBinder < TestBinderBase def test_import_from_env_listen_inherit @binder.parse ["tcp://127.0.0.1:0"], @events - removals = @binder.import_from_env(@binder.redirects_for_restart_env) + removals = @binder.create_inherited_fds(@binder.redirects_for_restart_env) @binder.listeners.each do |url, io| assert_equal io.to_i, @binder.inherited_fds[url] @@ -258,14 +258,52 @@ class TestBinder < TestBinderBase assert_includes removals, "PUMA_INHERIT_0" end - # test socket activation with tcp - # test socket activation with IPv6 - # test socket activation with Unix - # test socket activation logs to events - # test socket activation returns the right keys to remove + # Socket activation tests. We have to skip all of these on non-UNIX platforms + # because the check that we do in the code only works if you support UNIX sockets. + # This is OK, because systemd obviously only works on Linux. + def test_socket_activation_tcp + skip UNIX_SKT_MSG unless UNIX_SKT_EXIST + url = "127.0.0.1" + port = UniquePort.call + sock = Addrinfo.tcp(url, port).listen + assert_activates_sockets(url: url, port: port, sock: sock) + end + + def test_socket_activation_tcp_ipv6 + skip UNIX_SKT_MSG unless UNIX_SKT_EXIST + url = "::" + port = UniquePort.call + sock = Addrinfo.tcp(url, port).listen + assert_activates_sockets(url: url, port: port, sock: sock) + end + + def test_socket_activation_unix + skip UNIX_SKT_MSG unless UNIX_SKT_EXIST + path = "test/unixserver.state" + sock = Addrinfo.unix(path).listen + assert_activates_sockets(path: path, sock: sock) + ensure + File.unlink path if path + end private + def assert_activates_sockets(path: nil, port: nil, url: nil, sock: nil) + hash = { "LISTEN_FDS" => 1, "LISTEN_PID" => $$ } + @events.instance_variable_set(:@debug, true) + + @binder.instance_variable_set(:@sock_fd, sock.fileno) + def @binder.socket_activation_fd(int); @sock_fd; end + @result = @binder.create_activated_fds(hash) + + url = "[::]" if url == "::" + ary = path ? [:unix, path] : [:tcp, url, port] + + assert_kind_of TCPServer, @binder.activated_sockets[ary] + assert_match "Registered #{ary.join(":")} for activation from LISTEN_FDS", @events.stdout.string + assert_equal ["LISTEN_FDS", "LISTEN_PID"], @result + end + def assert_parsing_logs_uri(order = [:unix, :tcp]) skip UNIX_SKT_MSG if order.include?(:unix) && !UNIX_SKT_EXIST