Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Linux's abstract sockets #2564

Merged
merged 2 commits into from Apr 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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?
nateberkopec marked this conversation as resolved.
Show resolved Hide resolved
@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://@'
nateberkopec marked this conversation as resolved.
Show resolved Hide resolved
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|
nateberkopec marked this conversation as resolved.
Show resolved Hide resolved
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://@') ?
nateberkopec marked this conversation as resolved.
Show resolved Hide resolved
"\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