From fac83ae35a6ad9223c7c8da55f6289b100190299 Mon Sep 17 00:00:00 2001 From: MSP-Greg Date: Sat, 24 Apr 2021 18:24:44 -0500 Subject: [PATCH] Add support for Linux's abstract sockets (#2564) * Support Linux's abstract sockets Closes #2526 * Add two simple UNIXSocket tests --- lib/puma.rb | 14 ++++++++++++++ lib/puma/binder.rb | 28 ++++++++++++++++------------ lib/puma/control_cli.rb | 4 +++- test/test_pumactl.rb | 24 ++++++++++++++++++++++++ test/test_unix_socket.rb | 30 +++++++++++++++++++++--------- 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/lib/puma.rb b/lib/puma.rb index 2e74228d..c9a75088 100644 --- a/lib/puma.rb +++ b/lib/puma.rb @@ -39,6 +39,20 @@ module Puma HAS_SSL end + def self.abstract_unix_socket? + @abstract_unix ||= + if HAS_UNIX_SOCKET + begin + ::UNIXServer.new("\0puma.temp.unix").close + true + rescue ArgumentError # darwin + false + end + else + false + end + end + # @!attribute [rw] stats_object= def self.stats_object=(val) @get_stats = val diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index 13fbec06..c4baac79 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -177,11 +177,19 @@ module Puma @listeners << [str, io] if io when "unix" path = "#{uri.host}#{uri.path}".gsub("%20", " ") + abstract = false + if str.start_with? 'unix://@' + raise "OS does not support abstract UNIXSockets" unless Puma.abstract_unix_socket? + abstract = true + path = "@#{path}" + end if fd = @inherited_fds.delete(str) + @unix_paths << path unless abstract io = inherit_unix_listener path, fd logger.log "* Inherited #{str}" elsif sock = @activated_sockets.delete([ :unix, path ]) + @unix_paths << path unless abstract || File.exist?(path) io = inherit_unix_listener path, sock logger.log "* Activated #{str}" else @@ -205,6 +213,7 @@ module Puma end end + @unix_paths << path unless abstract || File.exist?(path) io = add_unix_listener path, umask, mode, backlog logger.log "* #{log_msg} on #{str}" end @@ -355,8 +364,6 @@ module Puma # Tell the server to listen on +path+ as a UNIX domain socket. # def add_unix_listener(path, umask=nil, mode=nil, backlog=1024) - @unix_paths << path unless File.exist? path - # Let anyone connect by default umask ||= 0 @@ -373,8 +380,7 @@ module Puma raise "There is already a server bound to: #{path}" end end - - s = UNIXServer.new(path) + s = UNIXServer.new path.sub(/\A@/, "\0") # check for abstract UNIXSocket s.listen backlog @ios << s ensure @@ -393,8 +399,6 @@ module Puma end def inherit_unix_listener(path, fd) - @unix_paths << path unless File.exist? path - s = fd.kind_of?(::TCPServer) ? fd : ::UNIXServer.for_fd(fd) @ios << s @@ -407,24 +411,24 @@ module Puma end def close_listeners - listeners.each do |l, io| - io.close unless io.closed? # Ruby 2.2 issue - uri = URI.parse(l) + @listeners.each do |l, io| + io.close unless io.closed? + uri = URI.parse l next unless uri.scheme == 'unix' unix_path = "#{uri.host}#{uri.path}" - File.unlink unix_path if unix_paths.include? unix_path + File.unlink unix_path if @unix_paths.include?(unix_path) && File.exist?(unix_path) end end def redirects_for_restart - redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h + redirects = @listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h redirects[:close_others] = true redirects end # @version 5.0.0 def redirects_for_restart_env - listeners.each_with_object({}).with_index do |(listen, memo), i| + @listeners.each_with_object({}).with_index do |(listen, memo), i| memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}" end end diff --git a/lib/puma/control_cli.rb b/lib/puma/control_cli.rb index cc874acc..dcf1f0ea 100644 --- a/lib/puma/control_cli.rb +++ b/lib/puma/control_cli.rb @@ -176,7 +176,9 @@ module Puma when 'tcp' TCPSocket.new uri.host, uri.port when 'unix' - UNIXSocket.new "#{uri.host}#{uri.path}" + # check for abstract UNIXSocket + UNIXSocket.new(@control_url.start_with?('unix://@') ? + "\0#{uri.host}#{uri.path}" : "#{uri.host}#{uri.path}") else raise "Invalid scheme: #{uri.scheme}" end diff --git a/test/test_pumactl.rb b/test/test_pumactl.rb index a88bee05..dba2dca6 100644 --- a/test/test_pumactl.rb +++ b/test/test_pumactl.rb @@ -175,6 +175,30 @@ class TestPumaControlCli < TestConfigFileBase assert_kind_of Thread, t.join, "server didn't stop" end + def test_control_aunix + skip_unless :aunix + + url = "unix://@test_control_aunix.unix" + + opts = [ + "--control-url", url, + "--control-token", "ctrl", + "--config-file", "test/config/app.rb", + ] + + control_cli = Puma::ControlCLI.new (opts + ["start"]), @ready, @ready + t = Thread.new do + control_cli.run + end + + wait_booted + + assert_command_cli_output opts + ["status"], "Puma is started" + assert_command_cli_output opts + ["stop"], "Command stop sent success" + + assert_kind_of Thread, t.join, "server didn't stop" + end + private def assert_command_cli_output(options, expected_out) diff --git a/test/test_unix_socket.rb b/test/test_unix_socket.rb index bd6dcfe5..854d44c4 100644 --- a/test/test_unix_socket.rb +++ b/test/test_unix_socket.rb @@ -8,21 +8,21 @@ class TestPumaUnixSocket < Minitest::Test App = lambda { |env| [200, {}, ["Works"]] } - def setup - return unless UNIX_SKT_EXIST - @tmp_socket_path = tmp_path('.sock') + def teardown + return if skipped? + @server.stop(true) + end + + def server_unix(type) + @tmp_socket_path = type == :unix ? tmp_path('.sock') : "@TestPumaUnixSocket" @server = Puma::Server.new App @server.add_unix_listener @tmp_socket_path @server.run end - def teardown - return unless UNIX_SKT_EXIST - @server.stop(true) - end - - def test_server + def test_server_unix skip_unless :unix + server_unix :unix sock = UNIXSocket.new @tmp_socket_path sock << "GET / HTTP/1.0\r\nHost: blah.com\r\n\r\n" @@ -31,4 +31,16 @@ class TestPumaUnixSocket < Minitest::Test assert_equal expected, sock.read(expected.size) end + + def test_server_aunix + skip_unless :aunix + server_unix :aunix + sock = UNIXSocket.new @tmp_socket_path.sub(/\A@/, "\0") + + sock << "GET / HTTP/1.0\r\nHost: blah.com\r\n\r\n" + + expected = "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nWorks" + + assert_equal expected, sock.read(expected.size) + end end