-
Notifications
You must be signed in to change notification settings - Fork 51
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
Unix Domain Socket Listeners #109
base: main
Are you sure you want to change the base?
Changes from all commits
1863d3a
9ea4930
f6894eb
82fc619
e7b49f0
9e8351e
5c7b10e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
*.gem | ||
*.rbc | ||
*.swp | ||
.bundle | ||
.config | ||
.yardoc | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,7 +53,11 @@ arguments: | |
|
||
Each address is specified as an ip/port pair, possibly accompanied by options: | ||
|
||
ADDR := (IP:PORT)[<,OPT>...] | ||
IP_ADDR := (IP:PORT)[<,OPT>...] | ||
|
||
or as a path to a UNIX domain socket, also possibly accompanied by options: | ||
|
||
UNIX_ADDR := /path/to/socket[<,OPT>...] | ||
|
||
In the worker process, the opened file descriptors will be represented | ||
as file descriptor numbers in a series of environment variables named | ||
|
@@ -181,6 +185,20 @@ EOF | |
end | ||
end | ||
|
||
|
||
BIND_PATTERN = /\A | ||
# we accept two types of socket address | ||
(?: | ||
# ip, host:port | ||
(?:(?<host>[^:]+):(?<port>\d+)) | | ||
# unix socket path | ||
(?<path>[^,:]+) | ||
) | ||
|
||
# flags are optional, comma delimited, and come at the end | ||
(?<flags>(?:,\w+)*) | ||
\Z/x | ||
|
||
# Would be nice if this could be loadable rather than always | ||
# executing, but when run under gem it's a bit hard to do so. | ||
if true # $0 == __FILE__ | ||
|
@@ -190,14 +208,19 @@ if true # $0 == __FILE__ | |
|
||
optparse = OptionParser.new do |opts| | ||
opts.on('-b ADDR', '--bind ADDR', 'Bind an address and add the corresponding FD via the environment') do |addr| | ||
unless addr =~ /\A([^:]+):(\d+)((?:,\w+)*)\Z/ | ||
raise "Invalid value for #{addr.inspect}: bind address must be of the form address:port[,flags...]" | ||
unless addr =~ BIND_PATTERN | ||
raise "Invalid value for #{addr.inspect}: bind address must be of the form address:port[,flags...] or /path/to/unix/socket[,flags...]" | ||
end | ||
|
||
flags = $~["flags"].split(",").reject(&:empty?).map(&:downcase) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can bind the match to a variable, instead of |
||
|
||
bind = if $~["path"] | ||
Einhorn::Bind::Unix.new($~["path"], flags) | ||
else | ||
Einhorn::Bind::Inet.new($~["host"], Integer($~["port"]), flags) | ||
end | ||
|
||
host = $1 | ||
port = Integer($2) | ||
flags = $3.split(',').select {|flag| flag.length > 0}.map {|flag| flag.downcase} | ||
Einhorn::State.bind << [host, port, flags] | ||
Einhorn::State.bind << bind | ||
end | ||
|
||
opts.on('-c CMD_NAME', '--command-name CMD_NAME', 'Set the command name in ps to this value') do |cmd_name| | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,6 @@ Gem::Specification.new do |gem| | |
|
||
gem.files = ["einhorn.gemspec", "README.md", "Changes.md", "LICENSE.txt"] + `git ls-files bin lib example`.split("\n") | ||
gem.executables = %w[einhorn einhornsh] | ||
gem.test_files = [] | ||
gem.name = "einhorn" | ||
gem.require_paths = ["lib"] | ||
gem.required_ruby_version = ">= 2.5.0" | ||
|
@@ -26,4 +25,6 @@ Gem::Specification.new do |gem| | |
gem.add_development_dependency "minitest", "~> 5" | ||
gem.add_development_dependency "mocha", "~> 1" | ||
gem.add_development_dependency "subprocess", "~> 1" | ||
gem.add_development_dependency "pry" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. debugging interprocess communication is still hard |
||
gem.add_development_dependency "pry-byebug" | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
require "socket" | ||
|
||
module Einhorn::Bind | ||
class Bind | ||
attr_reader :flags | ||
|
||
def ==(other) | ||
other.class == self.class && other.state == state | ||
end | ||
end | ||
|
||
class Inet < Bind | ||
def initialize(host, port, flags) | ||
@host = host | ||
@port = port | ||
@flags = flags | ||
end | ||
|
||
def state | ||
[@host, @port, @flags] | ||
end | ||
|
||
def family | ||
"AF_INET" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I tried making this When dumping this state, the string
This also lets one import state between platforms (probably only helpful for debugging/dev) (likewise for |
||
end | ||
|
||
def address | ||
"#{@host}:#{@port}" | ||
end | ||
|
||
def make_socket | ||
sd = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) | ||
|
||
if @flags.include?("r") || @flags.include?("so_reuseaddr") | ||
sd.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) | ||
end | ||
|
||
sd | ||
end | ||
|
||
def bind(sock) | ||
sock.bind(Socket.pack_sockaddr_in(@port, @host)) | ||
end | ||
end | ||
|
||
class Unix < Bind | ||
def initialize(path, flags) | ||
@path = path | ||
@flags = flags | ||
end | ||
|
||
def state | ||
[@path, @flags] | ||
end | ||
|
||
def family | ||
"AF_UNIX" | ||
end | ||
|
||
def address | ||
@path.to_s | ||
end | ||
|
||
def make_socket | ||
Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM, 0) | ||
end | ||
|
||
def clean_old_unix_socket | ||
begin | ||
sock = UNIXSocket.new(@path) | ||
rescue Errno::ECONNREFUSED | ||
# This happens with non-socket files and when the listening | ||
# end of a socket has exited. | ||
rescue Errno::ENOENT | ||
# Socket doesn't exist | ||
return | ||
else | ||
# Rats, it's still active | ||
sock.close | ||
raise Errno::EADDRINUSE.new("Another process is listening on the UNIX socket at #{@path}. If you'd like to run this Einhorn as well, pass a `-b PATH_TO_SOCKET` to change the socket location.") | ||
end | ||
|
||
stat = File.stat(@path) | ||
unless stat.socket? | ||
raise Errno::EADDRINUSE.new("Non-socket file present at UNIX socket path #{@path}. Either remove that file and restart Einhorn, or pass a different `-b PATH_TO_SOCKET` to change where you are binding.") | ||
end | ||
|
||
Einhorn.log_info("Blowing away old UNIX socket at #{@path}. This likely indicates a previous Einhorn master which exited uncleanly.") | ||
File.unlink(@path) | ||
end | ||
|
||
def bind(sock) | ||
clean_old_unix_socket | ||
sock.bind(Socket.pack_sockaddr_un(@path)) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -349,6 +349,7 @@ def self.prepare_child_environment(index) | |
|
||
ENV["EINHORN_FD_COUNT"] = Einhorn::State.bind_fds.length.to_s | ||
Einhorn::State.bind_fds.each_with_index { |fd, i| ENV["EINHORN_FD_#{i}"] = fd.to_s } | ||
Einhorn::State.bind.each_with_index { |bind, i| ENV["EINHORN_FD_FAMILY_#{i}"] = bind.family.to_s } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added It is currently silly, and boils down to: |
||
|
||
ENV["EINHORN_CHILD_INDEX"] = index.to_s | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,11 +6,11 @@ module SafeYAML | |
YAML.safe_load("---", permitted_classes: []) | ||
rescue ArgumentError | ||
def self.load(payload) | ||
YAML.safe_load(payload, [Set, Symbol, Time], [], true) | ||
YAML.safe_load(payload, [Set, Symbol, Time, Einhorn::Bind::Inet, Einhorn::Bind::Unix], [], true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and the line below feels bad. I can parse out the struct / array, but it may get dicey compatibility wise? |
||
end | ||
else | ||
def self.load(payload) # rubocop:disable Lint/DuplicateMethods | ||
YAML.safe_load(payload, permitted_classes: [Set, Symbol, Time], aliases: true) | ||
YAML.safe_load(payload, permitted_classes: [Set, Symbol, Time, Einhorn::Bind::Inet, Einhorn::Bind::Unix], aliases: true) | ||
end | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested this against abstract namespace sockets, but my wetware suggests it should work