omq-cli 0.1.0 → 0.3.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.
@@ -2,10 +2,16 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
+ # Template runner base class for all socket-type CLI runners.
6
+ # Subclasses override {#run_loop} to implement socket-specific behaviour.
5
7
  class BaseRunner
8
+ # @return [Config] frozen CLI configuration
9
+ # @return [Object] the OMQ socket instance
6
10
  attr_reader :config, :sock
7
11
 
8
12
 
13
+ # @param config [Config] frozen CLI configuration
14
+ # @param socket_class [Class] OMQ socket class to instantiate (e.g. OMQ::PUSH)
9
15
  def initialize(config, socket_class)
10
16
  @config = config
11
17
  @klass = socket_class
@@ -13,26 +19,18 @@ module OMQ
13
19
  end
14
20
 
15
21
 
22
+ # Runs the full lifecycle: socket setup, peer wait, BEGIN/END blocks, and the main loop.
23
+ #
24
+ # @param task [Async::Task] the parent async task
25
+ # @return [void]
16
26
  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
-
27
+ setup_socket
28
+ maybe_start_transient_monitor(task)
28
29
  sleep(config.delay) if config.delay && config.recv_only?
29
30
  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
31
+ run_begin_blocks
33
32
  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
33
+ run_end_blocks
36
34
  ensure
37
35
  @sock&.close
38
36
  end
@@ -46,34 +44,59 @@ module OMQ
46
44
  raise NotImplementedError
47
45
  end
48
46
 
47
+
49
48
  # ── Socket creation ─────────────────────────────────────────────
50
49
 
51
50
 
51
+ def setup_socket
52
+ @sock = create_socket
53
+ attach_endpoints unless config.parallel
54
+ setup_curve
55
+ setup_subscriptions
56
+ compile_expr
57
+ end
58
+
59
+
52
60
  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
61
+ SocketSetup.build(@klass, config)
63
62
  end
64
63
 
65
64
 
66
65
  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
66
+ SocketSetup.attach(@sock, config, verbose: config.verbose)
67
+ end
68
+
69
+
70
+ # ── Transient disconnect monitor ────────────────────────────────
71
+
72
+
73
+ def maybe_start_transient_monitor(task)
74
+ return unless config.transient
75
+ @transient_monitor = TransientMonitor.new(@sock, config, task, method(:log))
76
+ Async::Task.current.yield # let monitor start waiting
75
77
  end
76
78
 
79
+
80
+ def transient_ready!
81
+ @transient_monitor&.ready!
82
+ end
83
+
84
+
85
+ # ── BEGIN / END blocks ──────────────────────────────────────────
86
+
87
+
88
+ def run_begin_blocks
89
+ @sock.instance_exec(&@send_begin_proc) if @send_begin_proc
90
+ @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
91
+ end
92
+
93
+
94
+ def run_end_blocks
95
+ @sock.instance_exec(&@send_end_proc) if @send_end_proc
96
+ @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
97
+ end
98
+
99
+
77
100
  # ── Peer wait with grace period ─────────────────────────────────
78
101
 
79
102
 
@@ -86,46 +109,29 @@ module OMQ
86
109
  with_timeout(config.timeout) do
87
110
  @sock.peer_connected.wait
88
111
  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
112
+ wait_for_subscriber
113
+ apply_grace_period
101
114
  end
102
115
  end
103
116
 
104
- # ── Transient disconnect monitor ────────────────────────────────
105
117
 
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
118
+ def wait_for_subscriber
119
+ return unless %w[pub xpub].include?(config.type_name)
120
+ @sock.subscriber_joined.wait
121
+ log "Subscriber joined"
120
122
  end
121
123
 
122
124
 
123
- def transient_ready!
124
- if config.transient && !@transient_barrier.resolved?
125
- @transient_barrier.resolve(true)
126
- end
125
+ # Grace period: when multiple peers may be connecting (bind or
126
+ # multiple connect URLs), wait one reconnect interval so
127
+ # latecomers finish their handshake before we start sending.
128
+ def apply_grace_period
129
+ return unless config.binds.any? || config.connects.size > 1
130
+ ri = @sock.options.reconnect_interval
131
+ sleep(ri.is_a?(Range) ? ri.begin : ri)
127
132
  end
