omq 0.2.2 → 0.3.1
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 +27 -0
- data/README.md +2 -2
- data/exe/omqcat +505 -0
- data/lib/omq/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d77750001a2c45808301e5a005f83c8a6f4df44ffa033aa121cc9229b597347
|
|
4
|
+
data.tar.gz: b009c94d5ea30d5e0983831fb443e1ceb31e8980725b1b9c75b9403814a7a469
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6612b16ae23874f1539154f108359190ac70eeaf9691c326deb4b2545151590e4458071912a987258b9d59cf80c5015024fbfa546d1f5bbcd979564be5cdffe
|
|
7
|
+
data.tar.gz: 7706e73997b1a96446e6c5e6053756e57e4b33d6b084a82c49f78e40b9454aa26aed1d5e3ca14207c450be75864707618c3a87cf27c750020064393dbe85c71f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-03-26
|
|
4
|
+
|
|
5
|
+
### Improved
|
|
6
|
+
|
|
7
|
+
- `omqcat --help` responds in ~90ms (was ~470ms) — defer heavy gem loading
|
|
8
|
+
until after option parsing
|
|
9
|
+
|
|
10
|
+
## 0.3.0 — 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `omqcat` CLI tool — nngcat-like Swiss army knife for OMQ sockets
|
|
15
|
+
- Socket types: req, rep, pub, sub, push, pull, pair, dealer, router
|
|
16
|
+
- Formats: ascii (default, tab-separated), quoted, raw, jsonl, msgpack
|
|
17
|
+
- `-e` / `--eval` — Ruby code runs inside the socket instance
|
|
18
|
+
(`$F` = message parts, full socket API available: `self <<`, `send`,
|
|
19
|
+
`subscribe`, etc.). REP auto-replies with the return value;
|
|
20
|
+
PAIR/DEALER use `self <<` explicitly
|
|
21
|
+
- `-r` / `--require` to load gems for use in `-e`
|
|
22
|
+
- `-z` / `--compress` Zstandard compression per frame (requires `zstd-ruby`)
|
|
23
|
+
- `-D` / `-F` data sources, `-i` interval, `-n` count, `-d` delay
|
|
24
|
+
- CURVE encryption via `SERVER_KEY` / `SERVER_PUBLIC` + `SERVER_SECRET`
|
|
25
|
+
env vars (requires `omq-curve`)
|
|
26
|
+
- `--identity` / `--target` for DEALER/ROUTER patterns
|
|
27
|
+
- `tcp://:PORT` shorthand for `tcp://*:PORT` (no shell glob issues)
|
|
28
|
+
- 22 system tests via `rake test:cli`
|
|
29
|
+
|
|
3
30
|
## 0.2.2 — 2026-03-26
|
|
4
31
|
|
|
5
32
|
### Added
|
data/README.md
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
Pure Ruby implementation of the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol ([ZeroMQ](https://zeromq.org/)) using the [Async](https://github.com/socketry/async) gem. No native libraries required.
|
|
9
9
|
|
|
10
|
-
> **
|
|
10
|
+
> **145k msg/s** inproc | **40k msg/s** ipc | **32k msg/s** tcp
|
|
11
11
|
>
|
|
12
12
|
> **15 µs** inproc latency | **62 µs** ipc | **88 µs** tcp
|
|
13
13
|
>
|
|
14
|
-
> Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel)
|
|
14
|
+
> Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel) — [223k msg/s with io_uring](bench/README.md#io_uring)
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
data/exe/omqcat
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# omqcat — command-line access to OMQ (ZeroMQ) sockets.
|
|
6
|
+
#
|
|
7
|
+
# Usage: omqcat TYPE [options]
|
|
8
|
+
#
|
|
9
|
+
# Examples:
|
|
10
|
+
# omqcat rep -b tcp://:5555 -D "pong"
|
|
11
|
+
# echo "ping" | omqcat req -c tcp://localhost:5555
|
|
12
|
+
# omqcat sub -c tcp://localhost:5556 -s "weather."
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
require "optparse"
|
|
16
|
+
|
|
17
|
+
SOCKET_TYPE_NAMES = %w[req rep pub sub push pull pair dealer router].freeze
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Option parsing ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
opts = {
|
|
23
|
+
connects: [],
|
|
24
|
+
binds: [],
|
|
25
|
+
data: nil,
|
|
26
|
+
file: nil,
|
|
27
|
+
format: :ascii,
|
|
28
|
+
subscribes: [],
|
|
29
|
+
identity: nil,
|
|
30
|
+
target: nil,
|
|
31
|
+
interval: nil,
|
|
32
|
+
count: 0,
|
|
33
|
+
delay: nil,
|
|
34
|
+
recv_timeout: nil,
|
|
35
|
+
send_timeout: nil,
|
|
36
|
+
compress: false,
|
|
37
|
+
expr: nil,
|
|
38
|
+
verbose: false,
|
|
39
|
+
quiet: false,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
parser = OptionParser.new do |o|
|
|
43
|
+
o.banner = "Usage: omqcat TYPE [options]\n\n" \
|
|
44
|
+
"Types: #{SOCKET_TYPE_NAMES.join(', ')}\n\n"
|
|
45
|
+
|
|
46
|
+
o.separator "Connection:"
|
|
47
|
+
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
|
|
48
|
+
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
|
|
49
|
+
|
|
50
|
+
o.separator "\nData source:"
|
|
51
|
+
o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
|
|
52
|
+
o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
|
|
53
|
+
|
|
54
|
+
o.separator "\nFormat (input + output):"
|
|
55
|
+
o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
|
|
56
|
+
o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
|
|
57
|
+
o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
|
|
58
|
+
o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
|
|
59
|
+
o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
|
|
60
|
+
|
|
61
|
+
o.separator "\nSubscription:"
|
|
62
|
+
o.on("-s", "--subscribe PREFIX", "Subscribe prefix (repeatable, SUB only)") { |v| opts[:subscribes] << v }
|
|
63
|
+
|
|
64
|
+
o.separator "\nIdentity/routing:"
|
|
65
|
+
o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
|
|
66
|
+
o.on("--target ID", "Target peer identity (ROUTER sending)") { |v| opts[:target] = v }
|
|
67
|
+
|
|
68
|
+
o.separator "\nTiming:"
|
|
69
|
+
o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
|
|
70
|
+
o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
|
|
71
|
+
o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
|
|
72
|
+
o.on("-t", "--recv-timeout SECS", Float, "Receive timeout") { |v| opts[:recv_timeout] = v }
|
|
73
|
+
o.on( "--send-timeout SECS", Float, "Send timeout") { |v| opts[:send_timeout] = v }
|
|
74
|
+
|
|
75
|
+
o.separator "\nCompression:"
|
|
76
|
+
o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
|
|
77
|
+
|
|
78
|
+
o.separator "\nProcessing:"
|
|
79
|
+
o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
|
|
80
|
+
o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
|
|
81
|
+
|
|
82
|
+
o.separator "\nOther:"
|
|
83
|
+
o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
|
|
84
|
+
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
85
|
+
o.on("-V", "--version") { require "omq"; puts "omqcat #{OMQ::VERSION}"; exit }
|
|
86
|
+
o.on("-h", "--help") { puts o; exit }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
parser.parse!
|
|
91
|
+
rescue OptionParser::ParseError => e
|
|
92
|
+
abort e.message
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
type_name = ARGV.shift
|
|
96
|
+
abort parser.to_s unless type_name
|
|
97
|
+
abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}" unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
|
|
98
|
+
|
|
99
|
+
# ── Validation (fast, before loading gems) ──────────────────────────
|
|
100
|
+
|
|
101
|
+
abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
|
|
102
|
+
abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
|
|
103
|
+
abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name.downcase != "sub"
|
|
104
|
+
abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name.downcase)
|
|
105
|
+
abort "--target is only valid for ROUTER" if opts[:target] && type_name.downcase != "router"
|
|
106
|
+
|
|
107
|
+
# ── Load gems ───────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
require "omq"
|
|
110
|
+
require "async"
|
|
111
|
+
require "json"
|
|
112
|
+
require "console"
|
|
113
|
+
|
|
114
|
+
HAS_MSGPACK = begin; require "msgpack"; true; rescue LoadError; false; end
|
|
115
|
+
HAS_ZSTD = begin; require "zstd-ruby"; true; rescue LoadError; false; end
|
|
116
|
+
|
|
117
|
+
SOCKET_TYPES = {
|
|
118
|
+
"req" => OMQ::REQ, "rep" => OMQ::REP,
|
|
119
|
+
"pub" => OMQ::PUB, "sub" => OMQ::SUB,
|
|
120
|
+
"push" => OMQ::PUSH, "pull" => OMQ::PULL,
|
|
121
|
+
"pair" => OMQ::PAIR,
|
|
122
|
+
"dealer" => OMQ::DEALER, "router" => OMQ::ROUTER,
|
|
123
|
+
}.freeze
|
|
124
|
+
|
|
125
|
+
SEND_ONLY = [OMQ::PUB, OMQ::PUSH].freeze
|
|
126
|
+
RECV_ONLY = [OMQ::SUB, OMQ::PULL].freeze
|
|
127
|
+
|
|
128
|
+
klass = SOCKET_TYPES[type_name.downcase]
|
|
129
|
+
|
|
130
|
+
abort "--msgpack requires the msgpack gem" if opts[:format] == :msgpack && !HAS_MSGPACK
|
|
131
|
+
abort "--compress requires the zstd-ruby gem" if opts[:compress] && !HAS_ZSTD
|
|
132
|
+
|
|
133
|
+
(opts[:connects] + opts[:binds]).each do |url|
|
|
134
|
+
abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if RECV_ONLY.include?(klass) && (opts[:data] || opts[:file])
|
|
138
|
+
abort "--data/--file not valid for #{type_name} (receive-only)"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ── URL normalization ───────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
|
|
144
|
+
opts[:connects].map!(&normalize)
|
|
145
|
+
opts[:binds].map!(&normalize)
|
|
146
|
+
|
|
147
|
+
# ── Format helpers ──────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def format_output(parts, fmt)
|
|
150
|
+
case fmt
|
|
151
|
+
when :ascii
|
|
152
|
+
parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
|
|
153
|
+
when :quoted
|
|
154
|
+
parts.map { |p|
|
|
155
|
+
p.b.gsub(/[^[:print:]]/) { |c|
|
|
156
|
+
case c
|
|
157
|
+
when "\n" then "\\n"
|
|
158
|
+
when "\r" then "\\r"
|
|
159
|
+
when "\t" then "\\t"
|
|
160
|
+
when "\\" then "\\\\"
|
|
161
|
+
else format("\\x%02x", c.ord)
|
|
162
|
+
end
|
|
163
|
+
}
|
|
164
|
+
}.join("\t") + "\n"
|
|
165
|
+
when :raw
|
|
166
|
+
parts.join
|
|
167
|
+
when :jsonl
|
|
168
|
+
JSON.generate(parts) + "\n"
|
|
169
|
+
when :msgpack
|
|
170
|
+
MessagePack.pack(parts)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_input(line, fmt)
|
|
175
|
+
case fmt
|
|
176
|
+
when :ascii
|
|
177
|
+
line.chomp.split("\t")
|
|
178
|
+
when :quoted
|
|
179
|
+
line.chomp.split("\t").map { |p|
|
|
180
|
+
p.gsub(/\\(?:n|r|t|\\|x[0-9a-fA-F]{2})/) { |m|
|
|
181
|
+
case m
|
|
182
|
+
when "\\n" then "\n"
|
|
183
|
+
when "\\r" then "\r"
|
|
184
|
+
when "\\t" then "\t"
|
|
185
|
+
when "\\\\" then "\\"
|
|
186
|
+
else [m[2..].to_i(16)].pack("C")
|
|
187
|
+
end
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
when :raw
|
|
191
|
+
[line]
|
|
192
|
+
when :jsonl
|
|
193
|
+
arr = JSON.parse(line.chomp)
|
|
194
|
+
abort "JSON Lines input must be an array of strings" unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
|
|
195
|
+
arr
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def read_message_msgpack(io)
|
|
200
|
+
@msgpack_unpacker ||= MessagePack::Unpacker.new(io)
|
|
201
|
+
@msgpack_unpacker.read
|
|
202
|
+
rescue EOFError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# ── Compression helpers ─────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
def compress_frames(parts)
|
|
209
|
+
parts.map { |p| Zstd.compress(p) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def decompress_frames(parts)
|
|
213
|
+
parts.map { |p| Zstd.decompress(p) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# ── CURVE setup ─────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
def setup_curve(sock)
|
|
219
|
+
if ENV["SERVER_KEY"]
|
|
220
|
+
require "omq/curve"
|
|
221
|
+
server_key = OMQ::Z85.decode(ENV["SERVER_KEY"])
|
|
222
|
+
client_key = RbNaCl::PrivateKey.generate
|
|
223
|
+
sock.mechanism = OMQ::Curve.client(
|
|
224
|
+
client_key.public_key.to_s, client_key.to_s, server_key: server_key
|
|
225
|
+
)
|
|
226
|
+
$stderr.puts "CURVE client mode" if $VERBOSE
|
|
227
|
+
elsif ENV["SERVER_PUBLIC"] && ENV["SERVER_SECRET"]
|
|
228
|
+
require "omq/curve"
|
|
229
|
+
server_pub = OMQ::Z85.decode(ENV["SERVER_PUBLIC"])
|
|
230
|
+
server_sec = OMQ::Z85.decode(ENV["SERVER_SECRET"])
|
|
231
|
+
sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
|
|
232
|
+
$stderr.puts "SERVER_KEY=#{ENV["SERVER_PUBLIC"]}"
|
|
233
|
+
end
|
|
234
|
+
rescue LoadError
|
|
235
|
+
abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# ── I/O helpers ─────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def read_next(opts)
|
|
241
|
+
if opts[:data]
|
|
242
|
+
parse_input(opts[:data] + "\n", opts[:format])
|
|
243
|
+
elsif opts[:file]
|
|
244
|
+
@file_data ||= (opts[:file] == "-" ? $stdin.read : File.read(opts[:file])).chomp
|
|
245
|
+
parse_input(@file_data + "\n", opts[:format])
|
|
246
|
+
elsif opts[:format] == :msgpack
|
|
247
|
+
read_message_msgpack($stdin)
|
|
248
|
+
elsif opts[:format] == :raw
|
|
249
|
+
data = $stdin.read
|
|
250
|
+
return nil if data.nil? || data.empty?
|
|
251
|
+
[data]
|
|
252
|
+
else
|
|
253
|
+
line = $stdin.gets
|
|
254
|
+
return nil if line.nil?
|
|
255
|
+
parse_input(line, opts[:format])
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def send_msg(sock, parts, opts)
|
|
260
|
+
return if parts.empty?
|
|
261
|
+
parts = compress_frames(parts) if opts[:compress]
|
|
262
|
+
sock.send(parts)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def recv_msg(sock, opts)
|
|
266
|
+
parts = sock.receive
|
|
267
|
+
parts = decompress_frames(parts) if opts[:compress]
|
|
268
|
+
parts
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def eval_expr(parts, sock, opts)
|
|
272
|
+
return parts unless opts[:expr]
|
|
273
|
+
$F = parts
|
|
274
|
+
result = sock.instance_eval(opts[:expr], "-e") # rubocop:disable Security/Eval
|
|
275
|
+
case result
|
|
276
|
+
when nil then nil
|
|
277
|
+
when Array then result
|
|
278
|
+
when String then [result]
|
|
279
|
+
else [result.to_s]
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def output(parts, opts)
|
|
284
|
+
return if opts[:quiet]
|
|
285
|
+
parts = [""] if parts.nil?
|
|
286
|
+
$stdout.write(format_output(parts, opts[:format]))
|
|
287
|
+
$stdout.flush
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def log(msg)
|
|
291
|
+
$stderr.puts(msg)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# ── Loop methods ────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
def send_loop(sock, opts)
|
|
297
|
+
i = 0
|
|
298
|
+
if opts[:data] || opts[:file]
|
|
299
|
+
loop do
|
|
300
|
+
parts = read_next(opts)
|
|
301
|
+
break unless parts
|
|
302
|
+
parts = eval_expr(parts, sock, opts)
|
|
303
|
+
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
304
|
+
send_msg(sock, parts, opts) if parts
|
|
305
|
+
i += 1
|
|
306
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
307
|
+
if opts[:interval]
|
|
308
|
+
sleep(opts[:interval])
|
|
309
|
+
else
|
|
310
|
+
break # single send without -i
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
else
|
|
314
|
+
loop do
|
|
315
|
+
parts = read_next(opts)
|
|
316
|
+
break unless parts
|
|
317
|
+
parts = eval_expr(parts, sock, opts)
|
|
318
|
+
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
319
|
+
send_msg(sock, parts, opts) if parts
|
|
320
|
+
i += 1
|
|
321
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
322
|
+
sleep(opts[:interval]) if opts[:interval]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def recv_loop(sock, opts)
|
|
328
|
+
i = 0
|
|
329
|
+
loop do
|
|
330
|
+
parts = recv_msg(sock, opts)
|
|
331
|
+
parts = eval_expr(parts, sock, opts)
|
|
332
|
+
output(parts, opts)
|
|
333
|
+
i += 1
|
|
334
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def req_loop(sock, opts)
|
|
339
|
+
i = 0
|
|
340
|
+
loop do
|
|
341
|
+
parts = read_next(opts)
|
|
342
|
+
break unless parts
|
|
343
|
+
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
344
|
+
send_msg(sock, parts, opts)
|
|
345
|
+
reply = recv_msg(sock, opts)
|
|
346
|
+
reply = eval_expr(reply, sock, opts)
|
|
347
|
+
output(reply, opts)
|
|
348
|
+
i += 1
|
|
349
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
350
|
+
if opts[:interval]
|
|
351
|
+
sleep(opts[:interval])
|
|
352
|
+
elsif !opts[:data] && !opts[:file]
|
|
353
|
+
next # stdin mode: keep reading lines
|
|
354
|
+
else
|
|
355
|
+
break # single exchange with -D/-F and no -i
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def rep_loop(sock, opts)
|
|
361
|
+
i = 0
|
|
362
|
+
loop do
|
|
363
|
+
msg = recv_msg(sock, opts)
|
|
364
|
+
if opts[:expr]
|
|
365
|
+
reply = eval_expr(msg, sock, opts)
|
|
366
|
+
output(reply, opts)
|
|
367
|
+
send_msg(sock, reply || [""], opts)
|
|
368
|
+
else
|
|
369
|
+
output(msg, opts)
|
|
370
|
+
reply = opts[:data] ? parse_input(opts[:data] + "\n", opts[:format]) : msg
|
|
371
|
+
send_msg(sock, reply, opts)
|
|
372
|
+
end
|
|
373
|
+
i += 1
|
|
374
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def pair_loop(sock, opts, task)
|
|
379
|
+
receiver = task.async do
|
|
380
|
+
i = 0
|
|
381
|
+
loop do
|
|
382
|
+
parts = recv_msg(sock, opts)
|
|
383
|
+
parts = eval_expr(parts, sock, opts)
|
|
384
|
+
output(parts, opts)
|
|
385
|
+
i += 1
|
|
386
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
sender = task.async do
|
|
391
|
+
i = 0
|
|
392
|
+
loop do
|
|
393
|
+
parts = read_next(opts)
|
|
394
|
+
break unless parts
|
|
395
|
+
parts = eval_expr(parts, sock, opts)
|
|
396
|
+
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
397
|
+
send_msg(sock, parts, opts) if parts
|
|
398
|
+
i += 1
|
|
399
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
400
|
+
break if (opts[:data] || opts[:file]) && !opts[:interval]
|
|
401
|
+
sleep(opts[:interval]) if opts[:interval]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
if opts[:count] > 0
|
|
406
|
+
receiver.wait
|
|
407
|
+
sender.stop
|
|
408
|
+
else
|
|
409
|
+
sender.wait
|
|
410
|
+
receiver.stop
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def router_loop(sock, opts, task)
|
|
415
|
+
receiver = task.async do
|
|
416
|
+
i = 0
|
|
417
|
+
loop do
|
|
418
|
+
parts = recv_msg(sock, opts)
|
|
419
|
+
# parts = [identity, "", ...payload]
|
|
420
|
+
identity = parts.shift
|
|
421
|
+
parts.shift if parts.first == "" # remove empty delimiter
|
|
422
|
+
# Z85-encode binary identities for display
|
|
423
|
+
id_display = identity.bytes.all? { |b| b >= 0x20 && b <= 0x7E } ? identity : OMQ::Z85.encode(identity.ljust(((identity.bytesize + 3) / 4) * 4, "\x00"))
|
|
424
|
+
result = eval_expr([id_display, *parts], sock, opts)
|
|
425
|
+
output(result, opts)
|
|
426
|
+
i += 1
|
|
427
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
sender = task.async do
|
|
432
|
+
i = 0
|
|
433
|
+
loop do
|
|
434
|
+
parts = read_next(opts)
|
|
435
|
+
break unless parts
|
|
436
|
+
sleep(opts[:delay]) if opts[:delay] && i == 0
|
|
437
|
+
if opts[:target]
|
|
438
|
+
parts = [opts[:target], "", *parts]
|
|
439
|
+
end
|
|
440
|
+
send_msg(sock, parts, opts)
|
|
441
|
+
i += 1
|
|
442
|
+
break if opts[:count] > 0 && i >= opts[:count]
|
|
443
|
+
break if (opts[:data] || opts[:file]) && !opts[:interval]
|
|
444
|
+
sleep(opts[:interval]) if opts[:interval]
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# If count is set, the receiver will exit when count is reached.
|
|
449
|
+
# Otherwise, wait for Ctrl-C.
|
|
450
|
+
if opts[:count] > 0
|
|
451
|
+
receiver.wait
|
|
452
|
+
sender.stop
|
|
453
|
+
else
|
|
454
|
+
sender.wait
|
|
455
|
+
receiver.stop
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# ── Signal handling ─────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
trap("INT") { Process.exit!(0) }
|
|
462
|
+
trap("TERM") { Process.exit!(0) }
|
|
463
|
+
|
|
464
|
+
# Silence Async's noisy task warnings unless verbose.
|
|
465
|
+
Console.logger = Console::Logger.new(Console::Output::Null.new) unless opts[:verbose]
|
|
466
|
+
|
|
467
|
+
# ── Main ────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
Async do |task|
|
|
470
|
+
sock = klass.new(nil, linger: 1)
|
|
471
|
+
sock.recv_timeout = opts[:recv_timeout] if opts[:recv_timeout]
|
|
472
|
+
sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
|
|
473
|
+
sock.identity = opts[:identity] if opts[:identity]
|
|
474
|
+
|
|
475
|
+
setup_curve(sock)
|
|
476
|
+
|
|
477
|
+
opts[:binds].each do |url|
|
|
478
|
+
sock.bind(url)
|
|
479
|
+
log "Bound to #{sock.last_endpoint}" if opts[:verbose]
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
opts[:connects].each do |url|
|
|
483
|
+
sock.connect(url)
|
|
484
|
+
log "Connecting to #{url}" if opts[:verbose]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
if klass == OMQ::SUB
|
|
488
|
+
prefixes = opts[:subscribes].empty? ? [""] : opts[:subscribes]
|
|
489
|
+
prefixes.each { |p| sock.subscribe(p) }
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
sleep(opts[:delay]) if opts[:delay] && RECV_ONLY.include?(klass)
|
|
493
|
+
|
|
494
|
+
if SEND_ONLY.include?(klass) then send_loop(sock, opts)
|
|
495
|
+
elsif RECV_ONLY.include?(klass) then recv_loop(sock, opts)
|
|
496
|
+
elsif klass == OMQ::REQ then req_loop(sock, opts)
|
|
497
|
+
elsif klass == OMQ::REP then rep_loop(sock, opts)
|
|
498
|
+
elsif klass == OMQ::ROUTER then router_loop(sock, opts, task)
|
|
499
|
+
elsif opts[:data] || opts[:file] then send_loop(sock, opts)
|
|
500
|
+
else pair_loop(sock, opts, task)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
ensure
|
|
504
|
+
sock&.close
|
|
505
|
+
end
|
data/lib/omq/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -41,13 +41,15 @@ description: Pure Ruby implementation of the ZMTP 3.1 wire protocol (ZeroMQ) usi
|
|
|
41
41
|
the Async gem. No native libraries required.
|
|
42
42
|
email:
|
|
43
43
|
- paddor@gmail.com
|
|
44
|
-
executables:
|
|
44
|
+
executables:
|
|
45
|
+
- omqcat
|
|
45
46
|
extensions: []
|
|
46
47
|
extra_rdoc_files: []
|
|
47
48
|
files:
|
|
48
49
|
- CHANGELOG.md
|
|
49
50
|
- LICENSE
|
|
50
51
|
- README.md
|
|
52
|
+
- exe/omqcat
|
|
51
53
|
- lib/omq.rb
|
|
52
54
|
- lib/omq/pair.rb
|
|
53
55
|
- lib/omq/pub_sub.rb
|