omq-cli 0.1.0 → 0.3.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.
data/lib/omq/cli.rb CHANGED
@@ -3,7 +3,13 @@
3
3
  require "optparse"
4
4
  require_relative "cli/version"
5
5
  require_relative "cli/config"
6
+ require_relative "cli/cli_parser"
6
7
  require_relative "cli/formatter"
8
+ require_relative "cli/expression_evaluator"
9
+ require_relative "cli/socket_setup"
10
+ require_relative "cli/routing_helper"
11
+ require_relative "cli/transient_monitor"
12
+ require_relative "cli/parallel_recv_runner"
7
13
  require_relative "cli/base_runner"
8
14
  require_relative "cli/push_pull"
9
15
  require_relative "cli/pub_sub"
@@ -12,21 +18,32 @@ require_relative "cli/radio_dish"
12
18
  require_relative "cli/req_rep"
13
19
  require_relative "cli/pair"
14
20
  require_relative "cli/router_dealer"
15
- require_relative "cli/channel"
16
21
  require_relative "cli/client_server"
17
- require_relative "cli/peer"
18
22
  require_relative "cli/pipe"
19
23
 
20
24
  module OMQ
21
25
 
22
26
  class << self
23
- attr_reader :outgoing_proc, :incoming_proc
27
+ # @return [Proc, nil] registered outgoing message transform
28
+ attr_reader :outgoing_proc
29
+ # @return [Proc, nil] registered incoming message transform
30
+ attr_reader :incoming_proc
24
31
 
32
+ # Registers an outgoing message transform (used by -r scripts).
33
+ #
34
+ # @yield [Array<String>] message parts before sending
35
+ # @return [Proc]
25
36
  def outgoing(&block) = @outgoing_proc = block
37
+
38
+ # Registers an incoming message transform (used by -r scripts).
39
+ #
40
+ # @yield [Array<String>] message parts after receiving
41
+ # @return [Proc]
26
42
  def incoming(&block) = @incoming_proc = block
27
43
  end
28
44
 
29
45
 
46
+ # Command-line interface for OMQ socket operations.
30
47
  module CLI
31
48
  SOCKET_TYPE_NAMES = %w[
32
49
  req rep pub sub push pull pair dealer router
@@ -42,206 +59,21 @@ module OMQ
42
59
  "sub" => [SubRunner, :SUB],
43
60
  "req" => [ReqRunner, :REQ],
44
61
  "rep" => [RepRunner, :REP],
45
- "dealer" => [DealerRunner, :DEALER],
62
+ "dealer" => [PairRunner, :DEALER],
46
63
  "router" => [RouterRunner, :ROUTER],
47
64
  "pair" => [PairRunner, :PAIR],
48
- "client" => [ClientRunner, :CLIENT],
65
+ "client" => [ReqRunner, :CLIENT],
49
66
  "server" => [ServerRunner, :SERVER],
50
67
  "radio" => [RadioRunner, :RADIO],
51
68
  "dish" => [DishRunner, :DISH],
52
69
  "scatter" => [ScatterRunner, :SCATTER],
53
70
  "gather" => [GatherRunner, :GATHER],
54
- "channel" => [ChannelRunner, :CHANNEL],
55
- "peer" => [PeerRunner, :PEER],
71
+ "channel" => [PairRunner, :CHANNEL],
72
+ "peer" => [ServerRunner, :PEER],
56
73
  "pipe" => [PipeRunner, nil],
57
74
  }.freeze
58
75
 
59
76
 
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
77
  module_function
246
78
 
247
79
 
@@ -267,27 +99,128 @@ module OMQ
267
99
  end
268
100
 
269
101
 
