omq 0.5.1 → 0.6.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +468 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +24 -3
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +10 -5
  28. data/lib/omq/zmtp/routing/channel.rb +8 -2
  29. data/lib/omq/zmtp/routing/fan_out.rb +38 -8
  30. data/lib/omq/zmtp/routing/pair.rb +8 -2
  31. data/lib/omq/zmtp/routing/peer.rb +7 -1
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +32 -11
  34. data/lib/omq/zmtp/routing/rep.rb +11 -7
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +35 -1
  37. data/lib/omq/zmtp/routing/router.rb +7 -1
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +7 -1
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/transport/inproc.rb +40 -5
  42. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  43. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  44. data/lib/omq/zmtp/writable.rb +21 -4
  45. data/lib/omq.rb +7 -0
  46. metadata +18 -3
  47. data/exe/omqcat +0 -532
@@ -25,16 +25,11 @@ module OMQ
25
25
  host_part = host.include?(":") ? "[#{host}]" : host
26
26
  resolved = "tcp://#{host_part}:#{actual_port}"
27
27
 
28
- accept_task = Reactor.spawn_pump do
28
+ accept_task = Reactor.spawn_pump(annotation: "tcp accept #{resolved}") do
29
29
  loop do
30
30
  client = server.accept
31
- Reactor.run do
31
+ Async::Task.current.defer_stop do
32
32
  engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
33
- rescue ProtocolError, *ZMTP::CONNECTION_LOST
34
- # peer disconnected during handshake
35
- rescue
36
- client&.close rescue nil
37
- raise
38
33
  end
39
34
  end
40
35
  rescue IOError
@@ -58,6 +53,11 @@ module OMQ
58
53
 
59
54
  private
60
55
 
56
+ # Parses a TCP endpoint URI into host and port.
57
+ #
58
+ # @param endpoint [String]
59
+ # @return [Array(String, Integer)]
60
+ #
61
61
  def parse_endpoint(endpoint)
62
62
  uri = URI.parse(endpoint)
63
63
  [uri.hostname, uri.port]
@@ -75,6 +75,12 @@ module OMQ
75
75
  #
76
76
  attr_reader :port
77
77
 
78
+
79
+ # @param endpoint [String] resolved endpoint URI
80
+ # @param server [TCPServer]
81
+ # @param accept_task [#stop] the accept loop handle
82
+ # @param port [Integer] bound port number
83
+ #
78
84
  def initialize(endpoint, server, accept_task, port)
79
85
  @endpoint = endpoint
80
86
  @server = server
@@ -82,6 +88,7 @@ module OMQ
82
88
  @port = port
83
89
  end
84
90
 
91
+
85
92
  # Stops the listener.
86
93
  #
87
94
  def stop
@@ -14,10 +14,7 @@ module OMQ
14
14
  # @raise [IO::TimeoutError] if write_timeout exceeded
15
15
  #
16
16
  def send(message)
17
- parts = message.is_a?(Array) ? message : [message]
18
- raise ArgumentError, "message has no parts" if parts.empty?
19
- parts = parts.map { |p| p.b.freeze }
20
-
17
+ parts = freeze_message(message)
21
18
  with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
22
19
  self
23
20
  end
@@ -31,6 +28,26 @@ module OMQ
31
28
  send(message)
32
29
  end
33
30
 
31
+ private
32
+
33
+ # Converts a message into a frozen array of frozen binary strings.
34
+ #
35
+ # @param message [String, Array<String>]
36
+ # @return [Array<String>] frozen array of frozen binary strings
37
+ #
38
+ def freeze_message(message)
39
+ parts = message.is_a?(Array) ? message : [message]
40
+ raise ArgumentError, "message has no parts" if parts.empty?
41
+ if parts.frozen?
42
+ parts = parts.map { |p| p.to_str.b.freeze }
43
+ else
44
+ parts.map! { |p| p.to_str.b.freeze }
45
+ end
46
+ parts.freeze
47
+ end
48
+
49
+ public
50
+
34
51
  # Waits until the socket is writable.
