omq 0.7.0 → 0.9.0

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.
@@ -1,459 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- class BaseRunner
6
- attr_reader :config, :sock
7
-
8
-
9
- def initialize(config, socket_class)
10
- @config = config
11
- @klass = socket_class
12
- @fmt = Formatter.new(config.format, compress: config.compress)
13
- end
14
-
15
-
16
- def call(task)
17
- @sock = create_socket
18
- attach_endpoints
19
- setup_curve
20
- setup_subscriptions
21
- compile_expr
22
-
23
- if config.transient
24
- start_disconnect_monitor(task)
25
- Async::Task.current.yield # let monitor start waiting
26
- end
27
-
28
- sleep(config.delay) if config.delay && config.recv_only?
29
- wait_for_peer if needs_peer_wait?
30
-
31
- @sock.instance_exec(&@send_begin_proc) if @send_begin_proc
32
- @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
33
- run_loop(task)
34
- @sock.instance_exec(&@send_end_proc) if @send_end_proc
35
- @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
36
- ensure
37
- @sock&.close
38
- end
39
-
40
-
41
- private
42
-
43
-
44
- # Subclasses override this.
45
- def run_loop(task)
46
- raise NotImplementedError
47
- end
48
-
49
- # ── Socket creation ─────────────────────────────────────────────
50
-
51
-
52
- def create_socket
53
- sock_opts = { linger: config.linger }
54
- sock_opts[:conflate] = true if config.conflate && %w[pub radio].include?(config.type_name)
55
- sock = @klass.new(**sock_opts)
56
- sock.recv_timeout = config.timeout if config.timeout
57
- sock.send_timeout = config.timeout if config.timeout
58
- sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
59
- sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
60
- sock.identity = config.identity if config.identity
61
- sock.router_mandatory = true if config.type_name == "router"
62
- sock
63
- end
64
-
65
-
66
- def attach_endpoints
67
- config.binds.each do |url|
68
- @sock.bind(url)
69
- log "Bound to #{@sock.last_endpoint}"
70
- end
71
- config.connects.each do |url|
72
- @sock.connect(url)
73
- log "Connecting to #{url}"
74
- end
75
- end
76
-
77
- # ── Peer wait with grace period ─────────────────────────────────
78
-
79
-
80
- def needs_peer_wait?
81
- !config.recv_only? && (config.connects.any? || config.type_name == "router")
82
- end
83
-
84
-
85
- def wait_for_peer
86
- with_timeout(config.timeout) do
87
- @sock.peer_connected.wait
88
- log "Peer connected"
89
- if %w[pub xpub].include?(config.type_name)
90
- @sock.subscriber_joined.wait
91
- log "Subscriber joined"
92
- end
93
-
94
- # Grace period: when multiple peers may be connecting (bind or
95
- # multiple connect URLs), wait one reconnect interval so
96
- # latecomers finish their handshake before we start sending.
97
- if config.binds.any? || config.connects.size > 1
98
- ri = @sock.options.reconnect_interval
99
- sleep(ri.is_a?(Range) ? ri.begin : ri)
100
- end
101
- end
102
- end
103
-
104
- # ── Transient disconnect monitor ────────────────────────────────
105
-
106
-
107
- def start_disconnect_monitor(task)
108
- @transient_barrier = Async::Promise.new
109
- task.async do
110
- @transient_barrier.wait
111
- @sock.all_peers_gone.wait unless @sock.connection_count == 0
112
- log "All peers disconnected, exiting"
113
- @sock.reconnect_enabled = false
114
- if config.send_only?
115
- task.stop
116
- else
117
- @sock.close_read
118
- end
119
- end
120
- end
121
-
122
-
123
- def transient_ready!
124
- if config.transient && !@transient_barrier.resolved?
125
- @transient_barrier.resolve(true)
126
- end
127
- end
128
-
129
- # ── Timeout helper ──────────────────────────────────────────────
130
-
131
-
132
- def with_timeout(seconds)
133
- if seconds
134
- Async::Task.current.with_timeout(seconds) { yield }
135
- else
136
- yield
137
- end
138
- end
139
-
140
- # ── Socket setup ────────────────────────────────────────────────
141
-
142
-
143
- def setup_subscriptions
144
- case config.type_name
145
- when "sub"
146
- prefixes = config.subscribes.empty? ? [""] : config.subscribes
147
- prefixes.each { |p| @sock.subscribe(p) }
148
- when "dish"
149
- config.joins.each { |g| @sock.join(g) }
150
- end
151
- end
152
-
153
-
154
- def setup_curve
155
- server_key_z85 = config.curve_server_key || ENV["OMQ_SERVER_KEY"]
156
- server_mode = config.curve_server || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
157
-
158
- if server_key_z85
159
- if ENV["OMQ_DEV"]
160
- require_relative "../../../../omq-curve/lib/omq/curve"
161
- else
162
- require "omq/curve"
163
- end
164
- server_key = OMQ::Z85.decode(server_key_z85)
165
- client_key = RbNaCl::PrivateKey.generate
166
- @sock.mechanism = OMQ::Curve.client(
167
- client_key.public_key.to_s, client_key.to_s, server_key: server_key
168
- )
169
- elsif server_mode
170
- if ENV["OMQ_DEV"]
171
- require_relative "../../../../omq-curve/lib/omq/curve"
172
- else
173
- require "omq/curve"
174
- end
175
- if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
176
- server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
177
- server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
178
- else
179
- key = RbNaCl::PrivateKey.generate
180
- server_pub = key.public_key.to_s
181
- server_sec = key.to_s
182
- end
183
- @sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
184
- $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
185
- end
186
- rescue LoadError
187
- abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
188
- end
189
-
190
- # ── Shared loop bodies ──────────────────────────────────────────
191
-
192
-
193
- def run_send_logic
194
- n = config.count
195
- i = 0
196
- sleep(config.delay) if config.delay
197
- if config.interval
198
- i += send_tick
199
- unless @send_tick_eof || (n && n > 0 && i >= n)
200
- Async::Loop.quantized(interval: config.interval) do
201
- i += send_tick
202
- break if @send_tick_eof || (n && n > 0 && i >= n)
203
- end
204
- end
205
- elsif config.data || config.file
206
- parts = eval_send_expr(read_next)
207
- send_msg(parts) if parts
208
- elsif stdin_ready?
209
- loop do
210
- parts = read_next
211
- break unless parts
212
- parts = eval_send_expr(parts)
213
- send_msg(parts) if parts
214
- i += 1
215
- break if n && n > 0 && i >= n
216
- end
217
- elsif @send_eval_proc
218
- parts = eval_send_expr(nil)
219
- send_msg(parts) if parts
220
- end
221
- end
222
-
223
-
224
- def send_tick
225
- raw = read_next_or_nil
226
- if raw.nil? && !@send_eval_proc
227
- @send_tick_eof = true
228
- return 0
229
- end
230
- parts = eval_send_expr(raw)
231
- send_msg(parts) if parts
232
- 1
233
- end
234
-
235
-
236
- def run_recv_logic
237
- n = config.count
238
- i = 0
239
- loop do
240
- parts = recv_msg
241
- break if parts.nil?
242
- parts = eval_recv_expr(parts)
243
- output(parts)
244
- i += 1
245
- break if n && n > 0 && i >= n
246
- end
247
- end
248
-
249
-
250
- def wait_for_loops(receiver, sender)
251
- if config.data || config.file || config.send_expr || config.recv_expr || config.target
252
- sender.wait
253
- receiver.stop
254
- elsif config.count && config.count > 0
255
- receiver.wait
256
- sender.stop
257
- else
258
- sender.wait
259
- receiver.stop
260
- end
261
- end
262
-
263
- # ── Message I/O ─────────────────────────────────────────────────
264
-
265
-
266
- def send_msg(parts)
267
- return if parts.empty?
268
- parts = [Marshal.dump(parts)] if config.format == :marshal
269
- parts = @fmt.compress(parts)
270
- @sock.send(parts)
271
- transient_ready!
272
- end
273
-
274
-
275
- def recv_msg
276
- raw = @sock.receive
277
- return nil if raw.nil?
278
- parts = @fmt.decompress(raw)
279
- parts = Marshal.load(parts.first) if config.format == :marshal
280
- transient_ready!
281
- parts
282
- end
283
-
284
-
285
- def recv_msg_raw
286
- msg = @sock.receive
287
- msg&.dup
288
- end
289
-
290
-
291
- def read_next
292
- if config.data
293
- @fmt.decode(config.data + "\n")
294
- elsif config.file
295
- @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
296
- @fmt.decode(@file_data + "\n")
297
- elsif config.format == :msgpack
298
- @fmt.decode_msgpack($stdin)
299
- elsif config.format == :marshal
300
- @fmt.decode_marshal($stdin)
301
- elsif config.format == :raw
302
- data = $stdin.read
303
- return nil if data.nil? || data.empty?
304
- [data]
305
- else
306
- line = $stdin.gets
307
- return nil if line.nil?
308
- @fmt.decode(line)
309
- end
310
- end
311
-
312
-
313
- def stdin_ready?
314
- return @stdin_ready unless @stdin_ready.nil?
315
-
316
- @stdin_ready = !$stdin.closed? &&
317
- !config.stdin_is_tty &&
318
- IO.select([$stdin], nil, nil, 0.01) &&
319
- !$stdin.eof?
320
- end
321
-
322
-
323
- def read_next_or_nil
324
- if config.data || config.file
325
- read_next
326
- elsif @send_eval_proc
327
- nil
328
- else
329
- read_next
330
- end
331
- end
332
-
333
-
334
- def output(parts)
335
- return if config.quiet || parts.nil?
336
- $stdout.write(@fmt.encode(parts))
337
- $stdout.flush
338
- end
339
-
340
- # ── Routing helpers ─────────────────────────────────────────────
341
-
342
-
343
- def display_routing_id(id)
344
- if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
345
- id
346
- else
347
- "0x#{id.unpack1("H*")}"
348
- end
349
- end
350
-
351
-
352
- def resolve_target(target)
353
- if target.start_with?("0x")
354
- [target[2..].delete(" ")].pack("H*")
355
- else
356
- target
357
- end
358
- end
359
-
360
- # ── Eval ────────────────────────────────────────────────────────
361
-
362
-
363
- def compile_expr
364
- compile_one_expr(:send, config.send_expr)
365
- compile_one_expr(:recv, config.recv_expr)
366
- @send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
367
- @recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
368
- end
369
-
370
-
371
- def wrap_registered_proc(block)
372
- return unless block
373
- proc do |msg|
374
- $_ = msg&.first
375
- block.call(msg)
376
- end
377
- end
378
-
379
-
380
- def compile_one_expr(direction, src)
381
- return unless src
382
- expr, begin_body, end_body = extract_blocks(src)
383
- instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
384
- instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
385
- if expr && !expr.strip.empty?
386
- instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
387
- end
388
- end
389
-
390
-
391
- def extract_blocks(expr)
392
- begin_body = end_body = nil
393
- expr, begin_body = extract_block(expr, "BEGIN")
394
- expr, end_body = extract_block(expr, "END")
395
- [expr, begin_body, end_body]
396
- end
397
-
398
-
399
- def extract_block(expr, keyword)
400
- start = expr.index(/#{keyword}\s*\{/)
401
- return [expr, nil] unless start
402
-
403
- # Find the opening brace
404
- i = expr.index("{", start)
405
- depth = 1
406
- j = i + 1
407
- while j < expr.length && depth > 0
408
- case expr[j]
409
- when "{" then depth += 1
410
- when "}" then depth -= 1
411
- end
412
- j += 1
413
- end
414
-
415
- body = expr[(i + 1)..(j - 2)]
416
- trimmed = expr[0...start] + expr[j..]
417
- [trimmed, body]
418
- end
419
-
420
-
421
- SENT = Object.new.freeze # sentinel: eval already sent the reply
422
-
423
- def eval_send_expr(parts)
424
- return parts unless @send_eval_proc
425
- run_eval(@send_eval_proc, parts)
426
- end
427
-
428
-
429
- def eval_recv_expr(parts)
430
- return parts unless @recv_eval_proc
431
- run_eval(@recv_eval_proc, parts)
432
- end
433
-
434
-
435
- def run_eval(eval_proc, parts)
436
- $F = parts
437
- result = @sock.instance_exec(parts, &eval_proc)
438
- return nil if result.nil?
439
- return SENT if result.equal?(@sock)
440
- return [result] if config.format == :marshal
441
- case result
442
- when Array then result
443
- when String then [result]
444
- else [result.to_str]
445
- end
446
- rescue => e
447
- $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
448
- exit 3
449
- end
450
-
451
- # ── Logging ─────────────────────────────────────────────────────
452
-
453
-
454
- def log(msg)
455
- $stderr.puts(msg) if config.verbose
456
- end
457
- end
458
- end
459
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- class ChannelRunner < PairRunner
6
- end
7
- end
8
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- class ClientRunner < ReqRunner
6
- end
7
-
8
-
9
- class ServerRunner < BaseRunner
10
- private
11
-
12
-
13
- def run_loop(task)
14
- if config.echo || config.recv_expr || @recv_eval_proc || config.data || config.file || !config.stdin_is_tty
15
- reply_loop
16
- else
17
- monitor_loop(task)
18
- end
19
- end
20
-
21
-
22
- def reply_loop
23
- n = config.count
24
- i = 0
25
- loop do
26
- parts = recv_msg_raw
27
- break if parts.nil?
28
- routing_id = parts.shift
29
- body = @fmt.decompress(parts)
30
-
31
- if config.recv_expr || @recv_eval_proc
32
- reply = eval_recv_expr(body)
33
- output([display_routing_id(routing_id), *(reply || [""])])
34
- @sock.send_to(routing_id, @fmt.compress(reply || [""]).first)
35
- elsif config.echo
36
- output([display_routing_id(routing_id), *body])
37
- @sock.send_to(routing_id, @fmt.compress(body).first || "")
38
- elsif config.data || config.file || !config.stdin_is_tty
39
- reply = read_next
40
- break unless reply
41
- output([display_routing_id(routing_id), *body])
42
- @sock.send_to(routing_id, @fmt.compress(reply).first || "")
43
- end
44
- i += 1
45
- break if n && n > 0 && i >= n
46
- end
47
- end
48
-
49
-
50
- def monitor_loop(task)
51
- receiver = task.async do
52
- n = config.count
53
- i = 0
54
- loop do
55
- parts = recv_msg_raw
56
- break if parts.nil?
57
- routing_id = parts.shift
58
- parts = @fmt.decompress(parts)
59
- result = eval_recv_expr([display_routing_id(routing_id), *parts])
60
- output(result)
61
- i += 1
62
- break if n && n > 0 && i >= n
63
- end
64
- end
65
-
66
- sender = task.async do
67
- n = config.count
68
- i = 0
69
- sleep(config.delay) if config.delay
70
- if config.interval
71
- Async::Loop.quantized(interval: config.interval) do
72
- parts = read_next
73
- break unless parts
74
- send_targeted_or_eval(parts)
75
- i += 1
76
- break if n && n > 0 && i >= n
77
- end
78
- elsif config.data || config.file
79
- parts = read_next
80
- send_targeted_or_eval(parts) if parts
81
- else
82
- loop do
83
- parts = read_next
84
- break unless parts
85
- send_targeted_or_eval(parts)
86
- i += 1
87
- break if n && n > 0 && i >= n
88
- end
89
- end
90
- end
91
-
92
- wait_for_loops(receiver, sender)
93
- end
94
-
95
-
96
- def send_targeted_or_eval(parts)
97
- if @send_eval_proc
98
- parts = eval_send_expr(parts)
99
- return unless parts
100
- routing_id = resolve_target(parts.shift)
101
- @sock.send_to(routing_id, @fmt.compress(parts).first || "")
102
- elsif config.target
103
- parts = @fmt.compress(parts)
104
- @sock.send_to(resolve_target(config.target), parts.first || "")
105
- else
106
- send_msg(parts)
107
- end
108
- end
109
- end
110
- end
111
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- SEND_ONLY = %w[pub push scatter radio].freeze
6
- RECV_ONLY = %w[sub pull gather dish].freeze
7
-
8
- Endpoint = Data.define(:url, :bind?) do
9
- def connect? = !bind?
10
- end
11
-
12
-
13
- Config = Data.define(
14
- :type_name,
15
- :endpoints,
16
- :connects,
17
- :binds,
18
- :in_endpoints,
19
- :out_endpoints,
20
- :data,
21
- :file,
22
- :format,
23
- :subscribes,
24
- :joins,
25
- :group,
26
- :identity,
27
- :target,
28
- :interval,
29
- :count,
30
- :delay,
31
- :timeout,
32
- :linger,
33
- :reconnect_ivl,
34
- :heartbeat_ivl,
35
- :conflate,
36
- :compress,
37
- :send_expr,
38
- :recv_expr,
39
- :parallel,
40
- :transient,
41
- :verbose,
42
- :quiet,
43
- :echo,
44
- :curve_server,
45
- :curve_server_key,
46
- :has_msgpack,
47
- :has_zstd,
48
- :stdin_is_tty,
49
- ) do
50
- def send_only? = SEND_ONLY.include?(type_name)
51
- def recv_only? = RECV_ONLY.include?(type_name)
52
- end
53
- end
54
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- module CLI
5
- # Handles encoding/decoding messages in the configured format,
6
- # plus optional Zstandard compression.
7
- class Formatter
8
- def initialize(format, compress: false)
9
- @format = format
10
- @compress = compress
11
- end
12
-
13
-
14
- def encode(parts)
15
- case @format
16
- when :ascii
17
- parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
18
- when :quoted
19
- parts.map { |p| p.b.dump[1..-2] }.join("\t") + "\n"
20
- when :raw
21
- parts.each_with_index.map do |p, i|
22
- ZMTP::Codec::Frame.new(p.to_s, more: i < parts.size - 1).to_wire
23
- end.join
24
- when :jsonl
25
- JSON.generate(parts) + "\n"
26
- when :msgpack
27
- MessagePack.pack(parts)
28
- when :marshal
29
- parts.map(&:inspect).join("\t") + "\n"
30
- end
31
- end
32
-
33
-
34
- def decode(line)
35
- case @format
36
- when :ascii, :marshal
37
- line.chomp.split("\t")
38
- when :quoted
39
- line.chomp.split("\t").map { |p| "\"#{p}\"".undump }
40
- when :raw
41
- [line]
42
- when :jsonl
43
- arr = JSON.parse(line.chomp)
44
- abort "JSON Lines input must be an array of strings" unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
45
- arr
46
- end
47
- end
48
-
49
-
50
- def decode_marshal(io)
51
- Marshal.load(io)
52
- rescue EOFError, TypeError
53
- nil
54
- end
55
-
56
-
57
- def decode_msgpack(io)
58
- @msgpack_unpacker ||= MessagePack::Unpacker.new(io)
59
- @msgpack_unpacker.read
60
- rescue EOFError
61
- nil
62
- end
63
-
64
-
65
- def compress(parts)
66
- @compress ? parts.map { |p| Zstd.compress(p) } : parts
67
- end
68
-
69
-
70
- def decompress(parts)
71
- @compress ? parts.map { |p| Zstd.decompress(p) } : parts
72
- end
73
- end
74
- end
75
- end