128
133
 
134
+
129
135
  # ── Timeout helper ──────────────────────────────────────────────
130
136
 
131
137
 
@@ -137,101 +143,38 @@ module OMQ
137
143
  end
138
144
  end
139
145
 
146
+
140
147
  # ── Socket setup ────────────────────────────────────────────────
141
148
 
142
149
 
143
150
  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
+ SocketSetup.setup_subscriptions(@sock, config)
151
152
  end
152
153
 
153
154
 
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
- return unless server_key_z85 || server_mode
159
-
160
- crypto = load_curve_crypto(config.curve_crypto || ENV["OMQ_CURVE_CRYPTO"])
161
- require "protocol/zmtp/mechanism/curve"
162
-
163
- if server_key_z85
164
- server_key = Protocol::ZMTP::Z85.decode(server_key_z85)
165
- client_key = crypto::PrivateKey.generate
166
- @sock.mechanism = Protocol::ZMTP::Mechanism::Curve.client(
167
- client_key.public_key.to_s, client_key.to_s,
168
- server_key: server_key, crypto: crypto
169
- )
170
- elsif server_mode
171
- if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
172
- server_pub = Protocol::ZMTP::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
173
- server_sec = Protocol::ZMTP::Z85.decode(ENV["OMQ_SERVER_SECRET"])
174
- else
175
- key = crypto::PrivateKey.generate
176
- server_pub = key.public_key.to_s
177
- server_sec = key.to_s
178
- end
179
- @sock.mechanism = Protocol::ZMTP::Mechanism::Curve.server(
180
- server_pub, server_sec, crypto: crypto
181
- )
182
- $stderr.puts "OMQ_SERVER_KEY='#{Protocol::ZMTP::Z85.encode(server_pub)}'"
183
- end
155
+ def setup_subscriptions_on(sock)
156
+ SocketSetup.setup_subscriptions(sock, config)
184
157
  end
185
158
 
186
159
 
187
- def load_curve_crypto(name)
188
- case name&.downcase
189
- when "rbnacl" then require "rbnacl"; RbNaCl
190
- when "nuckle" then require "nuckle"; Nuckle
191
- when nil
192
- begin
193
- require "rbnacl"; RbNaCl
194
- rescue LoadError
195
- abort "CURVE requires a crypto backend. Install rbnacl (recommended):\n" \
196
- " gem install rbnacl # requires system libsodium\n" \
197
- "Or use pure Ruby (not audited):\n" \
198
- " --curve-crypto nuckle\n" \
199
- " # or: OMQ_CURVE_CRYPTO=nuckle"
200
- end
201
- else
202
- abort "Unknown CURVE crypto backend: #{name}. Use 'rbnacl' or 'nuckle'."
203
- end
204
- rescue LoadError
205
- abort "Could not load #{name} gem: gem install #{name}"
160
+ def setup_curve
161
+ SocketSetup.setup_curve(@sock, config)
206
162
  end
207
163
 
164
+
208
165
  # ── Shared loop bodies ──────────────────────────────────────────
209
166
 
210
167
 
211
168
  def run_send_logic
212
169
  n = config.count
213
- i = 0
214
170
  sleep(config.delay) if config.delay
215
171
  if config.interval
216
- i += send_tick
217
- unless @send_tick_eof || (n && n > 0 && i >= n)
218
- Async::Loop.quantized(interval: config.interval) do
219
- i += send_tick
220
- break if @send_tick_eof || (n && n > 0 && i >= n)
221
- end
222
- end
172
+ run_interval_send(n)
223
173
  elsif config.data || config.file
224
174
  parts = eval_send_expr(read_next)
225
175
  send_msg(parts) if parts
226
176
  elsif stdin_ready?
