ruflet_server 0.0.20 → 0.0.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8ce7c65f8922fb96c632f08c3fbbfbe5ab4d2f51ef010aca2938dada94bbb06
4
- data.tar.gz: '09a16364d80b69210248753a9eeb062a8d895be2cfa02844bd7850a96c4392d4'
3
+ metadata.gz: cd0a28621f5068581ced0ccb30b88d298b3e3fbcccb9747d89835cc301f00117
4
+ data.tar.gz: '028d69d0d2186e6cbbe2e30fab7b87a9403ed73d0261e4129f14a4458ebf1018'
5
5
  SHA512:
6
- metadata.gz: 0efb74cd09d74eb058e0cfbdaa3027483fb809552e15109070a80e3bad364f9d9d297173249033df16029c2f6c366ee24db3e40848d1adf7fa583927b4fe7a83
7
- data.tar.gz: 908da326003ca5cfff54b1a608148afaf0f557c8d9871c602e8234abe6710b6e79c27f0f0de0b9bcda78368ec67735d62ba31b41f84af9f6b551791bde15a889
6
+ metadata.gz: afed8a7dfd432ca20e3303766599a5e960ec44a8037d9345c9fdd8c03d7f70dd74e801809f42e39958c9330e6ac3c83b95bbd252992017bac78c8984ee5e13e1
7
+ data.tar.gz: 73e7bbcd814572b1b395042ca164dd219a2d273b0998f73c2e43f54c901a4acfc372c25895ad312280a370604782b652b6e3d508d67f22d472cf5a421bd094d4
@@ -96,6 +96,11 @@ module Ruflet
96
96
  on_update_control(ws, payload)
97
97
  when Protocol::ACTIONS[:invoke_control_method]
98
98
  on_invoke_control_method(ws, payload)
99
+ when Protocol::ACTIONS[:python_output]
100
+ # Flet clients can emit this legacy diagnostic action. It carries no
101
+ # server-side state, but it is a valid wire message and must not be
102
+ # treated as a protocol error or tear down host-adapter connections.
103
+ nil
99
104
  else
100
105
  raise "Unknown action: #{action.inspect}"
101
106
  end
@@ -149,7 +154,7 @@ module Ruflet
149
154
 
150
155
  if page
151
156
  attach_sender(page, ws)
152
- reset_mount_state(page)
157
+ page.prepare_for_reconnect!
153
158
  else
