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
data/lib/omq/cli.rb
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "cli/version"
|
|
5
|
+
require_relative "cli/config"
|
|
6
|
+
require_relative "cli/formatter"
|
|
7
|
+
require_relative "cli/base_runner"
|
|
8
|
+
require_relative "cli/push_pull"
|
|
9
|
+
require_relative "cli/pub_sub"
|
|
10
|
+
require_relative "cli/scatter_gather"
|
|
11
|
+
require_relative "cli/radio_dish"
|
|
12
|
+
require_relative "cli/req_rep"
|
|
13
|
+
require_relative "cli/pair"
|
|
14
|
+
require_relative "cli/router_dealer"
|
|
15
|
+
require_relative "cli/channel"
|
|
16
|
+
require_relative "cli/client_server"
|
|
17
|
+
require_relative "cli/peer"
|
|
18
|
+
require_relative "cli/pipe"
|
|
19
|
+
|
|
20
|
+
module OMQ
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_reader :outgoing_proc, :incoming_proc
|
|
24
|
+
|
|
25
|
+
def outgoing(&block) = @outgoing_proc = block
|
|
26
|
+
def incoming(&block) = @incoming_proc = block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
module CLI
|
|
31
|
+
SOCKET_TYPE_NAMES = %w[
|
|
32
|
+
req rep pub sub push pull pair dealer router
|
|
33
|
+
client server radio dish scatter gather channel peer
|
|
34
|
+
pipe
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
RUNNER_MAP = {
|
|
39
|
+
"push" => [PushRunner, :PUSH],
|
|
40
|
+
"pull" => [PullRunner, :PULL],
|
|
41
|
+
"pub" => [PubRunner, :PUB],
|
|
42
|
+
"sub" => [SubRunner, :SUB],
|
|
43
|
+
"req" => [ReqRunner, :REQ],
|
|
44
|
+
"rep" => [RepRunner, :REP],
|
|
45
|
+
"dealer" => [DealerRunner, :DEALER],
|
|
46
|
+
"router" => [RouterRunner, :ROUTER],
|
|
47
|
+
"pair" => [PairRunner, :PAIR],
|
|
48
|
+
"client" => [ClientRunner, :CLIENT],
|
|
49
|
+
"server" => [ServerRunner, :SERVER],
|
|
50
|
+
"radio" => [RadioRunner, :RADIO],
|
|
51
|
+
"dish" => [DishRunner, :DISH],
|
|
52
|
+
"scatter" => [ScatterRunner, :SCATTER],
|
|
53
|
+
"gather" => [GatherRunner, :GATHER],
|
|
54
|
+
"channel" => [ChannelRunner, :CHANNEL],
|
|
55
|
+
"peer" => [PeerRunner, :PEER],
|
|
56
|
+
"pipe" => [PipeRunner, nil],
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
EXAMPLES = <<~'TEXT'
|
|
61
|
+
── Request / Reply ──────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
┌─────┐ "hello" ┌─────┐
|
|
64
|
+
│ REQ │────────────→│ REP │
|
|
65
|
+
│ │←────────────│ │
|
|
66
|
+
└─────┘ "HELLO" └─────┘
|
|
67
|
+
|
|
68
|
+
# terminal 1: echo server
|
|
69
|
+
omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
70
|
+
|
|
71
|
+
# terminal 2: send a request
|
|
72
|
+
echo "hello" | omq req --connect tcp://localhost:5555
|
|
73
|
+
|
|
74
|
+
# or over IPC (unix socket, single machine)
|
|
75
|
+
omq rep --bind ipc:///tmp/echo.sock --echo &
|
|
76
|
+
echo "hello" | omq req --connect ipc:///tmp/echo.sock
|
|
77
|
+
|
|
78
|
+
── Publish / Subscribe ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
┌─────┐ "weather.nyc 72F" ┌─────┐
|
|
81
|
+
│ PUB │────────────────────→│ SUB │ --subscribe "weather."
|
|
82
|
+
└─────┘ └─────┘
|
|
83
|
+
|
|
84
|
+
# terminal 1: subscriber (all topics by default)
|
|
85
|
+
omq sub --bind tcp://:5556
|
|
86
|
+
|
|
87
|
+
# terminal 2: publisher (needs --delay for subscription to propagate)
|
|
88
|
+
echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
|
|
89
|
+
|
|
90
|
+
── Periodic Publish ───────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
┌─────┐ "tick 1" ┌─────┐
|
|
93
|
+
│ PUB │──(every 1s)─→│ SUB │
|
|
94
|
+
└─────┘ └─────┘
|
|
95
|
+
|
|
96
|
+
# terminal 1: subscriber
|
|
97
|
+
omq sub --bind tcp://:5556
|
|
98
|
+
|
|
99
|
+
# terminal 2: publish a tick every second (wall-clock aligned)
|
|
100
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
101
|
+
--data "tick" --interval 1
|
|
102
|
+
|
|
103
|
+
# 5 ticks, then exit
|
|
104
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
105
|
+
--data "tick" --interval 1 --count 5
|
|
106
|
+
|
|
107
|
+
── Pipeline ─────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
┌──────┐ ┌──────┐
|
|
110
|
+
│ PUSH │──────────→│ PULL │
|
|
111
|
+
└──────┘ └──────┘
|
|
112
|
+
|
|
113
|
+
# terminal 1: worker
|
|
114
|
+
omq pull --bind tcp://:5557
|
|
115
|
+
|
|
116
|
+
# terminal 2: send tasks
|
|
117
|
+
echo "task 1" | omq push --connect tcp://localhost:5557
|
|
118
|
+
|
|
119
|
+
# or over IPC (unix socket)
|
|
120
|
+
omq pull --bind ipc:///tmp/pipeline.sock &
|
|
121
|
+
echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
|
|
122
|
+
|
|
123
|
+
── Pipe (PULL → eval → PUSH) ────────────────────────────────
|
|
124
|
+
|
|
125
|
+
┌──────┐ ┌──────┐ ┌──────┐
|
|
126
|
+
│ PUSH │────────→│ pipe │────────→│ PULL │
|
|
127
|
+
└──────┘ └──────┘ └──────┘
|
|
128
|
+
|
|
129
|
+
# terminal 1: producer
|
|
130
|
+
echo -e "hello\nworld" | omq push --bind ipc://@work
|
|
131
|
+
|
|
132
|
+
# terminal 2: worker — uppercase each message
|
|
133
|
+
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
134
|
+
# terminal 3: collector
|
|
135
|
+
omq pull --bind ipc://@sink
|
|
136
|
+
|
|
137
|
+
# 4 Ractor workers in a single process (-P)
|
|
138
|
+
omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
|
|
139
|
+
|
|
140
|
+
# exit when producer disconnects (--transient)
|
|
141
|
+
omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
|
|
142
|
+
|
|
143
|
+
# fan-in: multiple sources → one sink
|
|
144
|
+
omq pipe --in -c ipc://@work1 -c ipc://@work2 \
|
|
145
|
+
--out -c ipc://@sink -e '$F.map(&:upcase)'
|
|
146
|
+
|
|
147
|
+
# fan-out: one source → multiple sinks (round-robin)
|
|
148
|
+
omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
|
|
149
|
+
|
|
150
|
+
── CLIENT / SERVER (draft) ──────────────────────────────────
|
|
151
|
+
|
|
152
|
+
┌────────┐ "hello" ┌────────┐
|
|
153
|
+
│ CLIENT │───────────→│ SERVER │ --recv-eval '$F.map(&:upcase)'
|
|
154
|
+
│ │←───────────│ │
|
|
155
|
+
└────────┘ "HELLO" └────────┘
|
|
156
|
+
|
|
157
|
+
# terminal 1: upcasing server
|
|
158
|
+
omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
159
|
+
|
|
160
|
+
# terminal 2: client
|
|
161
|
+
echo "hello" | omq client --connect tcp://localhost:5555
|
|
162
|
+
|
|
163
|
+
── Formats ──────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
# ascii (default) — non-printable replaced with dots
|
|
166
|
+
omq pull --bind tcp://:5557 --ascii
|
|
167
|
+
|
|
168
|
+
# quoted — lossless, round-trippable (uses String#dump escaping)
|
|
169
|
+
omq pull --bind tcp://:5557 --quoted
|
|
170
|
+
|
|
171
|
+
# JSON Lines — structured, multipart as arrays
|
|
172
|
+
echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
|
|
173
|
+
omq pull --bind tcp://:5557 --jsonl
|
|
174
|
+
|
|
175
|
+
# multipart via tabs
|
|
176
|
+
printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
|
|
177
|
+
|
|
178
|
+
── Compression ──────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
# both sides must use --compress
|
|
181
|
+
omq pull --bind tcp://:5557 --compress &
|
|
182
|
+
echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
|
|
183
|
+
|
|
184
|
+
── CURVE Encryption ─────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
# server (prints OMQ_SERVER_KEY=...)
|
|
187
|
+
omq rep --bind tcp://:5555 --echo --curve-server
|
|
188
|
+
|
|
189
|
+
# client (paste the server's key)
|
|
190
|
+
echo "secret" | omq req --connect tcp://localhost:5555 \
|
|
191
|
+
--curve-server-key '<key from server>'
|
|
192
|
+
|
|
193
|
+
── ROUTER / DEALER ──────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
┌────────┐ ┌────────┐
|
|
196
|
+
│ DEALER │─────────→│ ROUTER │
|
|
197
|
+
│ id=w1 │ │ │
|
|
198
|
+
└────────┘ └────────┘
|
|
199
|
+
|
|
200
|
+
# terminal 1: router shows identity + message
|
|
201
|
+
omq router --bind tcp://:5555
|
|
202
|
+
|
|
203
|
+
# terminal 2: dealer with identity
|
|
204
|
+
echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
|
|
205
|
+
|
|
206
|
+
── Ruby Eval ────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
# filter incoming: only pass messages containing "error"
|
|
209
|
+
omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
|
|
210
|
+
|
|
211
|
+
# transform incoming with gems
|
|
212
|
+
omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
|
|
213
|
+
|
|
214
|
+
# require a local file, use its methods
|
|
215
|
+
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
|
|
216
|
+
|
|
217
|
+
# next skips, break stops — regexps match against $_
|
|
218
|
+
omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
|
|
219
|
+
|
|
220
|
+
# BEGIN/END blocks (like awk) — accumulate and summarize
|
|
221
|
+
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
|
|
222
|
+
|
|
223
|
+
# transform outgoing messages
|
|
224
|
+
echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
|
|
225
|
+
|
|
226
|
+
# REQ: transform request and reply independently
|
|
227
|
+
echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
|
|
228
|
+
|
|
229
|
+
── Script Handlers (-r) ────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
# handler.rb — register transforms from a file
|
|
232
|
+
# db = PG.connect("dbname=app")
|
|
233
|
+
# OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
|
|
234
|
+
# at_exit { db.close }
|
|
235
|
+
omq pull --bind tcp://:5557 -r./handler.rb
|
|
236
|
+
|
|
237
|
+
# combine script handlers with inline eval
|
|
238
|
+
omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
|
|
239
|
+
|
|
240
|
+
# OMQ.outgoing { |msg| ... } — registered outgoing transform
|
|
241
|
+
# OMQ.incoming { |msg| ... } — registered incoming transform
|
|
242
|
+
# CLI flags (-e/-E) override registered handlers
|
|
243
|
+
TEXT
|
|
244
|
+
|
|
245
|
+
module_function
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Displays text through the system pager, or prints directly
|
|
249
|
+
# when stdout is not a terminal.
|
|
250
|
+
#
|
|
251
|
+
def page(text)
|
|
252
|
+
if $stdout.tty?
|
|
253
|
+
if ENV["PAGER"]
|
|
254
|
+
pager = ENV["PAGER"]
|
|
255
|
+
else
|
|
256
|
+
ENV["LESS"] ||= "-FR"
|
|
257
|
+
pager = "less"
|
|
258
|
+
end
|
|
259
|
+
IO.popen(pager, "w") { |io| io.puts text }
|
|
260
|
+
else
|
|
261
|
+
puts text
|
|
262
|
+
end
|
|
263
|
+
rescue Errno::ENOENT
|
|
264
|
+
puts text
|
|
265
|
+
rescue Errno::EPIPE
|
|
266
|
+
# user quit pager early
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Parses CLI arguments, validates options, and runs the main
|
|
271
|
+
# event loop inside an Async reactor.
|
|
272
|
+
#
|
|
273
|
+
def run(argv = ARGV)
|
|
274
|
+
config = build_config(argv)
|
|
275
|
+
|
|
276
|
+
require "omq"
|
|
277
|
+
require "async"
|
|
278
|
+
require "json"
|
|
279
|
+
require "console"
|
|
280
|
+
|
|
281
|
+
validate_gems!(config)
|
|
282
|
+
|
|
283
|
+
trap("INT") { Process.exit!(0) }
|
|
284
|
+
trap("TERM") { Process.exit!(0) }
|
|
285
|
+
|
|
286
|
+
Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
|
|
287
|
+
|
|
288
|
+
runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
|
|
289
|
+
|
|
290
|
+
Async do |task|
|
|
291
|
+
runner = if socket_sym
|
|
292
|
+
runner_class.new(config, OMQ.const_get(socket_sym))
|
|
293
|
+
else
|
|
294
|
+
runner_class.new(config)
|
|
295
|
+
end
|
|
296
|
+
runner.call(task)
|
|
297
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
298
|
+
$stderr.puts "omq: timeout" unless config.quiet
|
|
299
|
+
exit 2
|
|
300
|
+
rescue OMQ::SocketDeadError => e
|
|
301
|
+
$stderr.puts "omq: #{e.cause.class}: #{e.cause.message}"
|
|
302
|
+
exit 1
|
|
303
|
+
rescue ::Socket::ResolutionError => e
|
|
304
|
+
$stderr.puts "omq: #{e.message}"
|
|
305
|
+
exit 1
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Builds a frozen Config from command-line arguments.
|
|
311
|
+
#
|
|
312
|
+
def build_config(argv)
|
|
313
|
+
opts = parse_options(argv)
|
|
314
|
+
validate!(opts)
|
|
315
|
+
|
|
316
|
+
opts[:has_msgpack] = begin; require "msgpack"; true; rescue LoadError; false; end
|
|
317
|
+
opts[:has_zstd] = begin; require "zstd-ruby"; true; rescue LoadError; false; end
|
|
318
|
+
opts[:stdin_is_tty] = $stdin.tty?
|
|
319
|
+
|
|
320
|
+
Ractor.make_shareable(Config.new(**opts))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Parses command-line arguments into a mutable options hash.
|
|
325
|
+
#
|
|
326
|
+
def parse_options(argv)
|
|
327
|
+
opts = {
|
|
328
|
+
endpoints: [],
|
|
329
|
+
connects: [],
|
|
330
|
+
binds: [],
|
|
331
|
+
in_endpoints: [],
|
|
332
|
+
out_endpoints: [],
|
|
333
|
+
data: nil,
|
|
334
|
+
file: nil,
|
|
335
|
+
format: :ascii,
|
|
336
|
+
subscribes: [],
|
|
337
|
+
joins: [],
|
|
338
|
+
group: nil,
|
|
339
|
+
identity: nil,
|
|
340
|
+
target: nil,
|
|
341
|
+
interval: nil,
|
|
342
|
+
count: nil,
|
|
343
|
+
delay: nil,
|
|
344
|
+
timeout: nil,
|
|
345
|
+
linger: 5,
|
|
346
|
+
reconnect_ivl: nil,
|
|
347
|
+
heartbeat_ivl: nil,
|
|
348
|
+
conflate: false,
|
|
349
|
+
compress: false,
|
|
350
|
+
send_expr: nil,
|
|
351
|
+
recv_expr: nil,
|
|
352
|
+
parallel: nil,
|
|
353
|
+
transient: false,
|
|
354
|
+
verbose: false,
|
|
355
|
+
quiet: false,
|
|
356
|
+
echo: false,
|
|
357
|
+
curve_server: false,
|
|
358
|
+
curve_server_key: nil,
|
|
359
|
+
curve_crypto: nil,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
pipe_side = nil # nil = legacy positional mode; :in/:out = modal
|
|
363
|
+
|
|
364
|
+
parser = OptionParser.new do |o|
|
|
365
|
+
o.banner = "Usage: omq TYPE [options]\n\n" \
|
|
366
|
+
"Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
|
|
367
|
+
"Draft: client, server, radio, dish, scatter, gather, channel, peer\n" \
|
|
368
|
+
"Virtual: pipe (PULL → eval → PUSH)\n\n"
|
|
369
|
+
|
|
370
|
+
o.separator "Connection:"
|
|
371
|
+
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
|
|
372
|
+
ep = Endpoint.new(v, false)
|
|
373
|
+
case pipe_side
|
|
374
|
+
when :in then opts[:in_endpoints] << ep
|
|
375
|
+
when :out then opts[:out_endpoints] << ep
|
|
376
|
+
else opts[:endpoints] << ep; opts[:connects] << v
|
|
377
|
+
end
|
|
378
|
+
}
|
|
379
|
+
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
|
|
380
|
+
ep = Endpoint.new(v, true)
|
|
381
|
+
case pipe_side
|
|
382
|
+
when :in then opts[:in_endpoints] << ep
|
|
383
|
+
when :out then opts[:out_endpoints] << ep
|
|
384
|
+
else opts[:endpoints] << ep; opts[:binds] << v
|
|
385
|
+
end
|
|
386
|
+
}
|
|
387
|
+
o.on("--in", "Pipe: subsequent -b/-c attach to input (PULL) side") { pipe_side = :in }
|
|
388
|
+
o.on("--out", "Pipe: subsequent -b/-c attach to output (PUSH) side") { pipe_side = :out }
|
|
389
|
+
|
|
390
|
+
o.separator "\nData source (REP: reply source):"
|
|
391
|
+
o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
|
|
392
|
+
o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
|
|
393
|
+
o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
|
|
394
|
+
|
|
395
|
+
o.separator "\nFormat (input + output):"
|
|
396
|
+
o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
|
|
397
|
+
o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
|
|
398
|
+
o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
|
|
399
|
+
o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
|
|
400
|
+
o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
|
|
401
|
+
o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
|
|
402
|
+
|
|
403
|
+
o.separator "\nSubscription/groups:"
|
|
404
|
+
o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
|
|
405
|
+
o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
|
|
406
|
+
o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
|
|
407
|
+
|
|
408
|
+
o.separator "\nIdentity/routing:"
|
|
409
|
+
o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
|
|
410
|
+
o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
|
|
411
|
+
|
|
412
|
+
o.separator "\nTiming:"
|
|
413
|
+
o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
|
|
414
|
+
o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
|
|
415
|
+
o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
|
|
416
|
+
o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
|
|
417
|
+
o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
|
|
418
|
+
o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
|
|
419
|
+
opts[:reconnect_ivl] = if v.include?("..")
|
|
420
|
+
lo, hi = v.split("..", 2)
|
|
421
|
+
Float(lo)..Float(hi)
|
|
422
|
+
else
|
|
423
|
+
Float(v)
|
|
424
|
+
end
|
|
425
|
+
}
|
|
426
|
+
o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
|
|
427
|
+
|
|
428
|
+
o.separator "\nDelivery:"
|
|
429
|
+
o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
|
|
430
|
+
|
|
431
|
+
o.separator "\nCompression:"
|
|
432
|
+
o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
|
|
433
|
+
|
|
434
|
+
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
435
|
+
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
|
|
436
|
+
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
|
|
437
|
+
o.on("-r", "--require LIB", "Require lib/file; scripts can register OMQ.outgoing/incoming") { |v|
|
|
438
|
+
require "omq" unless defined?(OMQ::VERSION)
|
|
439
|
+
v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
|
|
440
|
+
}
|
|
441
|
+
o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
|
|
442
|
+
require "etc"
|
|
443
|
+
opts[:parallel] = v || Etc.nprocessors
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
o.separator "\nCURVE encryption (requires rbnacl or nuckle gem):"
|
|
447
|
+
o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
|
|
448
|
+
o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
|
|
449
|
+
o.on("--curve-crypto BACKEND", "Crypto backend: rbnacl (default if installed) or nuckle") { |v| opts[:curve_crypto] = v }
|
|
450
|
+
o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
|
|
451
|
+
o.separator " OMQ_CURVE_CRYPTO (backend: rbnacl or nuckle)"
|
|
452
|
+
|
|
453
|
+
o.separator "\nOther:"
|
|
454
|
+
o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
|
|
455
|
+
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
456
|
+
o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
|
|
457
|
+
o.on("-V", "--version") { require "omq"; puts "omq-cli #{OMQ::CLI::VERSION} (omq #{OMQ::VERSION})"; exit }
|
|
458
|
+
o.on("-h") { puts o; exit }
|
|
459
|
+
o.on( "--help") { page "#{o}\n#{EXAMPLES}"; exit }
|
|
460
|
+
o.on( "--examples") { page EXAMPLES; exit }
|
|
461
|
+
|
|
462
|
+
o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
begin
|
|
466
|
+
parser.parse!(argv)
|
|
467
|
+
rescue OptionParser::ParseError => e
|
|
468
|
+
abort e.message
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
type_name = argv.shift
|
|
472
|
+
abort parser.to_s unless type_name
|
|
473
|
+
unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
|
|
474
|
+
abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
opts[:type_name] = type_name.downcase
|
|
478
|
+
|
|
479
|
+
normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
|
|
480
|
+
normalize_ep = ->(ep) { Endpoint.new(normalize.call(ep.url), ep.bind?) }
|
|
481
|
+
opts[:binds].map!(&normalize)
|
|
482
|
+
opts[:connects].map!(&normalize)
|
|
483
|
+
opts[:endpoints].map!(&normalize_ep)
|
|
484
|
+
opts[:in_endpoints].map!(&normalize_ep)
|
|
485
|
+
opts[:out_endpoints].map!(&normalize_ep)
|
|
486
|
+
|
|
487
|
+
opts
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# Validates option combinations.
|
|
492
|
+
#
|
|
493
|
+
def validate!(opts)
|
|
494
|
+
type_name = opts[:type_name]
|
|
495
|
+
|
|
496
|
+
if type_name == "pipe"
|
|
497
|
+
has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
498
|
+
if has_in_out
|
|
499
|
+
abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
|
|
500
|
+
abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
|
|
501
|
+
abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
|
|
502
|
+
else
|
|
503
|
+
abort "pipe requires exactly 2 endpoints (pull-side and push-side), or use --in/--out" if opts[:endpoints].size != 2
|
|
504
|
+
end
|
|
505
|
+
else
|
|
506
|
+
abort "--in/--out are only valid for pipe" if opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
507
|
+
abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
|
|
508
|
+
end
|
|
509
|
+
abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
|
|
510
|
+
abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
|
|
511
|
+
abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
|
|
512
|
+
abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
|
|
513
|
+
abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
|
|
514
|
+
abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
|
|
515
|
+
abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
|
|
516
|
+
abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
|
|
517
|
+
abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
|
|
518
|
+
abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
|
|
519
|
+
|
|
520
|
+
if opts[:parallel]
|
|
521
|
+
abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
|
|
522
|
+
abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
|
|
523
|
+
all_pipe_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
|
|
524
|
+
abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_pipe_eps.any?(&:bind?)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
(opts[:connects] + opts[:binds]).each do |url|
|
|
528
|
+
abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# Validates that required gems are available.
|
|
534
|
+
#
|
|
535
|
+
def validate_gems!(config)
|
|
536
|
+
abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
|
|
537
|
+
abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
|
|
538
|
+
|
|
539
|
+
if config.recv_only? && (config.data || config.file)
|
|
540
|
+
abort "--data/--file not valid for #{config.type_name} (receive-only)"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omq-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: omq
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.8'
|
|
26
|
+
description: Command-line tool for sending and receiving ZeroMQ messages on any socket
|
|
27
|
+
type (REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, and all draft types). Supports
|
|
28
|
+
Ruby eval (-e/-E), script handlers (-r), pipe virtual socket with Ractor parallelism,
|
|
29
|
+
multiple formats (ASCII, JSON Lines, msgpack, Marshal), Zstandard compression, and
|
|
30
|
+
CURVE encryption. Like nngcat from libnng, but with Ruby superpowers.
|
|
31
|
+
email:
|
|
32
|
+
- paddor@gmail.com
|
|
33
|
+
executables:
|
|
34
|
+
- omq
|
|
35
|
+
extensions: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
files:
|
|
38
|
+
- LICENSE
|
|
39
|
+
- README.md
|
|
40
|
+
- exe/omq
|
|
41
|
+
- lib/omq/cli.rb
|
|
42
|
+
- lib/omq/cli/base_runner.rb
|
|
43
|
+
- lib/omq/cli/channel.rb
|
|
44
|
+
- lib/omq/cli/client_server.rb
|
|
45
|
+
- lib/omq/cli/config.rb
|
|
46
|
+
- lib/omq/cli/formatter.rb
|
|
47
|
+
- lib/omq/cli/pair.rb
|
|
48
|
+
- lib/omq/cli/peer.rb
|
|
49
|
+
- lib/omq/cli/pipe.rb
|
|
50
|
+
- lib/omq/cli/pub_sub.rb
|
|
51
|
+
- lib/omq/cli/push_pull.rb
|
|
52
|
+
- lib/omq/cli/radio_dish.rb
|
|
53
|
+
- lib/omq/cli/req_rep.rb
|
|
54
|
+
- lib/omq/cli/router_dealer.rb
|
|
55
|
+
- lib/omq/cli/scatter_gather.rb
|
|
56
|
+
- lib/omq/cli/version.rb
|
|
57
|
+
homepage: https://github.com/paddor/omq-cli
|
|
58
|
+
licenses:
|
|
59
|
+
- ISC
|
|
60
|
+
metadata: {}
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.3'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 4.0.6
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: ZeroMQ CLI — pipe, filter, and transform messages from the terminal
|
|
78
|
+
test_files: []
|