227
- loop do
228
- parts = read_next
229
- break unless parts
230
- parts = eval_send_expr(parts)
231
- send_msg(parts) if parts
232
- i += 1
233
- break if n && n > 0 && i >= n
234
- end
177
+ run_stdin_send(n)
235
178
  elsif @send_eval_proc
236
179
  parts = eval_send_expr(nil)
237
180
  send_msg(parts) if parts
@@ -239,6 +182,29 @@ module OMQ
239
182
  end
240
183
 
241
184
 
185
+ def run_interval_send(n)
186
+ i = send_tick
187
+ return if @send_tick_eof || (n && n > 0 && i >= n)
188
+ Async::Loop.quantized(interval: config.interval) do
189
+ i += send_tick
190
+ break if @send_tick_eof || (n && n > 0 && i >= n)
191
+ end
192
+ end
193
+
194
+
195
+ def run_stdin_send(n)
196
+ i = 0
197
+ loop do
198
+ parts = read_next
199
+ break unless parts
200
+ parts = eval_send_expr(parts)
201
+ send_msg(parts) if parts
202
+ i += 1
203
+ break if n && n > 0 && i >= n
204
+ end
205
+ end
206
+
207
+
242
208
  def send_tick
243
209
  raw = read_next_or_nil
244
210
  if raw.nil? && !@send_eval_proc
@@ -265,6 +231,17 @@ module OMQ
265
231
  end
266
232
 
267
233
 
234
+ # Parallel recv-eval: delegates to ParallelRecvRunner.
235
+ #
236
+ def run_parallel_recv(task)
237
+ # @sock was created by call() before run_loop; close it now so it doesn't
238
+ # steal messages from the N worker sockets ParallelRecvRunner creates.
239
+ @sock&.close
240
+ @sock = nil
241
+ ParallelRecvRunner.new(@klass, config, @fmt, method(:output)).run(task)
242
+ end
243
+
244
+
268
245
  def wait_for_loops(receiver, sender)
269
246
  if config.data || config.file || config.send_expr || config.recv_expr || config.target
270
247
  sender.wait
@@ -278,6 +255,7 @@ module OMQ
278
255
  end
279
256
  end
280
257
 
258
+
281
259
  # ── Message I/O ─────────────────────────────────────────────────
282
260
 
283
261
 
@@ -307,23 +285,32 @@ module OMQ
307
285
 
308
286
 
309
287
  def read_next
288
+ config.data || config.file ? read_inline_data : read_stdin_input
289
+ end
290
+
291
+
292
+ def read_inline_data
310
293
  if config.data
311
294
  @fmt.decode(config.data + "\n")
312
- elsif config.file
295
+ else
313
296
  @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
314
297
  @fmt.decode(@file_data + "\n")
315
- elsif config.format == :msgpack
298
+ end
299
+ end
300
+
301
+
302
+ def read_stdin_input
303
+ case config.format
304
+ when :msgpack
316
305
  @fmt.decode_msgpack($stdin)
317
- elsif config.format == :marshal
306
+ when :marshal
318
307
  @fmt.decode_marshal($stdin)
319
- elsif config.format == :raw
308
+ when :raw
320
309
  data = $stdin.read
321
- return nil if data.nil? || data.empty?
322
- [data]
310
+ data.nil? || data.empty? ? nil : [data]
323
311
  else
324
312
  line = $stdin.gets
325
- return nil if line.nil?
326
- @fmt.decode(line)
313
+ line.nil? ? nil : @fmt.decode(line)
327
314
  end
328
315
  end
329
316
 
@@ -355,116 +342,50 @@ module OMQ
355
342
  $stdout.flush
356
343
  end
357
344
 
358
- # ── Routing helpers ─────────────────────────────────────────────
359
-
360
-
361
- def display_routing_id(id)
362
- if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
363
- id
364
- else
365
- "0x#{id.unpack1("H*")}"
366
- end
367
- end
368
-
369
-
370
- def resolve_target(target)
371
- if target.start_with?("0x")
372
- [target[2..].delete(" ")].pack("H*")
373
- else
374
- target
375
- end
376
- end
377
345
 
