Skip to content

Commit

Permalink
Add support for Linux's abstract sockets (#2564)
Browse files Browse the repository at this point in the history
* Support Linux's abstract sockets

Closes #2526

* Add two simple UNIXSocket tests
  • Loading branch information
MSP-Greg committed Apr 24, 2021
1 parent 7a68835 commit fac83ae
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 22 deletions.
14 changes: 14 additions & 0 deletions lib/puma.rb
Expand Up @@ -39,6 +39,20 @@ def self.ssl?
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
Expand Down
28 changes: 16 additions & 12 deletions lib/puma/binder.rb
Expand Up @@ -177,11 +177,19 @@ def parse(binds, logger, log_msg = 'Listening')
@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
Expand All @@ -205,6 +213,7 @@ def parse(binds, logger, log_msg = 'Listening')
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
Expand Down Expand Up @@ -355,8 +364,6 @@ def inherit_ssl_listener(fd, ctx)
# 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

Expand All @@ -373,8 +380,7 @@ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
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
Expand All @@ -393,8 +399,6 @@ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
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
Expand All @@ -407,24 +411,24 @@ def inherit_unix_listener(path, fd)
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
Expand Down
4 changes: 3 additions & 1 deletion lib/puma/control_cli.rb
Expand Up @@ -176,7 +176,9 @@ def send_request
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
Expand Down
24 changes: 24 additions & 0 deletions test/test_pumactl.rb
Expand Up @@ -175,6 +175,30 @@ def test_control_ssl
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)
Expand Down
30 changes: 21 additions & 9 deletions test/test_unix_socket.rb
Expand Up @@ -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"
Expand All @@ -31,4 +31,16 @@ def test_server

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

0 comments on commit fac83ae

Please sign in to comment.