omq-cli 0.2.0 → 0.3.1

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,50 +143,22 @@ 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)
152
+ end
153
+
154
+
155
+ def setup_subscriptions_on(sock)
156
+ SocketSetup.setup_subscriptions(sock, config)
151
157
  end
152
158
 
153
159
 
154
160
  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 = CLI.load_curve_crypto(config.curve_crypto || ENV["OMQ_CURVE_CRYPTO"], verbose: config.verbose)
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
161
+ SocketSetup.setup_curve(@sock, config)
184
162
  end
185
163
 
186
164
 
@@ -189,28 +167,14 @@ module OMQ
189
167
 
190
168
  def run_send_logic
191
169
  n = config.count
192
- i = 0
193
170
  sleep(config.delay) if config.delay
194
171
  if config.interval
195
- i += send_tick
196
- unless @send_tick_eof || (n && n > 0 && i >= n)
197
- Async::Loop.quantized(interval: config.interval) do
198
- i += send_tick
199
- break if @send_tick_eof || (n && n > 0 && i >= n)
200
- end
201
- end
172
+ run_interval_send(n)
202
173
  elsif config.data || config.file
203
174
  parts = eval_send_expr(read_next)
204
175
  send_msg(parts) if parts
205
176
  elsif stdin_ready?
206
- loop do
207
- parts = read_next
208
- break unless parts
209
- parts = eval_send_expr(parts)
210
- send_msg(parts) if parts
211
- i += 1
212
- break if n && n > 0 && i >= n
213
- end
177
+ run_stdin_send(n)
214
178
  elsif @send_eval_proc
215
179
  parts = eval_send_expr(nil)
216
180
  send_msg(parts) if parts
@@ -218,9 +182,38 @@ module OMQ
218
182
  end
219
183
 
220
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
+
221
208
  def send_tick
222
209
  raw = read_next_or_nil
223
- if raw.nil? && !@send_eval_proc
210
+ if raw.nil?
211
+ if @send_eval_proc && !@stdin_ready
212
+ # Pure generator mode: no stdin, eval produces output from nothing.
213
+ parts = eval_send_expr(nil)
214
+ send_msg(parts) if parts
215
+ return 1
216
+ end
224
217
  @send_tick_eof = true
225
218
  return 0
226
219
  end
@@ -233,14 +226,52 @@ module OMQ
233
226
  def run_recv_logic
234
227
  n = config.count
235
228
  i = 0
236
- loop do
237
- parts = recv_msg
238
- break if parts.nil?
239
- parts = eval_recv_expr(parts)
240
- output(parts)
241
- i += 1
242
- break if n && n > 0 && i >= n
229
+ if config.interval
230
+ run_interval_recv(n)
231
+ else
232
+ loop do
233
+ parts = recv_msg
234
+ break if parts.nil?
235
+ parts = eval_recv_expr(parts)
236
+ output(parts)
237
+ i += 1
238
+ break if n && n > 0 && i >= n
239
+ end
240
+ end
241
+ end
242
+
243
+
244
+ def run_interval_recv(n)
245
+ i = recv_tick
246
+ return if i == 0
247
+ return if n && n > 0 && i >= n
248
+ Async::Loop.quantized(interval: config.interval) do
249
+ i += recv_tick
250
+ break if @recv_tick_eof || (n && n > 0 && i >= n)
251
+ end
252
+ end
253
+
254
+
255
+ def recv_tick
256
+ parts = recv_msg
257
+ if parts.nil?
258
+ @recv_tick_eof = true
259
+ return 0
243
260
  end
261
+ parts = eval_recv_expr(parts)
262
+ output(parts)
263
+ 1
264
+ end
265
+
266
+
267
+ # Parallel recv-eval: delegates to ParallelRecvRunner.
268
+ #
269
+ def run_parallel_recv(task)
270
+ # @sock was created by call() before run_loop; close it now so it doesn't
271
+ # steal messages from the N worker sockets ParallelRecvRunner creates.
272
+ @sock&.close
273
+ @sock = nil
274
+ ParallelRecvRunner.new(@klass, config, @fmt, method(:output)).run(task)
244
275
  end
245
276
 
246
277
 
@@ -257,6 +288,7 @@ module OMQ
257
288
  end
258
289
  end
259
290
 
291
+
260
292
  # ── Message I/O ─────────────────────────────────────────────────
261
293
 
262
294
 
@@ -286,23 +318,32 @@ module OMQ
286
318
 
287
319
 
288
320
  def read_next
321
+ config.data || config.file ? read_inline_data : read_stdin_input
322
+ end
323
+
324
+
325
+ def read_inline_data
289
326
  if config.data
290
327
  @fmt.decode(config.data + "\n")
291
- elsif config.file
328
+ else
292
329
  @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
293
330
  @fmt.decode(@file_data + "\n")
294
- elsif config.format == :msgpack
331
+ end
332
+ end
333
+
334
+
335
+ def read_stdin_input
336
+ case config.format
337
+ when :msgpack
295
338
  @fmt.decode_msgpack($stdin)
296
- elsif config.format == :marshal
339
+ when :marshal
297
340
  @fmt.decode_marshal($stdin)
298
- elsif config.format == :raw
341
+ when :raw
299
342
  data = $stdin.read
