summaryrefslogtreecommitdiff
path: root/spec/ruby/library/socket/fixtures/classes.rb
blob: 786629d2eff0484f8905aaafbfed70d084e0d45a (plain)
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
165
166
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
      # rm_r in spec/mspec/lib/mspec/helpers/fs.rb fails against
      # "/tmp/unix_server_spec.socket"
      skip "too long unix socket path: #{path}"
    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.nil? || 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