omq-cli 0.2.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
77
+ end
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
75
91
  end
76
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
-
106
117
 
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,6 +182,29 @@ 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
210
  if raw.nil? && !@send_eval_proc
@@ -244,6 +231,17 @@ module OMQ
244
231
  end
245
232
 
246
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
+
247
245
  def wait_for_loops(receiver, sender)
248
246
  if config.data || config.file || config.send_expr || config.recv_expr || config.target
249
247
  sender.wait
@@ -257,6 +255,7 @@ module OMQ
257
255
  end
258
256
  end
259
257
 
258
+
260
259
  # ── Message I/O ─────────────────────────────────────────────────
261
260
 
262
261
 
@@ -286,23 +285,32 @@ module OMQ
286
285
 
287
286
 
288
287
  def read_next
288
+ config.data || config.file ? read_inline_data : read_stdin_input
289
+ end
290
+
291
+
292
+ def read_inline_data
289
293
  if config.data
290
294
  @fmt.decode(config.data + "\n")
291
- elsif config.file
295
+ else
292
296
  @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
293
297
  @fmt.decode(@file_data + "\n")
294
- elsif config.format == :msgpack
298
+ end
299
+ end
300
+
301
+
302
+ def read_stdin_input
303
+ case config.format
304
+ when :msgpack
295
305
  @fmt.decode_msgpack($stdin)
296
- elsif config.format == :marshal
306
+ when :marshal
297
307
  @fmt.decode_marshal($stdin)
298
- elsif config.format == :raw
308
+ when :raw
299
309
  data = $stdin.read
300
- return nil if data.nil? || data.empty?
301
- [data]
310
+ data.nil? || data.empty? ? nil : [data]
302
311
  else
303
312
  line = $stdin.gets
304
- return nil if line.nil?
305
- @fmt.decode(line)
313
+ line.nil? ? nil : @fmt.decode(line)
306
314
  end
307
315
  end
308
316
 
@@ -334,116 +342,50 @@ module OMQ
334
342
  $stdout.flush
335
343
  end
336
344
 
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
345
 
357
346
  # ── Eval ────────────────────────────────────────────────────────
358
347
 
359
348
 
360
349
  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)
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
365
354
  end
366
355
 
367
356
 
368
- def wrap_registered_proc(block)
369
- return unless block
370
- proc do |msg|
371
- $_ = msg&.first
372
- block.call(msg)
373
- end
357
+ def compile_evaluator(src, fallback:)
358
+ ExpressionEvaluator.new(src, format: config.format, fallback_proc: fallback)
374
359
  end
375
360
 
376
361
 
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
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
385
367
  end
386
368
 
387
369
 
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]
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
393
374
  end
394
375
 
395
376
 
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]
415
- end
416
-
417
-
418
- SENT = Object.new.freeze # sentinel: eval already sent the reply
419
-
420
377
  def eval_send_expr(parts)
421
- return parts unless @send_eval_proc
422
- run_eval(@send_eval_proc, parts)
378
+ @send_evaluator.call(parts, @sock)
423
379
  end
424
380
 
425
381
 
426
382
  def eval_recv_expr(parts)
427
- return parts unless @recv_eval_proc
428
- run_eval(@recv_eval_proc, parts)
383
+ @recv_evaluator.call(parts, @sock)
429
384
  end
430
385
 
431
386
 
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
387
+ SENT = ExpressionEvaluator::SENT
388
+
447
389
 
448
390
  # ── Logging ─────────────────────────────────────────────────────
449
391