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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +158 -0
- data/README.md +14 -9
- data/lib/omq/cli/base_runner.rb +152 -210
- data/lib/omq/cli/cli_parser.rb +470 -0
- data/lib/omq/cli/client_server.rb +32 -59
- data/lib/omq/cli/config.rb +10 -0
- data/lib/omq/cli/expression_evaluator.rb +156 -0
- data/lib/omq/cli/formatter.rb +27 -1
- data/lib/omq/cli/pair.rb +9 -7
- data/lib/omq/cli/parallel_recv_runner.rb +150 -0
- data/lib/omq/cli/pipe.rb +175 -156
- data/lib/omq/cli/pub_sub.rb +5 -1
- data/lib/omq/cli/push_pull.rb +5 -1
- data/lib/omq/cli/radio_dish.rb +5 -1
- data/lib/omq/cli/req_rep.rb +44 -49
- data/lib/omq/cli/router_dealer.rb +13 -46
- data/lib/omq/cli/routing_helper.rb +95 -0
- data/lib/omq/cli/scatter_gather.rb +5 -1
- data/lib/omq/cli/socket_setup.rb +100 -0
- data/lib/omq/cli/transient_monitor.rb +41 -0
- data/lib/omq/cli/version.rb +1 -1
- data/lib/omq/cli.rb +72 -426
- metadata +94 -6
- data/lib/omq/cli/channel.rb +0 -8
- data/lib/omq/cli/peer.rb +0 -8
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Parses and validates command-line arguments for the omq CLI.
|
|
6
|
+
#
|
|
7
|
+
class CliParser
|
|
8
|
+
EXAMPLES = <<~'TEXT'
|
|
9
|
+
── Request / Reply ──────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
┌─────┐ "hello" ┌─────┐
|
|
12
|
+
│ REQ │────────────→│ REP │
|
|
13
|
+
│ │←────────────│ │
|
|
14
|
+
└─────┘ "HELLO" └─────┘
|
|
15
|
+
|
|
16
|
+
# terminal 1: echo server
|
|
17
|
+
omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
18
|
+
|
|
19
|
+
# terminal 2: send a request
|
|
20
|
+
echo "hello" | omq req --connect tcp://localhost:5555
|
|
21
|
+
|
|
22
|
+
# or over IPC (unix socket, single machine)
|
|
23
|
+
omq rep --bind ipc:///tmp/echo.sock --echo &
|
|
24
|
+
echo "hello" | omq req --connect ipc:///tmp/echo.sock
|
|
25
|
+
|
|
26
|
+
── Publish / Subscribe ──────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
┌─────┐ "weather.nyc 72F" ┌─────┐
|
|
29
|
+
│ PUB │────────────────────→│ SUB │ --subscribe "weather."
|
|
30
|
+
└─────┘ └─────┘
|
|
31
|
+
|
|
32
|
+
# terminal 1: subscriber (all topics by default)
|
|
33
|
+
omq sub --bind tcp://:5556
|
|
34
|
+
|
|
35
|
+
# terminal 2: publisher (needs --delay for subscription to propagate)
|
|
36
|
+
echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
|
|
37
|
+
|
|
38
|
+
── Periodic Publish ───────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
┌─────┐ "tick 1" ┌─────┐
|
|
41
|
+
│ PUB │──(every 1s)─→│ SUB │
|
|
42
|
+
└─────┘ └─────┘
|
|
43
|
+
|
|
44
|
+
# terminal 1: subscriber
|
|
45
|
+
omq sub --bind tcp://:5556
|
|
46
|
+
|
|
47
|
+
# terminal 2: publish a tick every second (wall-clock aligned)
|
|
48
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
49
|
+
--data "tick" --interval 1
|
|
50
|
+
|
|
51
|
+
# 5 ticks, then exit
|
|
52
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
53
|
+
--data "tick" --interval 1 --count 5
|
|
54
|
+
|
|
55
|
+
── Pipeline ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
┌──────┐ ┌──────┐
|
|
58
|
+
│ PUSH │──────────→│ PULL │
|
|
59
|
+
└──────┘ └──────┘
|
|
60
|
+
|
|
61
|
+
# terminal 1: worker
|
|
62
|
+
omq pull --bind tcp://:5557
|
|
63
|
+
|
|
64
|
+
# terminal 2: send tasks
|
|
65
|
+
echo "task 1" | omq push --connect tcp://localhost:5557
|
|
66
|
+
|
|
67
|
+
# or over IPC (unix socket)
|
|
68
|
+
omq pull --bind ipc:///tmp/pipeline.sock &
|
|
69
|
+
echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
|
|
70
|
+
|
|
71
|
+
── Pipe (PULL → eval → PUSH) ────────────────────────────────
|
|
72
|
+
|
|
73
|
+
┌──────┐ ┌──────┐ ┌──────┐
|
|
74
|
+
│ PUSH │────────→│ pipe │────────→│ PULL │
|
|
75
|
+
└──────┘ └──────┘ └──────┘
|
|
76
|
+
|
|
77
|
+
# terminal 1: producer
|
|
78
|
+
echo -e "hello\nworld" | omq push --bind ipc://@work
|
|
79
|
+
|
|
80
|
+
# terminal 2: worker — uppercase each message
|
|
81
|
+
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
82
|
+
# terminal 3: collector
|
|
83
|
+
omq pull --bind ipc://@sink
|
|
84
|
+
|
|
85
|
+
# 4 Ractor workers in a single process (-P)
|
|
86
|
+
omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
|
|
87
|
+
|
|
88
|
+
# exit when producer disconnects (--transient)
|
|
89
|
+
omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
|
|
90
|
+
|
|
91
|
+
# fan-in: multiple sources → one sink
|
|
92
|
+
omq pipe --in -c ipc://@work1 -c ipc://@work2 \
|
|
93
|
+
--out -c ipc://@sink -e '$F.map(&:upcase)'
|
|
94
|
+
|
|
95
|
+
# fan-out: one source → multiple sinks (round-robin)
|
|
96
|
+
omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
|
|
97
|
+
|
|
98
|
+
── CLIENT / SERVER (draft) ──────────────────────────────────
|
|
99
|
+
|
|
100
|
+
┌────────┐ "hello" ┌────────┐
|
|
101
|
+
│ CLIENT │───────────→│ SERVER │ --recv-eval '$F.map(&:upcase)'
|
|
102
|
+
│ │←───────────│ │
|
|
103
|
+
└────────┘ "HELLO" └────────┘
|
|
104
|
+
|
|
105
|
+
# terminal 1: upcasing server
|
|
106
|
+
omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
107
|
+
|
|
108
|
+
# terminal 2: client
|
|
109
|
+
echo "hello" | omq client --connect tcp://localhost:5555
|
|
110
|
+
|
|
111
|
+
── Formats ──────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
# ascii (default) — non-printable replaced with dots
|
|
114
|
+
omq pull --bind tcp://:5557 --ascii
|
|
115
|
+
|
|
116
|
+
# quoted — lossless, round-trippable (uses String#dump escaping)
|
|
117
|
+
omq pull --bind tcp://:5557 --quoted
|
|
118
|
+
|
|
119
|
+
# JSON Lines — structured, multipart as arrays
|
|
120
|
+
echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
|
|
121
|
+
omq pull --bind tcp://:5557 --jsonl
|
|
122
|
+
|
|
123
|
+
# multipart via tabs
|
|
124
|
+
printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
|
|
125
|
+
|
|
126
|
+
── Compression ──────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
# both sides must use --compress
|
|
129
|
+
omq pull --bind tcp://:5557 --compress &
|
|
130
|
+
echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
|
|
131
|
+
|
|
132
|
+
── CURVE Encryption ─────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
# server (prints OMQ_SERVER_KEY=...)
|
|
135
|
+
omq rep --bind tcp://:5555 --echo --curve-server
|
|
136
|
+
|
|
137
|
+
# client (paste the server's key)
|
|
138
|
+
echo "secret" | omq req --connect tcp://localhost:5555 \
|
|
139
|
+
--curve-server-key '<key from server>'
|
|
140
|
+
|
|
141
|
+
── ROUTER / DEALER ──────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
┌────────┐ ┌────────┐
|
|
144
|
+
│ DEALER │─────────→│ ROUTER │
|
|
145
|
+
│ id=w1 │ │ │
|
|
146
|
+
└────────┘ └────────┘
|
|
147
|
+
|
|
148
|
+
# terminal 1: router shows identity + message
|
|
149
|
+
omq router --bind tcp://:5555
|
|
150
|
+
|
|
151
|
+
# terminal 2: dealer with identity
|
|
152
|
+
echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
|
|
153
|
+
|
|
154
|
+
── Ruby Eval ────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
# filter incoming: only pass messages containing "error"
|
|
157
|
+
omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
|
|
158
|
+
|
|
159
|
+
# transform incoming with gems
|
|
160
|
+
omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
|
|
161
|
+
|
|
162
|
+
# require a local file, use its methods
|
|
163
|
+
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
|
|
164
|
+
|
|
165
|
+
# next skips, break stops — regexps match against $_
|
|
166
|
+
omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
|
|
167
|
+
|
|
168
|
+
# BEGIN/END blocks (like awk) — accumulate and summarize
|
|
169
|
+
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
|
|
170
|
+
|
|
171
|
+
# transform outgoing messages
|
|
172
|
+
echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
|
|
173
|
+
|
|
174
|
+
# REQ: transform request and reply independently
|
|
175
|
+
echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
|
|
176
|
+
|
|
177
|
+
── Script Handlers (-r) ────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
# handler.rb — register transforms from a file
|
|
180
|
+
# db = PG.connect("dbname=app")
|
|
181
|
+
# OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
|
|
182
|
+
# at_exit { db.close }
|
|
183
|
+
omq pull --bind tcp://:5557 -r./handler.rb
|
|
184
|
+
|
|
185
|
+
# combine script handlers with inline eval
|
|
186
|
+
omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
|
|
187
|
+
|
|
188
|
+
# OMQ.outgoing { |msg| ... } — registered outgoing transform
|
|
189
|
+
# OMQ.incoming { |msg| ... } — registered incoming transform
|
|
190
|
+
# CLI flags (-e/-E) override registered handlers
|
|
191
|
+
TEXT
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
DEFAULT_OPTS = {
|
|
195
|
+
type_name: nil,
|
|
196
|
+
endpoints: [],
|
|
197
|
+
connects: [],
|
|
198
|
+
binds: [],
|
|
199
|
+
in_endpoints: [],
|
|
200
|
+
out_endpoints: [],
|
|
201
|
+
data: nil,
|
|
202
|
+
file: nil,
|
|
203
|
+
format: :ascii,
|
|
204
|
+
subscribes: [],
|
|
205
|
+
joins: [],
|
|
206
|
+
group: nil,
|
|
207
|
+
identity: nil,
|
|
208
|
+
target: nil,
|
|
209
|
+
interval: nil,
|
|
210
|
+
count: nil,
|
|
211
|
+
delay: nil,
|
|
212
|
+
timeout: nil,
|
|
213
|
+
linger: 5,
|
|
214
|
+
reconnect_ivl: nil,
|
|
215
|
+
heartbeat_ivl: nil,
|
|
216
|
+
conflate: false,
|
|
217
|
+
compress: false,
|
|
218
|
+
send_expr: nil,
|
|
219
|
+
recv_expr: nil,
|
|
220
|
+
parallel: nil,
|
|
221
|
+
transient: false,
|
|
222
|
+
verbose: false,
|
|
223
|
+
quiet: false,
|
|
224
|
+
echo: false,
|
|
225
|
+
scripts: [],
|
|
226
|
+
recv_maxsz: nil,
|
|
227
|
+
curve_server: false,
|
|
228
|
+
curve_server_key: nil,
|
|
229
|
+
curve_crypto: nil,
|
|
230
|
+
}.freeze
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Parses +argv+ and returns a mutable options hash.
|
|
234
|
+
#
|
|
235
|
+
def self.parse(argv)
|
|
236
|
+
new.parse(argv)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Validates option combinations, aborting on bad combos.
|
|
241
|
+
#
|
|
242
|
+
def self.validate!(opts)
|
|
243
|
+
new.validate!(opts)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Validates that required gems are available.
|
|
248
|
+
#
|
|
249
|
+
def self.validate_gems!(config)
|
|
250
|
+
abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
|
|
251
|
+
abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
|
|
252
|
+
|
|
253
|
+
if config.recv_only? && (config.data || config.file)
|
|
254
|
+
abort "--data/--file not valid for #{config.type_name} (receive-only)"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Parses +argv+ and returns a mutable options hash.
|
|
260
|
+
#
|
|
261
|
+
# @param argv [Array<String>] command-line arguments (mutated in place)
|
|
262
|
+
# @return [Hash] parsed options
|
|
263
|
+
def parse(argv)
|
|
264
|
+
opts = DEFAULT_OPTS.transform_values { |v| v.is_a?(Array) ? v.dup : v }
|
|
265
|
+
pipe_side = nil # nil = legacy positional mode; :in/:out = modal
|
|
266
|
+
|
|
267
|
+
parser = OptionParser.new do |o|
|
|
268
|
+
o.banner = "Usage: omq TYPE [options]\n\n" \
|
|
269
|
+
"Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
|
|
270
|
+
"Draft: client, server, radio, dish, scatter, gather, channel, peer\n" \
|
|
271
|
+
"Virtual: pipe (PULL → eval → PUSH)\n\n"
|
|
272
|
+
|
|
273
|
+
o.separator "Connection:"
|
|
274
|
+
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
|
|
275
|
+
ep = Endpoint.new(v, false)
|
|
276
|
+
case pipe_side
|
|
277
|
+
when :in
|
|
278
|
+
opts[:in_endpoints] << ep
|
|
279
|
+
when :out
|
|
280
|
+
opts[:out_endpoints] << ep
|
|
281
|
+
else
|
|
282
|
+
opts[:endpoints] << ep
|
|
283
|
+
opts[:connects] << v
|
|
284
|
+
end
|
|
285
|
+
}
|
|
286
|
+
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
|
|
287
|
+
ep = Endpoint.new(v, true)
|
|
288
|
+
case pipe_side
|
|
289
|
+
when :in
|
|
290
|
+
opts[:in_endpoints] << ep
|
|
291
|
+
when :out
|
|
292
|
+
opts[:out_endpoints] << ep
|
|
293
|
+
else
|
|
294
|
+
opts[:endpoints] << ep
|
|
295
|
+
opts[:binds] << v
|
|
296
|
+
end
|
|
297
|
+
}
|
|
298
|
+
o.on("--in", "Pipe: subsequent -b/-c attach to input (PULL) side") { pipe_side = :in }
|
|
299
|
+
o.on("--out", "Pipe: subsequent -b/-c attach to output (PUSH) side") { pipe_side = :out }
|
|
300
|
+
|
|
301
|
+
o.separator "\nData source (REP: reply source):"
|
|
302
|
+
o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
|
|
303
|
+
o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
|
|
304
|
+
o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
|
|
305
|
+
|
|
306
|
+
o.separator "\nFormat (input + output):"
|
|
307
|
+
o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
|
|
308
|
+
o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
|
|
309
|
+
o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
|
|
310
|
+
o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
|
|
311
|
+
o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
|
|
312
|
+
o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
|
|
313
|
+
|
|
314
|
+
o.separator "\nSubscription/groups:"
|
|
315
|
+
o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
|
|
316
|
+
o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
|
|
317
|
+
o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
|
|
318
|
+
|
|
319
|
+
o.separator "\nIdentity/routing:"
|
|
320
|
+
o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
|
|
321
|
+
o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
|
|
322
|
+
|
|
323
|
+
o.separator "\nTiming:"
|
|
324
|
+
o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
|
|
325
|
+
o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
|
|
326
|
+
o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
|
|
327
|
+
o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
|
|
328
|
+
o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
|
|
329
|
+
o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
|
|
330
|
+
opts[:reconnect_ivl] = if v.include?("..")
|
|
331
|
+
lo, hi = v.split("..", 2)
|
|
332
|
+
Float(lo)..Float(hi)
|
|
333
|
+
else
|
|
334
|
+
Float(v)
|
|
335
|
+
end
|
|
336
|
+
}
|
|
337
|
+
o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
|
|
338
|
+
o.on("--recv-maxsz COUNT", Integer, "Max inbound message size in bytes (larger messages dropped)") { |v| opts[:recv_maxsz] = v }
|
|
339
|
+
|
|
340
|
+
o.separator "\nDelivery:"
|
|
341
|
+
o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
|
|
342
|
+
|
|
343
|
+
o.separator "\nCompression:"
|
|
344
|
+
o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
|
|
345
|
+
|
|
346
|
+
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
347
|
+
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
|
|
348
|
+
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
|
|
349
|
+
o.on("-r", "--require LIB", "Require lib/file in Async context; use '-' for stdin. Scripts can register OMQ.outgoing/incoming") { |v|
|
|
350
|
+
require "omq" unless defined?(OMQ::VERSION)
|
|
351
|
+
opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
|
|
352
|
+
}
|
|
353
|
+
o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (default: nproc); for non-pipe requires --recv-eval") { |v|
|
|
354
|
+
require "etc"
|
|
355
|
+
opts[:parallel] = v || Etc.nprocessors
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
o.separator "\nCURVE encryption (requires rbnacl or nuckle gem):"
|
|
359
|
+
o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
|
|
360
|
+
o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
|
|
361
|
+
o.on("--curve-crypto BACKEND", "Crypto backend: rbnacl (default if installed) or nuckle") { |v| opts[:curve_crypto] = v }
|
|
362
|
+
o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
|
|
363
|
+
o.separator " OMQ_CURVE_CRYPTO (backend: rbnacl or nuckle)"
|
|
364
|
+
|
|
365
|
+
o.separator "\nOther:"
|
|
366
|
+
o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
|
|
367
|
+
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
368
|
+
o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
|
|
369
|
+
o.on("-V", "--version") {
|
|
370
|
+
if ENV["OMQ_DEV"]
|
|
371
|
+
require_relative "../../../../omq/lib/omq/version"
|
|
372
|
+
else
|
|
373
|
+
require "omq/version"
|
|
374
|
+
end
|
|
375
|
+
puts "omq-cli #{OMQ::CLI::VERSION} (omq #{OMQ::VERSION})"
|
|
376
|
+
exit
|
|
377
|
+
}
|
|
378
|
+
o.on("-h") { puts o
|
|
379
|
+
exit }
|
|
380
|
+
o.on("--help") { CLI.page "#{o}\n#{EXAMPLES}"
|
|
381
|
+
exit }
|
|
382
|
+
o.on("--examples") { CLI.page EXAMPLES
|
|
383
|
+
exit }
|
|
384
|
+
|
|
385
|
+
o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
begin
|
|
389
|
+
parser.parse!(argv)
|
|
390
|
+
rescue OptionParser::ParseError => e
|
|
391
|
+
abort e.message
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
type_name = argv.shift
|
|
395
|
+
if type_name.nil?
|
|
396
|
+
abort parser.to_s if opts[:scripts].empty?
|
|
397
|
+
# bare script mode — type_name stays nil
|
|
398
|
+
elsif !SOCKET_TYPE_NAMES.include?(type_name.downcase)
|
|
399
|
+
abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
|
|
400
|
+
else
|
|
401
|
+
opts[:type_name] = type_name.downcase
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
|
|
405
|
+
normalize_ep = ->(ep) { Endpoint.new(normalize.call(ep.url), ep.bind?) }
|
|
406
|
+
opts[:binds].map!(&normalize)
|
|
407
|
+
opts[:connects].map!(&normalize)
|
|
408
|
+
opts[:endpoints].map!(&normalize_ep)
|
|
409
|
+
opts[:in_endpoints].map!(&normalize_ep)
|
|
410
|
+
opts[:out_endpoints].map!(&normalize_ep)
|
|
411
|
+
|
|
412
|
+
opts
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Validates option combinations, aborting on invalid combos.
|
|
417
|
+
#
|
|
418
|
+
# @param opts [Hash] parsed options from {#parse}
|
|
419
|
+
# @return [void]
|
|
420
|
+
def validate!(opts)
|
|
421
|
+
return if opts[:type_name].nil? # bare script mode
|
|
422
|
+
|
|
423
|
+
abort "-r- (stdin script) and -F- (stdin data) cannot both be used" if opts[:scripts]&.include?(:stdin) && opts[:file] == "-"
|
|
424
|
+
|
|
425
|
+
type_name = opts[:type_name]
|
|
426
|
+
|
|
427
|
+
if type_name == "pipe"
|
|
428
|
+
has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
429
|
+
if has_in_out
|
|
430
|
+
abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
|
|
431
|
+
abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
|
|
432
|
+
abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
|
|
433
|
+
else
|
|
434
|
+
abort "pipe requires exactly 2 endpoints (pull-side and push-side), or use --in/--out" if opts[:endpoints].size != 2
|
|
435
|
+
end
|
|
436
|
+
else
|
|
437
|
+
abort "--in/--out are only valid for pipe" if opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
438
|
+
abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
|
|
439
|
+
end
|
|
440
|
+
abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
|
|
441
|
+
abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
|
|
442
|
+
abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
|
|
443
|
+
abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
|
|
444
|
+
abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
|
|
445
|
+
abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
|
|
446
|
+
abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
|
|
447
|
+
abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
|
|
448
|
+
abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
|
|
449
|
+
abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
|
|
450
|
+
|
|
451
|
+
if opts[:parallel]
|
|
452
|
+
abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
|
|
453
|
+
if type_name == "pipe"
|
|
454
|
+
all_pipe_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
|
|
455
|
+
abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_pipe_eps.any?(&:bind?)
|
|
456
|
+
elsif RECV_ONLY.include?(type_name)
|
|
457
|
+
abort "-P/--parallel on #{type_name} requires --recv-eval (-e)" unless opts[:recv_expr]
|
|
458
|
+
abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if opts[:binds].any?
|
|
459
|
+
else
|
|
460
|
+
abort "-P/--parallel is only valid for pipe or recv-only socket types (#{RECV_ONLY.join(', ')})"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
(opts[:connects] + opts[:binds]).each do |url|
|
|
465
|
+
abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
module CLI
|
|
5
|
-
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
# Runner for SERVER and PEER sockets (draft; routing-id-based messaging).
|
|
9
6
|
class ServerRunner < BaseRunner
|
|
7
|
+
include RoutingHelper
|
|
8
|
+
|
|
10
9
|
private
|
|
11
10
|
|
|
12
11
|
|
|
@@ -27,28 +26,40 @@ module OMQ
|
|
|
27
26
|
break if parts.nil?
|
|
28
27
|
routing_id = parts.shift
|
|
29
28
|
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
|
|
29
|
+
break unless handle_server_request(routing_id, body)
|
|
44
30
|
i += 1
|
|
45
31
|
break if n && n > 0 && i >= n
|
|
46
32
|
end
|
|
47
33
|
end
|
|
48
34
|
|
|
49
35
|
|
|
36
|
+
def handle_server_request(routing_id, body)
|
|
37
|
+
if config.recv_expr || @recv_eval_proc
|
|
38
|
+
reply = eval_recv_expr(body)
|
|
39
|
+
output([display_routing_id(routing_id), *(reply || [""])])
|
|
40
|
+
@sock.send_to(routing_id, @fmt.compress(reply || [""]).first)
|
|
41
|
+
elsif config.echo
|
|
42
|
+
output([display_routing_id(routing_id), *body])
|
|
43
|
+
@sock.send_to(routing_id, @fmt.compress(body).first || "")
|
|
44
|
+
elsif config.data || config.file || !config.stdin_is_tty
|
|
45
|
+
reply = read_next
|
|
46
|
+
return false unless reply
|
|
47
|
+
output([display_routing_id(routing_id), *body])
|
|
48
|
+
@sock.send_to(routing_id, @fmt.compress(reply).first || "")
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
50
54
|
def monitor_loop(task)
|
|
51
|
-
receiver = task
|
|
55
|
+
receiver = recv_async(task)
|
|
56
|
+
sender = async_send_loop(task)
|
|
57
|
+
wait_for_loops(receiver, sender)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def recv_async(task)
|
|
62
|
+
task.async do
|
|
52
63
|
n = config.count
|
|
53
64
|
i = 0
|
|
54
65
|
loop do
|
|
@@ -62,49 +73,11 @@ module OMQ
|
|
|
62
73
|
break if n && n > 0 && i >= n
|
|
63
74
|
end
|
|
64
75
|
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
76
|
end
|
|
94
77
|
|
|
95
78
|
|
|
96
|
-
def
|
|
97
|
-
|
|
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
|
|
79
|
+
def send_to_peer(id, parts)
|
|
80
|
+
@sock.send_to(id, @fmt.compress(parts).first || "")
|
|
108
81
|
end
|
|
109
82
|
end
|
|
110
83
|
end
|
data/lib/omq/cli/config.rb
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
module CLI
|
|
5
|
+
# Socket type names that only send messages.
|
|
5
6
|
SEND_ONLY = %w[pub push scatter radio].freeze
|
|
7
|
+
# Socket type names that only receive messages.
|
|
6
8
|
RECV_ONLY = %w[sub pull gather dish].freeze
|
|
7
9
|
|
|
10
|
+
|
|
11
|
+
# A bind or connect endpoint with its URL and direction.
|
|
8
12
|
Endpoint = Data.define(:url, :bind?) do
|
|
13
|
+
# @return [Boolean] true if this endpoint connects rather than binds
|
|
9
14
|
def connect? = !bind?
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
|
|
18
|
+
# Frozen, Ractor-shareable configuration data class for a CLI invocation.
|
|
13
19
|
Config = Data.define(
|
|
14
20
|
:type_name,
|
|
15
21
|
:endpoints,
|
|
@@ -41,6 +47,8 @@ module OMQ
|
|
|
41
47
|
:verbose,
|
|
42
48
|
:quiet,
|
|
43
49
|
:echo,
|
|
50
|
+
:scripts,
|
|
51
|
+
:recv_maxsz,
|
|
44
52
|
:curve_server,
|
|
45
53
|
:curve_server_key,
|
|
46
54
|
:curve_crypto,
|
|
@@ -48,7 +56,9 @@ module OMQ
|
|
|
48
56
|
:has_zstd,
|
|
49
57
|
:stdin_is_tty,
|
|
50
58
|
) do
|
|
59
|
+
# @return [Boolean] true if this socket type only sends
|
|
51
60
|
def send_only? = SEND_ONLY.include?(type_name)
|
|
61
|
+
# @return [Boolean] true if this socket type only receives
|
|
52
62
|
def recv_only? = RECV_ONLY.include?(type_name)
|
|
53
63
|
end
|
|
54
64
|
end
|