154
159
  page = Page.new(
155
160
  session_id: session_id,
@@ -251,12 +256,6 @@ module Ruflet
251
256
  page.instance_variable_set(:@sender, sender_for(ws))
252
257
  end
253
258
 
254
- def reset_mount_state(page)
255
- page.instance_variable_set(:@overlay_container_mounted, false)
256
- page.instance_variable_set(:@dialogs_container_mounted, false)
257
- page.instance_variable_set(:@services_container_mounted, false)
258
- end
259
-
260
259
  def disconnect_error?(error)
261
260
  return true if error.is_a?(IOError)
262
261
  return true if error.is_a?(Errno::EPIPE)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ # Presents the embedded runtime bridge through the same connection contract
5
+ # used by the WebSocket transport. The server protocol therefore remains
6
+ # transport-agnostic: only the bytes' path changes.
7
+ class InProcessConnection
8
+ MAX_MESSAGE_BYTES = 16 * 1024 * 1024
9
+ TASK_PUMP_INTERVAL = 0.001
10
+ REQUIRED_BRIDGE_METHODS = %i[
11
+ __bridge_read_nonblock __bridge_write __bridge_close
12
+ ].freeze
13
+
14
+ def initialize(bridge: nil)
15
+ @bridge = bridge || default_bridge
16
+ @closed = false
17
+ validate_bridge!
18
+ end
19
+
20
+ def session_key
21
+ @session_key ||= object_id
22
+ end
23
+
24
+ def closed?
25
+ @closed
26
+ end
27
+
28
+ def read_message
29
+ return nil if closed?
30
+
31
+ payload = wait_for_message
32
+ if payload.nil?
33
+ @closed = true
34
+ return nil
35
+ end
36
+
37
+ checked_bytes(payload)
38
+ end
39
+
40
+ def send_binary(payload)
41
+ raise IOError, "Ruflet in-process connection is closed" if closed?
42
+
43
+ @bridge.__bridge_write(checked_bytes(payload))
44
+ end
45
+
46
+ def send_text(payload)
47
+ send_binary(payload)
48
+ end
49
+
50
+ def close
51
+ return if closed?
52
+
53
+ @closed = true
54
+ @bridge.__bridge_close
55
+ end
56
+
57
+ private
58
+
59
+ def default_bridge
60
+ Object.const_get(:RubyRuntime)
61
+ rescue NameError
62
+ raise RuntimeError, "Ruflet in-process transport requires the embedded RubyRuntime bridge"
63
+ end
64
+
65
+ def validate_bridge!
66
+ missing = REQUIRED_BRIDGE_METHODS.reject { |method_name| @bridge.respond_to?(method_name) }
67
+ return if missing.empty?
68
+
69
+ raise RuntimeError, "Ruflet in-process bridge is missing: #{missing.join(', ')}"
70
+ end
71
+
72
+ def wait_for_message
73
+ loop do
74
+ payload = @bridge.__bridge_read_nonblock
75
+ return payload unless payload == false
76
+
77
+ Thread.pass if Thread.respond_to?(:cooperative?) && Thread.cooperative?
78
+ sleep TASK_PUMP_INTERVAL
79
+ end
80
+ end
81
+
82
+ def checked_bytes(payload)
83
+ bytes = payload.to_s.b
84
+ if bytes.bytesize > MAX_MESSAGE_BYTES
85
+ raise ArgumentError, "Ruflet in-process message exceeds #{MAX_MESSAGE_BYTES} bytes"
86
+ end
87
+
88
+ bytes
89
+ end
90
+ end
91
+ end
@@ -22,7 +22,9 @@ module Ruflet
22
22
  end
23
23
 
24
24
  def send_binary(payload)
25
- send_frame(0x2, payload.to_s.b)
25
+ bytes = payload.to_s.b
26
+ Ruflet::WireCodec.trace("ruby->client conn=#{session_key}", bytes)
27
+ send_frame(0x2, bytes)
26
28
  end
27
29
 
28
30
  def send_text(payload)
@@ -46,7 +48,7 @@ module Ruflet
46
48
  when 0xA
47
49
  read_message
48
50
  when 0x1, 0x2
49
- return payload if frame[:fin]
51
+ return traced_incoming(payload) if frame[:fin]
50
52
 
51
53
  message = payload.dup
52
54
  loop do
@@ -61,7 +63,7 @@ module Ruflet
61
63
  next
62
64
  when 0x0
63
65
  message << continuation[:payload]
64
- return message if continuation[:fin]
66
+ return traced_incoming(message) if continuation[:fin]
65
67
  return nil if message.bytesize > MAX_FRAME_PAYLOAD_BYTES
66
68
  else
67
69
  return nil
@@ -84,6 +86,11 @@ module Ruflet
84
86
 
85
87
  private
86
88
 
89
+ def traced_incoming(payload)
90
+ Ruflet::WireCodec.trace("client->ruby conn=#{session_key}", payload)
91
+ payload
92
+ end
93
+
87
94
  def read_frame
88
95
  header = read_exact(2)
89
96
  return nil if header.nil?
@@ -3,6 +3,30 @@
3
3
  module Ruflet
4
4
  class WireCodec
5
5
  class << self
6
+ # Transport tracing is opt-in. Even compact summaries decode every frame
7
+ # and write to stdout, so enabling them by default would distort the
8
+ # renderer performance they are intended to diagnose.
9
+ #
10
+ # RUFLET_PROTOCOL_TRACE=summary compact frame metadata
11
+ # RUFLET_PROTOCOL_TRACE=1 exact bytes and decoded value
12
+ # RUFLET_PROTOCOL_TRACE=0 disabled (default)
13
+ def trace(direction, bytes)
14
+ mode = ENV.fetch("RUFLET_PROTOCOL_TRACE", "0")
15
+ return if mode == "0"
16
+
17
+ raw = bytes.to_s.b
18
+ decoded = unpack(raw)
19
+ action = decoded.is_a?(Array) ? decoded[0] : nil
20
+ prefix = "[RUFLET_PROTOCOL] t=#{format("%.6f", Process.clock_gettime(Process::CLOCK_MONOTONIC))} #{direction} bytes=#{raw.bytesize} action=#{action.inspect}"
21
+ if mode == "1" || mode == "full"
22
+ puts "#{prefix} hex=#{raw.unpack("H*").first} data=#{decoded.inspect}"
23
+ else
24
+ puts "#{prefix} #{trace_summary(action, decoded.is_a?(Array) ? decoded[1] : nil)}".rstrip
25
+ end
26
+ rescue StandardError => error
27
+ puts "[RUFLET_PROTOCOL] t=#{format("%.6f", Process.clock_gettime(Process::CLOCK_MONOTONIC))} #{direction} bytes=#{raw&.bytesize || 0} decode_error=#{error.class}: #{error.message}"
28
+ end
29
+
6
30
  def pack(value)
7
31
  case value
8
32
  when Ruflet::Protocol::DateTimeValue
@@ -41,6 +65,27 @@ module Ruflet
41
65
 
42
66
  private
43
67
 
68
+ def trace_summary(action, payload)
69
+ return "payload=#{payload.class}" unless payload.is_a?(Hash)
70
+
71
+ case action
72
+ when 1
73
+ "session_id=#{payload["session_id"].inspect} page_name=#{payload["page_name"].inspect}"
74
+ when 2
75
+ patch = payload["patch"]
76
+ "target=#{payload["id"].inspect} patch_items=#{patch.is_a?(Array) ? patch.length : 0}"
77
+ when 3
78
+ "target=#{payload["target"].inspect} name=#{payload["name"].inspect} data_type=#{payload["data"].class}"
79
+ when 4
80
+ properties = payload["props"]
81
+ "target=#{payload["id"].inspect} properties=#{properties.is_a?(Hash) ? properties.keys.sort.join(",") : ""}"
82
+ when 5
83
+ "target=#{payload["control_id"].inspect} name=#{payload["name"].inspect}"
84
+ else
85
+ "keys=#{payload.keys.map(&:to_s).sort.join(",")}"
86
+ end
87
+ end
88
+
44
89
  def pack_extension(type, value)
45
90
  bytes = value.to_s.b
46
91
  length = bytes.bytesize
data/lib/ruflet/server.rb CHANGED
@@ -5,6 +5,7 @@ require "socket"
5
5
  require "thread"
6
6
 
7
7
  require "ruflet_core"
8
+ require_relative "server/in_process_connection"
8
9
  require_relative "server/wire_codec"
9
10
  require_relative "server/web_socket_connection"
10
11
 
@@ -15,9 +16,10 @@ module Ruflet
15
16
  WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
16
17
  TASK_IO_POLL_INTERVAL = 0.01
17
18
 
18
- def initialize(host: "0.0.0.0", port: 8550, &app_block)
19
+ def initialize(host: "0.0.0.0", port: 8550, in_process_bridge: nil, &app_block)
19
20
  @host = host
20
21
  @port = port
22
+ @in_process_bridge = in_process_bridge
21
23
  @app_block = app_block
22
24
  @sessions = {}
23
25
  @sessions_mutex = Mutex.new
@@ -44,10 +46,14 @@ module Ruflet
44
46
  end
45
47
 
46
48
  previous_signals = trap_stop_signals
47
- bind_server_socket!
48
- @running = true
49
- print_server_banner
50
- accept_loop
49
+ if in_process_transport_requested?
50
+ start_in_process_transport
51
+ else
52
+ bind_server_socket!
53
+ @running = true
54
+ print_server_banner
55
+ accept_loop
56
+ end
51
57
  rescue Interrupt
52
58
  nil
53
59
  ensure
@@ -175,6 +181,16 @@ module Ruflet
175
181
 
176
182
  private
177
183
 
184
+ def in_process_transport_requested?
185
+ ENV["RUFLET_RUNTIME_TRANSPORT"] == "in_process"
186
+ end
187
+
188
+ def start_in_process_transport
189
+ connection = Ruflet::InProcessConnection.new(bridge: @in_process_bridge)
190
+ @running = true
191
+ run_connection(connection)
192
+ end
193
+
178
194
  def trap_stop_signals
179
195
  {
180
196
  "INT" => trap_signal("INT"),
@@ -380,7 +396,12 @@ module Ruflet
380
396
  # opens its websocket on one origin. Without that the client cannot reach
381
397
  # /ws at all.
382
398
  def web_client_root
383
- return @web_client_root if defined?(@web_client_root)
399
+ # Memoized with a separate flag rather than defined?(@web_client_root):
400
+ # the embedded mruby VM has no defined? keyword, so it parses as a method
401
+ # call and every request raises NoMethodError. The resolved value is
402
+ # legitimately nil when no web client is configured, so nil cannot double
403
+ # as "not resolved yet".
404
+ return @web_client_root if @web_client_root_resolved
384
405
 
385
406
  configured = ENV["RUFLET_WEB_CLIENT_DIR"].to_s.strip
386
407
  @web_client_root =
@@ -389,6 +410,8 @@ module Ruflet
389
410
  else
390
411
  File.expand_path(configured)
391
412
  end
413
+ @web_client_root_resolved = true
414
+ @web_client_root
392
415
  end
393
416
 
394
417
  def serve_web_client(socket, request_path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.20" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.22" unless const_defined?(:VERSION)
5
5
  end
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.20
4
+ version: 0.0.22
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.20
18
+ version: 0.0.22
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.20
25
+ version: 0.0.22
26
26
  description: Ruflet WebSocket server runtime compatible with Flet protocol.
27
27
  email:
28
28
  - adammusa2222@gmail.com
@@ -33,6 +33,7 @@ files:
33
33
  - README.md
34
34
  - lib/ruflet/server.rb
35
35
  - lib/ruflet/server/connection_protocol.rb
36
+ - lib/ruflet/server/in_process_connection.rb
36
37
  - lib/ruflet/server/web_socket_connection.rb
37
38
  - lib/ruflet/server/wire_codec.rb
38
39
  - lib/ruflet/version.rb