35
52
  #
36
53
  # @param timeout [Numeric, nil] timeout in seconds
data/lib/omq.rb CHANGED
@@ -10,6 +10,13 @@
10
10
  #
11
11
 
12
12
  require_relative "omq/version"
13
+
14
+ module OMQ
15
+ # Raised when an internal pump task crashes unexpectedly.
16
+ # The socket is no longer usable; the original error is available via #cause.
17
+ #
18
+ class SocketDeadError < RuntimeError; end
19
+ end
13
20
  require_relative "omq/zmtp"
14
21
  require_relative "omq/socket"
15
22
  require_relative "omq/req_rep"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -42,16 +42,31 @@ description: Pure Ruby implementation of the ZMTP 3.1 wire protocol (ZeroMQ) usi
42
42
  email:
43
43
  - paddor@gmail.com
44
44
  executables:
45
- - omqcat
45
+ - omq
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - CHANGELOG.md
50
50
  - LICENSE
51
51
  - README.md
52
- - exe/omqcat
52
+ - exe/omq
53
53
  - lib/omq.rb
54
54
  - lib/omq/channel.rb
55
+ - lib/omq/cli.rb
56
+ - lib/omq/cli/base_runner.rb
57
+ - lib/omq/cli/channel.rb
58
+ - lib/omq/cli/client_server.rb
59
+ - lib/omq/cli/config.rb
60
+ - lib/omq/cli/formatter.rb
61
+ - lib/omq/cli/pair.rb
62
+ - lib/omq/cli/peer.rb
63
+ - lib/omq/cli/pipe.rb
64
+ - lib/omq/cli/pub_sub.rb
65
+ - lib/omq/cli/push_pull.rb
66
+ - lib/omq/cli/radio_dish.rb
67
+ - lib/omq/cli/req_rep.rb
68
+ - lib/omq/cli/router_dealer.rb
69
+ - lib/omq/cli/scatter_gather.rb
55
70
  - lib/omq/client_server.rb
56
71
  - lib/omq/pair.rb
57
72
  - lib/omq/peer.rb
