ruflet_server 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ruflet/server/web_socket_connection.rb +46 -6
- data/lib/ruflet/server/wire_codec.rb +30 -64
- data/lib/ruflet/server.rb +323 -166
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_server.rb +1 -36
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5c44fadc8aa766bbfe16e6dc94afb0503a03408ee5bb8108fc9064cb10adf84
|
|
4
|
+
data.tar.gz: 81a8de81cf0cbbfee1deb8a3fdea5b448a9ba3a8b417ffc942d33fb5dc2d8d2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04c82e1accf25203c93e3fc987166453cec277b5e29b380c7539d3f32dd0e777843d1f084485513437108bda6cc8047a80c141a890ec926a0c63f8ef7b8ae688
|
|
7
|
+
data.tar.gz: 0013bf2ab8f22d327df1f48ced0d956148c495e310bcc7ac0230b0ba2344ebb97d5f744a26cafe23ce4969fa9776da17acdc66f64aacfbb5dcb1772e1826daf6
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Ruflet
|
|
4
4
|
class WebSocketConnection
|
|
5
|
+
TASK_IO_POLL_INTERVAL = 0.01
|
|
5
6
|
# Ruflet control messages are small; anything much larger is invalid or hostile.
|
|
6
7
|
MAX_FRAME_PAYLOAD_BYTES = 16 * 1024 * 1024
|
|
7
8
|
|
|
@@ -45,7 +46,27 @@ module Ruflet
|
|
|
45
46
|
when 0xA
|
|
46
47
|
read_message
|
|
47
48
|
when 0x1, 0x2
|
|
48
|
-
payload
|
|
49
|
+
return payload if frame[:fin]
|
|
50
|
+
|
|
51
|
+
message = payload.dup
|
|
52
|
+
loop do
|
|
53
|
+
continuation = read_frame
|
|
54
|
+
return nil if continuation.nil?
|
|
55
|
+
|
|
56
|
+
case continuation[:opcode]
|
|
57
|
+
when 0x9
|
|
58
|
+
send_frame(0xA, continuation[:payload])
|
|
59
|
+
next
|
|
60
|
+
when 0xA
|
|
61
|
+
next
|
|
62
|
+
when 0x0
|
|
63
|
+
message << continuation[:payload]
|
|
64
|
+
return message if continuation[:fin]
|
|
65
|
+
return nil if message.bytesize > MAX_FRAME_PAYLOAD_BYTES
|
|
66
|
+
else
|
|
67
|
+
return nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
49
70
|
else
|
|
50
71
|
read_message
|
|
51
72
|
end
|
|
@@ -70,6 +91,7 @@ module Ruflet
|
|
|
70
91
|
b1 = header.getbyte(0)
|
|
71
92
|
b2 = header.getbyte(1)
|
|
72
93
|
|
|
94
|
+
fin = (b1 & 0x80) != 0
|
|
73
95
|
masked = (b2 & 0x80) != 0
|
|
74
96
|
payload_len = b2 & 0x7f
|
|
75
97
|
|
|
@@ -92,7 +114,7 @@ module Ruflet
|
|
|
92
114
|
|
|
93
115
|
payload = unmask(payload, masking_key) if masked
|
|
94
116
|
|
|
95
|
-
{ opcode: b1 & 0x0f, payload: payload }
|
|
117
|
+
{ fin: fin, opcode: b1 & 0x0f, payload: payload }
|
|
96
118
|
end
|
|
97
119
|
|
|
98
120
|
def send_frame(opcode, payload)
|
|
@@ -110,8 +132,8 @@ module Ruflet
|
|
|
110
132
|
end
|
|
111
133
|
|
|
112
134
|
@write_mutex.synchronize do
|
|
113
|
-
@socket.write(header)
|
|
114
|
-
@socket.
|
|
135
|
+
@socket.write(header + bytes)
|
|
136
|
+
@socket.flush if @socket.respond_to?(:flush)
|
|
115
137
|
end
|
|
116
138
|
end
|
|
117
139
|
|
|
@@ -132,15 +154,33 @@ module Ruflet
|
|
|
132
154
|
chunk.force_encoding(Encoding::BINARY)
|
|
133
155
|
|
|
134
156
|
while chunk.bytesize < length
|
|
135
|
-
part =
|
|
157
|
+
part = if task_scheduler?
|
|
158
|
+
@socket.recv_nonblock(length - chunk.bytesize)
|
|
159
|
+
else
|
|
160
|
+
@socket.read(length - chunk.bytesize)
|
|
161
|
+
end
|
|
136
162
|
return nil if part.nil? || part.empty?
|
|
137
163
|
|
|
138
164
|
chunk << part
|
|
139
165
|
end
|
|
140
166
|
|
|
141
167
|
chunk
|
|
142
|
-
rescue
|
|
168
|
+
rescue StandardError => error
|
|
169
|
+
if task_scheduler? && would_block_error?(error)
|
|
170
|
+
sleep TASK_IO_POLL_INTERVAL
|
|
171
|
+
retry
|
|
172
|
+
end
|
|
173
|
+
|
|
143
174
|
nil
|
|
144
175
|
end
|
|
176
|
+
|
|
177
|
+
def task_scheduler?
|
|
178
|
+
concurrent = Object.const_defined?(:Task) || (Thread.respond_to?(:cooperative?) && Thread.cooperative?)
|
|
179
|
+
concurrent && @socket.respond_to?(:recv_nonblock)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def would_block_error?(error)
|
|
183
|
+
%w[Errno::EAGAIN Errno::EWOULDBLOCK IO::EAGAINWaitReadable IO::WaitReadable].include?(error.class.to_s)
|
|
184
|
+
end
|
|
145
185
|
end
|
|
146
186
|
end
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Ruflet
|
|
4
4
|
class WireCodec
|
|
5
|
-
PICKER_DATE_CONTROLS = %w[CupertinoDatePicker DatePicker DateRangePicker].freeze
|
|
6
|
-
PICKER_DATE_KEYS = %w[current_date end_value first_date last_date start_value value].freeze
|
|
7
|
-
PICKER_TIME_CONTROLS = %w[TimePicker].freeze
|
|
8
|
-
PICKER_TIME_KEYS = %w[value].freeze
|
|
9
|
-
|
|
10
5
|
class << self
|
|
11
|
-
def pack(value
|
|
6
|
+
def pack(value)
|
|
12
7
|
case value
|
|
8
|
+
when Ruflet::Protocol::DateTimeValue
|
|
9
|
+
pack_extension(1, value)
|
|
10
|
+
when Ruflet::Protocol::TimeOfDayValue
|
|
11
|
+
pack_extension(2, value)
|
|
12
|
+
when Ruflet::Protocol::DurationValue
|
|
13
|
+
pack_extension(3, value)
|
|
13
14
|
when NilClass
|
|
14
15
|
"\xc0".b
|
|
15
16
|
when TrueClass
|
|
@@ -21,14 +22,11 @@ module Ruflet
|
|
|
21
22
|
when Float
|
|
22
23
|
"\xcb".b + [value].pack("G")
|
|
23
24
|
when String
|
|
24
|
-
return pack_ext(1, normalize_date_ext(value)) if context == :date
|
|
25
|
-
return pack_ext(2, normalize_time_ext(value)) if context == :time
|
|
26
|
-
|
|
27
25
|
binary_string?(value) ? pack_binary(value) : pack_string(value)
|
|
28
26
|
when Symbol
|
|
29
27
|
pack_string(value.to_s)
|
|
30
28
|
when Array
|
|
31
|
-
pack_array(value
|
|
29
|
+
pack_array(value)
|
|
32
30
|
when Hash
|
|
33
31
|
pack_map(value)
|
|
34
32
|
else
|
|
@@ -43,6 +41,20 @@ module Ruflet
|
|
|
43
41
|
|
|
44
42
|
private
|
|
45
43
|
|
|
44
|
+
def pack_extension(type, value)
|
|
45
|
+
bytes = value.to_s.b
|
|
46
|
+
length = bytes.bytesize
|
|
47
|
+
header =
|
|
48
|
+
if length <= 0xff
|
|
49
|
+
"\xc7".b + [length].pack("C")
|
|
50
|
+
elsif length <= 0xffff
|
|
51
|
+
"\xc8".b + [length].pack("n")
|
|
52
|
+
else
|
|
53
|
+
"\xc9".b + [length].pack("N")
|
|
54
|
+
end
|
|
55
|
+
header + [type].pack("c") + bytes
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
def pack_integer(value)
|
|
47
59
|
if value >= 0
|
|
48
60
|
return [value].pack("C") if value <= 0x7f
|
|
@@ -94,41 +106,7 @@ module Ruflet
|
|
|
94
106
|
value.encoding == Encoding::BINARY || !value.valid_encoding?
|
|
95
107
|
end
|
|
96
108
|
|
|
97
|
-
def
|
|
98
|
-
bytes = payload.to_s.b
|
|
99
|
-
len = bytes.bytesize
|
|
100
|
-
|
|
101
|
-
if len == 1
|
|
102
|
-
"\xd4".b + [type].pack("c") + bytes
|
|
103
|
-
elsif len == 2
|
|
104
|
-
"\xd5".b + [type].pack("c") + bytes
|
|
105
|
-
elsif len == 4
|
|
106
|
-
"\xd6".b + [type].pack("c") + bytes
|
|
107
|
-
elsif len == 8
|
|
108
|
-
"\xd7".b + [type].pack("c") + bytes
|
|
109
|
-
elsif len == 16
|
|
110
|
-
"\xd8".b + [type].pack("c") + bytes
|
|
111
|
-
elsif len <= 0xff
|
|
112
|
-
"\xc7".b + [len, type].pack("Cc") + bytes
|
|
113
|
-
elsif len <= 0xffff
|
|
114
|
-
"\xc8".b + [len, type].pack("nc") + bytes
|
|
115
|
-
else
|
|
116
|
-
"\xc9".b + [len, type].pack("Nc") + bytes
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def normalize_date_ext(value)
|
|
121
|
-
raw = value.to_s
|
|
122
|
-
return "#{raw}T00:00:00+00:00" if raw.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
123
|
-
|
|
124
|
-
raw
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def normalize_time_ext(value)
|
|
128
|
-
value.to_s
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def pack_array(value, context = nil)
|
|
109
|
+
def pack_array(value)
|
|
132
110
|
len = value.length
|
|
133
111
|
head =
|
|
134
112
|
if len <= 15
|
|
@@ -140,7 +118,7 @@ module Ruflet
|
|
|
140
118
|
end
|
|
141
119
|
|
|
142
120
|
body = +"".b
|
|
143
|
-
value.each { |item| body << pack(item
|
|
121
|
+
value.each { |item| body << pack(item) }
|
|
144
122
|
head + body
|
|
145
123
|
end
|
|
146
124
|
|
|
@@ -157,22 +135,13 @@ module Ruflet
|
|
|
157
135
|
end
|
|
158
136
|
|
|
159
137
|
body = +"".b
|
|
160
|
-
control_type = pairs["_c"].to_s
|
|
161
138
|
pairs.each do |k, v|
|
|
162
139
|
body << pack(k)
|
|
163
|
-
body << pack(v
|
|
140
|
+
body << pack(v)
|
|
164
141
|
end
|
|
165
142
|
head + body
|
|
166
143
|
end
|
|
167
144
|
|
|
168
|
-
def picker_prop_context(control_type, key)
|
|
169
|
-
key = key.to_s
|
|
170
|
-
return :date if PICKER_DATE_CONTROLS.include?(control_type) && PICKER_DATE_KEYS.include?(key)
|
|
171
|
-
return :time if PICKER_TIME_CONTROLS.include?(control_type) && PICKER_TIME_KEYS.include?(key)
|
|
172
|
-
|
|
173
|
-
nil
|
|
174
|
-
end
|
|
175
|
-
|
|
176
145
|
def read_value(reader)
|
|
177
146
|
marker = reader.read_u8
|
|
178
147
|
|
|
@@ -247,15 +216,12 @@ module Ruflet
|
|
|
247
216
|
|
|
248
217
|
def read_ext(reader, size)
|
|
249
218
|
type = reader.read_i8
|
|
250
|
-
|
|
251
|
-
|
|
219
|
+
value = reader.read_exact(size).force_encoding("UTF-8")
|
|
252
220
|
case type
|
|
253
|
-
when 1
|
|
254
|
-
|
|
255
|
-
when 3
|
|
256
|
-
|
|
257
|
-
else
|
|
258
|
-
data
|
|
221
|
+
when 1 then Ruflet::Protocol.date_time(value)
|
|
222
|
+
when 2 then Ruflet::Protocol.time_of_day(value)
|
|
223
|
+
when 3 then Ruflet::Protocol.duration(value)
|
|
224
|
+
else value
|
|
259
225
|
end
|
|
260
226
|
end
|
|
261
227
|
end
|
data/lib/ruflet/server.rb
CHANGED
|
@@ -7,18 +7,13 @@ require "thread"
|
|
|
7
7
|
require "ruflet_core"
|
|
8
8
|
require_relative "server/wire_codec"
|
|
9
9
|
require_relative "server/web_socket_connection"
|
|
10
|
-
require_relative "server/connection_protocol"
|
|
11
10
|
|
|
12
11
|
module Ruflet
|
|
13
|
-
# Standalone TCP transport for the Ruflet protocol. The protocol itself
|
|
14
|
-
# lives in Ruflet::ConnectionProtocol and is shared with host-server
|
|
15
|
-
# adapters (e.g. ruflet_rails runs it on the Rails server's own socket).
|
|
16
12
|
class Server
|
|
17
|
-
include ConnectionProtocol
|
|
18
|
-
|
|
19
13
|
attr_reader :port
|
|
20
14
|
|
|
21
15
|
WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
16
|
+
TASK_IO_POLL_INTERVAL = 0.01
|
|
22
17
|
|
|
23
18
|
def initialize(host: "0.0.0.0", port: 8550, &app_block)
|
|
24
19
|
@host = host
|
|
@@ -41,6 +36,13 @@ module Ruflet
|
|
|
41
36
|
end
|
|
42
37
|
|
|
43
38
|
def start
|
|
39
|
+
if Object.const_defined?(:Task) && Task.current.name == "main"
|
|
40
|
+
server = self
|
|
41
|
+
task = Task.new(name: "ruflet-server") { server.start }
|
|
42
|
+
Task.run
|
|
43
|
+
return task
|
|
44
|
+
end
|
|
45
|
+
|
|
44
46
|
previous_signals = trap_stop_signals
|
|
45
47
|
bind_server_socket!
|
|
46
48
|
@running = true
|
|
@@ -53,6 +55,12 @@ module Ruflet
|
|
|
53
55
|
restore_stop_signals(previous_signals)
|
|
54
56
|
end
|
|
55
57
|
|
|
58
|
+
# For Rack-hosted mode: caller already performed the HTTP upgrade.
|
|
59
|
+
def handle_upgraded_socket(io)
|
|
60
|
+
ws = Ruflet::WebSocketConnection.new(io)
|
|
61
|
+
run_connection(ws)
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
def bind_server_socket!(max_attempts: 100)
|
|
57
65
|
requested = @port.to_i
|
|
58
66
|
candidate = requested
|
|
@@ -61,11 +69,11 @@ module Ruflet
|
|
|
61
69
|
attempts.times do
|
|
62
70
|
begin
|
|
63
71
|
@server_socket = TCPServer.new(@host, candidate)
|
|
64
|
-
@port =
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
@port = @server_socket.addr[1]
|
|
73
|
+
publish_runtime_port
|
|
74
|
+
if requested > 0 && @port != requested && ENV["RUFLET_SUPPRESS_SERVER_BANNER"] != "1"
|
|
75
|
+
warn "Requested port #{requested} is busy; bound to #{@port}"
|
|
67
76
|
end
|
|
68
|
-
publish_bound_port!
|
|
69
77
|
return
|
|
70
78
|
rescue Errno::EADDRINUSE
|
|
71
79
|
candidate += 1
|
|
@@ -81,7 +89,6 @@ module Ruflet
|
|
|
81
89
|
return unless @running || @server_socket
|
|
82
90
|
|
|
83
91
|
@running = false
|
|
84
|
-
remove_port_file!
|
|
85
92
|
|
|
86
93
|
server = @server_socket
|
|
87
94
|
@server_socket = nil
|
|
@@ -101,6 +108,15 @@ module Ruflet
|
|
|
101
108
|
end
|
|
102
109
|
end
|
|
103
110
|
|
|
111
|
+
def publish_runtime_port
|
|
112
|
+
path = ENV["RUFLET_RUNTIME_PORT_FILE"].to_s
|
|
113
|
+
return if path.empty?
|
|
114
|
+
|
|
115
|
+
File.open(path, "w") { |file| file.write(@port.to_s) }
|
|
116
|
+
rescue StandardError => error
|
|
117
|
+
warn "Unable to publish Ruflet runtime port: #{error.message}"
|
|
118
|
+
end
|
|
119
|
+
|
|
104
120
|
def reload_app!
|
|
105
121
|
snapshots = @sessions_mutex.synchronize { @sessions.to_a }
|
|
106
122
|
|
|
@@ -130,30 +146,6 @@ module Ruflet
|
|
|
130
146
|
|
|
131
147
|
private
|
|
132
148
|
|
|
133
|
-
# Lets embedding hosts (e.g. the ruby_runtime Flutter plugins) discover
|
|
134
|
-
# which port the server actually bound when the requested one was busy.
|
|
135
|
-
def publish_bound_port!
|
|
136
|
-
path = ENV["RUFLET_PORT_FILE"].to_s
|
|
137
|
-
return if path.empty?
|
|
138
|
-
|
|
139
|
-
begin
|
|
140
|
-
File.write(path, @port.to_s)
|
|
141
|
-
rescue StandardError
|
|
142
|
-
nil
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def remove_port_file!
|
|
147
|
-
path = ENV["RUFLET_PORT_FILE"].to_s
|
|
148
|
-
return if path.empty?
|
|
149
|
-
|
|
150
|
-
begin
|
|
151
|
-
File.delete(path)
|
|
152
|
-
rescue StandardError
|
|
153
|
-
nil
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
149
|
def trap_stop_signals
|
|
158
150
|
{
|
|
159
151
|
"INT" => trap_signal("INT"),
|
|
@@ -194,23 +186,39 @@ module Ruflet
|
|
|
194
186
|
end
|
|
195
187
|
|
|
196
188
|
def accept_client_socket
|
|
197
|
-
accepted = @server_socket.accept
|
|
189
|
+
accepted = task_scheduler? ? @server_socket.accept_nonblock : @server_socket.accept
|
|
198
190
|
accepted.is_a?(Array) ? accepted.first : accepted
|
|
199
191
|
rescue IOError, Errno::EBADF
|
|
200
192
|
nil
|
|
201
193
|
rescue StandardError => e
|
|
202
194
|
return nil unless @running && @server_socket
|
|
203
195
|
|
|
196
|
+
if task_scheduler? && would_block_error?(e)
|
|
197
|
+
sleep TASK_IO_POLL_INTERVAL
|
|
198
|
+
Thread.pass if Thread.respond_to?(:cooperative?)
|
|
199
|
+
retry
|
|
200
|
+
end
|
|
201
|
+
|
|
204
202
|
warn "accept error: #{e.class}: #{e.message}"
|
|
205
203
|
warn e.backtrace.join("\n") if e.backtrace
|
|
206
204
|
nil
|
|
207
205
|
end
|
|
208
206
|
|
|
207
|
+
def task_scheduler?
|
|
208
|
+
Object.const_defined?(:Task) || (Thread.respond_to?(:cooperative?) && Thread.cooperative?)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def would_block_error?(error)
|
|
212
|
+
%w[Errno::EAGAIN Errno::EWOULDBLOCK IO::EAGAINWaitReadable IO::WaitReadable].include?(error.class.to_s)
|
|
213
|
+
end
|
|
214
|
+
|
|
209
215
|
def start_client_thread(socket)
|
|
210
|
-
Thread.new(socket) do |client|
|
|
216
|
+
thread = Thread.new(socket) do |client|
|
|
211
217
|
Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
|
|
212
218
|
handle_socket(client)
|
|
213
219
|
end
|
|
220
|
+
Thread.pass if task_scheduler?
|
|
221
|
+
thread
|
|
214
222
|
end
|
|
215
223
|
|
|
216
224
|
def handle_socket(socket)
|
|
@@ -233,26 +241,34 @@ module Ruflet
|
|
|
233
241
|
warn e.backtrace.join("\n") if e.backtrace
|
|
234
242
|
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws
|
|
235
243
|
ensure
|
|
236
|
-
|
|
237
|
-
close_connection(ws)
|
|
238
|
-
else
|
|
239
|
-
# Plain HTTP request: we answer with `Connection: close`, so we must
|
|
240
|
-
# actually close the socket. Leaving it open exhausts the browser's
|
|
241
|
-
# per-host connection pool and the later /ws upgrade never opens —
|
|
242
|
-
# the app then hangs on its "connecting" screen.
|
|
243
|
-
close_http_socket(socket)
|
|
244
|
-
end
|
|
244
|
+
close_connection(ws)
|
|
245
245
|
end
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
-
def
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
def run_connection(ws)
|
|
249
|
+
register_connection(ws)
|
|
250
|
+
|
|
251
|
+
while (raw = ws.read_message)
|
|
252
|
+
handle_message(ws, raw)
|
|
253
|
+
end
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
return if disconnect_error?(e)
|
|
256
|
+
|
|
257
|
+
warn "server error: #{e.class}: #{e.message}"
|
|
258
|
+
warn e.backtrace.join("\n") if e.backtrace
|
|
259
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
|
|
260
|
+
ensure
|
|
261
|
+
close_connection(ws)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def close_connection(ws)
|
|
265
|
+
remove_session(ws)
|
|
266
|
+
unregister_connection(ws)
|
|
267
|
+
ws&.close
|
|
252
268
|
end
|
|
253
269
|
|
|
254
270
|
def read_http_upgrade_request(socket)
|
|
255
|
-
request_line = socket
|
|
271
|
+
request_line = read_http_line(socket)
|
|
256
272
|
return [nil, {}] if request_line.nil?
|
|
257
273
|
return [nil, {}] unless request_line.include?(" ")
|
|
258
274
|
|
|
@@ -262,7 +278,7 @@ module Ruflet
|
|
|
262
278
|
|
|
263
279
|
headers = {}
|
|
264
280
|
loop do
|
|
265
|
-
line = socket
|
|
281
|
+
line = read_http_line(socket)
|
|
266
282
|
break if line.nil? || line == "\r\n"
|
|
267
283
|
|
|
268
284
|
key, value = line.split(":", 2)
|
|
@@ -272,10 +288,34 @@ module Ruflet
|
|
|
272
288
|
end
|
|
273
289
|
|
|
274
290
|
[path, headers]
|
|
291
|
+
ensure
|
|
292
|
+
@http_read_buffers&.delete(socket.object_id) if task_scheduler?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def read_http_line(socket)
|
|
296
|
+
return socket.gets("\r\n") unless task_scheduler? && socket.respond_to?(:recv_nonblock)
|
|
297
|
+
|
|
298
|
+
@http_read_buffers ||= {}
|
|
299
|
+
buffer = (@http_read_buffers[socket.object_id] ||= +"")
|
|
300
|
+
loop do
|
|
301
|
+
newline = buffer.index("\r\n")
|
|
302
|
+
return buffer.slice!(0, newline + 2) if newline
|
|
303
|
+
|
|
304
|
+
begin
|
|
305
|
+
part = socket.recv_nonblock(4096)
|
|
306
|
+
return nil if part.nil? || part.empty?
|
|
307
|
+
|
|
308
|
+
buffer << part
|
|
309
|
+
rescue StandardError => error
|
|
310
|
+
raise unless would_block_error?(error)
|
|
311
|
+
|
|
312
|
+
sleep TASK_IO_POLL_INTERVAL
|
|
313
|
+
end
|
|
314
|
+
end
|
|
275
315
|
end
|
|
276
316
|
|
|
277
317
|
def websocket_upgrade_request?(path, headers)
|
|
278
|
-
return false unless path
|
|
318
|
+
return false unless path == "/ws"
|
|
279
319
|
return false unless headers["upgrade"]&.downcase == "websocket"
|
|
280
320
|
return false unless headers["connection"]&.downcase&.include?("upgrade")
|
|
281
321
|
return false if headers["sec-websocket-key"].to_s.empty?
|
|
@@ -284,95 +324,21 @@ module Ruflet
|
|
|
284
324
|
end
|
|
285
325
|
|
|
286
326
|
def handle_http_request(socket, path)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
# In web mode the standalone backend also serves the Flutter web client,
|
|
291
|
-
# so the browser loads the app and opens its websocket on this same
|
|
292
|
-
# origin/port — no separate static server or proxy is needed.
|
|
293
|
-
return serve_web_client(socket, clean) if web_client_root
|
|
294
|
-
|
|
295
|
-
case clean
|
|
327
|
+
case path
|
|
328
|
+
when "/health"
|
|
329
|
+
write_http_response(socket, 200, "text/plain", "ok")
|
|
296
330
|
when "/"
|
|
297
331
|
write_http_response(socket, 200, "text/plain", "ruflet server")
|
|
298
332
|
else
|
|
299
|
-
if
|
|
300
|
-
serve_asset(socket,
|
|
333
|
+
if path.start_with?("/assets/")
|
|
334
|
+
serve_asset(socket, path)
|
|
301
335
|
else
|
|
302
336
|
write_http_response(socket, 404, "text/plain", "not found")
|
|
303
337
|
end
|
|
304
338
|
end
|
|
305
339
|
rescue StandardError => e
|
|
306
|
-
# The browser routinely cancels in-flight asset requests (preloads,
|
|
307
|
-
# duplicate connections); writing to a reset socket raises EPIPE/ECONNRESET
|
|
308
|
-
# and is expected, not an error.
|
|
309
|
-
return if disconnect_error?(e)
|
|
310
|
-
|
|
311
340
|
warn "http error: #{e.class}: #{e.message}"
|
|
312
|
-
|
|
313
|
-
write_http_response(socket, 500, "text/plain", "server error")
|
|
314
|
-
rescue StandardError
|
|
315
|
-
nil
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def web_client_root
|
|
320
|
-
dir = ENV["RUFLET_WEB_CLIENT_DIR"].to_s
|
|
321
|
-
return nil if dir.empty?
|
|
322
|
-
|
|
323
|
-
full = File.expand_path(dir)
|
|
324
|
-
File.directory?(full) ? full : nil
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
def serve_web_client(socket, path)
|
|
328
|
-
root = web_client_root
|
|
329
|
-
|
|
330
|
-
# Neutralize the Flutter service worker: when this dev server hops between
|
|
331
|
-
# localhost ports a cached worker would otherwise keep a stale client
|
|
332
|
-
# alive that reconnects to the wrong backend. This unregisters it and
|
|
333
|
-
# clears caches so the browser always loads the current client.
|
|
334
|
-
if path == "/flutter_service_worker.js"
|
|
335
|
-
return write_http_response(socket, 200, "text/javascript", service_worker_reset_js, cache: false)
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
relative = path == "/" ? "index.html" : path.sub(%r{\A/}, "")
|
|
339
|
-
full = File.expand_path(File.join(root, relative))
|
|
340
|
-
if (full == root || full.start_with?(root + File::SEPARATOR)) && File.file?(full)
|
|
341
|
-
return write_http_response(socket, 200, content_type_for(full), File.binread(full), binary: true, cache: false)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# App runtime assets (images referenced by the app) fall back to the
|
|
345
|
-
# configured assets directory when not part of the client bundle.
|
|
346
|
-
if path.start_with?("/assets/") && (asset = resolve_asset_path(path))
|
|
347
|
-
return write_http_response(socket, 200, content_type_for(asset), File.binread(asset), binary: true, cache: false)
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# SPA fallback: serve index.html for extension-less route paths.
|
|
351
|
-
index = File.join(root, "index.html")
|
|
352
|
-
if File.extname(path).empty? && File.file?(index)
|
|
353
|
-
return write_http_response(socket, 200, "text/html", File.binread(index), binary: true, cache: false)
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
write_http_response(socket, 404, "text/plain", "not found")
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# A no-op service worker: it registers cleanly (so Flutter's loader, which
|
|
360
|
-
# awaits navigator.serviceWorker.ready, never hangs), wipes any caches a
|
|
361
|
-
# previous run left behind, claims the page, and installs NO fetch handler —
|
|
362
|
-
# so every request (including the app shell) goes straight to the network and
|
|
363
|
-
# the client always loads fresh and connects to the current origin/port.
|
|
364
|
-
# (Self-unregistering here can leave serviceWorker.ready unresolved.)
|
|
365
|
-
def service_worker_reset_js
|
|
366
|
-
<<~JS
|
|
367
|
-
self.addEventListener('install', function (e) { self.skipWaiting(); });
|
|
368
|
-
self.addEventListener('activate', function (e) {
|
|
369
|
-
e.waitUntil((async function () {
|
|
370
|
-
var keys = await caches.keys();
|
|
371
|
-
await Promise.all(keys.map(function (k) { return caches.delete(k); }));
|
|
372
|
-
await self.clients.claim();
|
|
373
|
-
})());
|
|
374
|
-
});
|
|
375
|
-
JS
|
|
341
|
+
write_http_response(socket, 500, "text/plain", "server error")
|
|
376
342
|
end
|
|
377
343
|
|
|
378
344
|
def serve_asset(socket, path)
|
|
@@ -382,16 +348,23 @@ module Ruflet
|
|
|
382
348
|
return
|
|
383
349
|
end
|
|
384
350
|
|
|
385
|
-
content =
|
|
351
|
+
content = read_binary_file(asset_path)
|
|
386
352
|
write_http_response(socket, 200, content_type_for(asset_path), content, binary: true)
|
|
387
353
|
end
|
|
388
354
|
|
|
355
|
+
def read_binary_file(path)
|
|
356
|
+
File.open(path, "rb") { |file| file.read }
|
|
357
|
+
end
|
|
358
|
+
|
|
389
359
|
def resolve_asset_path(path)
|
|
390
360
|
root = assets_root
|
|
391
361
|
return nil unless root
|
|
392
362
|
|
|
393
363
|
root = File.expand_path(root)
|
|
394
|
-
|
|
364
|
+
prefix = "/assets/"
|
|
365
|
+
return nil unless path.start_with?(prefix)
|
|
366
|
+
|
|
367
|
+
relative = path[prefix.length..-1].to_s
|
|
395
368
|
full = File.expand_path(File.join(root, relative))
|
|
396
369
|
return nil unless full.start_with?(root + File::SEPARATOR) || full == root
|
|
397
370
|
return nil unless File.file?(full)
|
|
@@ -403,39 +376,28 @@ module Ruflet
|
|
|
403
376
|
root = ENV["RUFLET_ASSETS_DIR"].to_s
|
|
404
377
|
return root unless root.empty?
|
|
405
378
|
|
|
406
|
-
embedded_root = defined?($__ruflet_app_root) ? $__ruflet_app_root.to_s : ""
|
|
407
|
-
unless embedded_root.empty?
|
|
408
|
-
embedded_assets = File.join(embedded_root, "assets")
|
|
409
|
-
return embedded_assets if File.directory?(embedded_assets)
|
|
410
|
-
end
|
|
411
|
-
|
|
412
379
|
default_root = File.join(Dir.pwd, "assets")
|
|
413
380
|
File.directory?(default_root) ? default_root : nil
|
|
414
381
|
end
|
|
415
382
|
|
|
416
383
|
def content_type_for(path)
|
|
417
384
|
case File.extname(path).downcase
|
|
418
|
-
when ".
|
|
419
|
-
|
|
420
|
-
when ".
|
|
421
|
-
|
|
422
|
-
when ".
|
|
423
|
-
|
|
424
|
-
when ".
|
|
425
|
-
|
|
426
|
-
when ".
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
when ".otf" then "font/otf"
|
|
431
|
-
when ".woff" then "font/woff"
|
|
432
|
-
when ".woff2" then "font/woff2"
|
|
433
|
-
when ".txt" then "text/plain; charset=utf-8"
|
|
434
|
-
else "application/octet-stream"
|
|
385
|
+
when ".png"
|
|
386
|
+
"image/png"
|
|
387
|
+
when ".jpg", ".jpeg"
|
|
388
|
+
"image/jpeg"
|
|
389
|
+
when ".gif"
|
|
390
|
+
"image/gif"
|
|
391
|
+
when ".webp"
|
|
392
|
+
"image/webp"
|
|
393
|
+
when ".svg"
|
|
394
|
+
"image/svg+xml"
|
|
395
|
+
else
|
|
396
|
+
"application/octet-stream"
|
|
435
397
|
end
|
|
436
398
|
end
|
|
437
399
|
|
|
438
|
-
def write_http_response(socket, status, content_type, body, binary: false
|
|
400
|
+
def write_http_response(socket, status, content_type, body, binary: false)
|
|
439
401
|
reason = {
|
|
440
402
|
200 => "OK",
|
|
441
403
|
404 => "Not Found",
|
|
@@ -448,10 +410,6 @@ module Ruflet
|
|
|
448
410
|
socket.write("HTTP/1.1 #{status} #{reason}\r\n")
|
|
449
411
|
socket.write("Content-Type: #{content_type}\r\n")
|
|
450
412
|
socket.write("Content-Length: #{length}\r\n")
|
|
451
|
-
unless cache
|
|
452
|
-
socket.write("Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n")
|
|
453
|
-
socket.write("Pragma: no-cache\r\n")
|
|
454
|
-
end
|
|
455
413
|
socket.write("Connection: close\r\n")
|
|
456
414
|
socket.write("\r\n")
|
|
457
415
|
socket.write(body_str)
|
|
@@ -467,8 +425,15 @@ module Ruflet
|
|
|
467
425
|
socket.write("\r\n")
|
|
468
426
|
end
|
|
469
427
|
|
|
470
|
-
|
|
471
|
-
|
|
428
|
+
def remove_session(ws)
|
|
429
|
+
return unless ws
|
|
430
|
+
|
|
431
|
+
@sessions_mutex.synchronize do
|
|
432
|
+
@sessions.delete(ws.session_key)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def register_connection(ws)
|
|
472
437
|
return unless ws
|
|
473
438
|
|
|
474
439
|
@connections_mutex.synchronize do
|
|
@@ -476,7 +441,7 @@ module Ruflet
|
|
|
476
441
|
end
|
|
477
442
|
end
|
|
478
443
|
|
|
479
|
-
def
|
|
444
|
+
def unregister_connection(ws)
|
|
480
445
|
return unless ws
|
|
481
446
|
|
|
482
447
|
@connections_mutex.synchronize do
|
|
@@ -484,5 +449,197 @@ module Ruflet
|
|
|
484
449
|
end
|
|
485
450
|
end
|
|
486
451
|
|
|
452
|
+
def handle_message(ws, raw)
|
|
453
|
+
action, payload = decode_incoming(raw)
|
|
454
|
+
payload ||= {}
|
|
455
|
+
|
|
456
|
+
warn "incoming action=#{action.inspect}" if ENV["RUFLET_DEBUG"] == "1"
|
|
457
|
+
|
|
458
|
+
case action
|
|
459
|
+
when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
|
|
460
|
+
on_register_client(ws, payload)
|
|
461
|
+
when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
|
|
462
|
+
on_control_event(ws, payload)
|
|
463
|
+
when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
|
|
464
|
+
on_update_control(ws, payload)
|
|
465
|
+
when Protocol::ACTIONS[:invoke_control_method]
|
|
466
|
+
on_invoke_control_method(ws, payload)
|
|
467
|
+
when Protocol::ACTIONS[:python_output]
|
|
468
|
+
nil
|
|
469
|
+
else
|
|
470
|
+
raise "Unknown action: #{action.inspect}"
|
|
471
|
+
end
|
|
472
|
+
rescue StandardError => e
|
|
473
|
+
# A per-message handler error (e.g. an event callback that renders a
|
|
474
|
+
# control the runtime does not implement) must not tear down the whole
|
|
475
|
+
# WebSocket connection — that would disconnect the client on every
|
|
476
|
+
# navigation. Log it and keep the session alive.
|
|
477
|
+
warn "[embedded server] handle_message error: #{e.class}: #{e.message}"
|
|
478
|
+
warn e.backtrace.join("\n") if e.backtrace && ENV["RUFLET_DEBUG"] == "1"
|
|
479
|
+
report_runtime_error(e, "handle_message")
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def report_runtime_error(error, context)
|
|
483
|
+
path = ENV["RUFLET_RUNTIME_ERROR_FILE"].to_s
|
|
484
|
+
return if path.empty?
|
|
485
|
+
|
|
486
|
+
lines = ["#{context}: #{error.class}: #{error.message}"]
|
|
487
|
+
lines.concat(error.backtrace) if error.backtrace
|
|
488
|
+
File.open(path, "w") { |file| file.write(lines.join("\n")) }
|
|
489
|
+
rescue StandardError => report_error
|
|
490
|
+
warn "runtime error reporting failed: #{report_error.class}: #{report_error.message}"
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def decode_incoming(raw)
|
|
494
|
+
parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
|
|
495
|
+
|
|
496
|
+
if parsed.is_a?(Array) && parsed.length >= 2
|
|
497
|
+
return [parsed[0], parsed[1]]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
if parsed.is_a?(Hash)
|
|
501
|
+
action = parsed["action"] || parsed[:action]
|
|
502
|
+
payload = parsed["payload"] || parsed[:payload]
|
|
503
|
+
return [action, payload] unless action.nil?
|
|
504
|
+
|
|
505
|
+
if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
|
|
506
|
+
return [Protocol::ACTIONS[:control_event], parsed]
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
raise "Unsupported payload format"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def normalize_incoming(value)
|
|
514
|
+
case value
|
|
515
|
+
when String
|
|
516
|
+
value.dup.force_encoding("UTF-8")
|
|
517
|
+
when Integer, Float, TrueClass, FalseClass, NilClass
|
|
518
|
+
value
|
|
519
|
+
when Symbol
|
|
520
|
+
value.to_s
|
|
521
|
+
when Array
|
|
522
|
+
value.map { |v| normalize_incoming(v) }
|
|
523
|
+
when Hash
|
|
524
|
+
value.each_with_object({}) do |(k, v), out|
|
|
525
|
+
out[k.to_s] = normalize_incoming(v)
|
|
526
|
+
end
|
|
527
|
+
else
|
|
528
|
+
value.to_s
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def on_register_client(ws, payload)
|
|
533
|
+
normalized = Protocol.normalize_register_payload(payload)
|
|
534
|
+
session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
|
|
535
|
+
|
|
536
|
+
page = Page.new(
|
|
537
|
+
session_id: session_id,
|
|
538
|
+
client_details: normalized,
|
|
539
|
+
sender: lambda do |action, msg_payload|
|
|
540
|
+
send_message(ws, action, msg_payload)
|
|
541
|
+
end
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
page.title = "Ruflet App"
|
|
545
|
+
|
|
546
|
+
@sessions_mutex.synchronize do
|
|
547
|
+
@sessions[ws.session_key] = page
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
initial_response = [
|
|
551
|
+
Protocol::ACTIONS[:register_client],
|
|
552
|
+
Protocol.register_response(session_id: session_id)
|
|
553
|
+
]
|
|
554
|
+
ws.send_binary(Ruflet::WireCodec.pack(initial_response))
|
|
555
|
+
|
|
556
|
+
@app_block.call(page)
|
|
557
|
+
page.update
|
|
558
|
+
rescue StandardError => e
|
|
559
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
|
|
560
|
+
raise e
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def on_invoke_control_method(ws, payload)
|
|
564
|
+
page = fetch_page(ws)
|
|
565
|
+
page.handle_invoke_method_result(payload)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def on_control_event(ws, payload)
|
|
569
|
+
event = Protocol.normalize_control_event_payload(payload)
|
|
570
|
+
page = fetch_page(ws)
|
|
571
|
+
return if event["target"].nil? || event["name"].to_s.empty?
|
|
572
|
+
|
|
573
|
+
page.dispatch_event(
|
|
574
|
+
target: event["target"],
|
|
575
|
+
name: event["name"],
|
|
576
|
+
data: normalize_event_data(event["data"])
|
|
577
|
+
)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def on_update_control(ws, payload)
|
|
581
|
+
update = Protocol.normalize_update_control_payload(payload)
|
|
582
|
+
page = fetch_page(ws)
|
|
583
|
+
return if update["id"].nil?
|
|
584
|
+
|
|
585
|
+
page.apply_client_update(update["id"], update["props"] || {})
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def fetch_page(ws)
|
|
589
|
+
page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
|
|
590
|
+
raise "Session not found" unless page
|
|
591
|
+
|
|
592
|
+
page
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def normalize_event_data(value)
|
|
596
|
+
case value
|
|
597
|
+
when Hash
|
|
598
|
+
value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
|
|
599
|
+
when Array
|
|
600
|
+
value.map { |entry| normalize_event_data(entry) }
|
|
601
|
+
else
|
|
602
|
+
value
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def send_message(ws, action, payload)
|
|
607
|
+
return if ws.nil? || ws.closed?
|
|
608
|
+
|
|
609
|
+
message = [action, payload]
|
|
610
|
+
packed = Ruflet::WireCodec.pack(message)
|
|
611
|
+
ws.send_binary(packed)
|
|
612
|
+
rescue StandardError => e
|
|
613
|
+
unless disconnect_error?(e)
|
|
614
|
+
warn "send error: #{e.class}: #{e.message}"
|
|
615
|
+
end
|
|
616
|
+
remove_session(ws)
|
|
617
|
+
unregister_connection(ws)
|
|
618
|
+
ws&.close
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def disconnect_error?(error)
|
|
622
|
+
return true if error.is_a?(IOError)
|
|
623
|
+
return true if error.is_a?(Errno::EPIPE)
|
|
624
|
+
return true if error.is_a?(Errno::ECONNRESET)
|
|
625
|
+
return true if error.is_a?(Errno::ECONNABORTED)
|
|
626
|
+
return true if error.is_a?(Errno::ENOTCONN)
|
|
627
|
+
return true if error.is_a?(Errno::ESHUTDOWN)
|
|
628
|
+
return true if error.is_a?(Errno::EBADF)
|
|
629
|
+
return true if error.is_a?(Errno::EINVAL)
|
|
630
|
+
|
|
631
|
+
false
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def pseudo_uuid
|
|
635
|
+
rnd = (rand(0..0xffff) << 16) | rand(0..0xffff)
|
|
636
|
+
"%08x-%04x-%04x-%04x-%012x" % [
|
|
637
|
+
rnd,
|
|
638
|
+
rand(0..0xffff),
|
|
639
|
+
rand(0..0xffff),
|
|
640
|
+
rand(0..0xffff),
|
|
641
|
+
(rand(0..0xffff) << 32) | (rand(0..0xffff) << 16) | rand(0..0xffff)
|
|
642
|
+
]
|
|
643
|
+
end
|
|
487
644
|
end
|
|
488
645
|
end
|
data/lib/ruflet/version.rb
CHANGED
data/lib/ruflet_server.rb
CHANGED
|
@@ -2,39 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
require "ruflet_core"
|
|
4
4
|
require_relative "ruflet/server"
|
|
5
|
-
|
|
6
|
-
module Ruflet
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def run(entrypoint = nil, host: "0.0.0.0", port: nil, &block)
|
|
10
|
-
port = normalize_run_port(port || ENV["RUFLET_PORT"] || 8550)
|
|
11
|
-
callback = entrypoint || block
|
|
12
|
-
raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
|
|
13
|
-
|
|
14
|
-
interceptor = run_interceptor
|
|
15
|
-
if interceptor
|
|
16
|
-
result = interceptor.call(entrypoint: callback, host: host, port: port)
|
|
17
|
-
return result unless result == :pass
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
Server.new(host: host, port: port) do |page|
|
|
21
|
-
callback.call(page)
|
|
22
|
-
end.start
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def run_interceptor
|
|
26
|
-
return nil unless instance_variable_defined?(:@run_interceptors_mutex)
|
|
27
|
-
return nil unless instance_variable_defined?(:@run_interceptors)
|
|
28
|
-
|
|
29
|
-
@run_interceptors_mutex.synchronize { @run_interceptors.last }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def normalize_run_port(value)
|
|
33
|
-
Integer(value)
|
|
34
|
-
rescue ArgumentError, TypeError
|
|
35
|
-
8550
|
|
36
|
-
end
|
|
37
|
-
class << self
|
|
38
|
-
private :run_interceptor, :normalize_run_port
|
|
39
|
-
end
|
|
40
|
-
end
|
|
5
|
+
require_relative "ruflet/server/connection_protocol"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_server
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.19
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.19
|
|
26
26
|
description: Ruflet WebSocket server runtime compatible with Flet protocol.
|
|
27
27
|
email:
|
|
28
28
|
- adammusa2222@gmail.com
|