File: server.rb

package info (click to toggle)
ruby-ftw 0.0.49-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 548 kB
  • sloc: ruby: 1,922; makefile: 5
file content (125 lines) | stat: -rw-r--r-- 4,074 bytes parent folder | download
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
require "ftw/namespace"
require "ftw/dns"
require "ftw/connection"

# A web server.
class FTW::Server
  # This class is raised when an error occurs starting the server sockets.
  class ServerSetupFailure < StandardError; end

  # This class is raised when an invalid address is given to the server to
  # listen on.
  class InvalidAddress < StandardError; end

  private

  # The pattern addresses must match. This is used in FTW::Server#initialize.
  ADDRESS_RE = /^(.*):([^:]+)$/

  # Create a new server listening on the given addresses
  #
  # This method will create, bind, and listen, so any errors during that
  # process be raised as ServerSetupFailure
  #
  # The parameter 'addresses' can be a single string or an array of strings.
  # These strings MUST have the form "address:port". If the 'address' part
  # is missing, it is assumed to be 0.0.0.0
  def initialize(addresses)
    addresses = [addresses] if !addresses.is_a?(Array)
    dns = FTW::DNS.singleton

    @control_lock = Mutex.new
    @sockets = {}

    failures = []
    # address format is assumed to be 'host:port'
    # TODO(sissel): The split on ":" breaks ipv6 addresses, yo.
    addresses.each do |address|
      m = ADDRESS_RE.match(address)
      if !m
        raise InvalidAddress.new("Invalid address #{address.inspect}, spected string with format 'host:port'")
      end
      host, port = m[1..2] # first capture is host, second capture is port

      # Permit address being simply ':PORT'
      host = "0.0.0.0" if host.nil?

      # resolve each hostname, use the first one that successfully binds.
      local_failures = []
      dns.resolve(host).each do |ip|
        #family = ip.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
        #socket = Socket.new(family, Socket::SOCK_STREAM, 0)
        #sockaddr = Socket.pack_sockaddr_in(port, ip)
        socket = TCPServer.new(ip, port)
        #begin
          #socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
          #socket.bind(sockaddr)
          # If we get here, bind was successful
        #rescue Errno::EADDRNOTAVAIL => e
          # TODO(sissel): Record this failure.
          #local_failures << "Could not bind to #{ip}:#{port}, address not available on this system."
          #next
        #rescue Errno::EACCES
          # TODO(sissel): Record this failure.
          #local_failures << "No permission to bind to #{ip}:#{port}: #{e.inspect}"
          #next
        #end

        begin
          socket.listen(100)
        rescue Errno::EADDRINUSE
          local_failures << "Address in use, #{ip}:#{port}, cannot listen."
          next
        end

        # Break when successfully listened
        #p :accept? => socket.respond_to?(:accept)
        @sockets["#{host}(#{ip}):#{port}"] = socket
        local_failures.clear
        break
      end
      failures += local_failures
    end

    # This allows us to interrupt the #each_connection's select() later
    # when anyone calls stop()
    @stopper = IO.pipe

    # Abort if there were failures
    raise ServerSetupFailure.new(failures) if failures.any?
  end # def initialize

  # Stop serving.
  def stop
    @stopper[1].syswrite(".")
    @stopper[1].close()
    @control_lock.synchronize do
      @sockets.each do |name, socket|
        socket.close
      end
      @sockets.clear
    end
  end # def stop

  # Yield FTW::Connection instances to the block as clients connect.
  def each_connection(&block)
    # TODO(sissel): Select on all sockets
    # TODO(sissel): Accept and yield to the block
    stopper = @stopper[0]
    while !@sockets.empty?
      @control_lock.synchronize do
        sockets = @sockets.values + [stopper]
        read, write, error = IO.select(sockets, nil, nil, nil)
        break if read.include?(stopper)
        read.each do |serversocket|
          socket, addrinfo = serversocket.accept
          connection = FTW::Connection.from_io(socket)
          yield connection
        end
      end
    end
  end # def each_connection

  public(:initialize, :stop, :each_connection)
end # class FTW::Server