data/exe/omqcat DELETED
@@ -1,532 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- Warning[:experimental] = false
4
-
5
- #
6
- # omqcat — command-line access to OMQ (ZeroMQ) sockets.
7
- #
8
- # Usage: omqcat TYPE [options]
9
- #
10
- # Examples:
11
- # omqcat rep -b tcp://:5555 -D "pong"
12
- # echo "ping" | omqcat req -c tcp://localhost:5555
13
- # omqcat sub -c tcp://localhost:5556 -s "weather."
14
- #
15
-
16
- require "optparse"
17
-
18
- SOCKET_TYPE_NAMES = %w[req rep pub sub push pull pair dealer router].freeze
19
-
20
-
21
- # ── Option parsing ──────────────────────────────────────────────────
22
-
23
- opts = {
24
- connects: [],
25
- binds: [],
26
- data: nil,
27
- file: nil,
28
- format: :ascii,
29
- subscribes: [],
30
- identity: nil,
31
- target: nil,
32
- interval: nil,
33
- count: nil,
34
- delay: nil,
35
- recv_timeout: nil,
36
- send_timeout: nil,
37
- compress: false,
38
- expr: nil,
39
- verbose: false,
40
- quiet: false,
41
- }
42
-
43
- parser = OptionParser.new do |o|
44
- o.banner = "Usage: omqcat TYPE [options]\n\n" \
45
- "Types: #{SOCKET_TYPE_NAMES.join(', ')}\n\n"
46
-
47
- o.separator "Connection:"
48
- o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
49
- o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
50
-
51
- o.separator "\nData source (REP: reply source):"
52
- o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
53
- o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
54
- o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
55
-
56
- o.separator "\nFormat (input + output):"
57
- o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
58
- o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
59
- o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
60
- o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
61
- o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
62
-
63
- o.separator "\nSubscription:"
64
- o.on("-s", "--subscribe PREFIX", "Subscribe prefix (repeatable, SUB only)") { |v| opts[:subscribes] << v }
65
-
66
- o.separator "\nIdentity/routing:"
67
- o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
68
- o.on("--target ID", "Target peer identity (ROUTER sending)") { |v| opts[:target] = v }
69
-
70
- o.separator "\nTiming:"
71
- o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
72
- o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
73
- o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
74
- o.on("-t", "--recv-timeout SECS", Float, "Receive timeout") { |v| opts[:recv_timeout] = v }
75
- o.on( "--send-timeout SECS", Float, "Send timeout") { |v| opts[:send_timeout] = v }
76
-
77
- o.separator "\nCompression:"
78
- o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
79
-
80
- o.separator "\nProcessing:"
81
- o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
82
- o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
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
-
89
- o.separator "\nOther:"
90
- o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
91
- o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
92
- o.on("-V", "--version") { require "omq"; puts "omqcat #{OMQ::VERSION}"; exit }
93
- o.on("-h", "--help") { puts o; exit }
94
- end
95
-
96
- begin
97
- parser.parse!
98
- rescue OptionParser::ParseError => e
99
- abort e.message
100
- end
101
-
102
- type_name = ARGV.shift
103
- abort parser.to_s unless type_name
104
- abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}" unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
105
-
106
- # ── Validation (fast, before loading gems) ──────────────────────────
107
-
108
- abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
109
- abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
110
- abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name.downcase != "sub"
111
- abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name.downcase)
112
- abort "--target is only valid for ROUTER" if opts[:target] && type_name.downcase != "router"
113
-
114
- # ── Load gems ───────────────────────────────────────────────────────
115
-
116
- require "omq"
117
- require "async"
118
- require "json"
119
- require "console"
120
-
121
- HAS_MSGPACK = begin; require "msgpack"; true; rescue LoadError; false; end
122
- HAS_ZSTD = begin; require "zstd-ruby"; true; rescue LoadError; false; end
123
-
124
- SOCKET_TYPES = {
125
- "req" => OMQ::REQ, "rep" => OMQ::REP,
126
- "pub" => OMQ::PUB, "sub" => OMQ::SUB,
127
- "push" => OMQ::PUSH, "pull" => OMQ::PULL,
128
- "pair" => OMQ::PAIR,
129
- "dealer" => OMQ::DEALER, "router" => OMQ::ROUTER,
130
- }.freeze
131
-
132
- SEND_ONLY = [OMQ::PUB, OMQ::PUSH].freeze
133
- RECV_ONLY = [OMQ::SUB, OMQ::PULL].freeze
134
-
135
- klass = SOCKET_TYPES[type_name.downcase]
136
-
137
- abort "--msgpack requires the msgpack gem" if opts[:format] == :msgpack && !HAS_MSGPACK
138
- abort "--compress requires the zstd-ruby gem" if opts[:compress] && !HAS_ZSTD
139
-
140
- (opts[:connects] + opts[:binds]).each do |url|
141
- abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
142
- end
143
-
144
- if RECV_ONLY.include?(klass) && (opts[:data] || opts[:file])
145
- abort "--data/--file not valid for #{type_name} (receive-only)"
146
- end
147
-
148
- # ── URL normalization ───────────────────────────────────────────────
149
-
150
- normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
151
- opts[:connects].map!(&normalize)
152
- opts[:binds].map!(&normalize)
153
-
154
- # ── Format helpers ──────────────────────────────────────────────────
155
-
156
- def format_output(parts, fmt)
157
- case fmt
158
- when :ascii
159
- parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
160
- when :quoted
161
- parts.map { |p|
162
- p.b.gsub(/[^[:print:]]/) { |c|
163
- case c
164
- when "\n" then "\\n"
165
- when "\r" then "\\r"
166
- when "\t" then "\\t"
167
- when "\\" then "\\\\"
168
- else format("\\x%02x", c.ord)
169
- end
170
- }
171
- }.join("\t") + "\n"
172
- when :raw
173
- parts.join
174
- when :jsonl
175
- JSON.generate(parts) + "\n"
176
- when :msgpack
177
- MessagePack.pack(parts)
178
- end
179
- end
180
-
181
- def parse_input(line, fmt)
182
- case fmt
183
- when :ascii
184
- line.chomp.split("\t")
185
- when :quoted
186
- line.chomp.split("\t").map { |p|
187
- p.gsub(/\\(?:n|r|t|\\|x[0-9a-fA-F]{2})/) { |m|
188
- case m
189
- when "\\n" then "\n"
190
- when "\\r" then "\r"
191
- when "\\t" then "\t"
192
- when "\\\\" then "\\"
193
- else [m[2..].to_i(16)].pack("C")
194
- end
195
- }
196
- }
197
- when :raw
198
- [line]
199
- when :jsonl
200
- arr = JSON.parse(line.chomp)
201
- abort "JSON Lines input must be an array of strings" unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
202
- arr
203
- end
204
- end
205
-
206
- def read_message_msgpack(io)
207
- @msgpack_unpacker ||= MessagePack::Unpacker.new(io)
208
- @msgpack_unpacker.read
209
- rescue EOFError
210
- nil
211
- end
212
-
213
- # ── Compression helpers ─────────────────────────────────────────────
214
-
215
- def compress_frames(parts)
216
- parts.map { |p| Zstd.compress(p) }
217
- end
218
-
219
- def decompress_frames(parts)
220
- parts.map { |p| Zstd.decompress(p) }
221
- end
222
-
223
- # ── CURVE setup ─────────────────────────────────────────────────────
224
-
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
231
- require "omq/curve"
232
- server_key = OMQ::Z85.decode(server_key_z85)
233
- client_key = RbNaCl::PrivateKey.generate
234
- sock.mechanism = OMQ::Curve.client(
235
- client_key.public_key.to_s, client_key.to_s, server_key: server_key
236
- )
237
- elsif server_mode
238
- # Server mode
239
- require "omq/curve"
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
248
- sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
249
- $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
250
- end
251
- rescue LoadError
252
- abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
253
- end
254
-
255
- # ── I/O helpers ─────────────────────────────────────────────────────
256
-
257
- def read_next(opts)
258
- if opts[:data]
259
- parse_input(opts[:data] + "\n", opts[:format])
260
- elsif opts[:file]
261
- @file_data ||= (opts[:file] == "-" ? $stdin.read : File.read(opts[:file])).chomp
262
- parse_input(@file_data + "\n", opts[:format])
263
- elsif opts[:format] == :msgpack
264
- read_message_msgpack($stdin)
265
- elsif opts[:format] == :raw
266
- data = $stdin.read
267
- return nil if data.nil? || data.empty?
268
- [data]
269
- else
270
- line = $stdin.gets
271
- return nil if line.nil?
272
- parse_input(line, opts[:format])
273
- end
274
- end
275
-
276
- def send_msg(sock, parts, opts)
277
- return if parts.empty?
278
- parts = compress_frames(parts) if opts[:compress]
279
- sock.send(parts)
280
- end
281
-
282
- def recv_msg(sock, opts)
283
- parts = sock.receive
284
- parts = decompress_frames(parts) if opts[:compress]
285
- parts
286
- end
287
-
288
- def eval_expr(parts, sock, opts)
289
- return parts unless opts[:expr]
290
- $F = parts
291
- result = sock.instance_eval(opts[:expr], "-e") # rubocop:disable Security/Eval
292
- case result
293
- when nil then nil
294
- when Array then result
295
- when String then [result]
296
- else [result.to_s]
297
- end
298
- end
299
-
300
- def count_reached?(i, opts)
301
- opts[:count] && opts[:count] > 0 && i >= opts[:count]
302
- end
303
-
304
- def output(parts, opts)
305
- return if opts[:quiet]
306
- parts = [""] if parts.nil?
307
- $stdout.write(format_output(parts, opts[:format]))
308
- $stdout.flush
309
- end
310
-
311
- def log(msg)
312
- $stderr.puts(msg)
313
- end
314
-
315
- # ── Loop methods ────────────────────────────────────────────────────
316
-
317
- def send_loop(sock, opts)
318
- i = 0
319
- if opts[:data] || opts[:file]
320
- loop do
321
- parts = read_next(opts)
322
- break unless parts
323
- parts = eval_expr(parts, sock, opts)
324
- sleep(opts[:delay]) if opts[:delay] && i == 0
325
- send_msg(sock, parts, opts) if parts
326
- i += 1
327
- break if count_reached?(i, opts)
328
- if opts[:interval]
329
- sleep(opts[:interval])
330
- else
331
- break # single send without -i
332
- end
333
- end
334
- else
335
- loop do
336
- parts = read_next(opts)
337
- break unless parts
338
- parts = eval_expr(parts, sock, opts)
339
- sleep(opts[:delay]) if opts[:delay] && i == 0
340
- send_msg(sock, parts, opts) if parts
341
- i += 1
342
- break if count_reached?(i, opts)
343
- sleep(opts[:interval]) if opts[:interval]
344
- end
345
- end
346
- end
347
-
348
- def recv_loop(sock, opts)
349
- i = 0
350
- loop do
351
- parts = recv_msg(sock, opts)
352
- parts = eval_expr(parts, sock, opts)
353
- output(parts, opts)
354
- i += 1
355
- break if count_reached?(i, opts)
356
- end
357
- end
358
-
359
- def req_loop(sock, opts)
360
- i = 0
361
- loop do
362
- parts = read_next(opts)
363
- break unless parts
364
- sleep(opts[:delay]) if opts[:delay] && i == 0
365
- send_msg(sock, parts, opts)
366
- reply = recv_msg(sock, opts)
367
- reply = eval_expr(reply, sock, opts)
368
- output(reply, opts)
369
- i += 1
370
- break if count_reached?(i, opts)
371
- if opts[:interval]
372
- sleep(opts[:interval])
373
- elsif !opts[:data] && !opts[:file]
374
- next # stdin mode: keep reading lines
375
- else
376
- break # single exchange with -D/-F and no -i
377
- end
378
- end
379
- end
380
-
381
- def rep_loop(sock, opts)
382
- i = 0
383
- loop do
384
- msg = recv_msg(sock, opts)
385
- if opts[:expr]
386
- reply = eval_expr(msg, sock, opts)
387
- output(reply, opts)
388
- send_msg(sock, reply || [""], opts)
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
395
- output(msg, opts)
396
- send_msg(sock, reply, opts)
397
- else
398
- abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
399
- end
400
- i += 1
401
- break if count_reached?(i, opts)
402
- end
403
- end
404
-
405
- def pair_loop(sock, opts, task)
406
- receiver = task.async do
407
- i = 0
408
- loop do
409
- parts = recv_msg(sock, opts)
410
- parts = eval_expr(parts, sock, opts)
411
- output(parts, opts)
412
- i += 1
413
- break if count_reached?(i, opts)
414
- end
415
- end
416
-
417
- sender = task.async do
418
- i = 0
419
- loop do
420
- parts = read_next(opts)
421
- break unless parts
422
- parts = eval_expr(parts, sock, opts)
423
- sleep(opts[:delay]) if opts[:delay] && i == 0
424
- send_msg(sock, parts, opts) if parts
425
- i += 1
426
- break if count_reached?(i, opts)
427
- break if (opts[:data] || opts[:file]) && !opts[:interval]
428
- sleep(opts[:interval]) if opts[:interval]
429
- end
430
- end
431
-
432
- if opts[:count] && opts[:count] > 0
433
- receiver.wait
434
- sender.stop
435
- else
436
- sender.wait
437
- receiver.stop
438
- end
439
- end
440
-
441
- def router_loop(sock, opts, task)
442
- receiver = task.async do
443
- i = 0
444
- loop do
445
- parts = recv_msg(sock, opts)
446
- # parts = [identity, "", ...payload]
447
- identity = parts.shift
448
- parts.shift if parts.first == "" # remove empty delimiter
449
- # Z85-encode binary identities for display
450
- id_display = identity.bytes.all? { |b| b >= 0x20 && b <= 0x7E } ? identity : OMQ::Z85.encode(identity.ljust(((identity.bytesize + 3) / 4) * 4, "\x00"))
451
- result = eval_expr([id_display, *parts], sock, opts)
452
- output(result, opts)
453
- i += 1
454
- break if count_reached?(i, opts)
455
- end
456
- end
457
-
458
- sender = task.async do
459
- i = 0
460
- loop do
461
- parts = read_next(opts)
462
- break unless parts
463
- sleep(opts[:delay]) if opts[:delay] && i == 0
464
- if opts[:target]
465
- parts = [opts[:target], "", *parts]
466
- end
467
- send_msg(sock, parts, opts)
468
- i += 1
469
- break if count_reached?(i, opts)
470
- break if (opts[:data] || opts[:file]) && !opts[:interval]
471
- sleep(opts[:interval]) if opts[:interval]
472
- end
473
- end
474
-
475
- # If count is set, the receiver will exit when count is reached.
476
- # Otherwise, wait for Ctrl-C.
477
- if opts[:count] && opts[:count] > 0
478
- receiver.wait
479
- sender.stop
480
- else
481
- sender.wait
482
- receiver.stop
483
- end
484
- end
485
-
486
- # ── Signal handling ─────────────────────────────────────────────────
487
-
488
- trap("INT") { Process.exit!(0) }
489
- trap("TERM") { Process.exit!(0) }
490
-
491
- # Silence Async's noisy task warnings unless verbose.
492
- Console.logger = Console::Logger.new(Console::Output::Null.new) unless opts[:verbose]
493
-
494
- # ── Main ────────────────────────────────────────────────────────────
495
-
496
- Async do |task|
497
- sock = klass.new(nil, linger: 1)
498
- sock.recv_timeout = opts[:recv_timeout] if opts[:recv_timeout]
499
- sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
500
- sock.identity = opts[:identity] if opts[:identity]
501
-
502
- setup_curve(sock, opts)
503
-
504
- opts[:binds].each do |url|
505
- sock.bind(url)
506
- log "Bound to #{sock.last_endpoint}" if opts[:verbose]
507
- end
508
-
509
- opts[:connects].each do |url|
510
- sock.connect(url)
511
- log "Connecting to #{url}" if opts[:verbose]
512
- end
513
-
514
- if klass == OMQ::SUB
515
- prefixes = opts[:subscribes].empty? ? [""] : opts[:subscribes]
516
- prefixes.each { |p| sock.subscribe(p) }
517
- end
518
-
519
- sleep(opts[:delay]) if opts[:delay] && RECV_ONLY.include?(klass)
520
-
521
- if SEND_ONLY.include?(klass) then send_loop(sock, opts)
522
- elsif RECV_ONLY.include?(klass) then recv_loop(sock, opts)
523
- elsif klass == OMQ::REQ then req_loop(sock, opts)
524
- elsif klass == OMQ::REP then rep_loop(sock, opts)
525
- elsif klass == OMQ::ROUTER then router_loop(sock, opts, task)
526
- elsif opts[:data] || opts[:file] then send_loop(sock, opts)
527
- else pair_loop(sock, opts, task)
528
- end
529
-
530
- ensure
531
- sock&.close
532
- end