Class: Herb::Dev::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/herb/dev/server.rb,
sig/herb/dev/server.rbs

Defined Under Namespace

Classes: Client

Constant Summary collapse

DEFAULT_PORT =

Returns:

  • (::Integer)
8592
HANDSHAKE_TIMEOUT =

Returns:

  • (::Integer)
5

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(port: DEFAULT_PORT, project_path: nil) ⇒ Server

Returns a new instance of Server.

Parameters:

  • port: (Object) (defaults to: DEFAULT_PORT)
  • project_path: (Object) (defaults to: nil)


32
33
34
35
36
37
38
39
40
# File 'lib/herb/dev/server.rb', line 32

def initialize(port: DEFAULT_PORT, project_path: nil)
  @port = port
  @project_path = project_path
  @clients = [] #: Array[Client]
  @mutex = Mutex.new
  @server = nil
  @accept_thread = nil
  @entry = nil
end

Instance Attribute Details

#portObject (readonly)

Returns the value of attribute port.

Returns:

  • (Object)


28
29
30
# File 'lib/herb/dev/server.rb', line 28

def port
  @port
end

#project_pathObject (readonly)

Returns the value of attribute project_path.

Returns:

  • (Object)


28
29
30
# File 'lib/herb/dev/server.rb', line 28

def project_path
  @project_path
end

Class Method Details

.find_available_port(starting_port = DEFAULT_PORT) ⇒ Object

Parameters:

  • starting_port (Object) (defaults to: DEFAULT_PORT)

Returns:

  • (Object)


108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/herb/dev/server.rb', line 108

def self.find_available_port(starting_port = DEFAULT_PORT)
  port = starting_port

  loop do
    return port if port_available?(port)

    port += 1
    break if port > starting_port + 100
  end

  nil
end

.port_available?(port) ⇒ Boolean

Parameters:

  • port (Object)

Returns:

  • (Boolean)


99
100
101
102
103
104
105
106
# File 'lib/herb/dev/server.rb', line 99

def self.port_available?(port)
  server = TCPServer.new("0.0.0.0", port)
  server.close

  true
rescue Errno::EADDRINUSE
  false
end

Instance Method Details

#broadcast(message) ⇒ Object

Parameters:

  • message (Object)

Returns:

  • (Object)


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
# File 'lib/herb/dev/server.rb', line 69

def broadcast(message)
  data = message.is_a?(String) ? message : JSON.generate(message)

  failed_clients = []
  clients_snapshot = @mutex.synchronize { @clients.dup }

  clients_snapshot.each do |client|
    frame = WebSocket::Frame::Outgoing::Server.new(version: client.version, data: data, type: :text)

    client.mutex.synchronize do
      client.socket.write(frame.to_s)
      client.socket.flush
    end
  rescue StandardError
    safely_close(client.socket)

    failed_clients << client
  end

  return unless failed_clients.any?

  @mutex.synchronize do
    failed_clients.each { |client| @clients.delete(client) }
  end
end

#client_countObject

Returns:

  • (Object)


95
96
97
# File 'lib/herb/dev/server.rb', line 95

def client_count
  @mutex.synchronize { @clients.size }
end

#handle_connection(socket) ⇒ Object

Parameters:

  • socket (Object)

Returns:

  • (Object)


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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/herb/dev/server.rb', line 129

def handle_connection(socket)
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

  handshake = WebSocket::Handshake::Server.new

  until handshake.finished?
    readable = socket.wait_readable(HANDSHAKE_TIMEOUT)

    unless readable
      socket.close
      return
    end

    data = socket.read_nonblock(4096, exception: false)
    break if data.nil? || data == :wait_readable

    data.each_byte { |byte| handshake << byte.chr }
  end

  unless handshake.valid?
    socket.close
    return
  end

  socket.write(handshake.to_s)
  socket.flush

  welcome = WebSocket::Frame::Outgoing::Server.new(
    version: handshake.version,
    data: JSON.generate({ type: "welcome", project: @project_path }),
    type: :text
  )

  socket.write(welcome.to_s)
  socket.flush

  client = Client.new(socket: socket, version: handshake.version, mutex: Mutex.new)
  @mutex.synchronize { @clients << client }

  frame_parser = WebSocket::Frame::Incoming::Server.new(version: handshake.version)

  loop do
    chunk = socket.readpartial(4096)

    frame_parser << chunk

    while (frame = frame_parser.next)
      case frame.type
      when :close
        close_frame = WebSocket::Frame::Outgoing::Server.new(version: handshake.version, data: "", type: :close)

        begin
          client.mutex.synchronize { socket.write(close_frame.to_s) }
        rescue StandardError
          nil
        end

        return
      when :ping
        pong = WebSocket::Frame::Outgoing::Server.new(version: handshake.version, data: frame.data, type: :pong)

        client.mutex.synchronize { socket.write(pong.to_s) }
      when :text
        nil
      end
    end
  end
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
  # client disconnected
rescue StandardError => e
  warn "[herb-dev-server] connection error: #{e.class}: #{e.message}"
ensure
  @mutex.synchronize { @clients.delete_if { |client| client.socket == socket } }

  safely_close(socket)
end

#safely_close(resource) ⇒ Object

Parameters:

  • resource (Object)

Returns:

  • (Object)


123
124
125
126
127
# File 'lib/herb/dev/server.rb', line 123

def safely_close(resource)
  resource&.close
rescue StandardError
  nil
end

#startObject

Returns:

  • (Object)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/herb/dev/server.rb', line 42

def start
  @entry = ServerEntry.new(pid: Process.pid, port: @port, project: @project_path)
  @entry.save
  @server = TCPServer.new("0.0.0.0", @port)

  @accept_thread = Thread.new do
    loop do
      socket = @server.accept
      Thread.new(socket) { |s| handle_connection(s) }
    rescue IOError
      break
    end
  end
end

#stopObject

Returns:

  • (Object)


57
58
59
60
61
62
63
64
65
66
67
# File 'lib/herb/dev/server.rb', line 57

def stop
  @mutex.synchronize do
    @clients.each { |client| safely_close(client.socket) }
    @clients.clear
  end

  safely_close(@server)

  @accept_thread&.kill
  @entry&.remove
end