1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
require 'socket'
module SocketSpecs
# helper to get the hostname associated to 127.0.0.1 or the given ip
def self.hostname(ip = "127.0.0.1")
# Calculate each time, without caching, since the result might
# depend on things like do_not_reverse_lookup mode, which is
# changing from test to test
Socket.getaddrinfo(ip, nil)[0][2]
end
def self.hostname_reverse_lookup(ip = "127.0.0.1")
Socket.getaddrinfo(ip, nil, 0, 0, 0, 0, true)[0][2]
end
def self.addr(which=:ipv4)
case which
when :ipv4
host = "127.0.0.1"
when :ipv6
host = "::1"
end
Socket.getaddrinfo(host, nil)[0][3]
end
def self.reserved_unused_port
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
0
end
def self.sockaddr_in(port, host)
Socket::SockAddr_In.new(Socket.sockaddr_in(port, host))
end
def self.socket_path
path = tmp("unix.sock", false)
# Check for too long unix socket path (max 104 bytes on macOS)
# Note that Linux accepts not null-terminated paths but the man page advises against it.
if path.bytesize > 104
path = "/tmp/unix_server_spec.socket"
end
rm_socket(path)
path
end
def self.rm_socket(path)
File.delete(path) if File.exist?(path)
end
def self.ipv6_available?
@ipv6_available ||= begin
server = TCPServer.new('::1', 0)
rescue Errno::EAFNOSUPPORT, Errno::EADDRNOTAVAIL, SocketError
:no
else
server.close
:yes
end
@ipv6_available == :yes
end
def self.each_ip_protocol
describe 'using IPv4' do
yield Socket::AF_INET, '127.0.0.1', 'AF_INET'
end
guard -> { SocketSpecs.ipv6_available? } do
describe 'using IPv6' do
yield Socket::AF_INET6, '::1', 'AF_INET6'
end
end
end
def self.loop_with_timeout(timeout = TIME_TOLERANCE)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
while yield == :retry
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start >= timeout
raise RuntimeError, "Did not succeed within #{timeout} seconds"
end
end
end
def self.dest_addr_req_error
error = Errno::EDESTADDRREQ
platform_is :windows do
error = Errno::ENOTCONN
end
error
end
# TCPServer echo server accepting one connection
class SpecTCPServer
attr_reader :hostname, :port
def initialize
@hostname = SocketSpecs.hostname
@server = TCPServer.new @hostname, 0
@port = @server.addr[1]
log "SpecTCPServer starting on #{@hostname}:#{@port}"
@thread = Thread.new do
socket = @server.accept
log "SpecTCPServer accepted connection: #{socket}"
service socket
end
end
def service(socket)
begin
data = socket.recv(1024)
return if data.empty?
log "SpecTCPServer received: #{data.inspect}"
return if data == "QUIT"
socket.send data, 0
ensure
socket.close
end
end
def shutdown
log "SpecTCPServer shutting down"
@thread.join
@server.close
end
def log(message)
@logger.puts message if @logger
end
end
# We need to find a free port for Socket.tcp_server_loop and Socket.udp_server_loop,
# and the only reliable way to do that is to pass 0 as the port, but then we need to
# find out which one was chosen and the API doesn't let us find what it is. So we
# intercept one of the public API methods called by these methods.
class ServerLoopPortFinder < Socket
def self.tcp_server_sockets(*args)
super(*args) { |sockets|
@port = sockets.first.local_address.ip_port
yield(sockets)
}
end
def self.udp_server_sockets(*args, &block)
super(*args) { |sockets|
@port = sockets.first.local_address.ip_port
yield(sockets)
}
end
def self.cleanup
@port = nil
end
def self.port
sleep 0.001 until @port
@port
end
end
end
|