378
346
  # ── Eval ────────────────────────────────────────────────────────
379
347
 
380
348
 
381
349
  def compile_expr
382
- compile_one_expr(:send, config.send_expr)
383
- compile_one_expr(:recv, config.recv_expr)
384
- @send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
385
- @recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
350
+ @send_evaluator = compile_evaluator(config.send_expr, fallback: OMQ.outgoing_proc)
351
+ @recv_evaluator = compile_evaluator(config.recv_expr, fallback: OMQ.incoming_proc)
352
+ assign_send_aliases
353
+ assign_recv_aliases
386
354
  end
387
355
 
388
356
 
389
- def wrap_registered_proc(block)
390
- return unless block
391
- proc do |msg|
392
- $_ = msg&.first
393
- block.call(msg)
394
- end
357
+ def compile_evaluator(src, fallback:)
358
+ ExpressionEvaluator.new(src, format: config.format, fallback_proc: fallback)
395
359
  end
396
360
 
397
361
 
398
- def compile_one_expr(direction, src)
399
- return unless src
400
- expr, begin_body, end_body = extract_blocks(src)
401
- instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
402
- instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
403
- if expr && !expr.strip.empty?
404
- instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
405
- end
362
+ def assign_send_aliases
363
+ # Keep ivar aliases — subclasses check these directly
364
+ @send_begin_proc = @send_evaluator.begin_proc
365
+ @send_eval_proc = @send_evaluator.eval_proc
366
+ @send_end_proc = @send_evaluator.end_proc
406
367
  end
407
368
 
408
369
 
409
- def extract_blocks(expr)
410
- begin_body = end_body = nil
411
- expr, begin_body = extract_block(expr, "BEGIN")
412
- expr, end_body = extract_block(expr, "END")
413
- [expr, begin_body, end_body]
370
+ def assign_recv_aliases
371
+ @recv_begin_proc = @recv_evaluator.begin_proc
372
+ @recv_eval_proc = @recv_evaluator.eval_proc
373
+ @recv_end_proc = @recv_evaluator.end_proc
414
374
  end
415
375
 
416
376
 
417
- def extract_block(expr, keyword)
418
- start = expr.index(/#{keyword}\s*\{/)
419
- return [expr, nil] unless start
420
-
421
- # Find the opening brace
422
- i = expr.index("{", start)
423
- depth = 1
424
- j = i + 1
425
- while j < expr.length && depth > 0
426
- case expr[j]
427
- when "{" then depth += 1
428
- when "}" then depth -= 1
429
- end
430
- j += 1
431
- end
432
-
433
- body = expr[(i + 1)..(j - 2)]
434
- trimmed = expr[0...start] + expr[j..]
435
- [trimmed, body]
436
- end
437
-
438
-
439
- SENT = Object.new.freeze # sentinel: eval already sent the reply
440
-
441
377
  def eval_send_expr(parts)
442
- return parts unless @send_eval_proc
443
- run_eval(@send_eval_proc, parts)
378
+ @send_evaluator.call(parts, @sock)
444
379
  end
445
380
 
446
381
 
447
382
  def eval_recv_expr(parts)
448
- return parts unless @recv_eval_proc
449
- run_eval(@recv_eval_proc, parts)
383
+ @recv_evaluator.call(parts, @sock)
450
384
  end
451
385
 
452
386
 
453
- def run_eval(eval_proc, parts)
454
- $F = parts
455
- result = @sock.instance_exec(parts, &eval_proc)
456
- return nil if result.nil?
457
- return SENT if result.equal?(@sock)
458
- return [result] if config.format == :marshal
459
- case result
460
- when Array then result
461
- when String then [result]
462
- else [result.to_str]
463
- end
464
- rescue => e
465
- $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
466
- exit 3
467
- end
387
+ SENT = ExpressionEvaluator::SENT
388
+
468
389
 
469
390
  # ── Logging ─────────────────────────────────────────────────────
470
391