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
@@ -0,0 +1,423 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class BaseRunner
6
+ attr_reader :config, :sock
7
+
8
+
9
+ def initialize(config, socket_class)
10
+ @config = config
11
+ @klass = socket_class
12
+ @fmt = Formatter.new(config.format, compress: config.compress)
13
+ end
14
+
15
+
16
+ def call(task)
17
+ @sock = create_socket
18
+ attach_endpoints
19
+ setup_curve
20
+ setup_subscriptions
21
+ compile_expr
22
+
23
+ if config.transient
24
+ start_disconnect_monitor(task)
25
+ Async::Task.current.yield # let monitor start waiting
26
+ end
27
+
28
+ sleep(config.delay) if config.delay && config.recv_only?
29
+ wait_for_peer if needs_peer_wait?
30
+
31
+ @sock.instance_exec(&@begin_proc) if @begin_proc
32
+ run_loop(task)
33
+ @sock.instance_exec(&@end_proc) if @end_proc
34
+ ensure
35
+ @sock&.close
36
+ end
37
+
38
+
39
+ private
40
+
41
+
42
+ # Subclasses override this.
43
+ def run_loop(task)
44
+ raise NotImplementedError
45
+ end
46
+
47
+ # ── Socket creation ─────────────────────────────────────────────
48
+
49
+
50
+ def create_socket
51
+ sock_opts = { linger: config.linger }
52
+ sock_opts[:conflate] = true if config.conflate && %w[pub radio].include?(config.type_name)
53
+ sock = @klass.new(**sock_opts)
54
+ sock.recv_timeout = config.timeout if config.timeout
55
+ sock.send_timeout = config.timeout if config.timeout
56
+ sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
57
+ sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
58
+ sock.identity = config.identity if config.identity
59
+ sock.router_mandatory = true if config.type_name == "router"
60
+ sock
61
+ end
62
+
63
+
64
+ def attach_endpoints
65
+ config.binds.each do |url|
66
+ @sock.bind(url)
67
+ log "Bound to #{@sock.last_endpoint}"
68
+ end
69
+ config.connects.each do |url|
70
+ @sock.connect(url)
71
+ log "Connecting to #{url}"
72
+ end
73
+ end
74
+
75
+ # ── Peer wait with grace period ─────────────────────────────────
76
+
77
+
78
+ def needs_peer_wait?
79
+ !config.recv_only? && (config.connects.any? || config.type_name == "router")
80
+ end
81
+
82
+
83
+ def wait_for_peer
84
+ with_timeout(config.timeout) do
85
+ @sock.peer_connected.wait
86
+ log "Peer connected"
87
+ if %w[pub xpub].include?(config.type_name)
88
+ @sock.subscriber_joined.wait
89
+ log "Subscriber joined"
90
+ end
91
+
92
+ # Grace period: when multiple peers may be connecting (bind or
93
+ # multiple connect URLs), wait one reconnect interval so
94
+ # latecomers finish their handshake before we start sending.
95
+ if config.binds.any? || config.connects.size > 1
96
+ ri = @sock.options.reconnect_interval
97
+ sleep(ri.is_a?(Range) ? ri.begin : ri)
98
+ end
99
+ end
100
+ end
101
+
102
+ # ── Transient disconnect monitor ────────────────────────────────
103
+
104
+
105
+ def start_disconnect_monitor(task)
106
+ @transient_barrier = Async::Promise.new
107
+ task.async do
108
+ @transient_barrier.wait
109
+ @sock.all_peers_gone.wait unless @sock.connection_count == 0
110
+ log "All peers disconnected, exiting"
111
+ @sock.reconnect_enabled = false
112
+ if config.send_only?
113
+ task.stop
114
+ else
115
+ @sock.close_read
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ def transient_ready!
122
+ if config.transient && !@transient_barrier.resolved?
123
+ @transient_barrier.resolve(true)
124
+ end
125
+ end
126
+
127
+ # ── Timeout helper ──────────────────────────────────────────────
128
+
129
+
130
+ def with_timeout(seconds)
131
+ if seconds
132
+ Async::Task.current.with_timeout(seconds) { yield }
133
+ else
134
+ yield
135
+ end
136
+ end
137
+
138
+ # ── Socket setup ────────────────────────────────────────────────
139
+
140
+
141
+ def setup_subscriptions
142
+ case config.type_name
143
+ when "sub"
144
+ prefixes = config.subscribes.empty? ? [""] : config.subscribes
145
+ prefixes.each { |p| @sock.subscribe(p) }
146
+ when "dish"
147
+ config.joins.each { |g| @sock.join(g) }
148
+ end
149
+ end
150
+
151
+
152
+ def setup_curve
153
+ server_key_z85 = config.curve_server_key || ENV["OMQ_SERVER_KEY"]
154
+ server_mode = config.curve_server || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
155
+
156
+ if server_key_z85
157
+ if ENV["OMQ_DEV"]
158
+ require_relative "../../../../omq-curve/lib/omq/curve"
159
+ else
160
+ require "omq/curve"
161
+ end
162
+ server_key = OMQ::Z85.decode(server_key_z85)
163
+ client_key = RbNaCl::PrivateKey.generate
164
+ @sock.mechanism = OMQ::Curve.client(
165
+ client_key.public_key.to_s, client_key.to_s, server_key: server_key
166
+ )
167
+ elsif server_mode
168
+ if ENV["OMQ_DEV"]
169
+ require_relative "../../../../omq-curve/lib/omq/curve"
170
+ else
171
+ require "omq/curve"
172
+ end
173
+ if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
174
+ server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
175
+ server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
176
+ else
177
+ key = RbNaCl::PrivateKey.generate
178
+ server_pub = key.public_key.to_s
179
+ server_sec = key.to_s
180
+ end
181
+ @sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
182
+ $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
183
+ end
184
+ rescue LoadError
185
+ abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
186
+ end
187
+
188
+ # ── Shared loop bodies ──────────────────────────────────────────
189
+
190
+
191
+ def run_send_logic
192
+ n = config.count
193
+ i = 0
194
+ sleep(config.delay) if config.delay
195
+ if config.interval
196
+ i += send_tick
197
+ unless @send_tick_eof || (n && n > 0 && i >= n)
198
+ Async::Loop.quantized(interval: config.interval) do
199
+ i += send_tick
200
+ break if @send_tick_eof || (n && n > 0 && i >= n)
201
+ end
202
+ end
203
+ elsif config.data || config.file
204
+ parts = eval_expr(read_next)
205
+ send_msg(parts) if parts
206
+ elsif stdin_ready?
207
+ loop do
208
+ parts = read_next
209
+ break unless parts
210
+ parts = eval_expr(parts)
211
+ send_msg(parts) if parts
212
+ i += 1
213
+ break if n && n > 0 && i >= n
214
+ end
215
+ elsif @eval_proc
216
+ parts = eval_expr(nil)
217
+ send_msg(parts) if parts
218
+ end
219
+ end
220
+
221
+
222
+ def send_tick
223
+ raw = read_next_or_nil
224
+ if raw.nil? && !@eval_proc
225
+ @send_tick_eof = true
226
+ return 0
227
+ end
228
+ parts = eval_expr(raw)
229
+ send_msg(parts) if parts
230
+ 1
231
+ end
232
+
233
+
234
+ def run_recv_logic
235
+ n = config.count
236
+ i = 0
237
+ loop do
238
+ parts = recv_msg
239
+ break if parts.nil?
240
+ parts = eval_expr(parts)
241
+ output(parts)
242
+ i += 1
243
+ break if n && n > 0 && i >= n
244
+ end
245
+ end
246
+
247
+
248
+ def wait_for_loops(receiver, sender)
249
+ if config.data || config.file || config.expr || config.target
250
+ sender.wait
251
+ receiver.stop
252
+ elsif config.count && config.count > 0
253
+ receiver.wait
254
+ sender.stop
255
+ else
256
+ sender.wait
257
+ receiver.stop
258
+ end
259
+ end
260
+
261
+ # ── Message I/O ─────────────────────────────────────────────────
262
+
263
+
264
+ def send_msg(parts)
265
+ return if parts.empty?
266
+ parts = [Marshal.dump(parts)] if config.format == :marshal
267
+ parts = @fmt.compress(parts)
268
+ @sock.send(parts)
269
+ transient_ready!
270
+ end
271
+
272
+
273
+ def recv_msg
274
+ raw = @sock.receive
275
+ return nil if raw.nil?
276
+ parts = @fmt.decompress(raw)
277
+ parts = Marshal.load(parts.first) if config.format == :marshal
278
+ transient_ready!
279
+ parts
280
+ end
281
+
282
+
283
+ def recv_msg_raw
284
+ @sock.receive
285
+ end
286
+
287
+
288
+ def read_next
289
+ if config.data
290
+ @fmt.decode(config.data + "\n")
291
+ elsif config.file
292
+ @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
293
+ @fmt.decode(@file_data + "\n")
294
+ elsif config.format == :msgpack
295
+ @fmt.decode_msgpack($stdin)
296
+ elsif config.format == :marshal
297
+ @fmt.decode_marshal($stdin)
298
+ elsif config.format == :raw
299
+ data = $stdin.read
300
+ return nil if data.nil? || data.empty?
301
+ [data]
302
+ else
303
+ line = $stdin.gets
304
+ return nil if line.nil?
305
+ @fmt.decode(line)
306
+ end
307
+ end
308
+
309
+
310
+ def stdin_ready?
311
+ return @stdin_ready unless @stdin_ready.nil?
312
+
313
+ @stdin_ready = !$stdin.closed? &&
314
+ !config.stdin_is_tty &&
315
+ IO.select([$stdin], nil, nil, 0.01) &&
316
+ !$stdin.eof?
317
+ end
318
+
319
+
320
+ def read_next_or_nil
321
+ if config.data || config.file
322
+ read_next
323
+ elsif @eval_proc
324
+ nil
325
+ else
326
+ read_next
327
+ end
328
+ end
329
+
330
+
331
+ def output(parts)
332
+ return if config.quiet || parts.nil?
333
+ $stdout.write(@fmt.encode(parts))
334
+ $stdout.flush
335
+ end
336
+
337
+ # ── Routing helpers ─────────────────────────────────────────────
338
+
339
+
340
+ def display_routing_id(id)
341
+ if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
342
+ id
343
+ else
344
+ "0x#{id.unpack1("H*")}"
345
+ end
346
+ end
347
+
348
+
349
+ def resolve_target(target)
350
+ if target.start_with?("0x")
351
+ [target[2..].delete(" ")].pack("H*")
352
+ else
353
+ target
354
+ end
355
+ end
356
+
357
+ # ── Eval ────────────────────────────────────────────────────────
358
+
359
+
360
+ def compile_expr
361
+ return unless config.expr
362
+ expr, begin_body, end_body = extract_blocks(config.expr)
363
+ @begin_proc = eval("proc { #{begin_body} }") if begin_body
364
+ @end_proc = eval("proc { #{end_body} }") if end_body
365
+ @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
366
+ end
367
+
368
+
369
+ def extract_blocks(expr)
370
+ begin_body = end_body = nil
371
+ expr, begin_body = extract_block(expr, "BEGIN")
372
+ expr, end_body = extract_block(expr, "END")
373
+ [expr, begin_body, end_body]
374
+ end
375
+
376
+
377
+ def extract_block(expr, keyword)
378
+ start = expr.index(/#{keyword}\s*\{/)
379
+ return [expr, nil] unless start
380
+
381
+ # Find the opening brace
382
+ i = expr.index("{", start)
383
+ depth = 1
384
+ j = i + 1
385
+ while j < expr.length && depth > 0
386
+ case expr[j]
387
+ when "{" then depth += 1
388
+ when "}" then depth -= 1
389
+ end
390
+ j += 1
391
+ end
392
+
393
+ body = expr[(i + 1)..(j - 2)]
394
+ trimmed = expr[0...start] + expr[j..]
395
+ [trimmed, body]
396
+ end
397
+
398
+
399
+ def eval_expr(parts)
400
+ return parts unless @eval_proc
401
+ $F = parts
402
+ result = @sock.instance_exec(&@eval_proc)
403
+ return nil if result.nil?
404
+ return [result] if config.format == :marshal
405
+ case result
406
+ when Array then result
407
+ when String then [result]
408
+ else [result.to_str]
409
+ end
410
+ rescue => e
411
+ $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
412
+ Process.exit!(3)
413
+ end
414
+
415
+ # ── Logging ─────────────────────────────────────────────────────
416
+
417
+
418
+ def log(msg)
419
+ $stderr.puts(msg) if config.verbose
420
+ end
421
+ end
422
+ end
423
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class ChannelRunner < PairRunner
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class ClientRunner < ReqRunner
6
+ end
7
+
8
+
9
+ class ServerRunner < BaseRunner
10
+ private
11
+
12
+
13
+ def run_loop(task)
14
+ if config.echo || config.expr || config.data || config.file || !config.stdin_is_tty
15
+ reply_loop
16
+ else
17
+ monitor_loop(task)
18
+ end
19
+ end
20
+
21
+
22
+ def reply_loop
23
+ n = config.count
24
+ i = 0
25
+ loop do
26
+ parts = recv_msg_raw
27
+ break if parts.nil?
28
+ routing_id = parts.shift
29
+ body = @fmt.decompress(parts)
30
+
31
+ if config.expr
32
+ reply = eval_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
44
+ i += 1
45
+ break if n && n > 0 && i >= n
46
+ end
47
+ end
48
+
49
+
50
+ def monitor_loop(task)
51
+ receiver = task.async do
52
+ n = config.count
53
+ i = 0
54
+ loop do
55
+ parts = recv_msg_raw
56
+ break if parts.nil?
57
+ routing_id = parts.shift
58
+ parts = @fmt.decompress(parts)
59
+ result = eval_expr([display_routing_id(routing_id), *parts])
60
+ output(result)
61
+ i += 1
62
+ break if n && n > 0 && i >= n
63
+ end
64
+ 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_plain(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_plain(parts) if parts
81
+ else
82
+ loop do
83
+ parts = read_next
84
+ break unless parts
85
+ send_targeted_or_plain(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
+ end
94
+
95
+
96
+ def send_targeted_or_plain(parts)
97
+ if config.target
98
+ parts = @fmt.compress(parts)
99
+ @sock.send_to(resolve_target(config.target), parts.first || "")
100
+ else
101
+ send_msg(parts)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ Endpoint = Data.define(:url, :bind?) do
6
+ def connect? = !bind?
7
+ end
8
+
9
+
10
+ Config = Data.define(
11
+ :type_name,
12
+ :endpoints,
13
+ :connects,
14
+ :binds,
15
+ :data,
16
+ :file,
17
+ :format,
18
+ :subscribes,
19
+ :joins,
20
+ :group,
21
+ :identity,
22
+ :target,
23
+ :interval,
24
+ :count,
25
+ :delay,
26
+ :timeout,
27
+ :linger,
28
+ :reconnect_ivl,
29
+ :heartbeat_ivl,
30
+ :conflate,
31
+ :compress,
32
+ :expr,
33
+ :parallel,
34
+ :transient,
35
+ :verbose,
36
+ :quiet,
37
+ :echo,
38
+ :curve_server,
39
+ :curve_server_key,
40
+ :has_msgpack,
41
+ :has_zstd,
42
+ :stdin_is_tty,
43
+ ) do
44
+ SEND_ONLY = %w[pub push scatter radio].freeze
45
+ RECV_ONLY = %w[sub pull gather dish].freeze
46
+
47
+ def send_only? = SEND_ONLY.include?(type_name)
48
+ def recv_only? = RECV_ONLY.include?(type_name)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Handles encoding/decoding messages in the configured format,
6
+ # plus optional Zstandard compression.
7
+ class Formatter
8
+ def initialize(format, compress: false)
9
+ @format = format
10
+ @compress = compress
11
+ end
12
+
13
+
14
+ def encode(parts)
15
+ case @format
16
+ when :ascii
17
+ parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
18
+ when :quoted
19
+ parts.map { |p| p.b.dump[1..-2] }.join("\t") + "\n"
20
+ when :raw
21
+ parts.each_with_index.map do |p, i|
22
+ ZMTP::Codec::Frame.new(p.to_s, more: i < parts.size - 1).to_wire
23
+ end.join
24
+ when :jsonl
25
+ JSON.generate(parts) + "\n"
26
+ when :msgpack
27
+ MessagePack.pack(parts)
28
+ when :marshal
29
+ parts.map(&:inspect).join("\t") + "\n"
30
+ end
31
+ end
32
+
33
+
34
+ def decode(line)
35
+ case @format
36
+ when :ascii, :marshal
37
+ line.chomp.split("\t")
38
+ when :quoted
39
+ line.chomp.split("\t").map { |p| "\"#{p}\"".undump }
40
+ when :raw
41
+ [line]
42
+ when :jsonl
43
+ arr = JSON.parse(line.chomp)
44
+ abort "JSON Lines input must be an array of strings" unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
45
+ arr
46
+ end
47
+ end
48
+
49
+
50
+ def decode_marshal(io)
51
+ Marshal.load(io)
52
+ rescue EOFError, TypeError
53
+ nil
54
+ end
55
+
56
+
57
+ def decode_msgpack(io)
58
+ @msgpack_unpacker ||= MessagePack::Unpacker.new(io)
59
+ @msgpack_unpacker.read
60
+ rescue EOFError
61
+ nil
62
+ end
63
+
64
+
65
+ def compress(parts)
66
+ @compress ? parts.map { |p| Zstd.compress(p) } : parts
67
+ end
68
+
69
+
70
+ def decompress(parts)
71
+ @compress ? parts.map { |p| Zstd.decompress(p) } : parts
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class PairRunner < BaseRunner
6
+ private
7
+
8
+
9
+ def run_loop(task)
10
+ receiver = task.async do
11
+ n = config.count
12
+ i = 0
13
+ loop do
14
+ parts = recv_msg
15
+ break if parts.nil?
16
+ parts = eval_expr(parts)
17
+ output(parts)
18
+ i += 1
19
+ break if n && n > 0 && i >= n
20
+ end
21
+ end
22
+
23
+ sender = task.async do
24
+ run_send_logic
25
+ end
26
+
27
+ wait_for_loops(receiver, sender)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class PeerRunner < ServerRunner
6
+ end
7
+ end
8
+ end