102
+ # Main entry point: dispatches to keygen or socket runner.
103
+ #
104
+ # @param argv [Array<String>] command-line arguments
105
+ # @return [void]
106
+ def run(argv = ARGV)
107
+ case argv.first
108
+ when "keygen"
109
+ argv.shift
110
+ run_keygen(argv)
111
+ else
112
+ run_socket(argv)
113
+ end
114
+ end
115
+
116
+
117
+ # Generates a persistent CURVE keypair and prints it as
118
+ # Z85-encoded env vars.
119
+ #
120
+ def run_keygen(argv)
121
+ crypto_name = nil
122
+ verbose = false
123
+ while (arg = argv.shift)
124
+ case arg
125
+ when "--curve-crypto"
126
+ crypto_name = argv.shift
127
+ when "-v", "--verbose"
128
+ verbose = true
129
+ when "-h", "--help"
130
+ puts "Usage: omq keygen [--curve-crypto rbnacl|nuckle] [-v]\n\n" \
131
+ "Generates a CURVE keypair for persistent server identity.\n" \
132
+ "Output: Z85-encoded env vars for use with --curve-server."
133
+ exit
134
+ else
135
+ abort "omq keygen: unknown option: #{arg}"
136
+ end
137
+ end
138
+ crypto_name ||= ENV["OMQ_CURVE_CRYPTO"]
139
+
140
+ crypto = load_curve_crypto(crypto_name, verbose: verbose)
141
+ require "protocol/zmtp/mechanism/curve"
142
+ require "protocol/zmtp/z85"
143
+
144
+ key = crypto::PrivateKey.generate
145
+ puts "OMQ_SERVER_PUBLIC='#{Protocol::ZMTP::Z85.encode(key.public_key.to_s)}'"
146
+ puts "OMQ_SERVER_SECRET='#{Protocol::ZMTP::Z85.encode(key.to_s)}'"
147
+ end
148
+
149
+
150
+ # Loads the named NaCl-compatible crypto backend.
151
+ #
152
+ # @param name [String, nil] "rbnacl", "nuckle", or nil (auto-detect rbnacl)
153
+ # @param verbose [Boolean] log which backend was loaded to stderr
154
+ # @return [Module] RbNaCl or Nuckle
155
+ #
156
+ def load_curve_crypto(name, verbose: false)
157
+ crypto = case name&.downcase
158
+ when "rbnacl"
159
+ require "rbnacl"
160
+ RbNaCl
161
+ when "nuckle"
162
+ require "nuckle"
163
+ Nuckle
164
+ when nil
165
+ begin
166
+ require "rbnacl"
167
+ RbNaCl
168
+ rescue LoadError
169
+ abort "CURVE requires a crypto backend. Install rbnacl (recommended):\n" \
170
+ " gem install rbnacl # requires system libsodium\n" \
171
+ "Or use pure Ruby (not audited):\n" \
172
+ " --curve-crypto nuckle\n" \
173
+ " # or: OMQ_CURVE_CRYPTO=nuckle"
174
+ end
175
+ else
176
+ abort "Unknown CURVE crypto backend: #{name}. Use 'rbnacl' or 'nuckle'."
177
+ end
178
+ $stderr.puts "omq: CURVE crypto backend: #{crypto.name}" if verbose
179
+ crypto
180
+ rescue LoadError
181
+ abort "Could not load #{name} gem: gem install #{name}"
182
+ end
183
+
184
+
270
185
  # Parses CLI arguments, validates options, and runs the main
271
186
  # event loop inside an Async reactor.
272
187
  #
273
- def run(argv = ARGV)
188
+ def run_socket(argv)
274
189
  config = build_config(argv)
275
190
 
276
191
  require "omq"
192
+ require "omq/rfc/clientserver"
193
+ require "omq/rfc/radiodish"
194
+ require "omq/rfc/scattergather"
195
+ require "omq/rfc/channel"
196
+ require "omq/rfc/p2p"
277
197
  require "async"
278
198
  require "json"
279
199
  require "console"
280
200
 
281
- validate_gems!(config)
201
+ CliParser.validate_gems!(config)
202
+ require "omq/ractor" if config.parallel
282
203
 
283
204
  trap("INT") { Process.exit!(0) }
284
205
  trap("TERM") { Process.exit!(0) }
285
206
 
286
207
  Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
287
208
 
209
+ if config.type_name.nil?
210
+ Object.include(OMQ) unless Object.include?(OMQ)
211
+ Async do
212
+ config.scripts.each { |s| load_script(s) }
213
+ rescue => e
214
+ $stderr.puts "omq: #{e.message}"
215
+ exit 1
216
+ end
217
+ return
218
+ end
219
+
288
220
  runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
289
221
 
290
222
  Async do |task|
223
+ config.scripts.each { |s| load_script(s) }
291
224
  runner = if socket_sym
292
225
  runner_class.new(config, OMQ.const_get(socket_sym))
293
226
  else
@@ -307,238 +240,37 @@ module OMQ
307
240
  end
308
241
 
309
242
 
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(', ')}"
243
+ def load_script(s)
244
+ if s == :stdin
245
+ eval($stdin.read, TOPLEVEL_BINDING, "(stdin)", 1) # rubocop:disable Security/Eval
246
+ else
247
+ require s
475
248
  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
249
  end
250
+ private_class_method :load_script
489
251
 
490
252
 
491
- # Validates option combinations.
253
+ # Builds a frozen Config from command-line arguments.
492
254
  #
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?)
255
+ def build_config(argv)
256
+ opts = CliParser.parse(argv)
257
+ CliParser.validate!(opts)
258
+
259
+ opts[:has_msgpack] = begin
260
+ require "msgpack"
261
+ true
262
+ rescue LoadError
263
+ false
525
264
  end
526
-
527
- (opts[:connects] + opts[:binds]).each do |url|
528
- abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
265
+ opts[:has_zstd] = begin
266
+ require "zstd-ruby"
267
+ true
268
+ rescue LoadError
269
+ false
529
270
  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
271
+ opts[:stdin_is_tty] = $stdin.tty?
538
272
 
539
- if config.recv_only? && (config.data || config.file)
540
- abort "--data/--file not valid for #{config.type_name} (receive-only)"
541
- end
273
+ Ractor.make_shareable(Config.new(**opts))
542
274
  end
543
275
  end
544
276
  end