omq-cli 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +15 -0
- data/README.md +464 -0
- data/exe/omq +6 -0
- data/lib/omq/cli/base_runner.rb +477 -0
- data/lib/omq/cli/channel.rb +8 -0
- data/lib/omq/cli/client_server.rb +111 -0
- data/lib/omq/cli/config.rb +55 -0
- data/lib/omq/cli/formatter.rb +75 -0
- data/lib/omq/cli/pair.rb +31 -0
- data/lib/omq/cli/peer.rb +8 -0
- data/lib/omq/cli/pipe.rb +265 -0
- data/lib/omq/cli/pub_sub.rb +14 -0
- data/lib/omq/cli/push_pull.rb +14 -0
- data/lib/omq/cli/radio_dish.rb +27 -0
- data/lib/omq/cli/req_rep.rb +83 -0
- data/lib/omq/cli/router_dealer.rb +76 -0
- data/lib/omq/cli/scatter_gather.rb +14 -0
- data/lib/omq/cli/version.rb +7 -0
- data/lib/omq/cli.rb +544 -0
- metadata +78 -0
|
@@ -0,0 +1,477 @@
|
|
|
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
|
+
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
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
|
|
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}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ── Shared loop bodies ──────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def run_send_logic
|
|
212
|
+
n = config.count
|
|
213
|
+
i = 0
|
|
214
|
+
sleep(config.delay) if config.delay
|
|
215
|
+
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
|
|
223
|
+
elsif config.data || config.file
|
|
224
|
+
parts = eval_send_expr(read_next)
|
|
225
|
+
send_msg(parts) if parts
|
|
226
|
+
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
|
|
235
|
+
elsif @send_eval_proc
|
|
236
|
+
parts = eval_send_expr(nil)
|
|
237
|
+
send_msg(parts) if parts
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def send_tick
|
|
243
|
+
raw = read_next_or_nil
|
|
244
|
+
if raw.nil? && !@send_eval_proc
|
|
245
|
+
@send_tick_eof = true
|
|
246
|
+
return 0
|
|
247
|
+
end
|
|
248
|
+
parts = eval_send_expr(raw)
|
|
249
|
+
send_msg(parts) if parts
|
|
250
|
+
1
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def run_recv_logic
|
|
255
|
+
n = config.count
|
|
256
|
+
i = 0
|
|
257
|
+
loop do
|
|
258
|
+
parts = recv_msg
|
|
259
|
+
break if parts.nil?
|
|
260
|
+
parts = eval_recv_expr(parts)
|
|
261
|
+
output(parts)
|
|
262
|
+
i += 1
|
|
263
|
+
break if n && n > 0 && i >= n
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def wait_for_loops(receiver, sender)
|
|
269
|
+
if config.data || config.file || config.send_expr || config.recv_expr || config.target
|
|
270
|
+
sender.wait
|
|
271
|
+
receiver.stop
|
|
272
|
+
elsif config.count && config.count > 0
|
|
273
|
+
receiver.wait
|
|
274
|
+
sender.stop
|
|
275
|
+
else
|
|
276
|
+
sender.wait
|
|
277
|
+
receiver.stop
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# ── Message I/O ─────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def send_msg(parts)
|
|
285
|
+
return if parts.empty?
|
|
286
|
+
parts = [Marshal.dump(parts)] if config.format == :marshal
|
|
287
|
+
parts = @fmt.compress(parts)
|
|
288
|
+
@sock.send(parts)
|
|
289
|
+
transient_ready!
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def recv_msg
|
|
294
|
+
raw = @sock.receive
|
|
295
|
+
return nil if raw.nil?
|
|
296
|
+
parts = @fmt.decompress(raw)
|
|
297
|
+
parts = Marshal.load(parts.first) if config.format == :marshal
|
|
298
|
+
transient_ready!
|
|
299
|
+
parts
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def recv_msg_raw
|
|
304
|
+
msg = @sock.receive
|
|
305
|
+
msg&.dup
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def read_next
|
|
310
|
+
if config.data
|
|
311
|
+
@fmt.decode(config.data + "\n")
|
|
312
|
+
elsif config.file
|
|
313
|
+
@file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
|
|
314
|
+
@fmt.decode(@file_data + "\n")
|
|
315
|
+
elsif config.format == :msgpack
|
|
316
|
+
@fmt.decode_msgpack($stdin)
|
|
317
|
+
elsif config.format == :marshal
|
|
318
|
+
@fmt.decode_marshal($stdin)
|
|
319
|
+
elsif config.format == :raw
|
|
320
|
+
data = $stdin.read
|
|
321
|
+
return nil if data.nil? || data.empty?
|
|
322
|
+
[data]
|
|
323
|
+
else
|
|
324
|
+
line = $stdin.gets
|
|
325
|
+
return nil if line.nil?
|
|
326
|
+
@fmt.decode(line)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def stdin_ready?
|
|
332
|
+
return @stdin_ready unless @stdin_ready.nil?
|
|
333
|
+
|
|
334
|
+
@stdin_ready = !$stdin.closed? &&
|
|
335
|
+
!config.stdin_is_tty &&
|
|
336
|
+
IO.select([$stdin], nil, nil, 0.01) &&
|
|
337
|
+
!$stdin.eof?
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def read_next_or_nil
|
|
342
|
+
if config.data || config.file
|
|
343
|
+
read_next
|
|
344
|
+
elsif @send_eval_proc
|
|
345
|
+
nil
|
|
346
|
+
else
|
|
347
|
+
read_next
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def output(parts)
|
|
353
|
+
return if config.quiet || parts.nil?
|
|
354
|
+
$stdout.write(@fmt.encode(parts))
|
|
355
|
+
$stdout.flush
|
|
356
|
+
end
|
|
357
|
+
|
|
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
|
+
|
|
378
|
+
# ── Eval ────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
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)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def wrap_registered_proc(block)
|
|
390
|
+
return unless block
|
|
391
|
+
proc do |msg|
|
|
392
|
+
$_ = msg&.first
|
|
393
|
+
block.call(msg)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
|
|
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
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
|
|
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]
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
|
|
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
|
+
def eval_send_expr(parts)
|
|
442
|
+
return parts unless @send_eval_proc
|
|
443
|
+
run_eval(@send_eval_proc, parts)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def eval_recv_expr(parts)
|
|
448
|
+
return parts unless @recv_eval_proc
|
|
449
|
+
run_eval(@recv_eval_proc, parts)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
|
|
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
|
|
468
|
+
|
|
469
|
+
# ── Logging ─────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def log(msg)
|
|
473
|
+
$stderr.puts(msg) if config.verbose
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
:curve_crypto,
|
|
47
|
+
:has_msgpack,
|
|
48
|
+
:has_zstd,
|
|
49
|
+
:stdin_is_tty,
|
|
50
|
+
) do
|
|
51
|
+
def send_only? = SEND_ONLY.include?(type_name)
|
|
52
|
+
def recv_only? = RECV_ONLY.include?(type_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|