300
- return nil if data.nil? || data.empty?
301
- [data]
343
+ data.nil? || data.empty? ? nil : [data]
302
344
  else
303
345
  line = $stdin.gets
304
- return nil if line.nil?
305
- @fmt.decode(line)
346
+ line.nil? ? nil : @fmt.decode(line)
306
347
  end
307
348
  end
308
349
 
@@ -320,10 +361,10 @@ module OMQ
320
361
  def read_next_or_nil
321
362
  if config.data || config.file
322
363
  read_next
323
- elsif @send_eval_proc
324
- nil
364
+ elsif stdin_ready?
365
+ read_stdin_input
325
366
  else
326
- read_next
367
+ nil
327
368
  end
328
369
  end
329
370
 
@@ -334,116 +375,50 @@ module OMQ
334
375
  $stdout.flush
335
376
  end
336
377
 
337
- # ── Routing helpers ─────────────────────────────────────────────
338
-
339
-
340
- def display_routing_id(id)
341
- if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
342
- id
343
- else
344
- "0x#{id.unpack1("H*")}"
345
- end
346
- end
347
-
348
-
349
- def resolve_target(target)
350
- if target.start_with?("0x")
351
- [target[2..].delete(" ")].pack("H*")
352
- else
353
- target
354
- end
355
- end
356
378
 
357
379
  # ── Eval ────────────────────────────────────────────────────────
358
380
 
359
381
 
360
382
  def compile_expr
361
- compile_one_expr(:send, config.send_expr)
362
- compile_one_expr(:recv, config.recv_expr)
363
- @send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
364
- @recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
365
- end
366
-
367
-
368
- def wrap_registered_proc(block)
369
- return unless block
370
- proc do |msg|
371
- $_ = msg&.first
372
- block.call(msg)
373
- end
383
+ @send_evaluator = compile_evaluator(config.send_expr, fallback: OMQ.outgoing_proc)
384
+ @recv_evaluator = compile_evaluator(config.recv_expr, fallback: OMQ.incoming_proc)
385
+ assign_send_aliases
386
+ assign_recv_aliases
374
387
  end
375
388
 
376
389
 
377
- def compile_one_expr(direction, src)
378
- return unless src
379
- expr, begin_body, end_body = extract_blocks(src)
380
- instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
381
- instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
382
- if expr && !expr.strip.empty?
383
- instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
384
- end
390
+ def compile_evaluator(src, fallback:)
391
+ ExpressionEvaluator.new(src, format: config.format, fallback_proc: fallback)
385
392
  end
386
393
 
387
394
 
388
- def extract_blocks(expr)
389
- begin_body = end_body = nil
390
- expr, begin_body = extract_block(expr, "BEGIN")
391
- expr, end_body = extract_block(expr, "END")
392
- [expr, begin_body, end_body]
395
+ def assign_send_aliases
396
+ # Keep ivar aliases — subclasses check these directly
397
+ @send_begin_proc = @send_evaluator.begin_proc
398
+ @send_eval_proc = @send_evaluator.eval_proc
399
+ @send_end_proc = @send_evaluator.end_proc
393
400
  end
394
401
 
395
402
 
396
- def extract_block(expr, keyword)
397
- start = expr.index(/#{keyword}\s*\{/)
398
- return [expr, nil] unless start
399
-
400
- # Find the opening brace
401
- i = expr.index("{", start)
402
- depth = 1
403
- j = i + 1
404
- while j < expr.length && depth > 0
405
- case expr[j]
406
- when "{" then depth += 1
407
- when "}" then depth -= 1
408
- end
409
- j += 1
410
- end
411
-
412
- body = expr[(i + 1)..(j - 2)]
413
- trimmed = expr[0...start] + expr[j..]
414
- [trimmed, body]
403
+ def assign_recv_aliases
404
+ @recv_begin_proc = @recv_evaluator.begin_proc
405
+ @recv_eval_proc = @recv_evaluator.eval_proc
406
+ @recv_end_proc = @recv_evaluator.end_proc
415
407
  end
416
408
 
417
409
 
418
- SENT = Object.new.freeze # sentinel: eval already sent the reply
419
-
420
410
  def eval_send_expr(parts)
421
- return parts unless @send_eval_proc
422
- run_eval(@send_eval_proc, parts)
411
+ @send_evaluator.call(parts, @sock)
423
412
  end
424
413
 
425
414
 
426
415
  def eval_recv_expr(parts)
427
- return parts unless @recv_eval_proc
428
- run_eval(@recv_eval_proc, parts)
416
+ @recv_evaluator.call(parts, @sock)
429
417
  end
430
418
 
431
419
 
432
- def run_eval(eval_proc, parts)
433
- $F = parts
434
- result = @sock.instance_exec(parts, &eval_proc)
435
- return nil if result.nil?
436
- return SENT if result.equal?(@sock)
437
- return [result] if config.format == :marshal
438
- case result
439
- when Array then result
440
- when String then [result]
441
- else [result.to_str]
442
- end
443
- rescue => e
444
- $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
445
- exit 3
446
- end
420
+ SENT = ExpressionEvaluator::SENT
421
+
447
422
 
448
423
  # ── Logging ─────────────────────────────────────────────────────
449
424