omq 0.3.1 → 0.4.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 +25 -0
- data/README.md +46 -0
- data/exe/omqcat +51 -24
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/transport/ipc.rb +1 -1
- data/lib/omq/zmtp/transport/tcp.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49a1c7caa7a1548615285ae294341b31833fd02bb60a7fa7cbfc0ed5c707d79
|
|
4
|
+
data.tar.gz: db36537c5dab13faa9cbdbe611c282cbf9dd96726e88145f80c3aaa7bf3df8a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e3a9e5a0b9ee3857a2946e7468ea2ca30c87544fa75965c79b1ea905c3a8d969aeb94b49282acd502d2ffbb6a0db5b2952f7a18a17e90363f1c5ed13cfbecd4
|
|
7
|
+
data.tar.gz: 1cb5f13312278d55684f22c420f17287a922633eebdfd143336415aafdc725dd7a38e23b5f9387cc55bf28acc0b125840c5770ef9112556b97b7c3cca8578ee6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Added (omqcat)
|
|
6
|
+
|
|
7
|
+
- `--curve-server` flag — generates ephemeral keypair, prints
|
|
8
|
+
`OMQ_SERVER_KEY=...` to stderr for easy copy-paste
|
|
9
|
+
- `--curve-server-key KEY` flag — CURVE client mode from the CLI
|
|
10
|
+
- `--echo` flag for REP — explicit echo mode
|
|
11
|
+
- REP reads stdin/`-F` as reply source (one line per reply, exits at EOF)
|
|
12
|
+
- REP without a reply source now aborts with a helpful error message
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- CURVE env vars renamed: `OMQ_SERVER_KEY`, `OMQ_SERVER_PUBLIC`,
|
|
17
|
+
`OMQ_SERVER_SECRET` (was `SERVER_KEY`, `SERVER_PUBLIC`, `SERVER_SECRET`)
|
|
18
|
+
- REP with `--echo`/`-D`/`-e` serves forever by default (like a server).
|
|
19
|
+
Use `-n 1` for one-shot, `-n` to limit exchanges. Stdin/`-F` replies
|
|
20
|
+
naturally terminate at EOF.
|
|
21
|
+
|
|
22
|
+
## 0.3.2 — 2026-03-26
|
|
23
|
+
|
|
24
|
+
### Improved
|
|
25
|
+
|
|
26
|
+
- Hide the warning about the experimental `IO::Buffer` (used by io-stream)
|
|
27
|
+
|
|
3
28
|
## 0.3.1 — 2026-03-26
|
|
4
29
|
|
|
5
30
|
### Improved
|
data/README.md
CHANGED
|
@@ -137,6 +137,52 @@ Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.1 +YJIT):
|
|
|
137
137
|
|
|
138
138
|
See [`bench/`](bench/) for full results and scripts.
|
|
139
139
|
|
|
140
|
+
## omqcat — CLI tool
|
|
141
|
+
|
|
142
|
+
`omqcat` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
# Echo server in one line
|
|
146
|
+
omqcat rep -b tcp://:5555 -e '$F.map(&:upcase)'
|
|
147
|
+
|
|
148
|
+
# Client
|
|
149
|
+
echo "hello" | omqcat req -c tcp://localhost:5555
|
|
150
|
+
# => HELLO
|
|
151
|
+
|
|
152
|
+
# PUB/SUB
|
|
153
|
+
omqcat sub -b tcp://:5556 -s "weather." &
|
|
154
|
+
echo "weather.nyc 72F" | omqcat pub -c tcp://localhost:5556 -d 0.3
|
|
155
|
+
|
|
156
|
+
# Pipeline with filtering
|
|
157
|
+
tail -f /var/log/syslog | omqcat push -c tcp://collector:5557
|
|
158
|
+
omqcat pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
|
|
159
|
+
|
|
160
|
+
# Multipart messages via tabs
|
|
161
|
+
printf "routing-key\tpayload data" | omqcat push -c tcp://localhost:5557
|
|
162
|
+
omqcat pull -b tcp://:5557
|
|
163
|
+
# => routing-key payload data
|
|
164
|
+
|
|
165
|
+
# JSONL for structured data
|
|
166
|
+
echo '["key","value"]' | omqcat push -c tcp://localhost:5557 -J
|
|
167
|
+
omqcat pull -b tcp://:5557 -J
|
|
168
|
+
|
|
169
|
+
# Zstandard compression
|
|
170
|
+
omqcat push -c tcp://remote:5557 -z < data.txt
|
|
171
|
+
omqcat pull -b tcp://:5557 -z
|
|
172
|
+
|
|
173
|
+
# CURVE encryption (auto-detected from env vars)
|
|
174
|
+
SERVER_KEY=... omqcat req -c tcp://secure:5555 -D "secret"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. Use `-r` to require gems:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
omqcat sub -c tcp://localhost:5556 -s "" -r json \
|
|
181
|
+
-e 'JSON.parse($F.first)["temperature"]'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Formats: `--ascii` (default, tab-separated), `--quoted`, `--raw`, `--jsonl`, `--msgpack`. See `omqcat --help` for all options.
|
|
185
|
+
|
|
140
186
|
## Interop with native ZMQ
|
|
141
187
|
|
|
142
188
|
OMQ speaks ZMTP 3.1 on the wire and interoperates with libzmq, CZMQ, pyzmq, etc. over **tcp** and **ipc**. The `inproc://` transport is OMQ-internal (in-process Ruby queues) and is not visible to native ZMQ running in the same process — use `ipc://` to talk across library boundaries.
|
data/exe/omqcat
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
|
+
Warning[:experimental] = false
|
|
3
4
|
|
|
4
5
|
#
|
|
5
6
|
# omqcat — command-line access to OMQ (ZeroMQ) sockets.
|
|
@@ -29,7 +30,7 @@ opts = {
|
|
|
29
30
|
identity: nil,
|
|
30
31
|
target: nil,
|
|
31
32
|
interval: nil,
|
|
32
|
-
count:
|
|
33
|
+
count: nil,
|
|
33
34
|
delay: nil,
|
|
34
35
|
recv_timeout: nil,
|
|
35
36
|
send_timeout: nil,
|
|
@@ -47,7 +48,8 @@ parser = OptionParser.new do |o|
|
|
|
47
48
|
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
|
|
48
49
|
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
|
|
49
50
|
|
|
50
|
-
o.separator "\nData source:"
|
|
51
|
+
o.separator "\nData source (REP: reply source):"
|
|
52
|
+
o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
|
|
51
53
|
o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
|
|
52
54
|
o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
|
|
53
55
|
|
|
@@ -79,6 +81,11 @@ parser = OptionParser.new do |o|
|
|
|
79
81
|
o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
|
|
80
82
|
o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
|
|
81
83
|
|
|
84
|
+
o.separator "\nCURVE encryption (requires omq-curve gem):"
|
|
85
|
+
o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
|
|
86
|
+
o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
|
|
87
|
+
o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
|
|
88
|
+
|
|
82
89
|
o.separator "\nOther:"
|
|
83
90
|
o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
|
|
84
91
|
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
@@ -215,21 +222,31 @@ end
|
|
|
215
222
|
|
|
216
223
|
# ── CURVE setup ─────────────────────────────────────────────────────
|
|
217
224
|
|
|
218
|
-
def setup_curve(sock)
|
|
219
|
-
|
|
225
|
+
def setup_curve(sock, opts)
|
|
226
|
+
server_key_z85 = opts[:curve_server_key] || ENV["OMQ_SERVER_KEY"]
|
|
227
|
+
server_mode = opts[:curve_server] || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
|
|
228
|
+
|
|
229
|
+
if server_key_z85
|
|
230
|
+
# Client mode
|
|
220
231
|
require "omq/curve"
|
|
221
|
-
server_key = OMQ::Z85.decode(
|
|
232
|
+
server_key = OMQ::Z85.decode(server_key_z85)
|
|
222
233
|
client_key = RbNaCl::PrivateKey.generate
|
|
223
234
|
sock.mechanism = OMQ::Curve.client(
|
|
224
235
|
client_key.public_key.to_s, client_key.to_s, server_key: server_key
|
|
225
236
|
)
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
elsif server_mode
|
|
238
|
+
# Server mode
|
|
228
239
|
require "omq/curve"
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
|
|
241
|
+
server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
|
|
242
|
+
server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
|
|
243
|
+
else
|
|
244
|
+
key = RbNaCl::PrivateKey.generate
|
|
245
|
+
server_pub = key.public_key.to_s
|
|
246
|
+
server_sec = key.to_s
|
|
247
|
+
end
|
|
231
248
|
sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
|
|
232
|
-
$stderr.puts "
|
|
249
|
+
$stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
|
|
233
250
|
end
|
|
234
251
|
rescue LoadError
|
|
235
252
|
abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
|
|
@@ -280,6 +297,10 @@ def eval_expr(parts, sock, opts)
|
|
|
280
297
|
end
|
|
281
298
|
end
|
|
282
299
|
|
|
300
|
+
def count_reached?(i, opts)
|
|
301
|
+
opts[:count] && opts[:count] > 0 && i >= opts[:count]
|
|
302
|
+
end
|
|
303
|
+
|
|
283
304
|
def output(parts, opts)
|
|
284
305
|
return if opts[:quiet]
|
|
285
306
|
parts = [""] if parts.nil?
|
|
@@ -303,7 +324,7 @@ def send_loop(sock, opts)
|
|
|
303
324
|
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
304
325
|
send_msg(sock, parts, opts) if parts
|
|
305
326
|
i += 1
|
|
306
|
-
break if
|
|
327
|
+
break if count_reached?(i, opts)
|
|
307
328
|
if opts[:interval]
|
|
308
329
|
sleep(opts[:interval])
|
|
309
330
|
else
|
|
@@ -318,7 +339,7 @@ def send_loop(sock, opts)
|
|
|
318
339
|
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
319
340
|
send_msg(sock, parts, opts) if parts
|
|
320
341
|
i += 1
|
|
321
|
-
break if
|
|
342
|
+
break if count_reached?(i, opts)
|
|
322
343
|
sleep(opts[:interval]) if opts[:interval]
|
|
323
344
|
end
|
|
324
345
|
end
|
|
@@ -331,7 +352,7 @@ def recv_loop(sock, opts)
|
|
|
331
352
|
parts = eval_expr(parts, sock, opts)
|
|
332
353
|
output(parts, opts)
|
|
333
354
|
i += 1
|
|
334
|
-
break if
|
|
355
|
+
break if count_reached?(i, opts)
|
|
335
356
|
end
|
|
336
357
|
end
|
|
337
358
|
|
|
@@ -346,7 +367,7 @@ def req_loop(sock, opts)
|
|
|
346
367
|
reply = eval_expr(reply, sock, opts)
|
|
347
368
|
output(reply, opts)
|
|
348
369
|
i += 1
|
|
349
|
-
break if
|
|
370
|
+
break if count_reached?(i, opts)
|
|
350
371
|
if opts[:interval]
|
|
351
372
|
sleep(opts[:interval])
|
|
352
373
|
elsif !opts[:data] && !opts[:file]
|
|
@@ -365,13 +386,19 @@ def rep_loop(sock, opts)
|
|
|
365
386
|
reply = eval_expr(msg, sock, opts)
|
|
366
387
|
output(reply, opts)
|
|
367
388
|
send_msg(sock, reply || [""], opts)
|
|
368
|
-
|
|
389
|
+
elsif opts[:echo]
|
|
390
|
+
output(msg, opts)
|
|
391
|
+
send_msg(sock, msg, opts)
|
|
392
|
+
elsif opts[:data] || opts[:file] || !$stdin.tty?
|
|
393
|
+
reply = read_next(opts)
|
|
394
|
+
break unless reply # EOF on stdin/-F
|
|
369
395
|
output(msg, opts)
|
|
370
|
-
reply = opts[:data] ? parse_input(opts[:data] + "\n", opts[:format]) : msg
|
|
371
396
|
send_msg(sock, reply, opts)
|
|
397
|
+
else
|
|
398
|
+
abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
|
|
372
399
|
end
|
|
373
400
|
i += 1
|
|
374
|
-
break if
|
|
401
|
+
break if count_reached?(i, opts)
|
|
375
402
|
end
|
|
376
403
|
end
|
|
377
404
|
|
|
@@ -383,7 +410,7 @@ def pair_loop(sock, opts, task)
|
|
|
383
410
|
parts = eval_expr(parts, sock, opts)
|
|
384
411
|
output(parts, opts)
|
|
385
412
|
i += 1
|
|
386
|
-
break if
|
|
413
|
+
break if count_reached?(i, opts)
|
|
387
414
|
end
|
|
388
415
|
end
|
|
389
416
|
|
|
@@ -396,13 +423,13 @@ def pair_loop(sock, opts, task)
|
|
|
396
423
|
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
397
424
|
send_msg(sock, parts, opts) if parts
|
|
398
425
|
i += 1
|
|
399
|
-
break if
|
|
426
|
+
break if count_reached?(i, opts)
|
|
400
427
|
break if (opts[:data] || opts[:file]) && !opts[:interval]
|
|
401
428
|
sleep(opts[:interval]) if opts[:interval]
|
|
402
429
|
end
|
|
403
430
|
end
|
|
404
431
|
|
|
405
|
-
if opts[:count] > 0
|
|
432
|
+
if opts[:count] && opts[:count] > 0
|
|
406
433
|
receiver.wait
|
|
407
434
|
sender.stop
|
|
408
435
|
else
|
|
@@ -424,7 +451,7 @@ def router_loop(sock, opts, task)
|
|
|
424
451
|
result = eval_expr([id_display, *parts], sock, opts)
|
|
425
452
|
output(result, opts)
|
|
426
453
|
i += 1
|
|
427
|
-
break if
|
|
454
|
+
break if count_reached?(i, opts)
|
|
428
455
|
end
|
|
429
456
|
end
|
|
430
457
|
|
|
@@ -439,7 +466,7 @@ def router_loop(sock, opts, task)
|
|
|
439
466
|
end
|
|
440
467
|
send_msg(sock, parts, opts)
|
|
441
468
|
i += 1
|
|
442
|
-
break if
|
|
469
|
+
break if count_reached?(i, opts)
|
|
443
470
|
break if (opts[:data] || opts[:file]) && !opts[:interval]
|
|
444
471
|
sleep(opts[:interval]) if opts[:interval]
|
|
445
472
|
end
|
|
@@ -447,7 +474,7 @@ def router_loop(sock, opts, task)
|
|
|
447
474
|
|
|
448
475
|
# If count is set, the receiver will exit when count is reached.
|
|
449
476
|
# Otherwise, wait for Ctrl-C.
|
|
450
|
-
if opts[:count] > 0
|
|
477
|
+
if opts[:count] && opts[:count] > 0
|
|
451
478
|
receiver.wait
|
|
452
479
|
sender.stop
|
|
453
480
|
else
|
|
@@ -472,7 +499,7 @@ Async do |task|
|
|
|
472
499
|
sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
|
|
473
500
|
sock.identity = opts[:identity] if opts[:identity]
|
|
474
501
|
|
|
475
|
-
setup_curve(sock)
|
|
502
|
+
setup_curve(sock, opts)
|
|
476
503
|
|
|
477
504
|
opts[:binds].each do |url|
|
|
478
505
|
sock.bind(url)
|
data/lib/omq/version.rb
CHANGED
|
@@ -35,7 +35,7 @@ module OMQ
|
|
|
35
35
|
engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: endpoint)
|
|
36
36
|
rescue ProtocolError, *ZMTP::CONNECTION_LOST
|
|
37
37
|
# peer disconnected during handshake
|
|
38
|
-
rescue
|
|
38
|
+
rescue
|
|
39
39
|
client&.close rescue nil
|
|
40
40
|
raise
|
|
41
41
|
end
|
|
@@ -32,7 +32,7 @@ module OMQ
|
|
|
32
32
|
engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: resolved)
|
|
33
33
|
rescue ProtocolError, *ZMTP::CONNECTION_LOST
|
|
34
34
|
# peer disconnected during handshake
|
|
35
|
-
rescue
|
|
35
|
+
rescue
|
|
36
36
|
client&.close rescue nil
|
|
37
37
|
raise
|
|
38
38
|
end
|