omq-cli 0.2.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.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Shared routing behaviour for socket types that address peers by ID
6
+ # (ROUTER, SERVER). Include in a runner to get display_routing_id,
7
+ # resolve_target, and a send_targeted_or_eval template method.
8
+ #
9
+ # Including class must implement #send_to_peer(routing_id, parts).
10
+ #
11
+ module RoutingHelper
12
+ # Format a raw routing ID for display: printable ASCII as-is,
13
+ # binary as a 0x-prefixed hex string.
14
+ #
15
+ def display_routing_id(id)
16
+ if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
17
+ id
18
+ else
19
+ "0x#{id.unpack1("H*")}"
20
+ end
21
+ end
22
+
23
+
24
+ # Decode a target string: 0x-prefixed hex is converted to binary,
25
+ # plain strings are returned as-is.
26
+ #
27
+ def resolve_target(target)
28
+ if target.start_with?("0x")
29
+ [target[2..].delete(" ")].pack("H*")
30
+ else
31
+ target
32
+ end
33
+ end
34
+
35
+
36
+ # Send +parts+ to a peer, routing by identity or eval result.
37
+ #
38
+ # Template method: calls #send_to_peer(id, parts) which the
39
+ # including class must implement for its socket type.
40
+ #
41
+ def send_targeted_or_eval(parts)
42
+ if @send_eval_proc
43
+ parts = eval_send_expr(parts)
44
+ return unless parts
45
+ send_to_peer(resolve_target(parts.shift), parts)
46
+ elsif config.target
47
+ send_to_peer(resolve_target(config.target), parts)
48
+ else
49
+ send_msg(parts)
50
+ end
51
+ end
52
+
53
+
54
+ # Async sender shared by ROUTER and SERVER monitor mode.
55
+ #
56
+ def async_send_loop(task)
57
+ task.async do
58
+ n = config.count
59
+ i = 0
60
+ sleep(config.delay) if config.delay
61
+ if config.interval
62
+ interval_send_loop(n, i)
63
+ elsif config.data || config.file
64
+ parts = read_next
65
+ send_targeted_or_eval(parts) if parts
66
+ else
67
+ stdin_send_loop(n, i)
68
+ end
69
+ end
70
+ end
71
+
72
+
73
+ def interval_send_loop(n, i)
74
+ Async::Loop.quantized(interval: config.interval) do
75
+ parts = read_next
76
+ break unless parts
77
+ send_targeted_or_eval(parts)
78
+ i += 1
79
+ break if n && n > 0 && i >= n
80
+ end
81
+ end
82
+
83
+
84
+ def stdin_send_loop(n, i)
85
+ loop do
86
+ parts = read_next
87
+ break unless parts
88
+ send_targeted_or_eval(parts)
89
+ i += 1
90
+ break if n && n > 0 && i >= n
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -2,13 +2,17 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
+ # Runner for SCATTER sockets (draft; fan-out send).
5
6
  class ScatterRunner < BaseRunner
6
7
  def run_loop(task) = run_send_logic
7
8
  end
8
9
 
9
10
 
11
+ # Runner for GATHER sockets (draft; fan-in receive).
10
12
  class GatherRunner < BaseRunner
11
- def run_loop(task) = run_recv_logic
13
+ def run_loop(task)
14
+ config.parallel ? run_parallel_recv(task) : run_recv_logic
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Stateless helper for socket construction and configuration.
6
+ # All methods are module-level so callers compose rather than inherit.
7
+ #
8
+ module SocketSetup
9
+ # Create and fully configure a socket from +klass+ and +config+.
10
+ #
11
+ def self.build(klass, config)
12
+ sock_opts = { linger: config.linger }
13
+ sock_opts[:conflate] = true if config.conflate && %w[pub radio].include?(config.type_name)
14
+ sock = klass.new(**sock_opts)
15
+ sock.recv_timeout = config.timeout if config.timeout
16
+ sock.send_timeout = config.timeout if config.timeout
17
+ sock.max_message_size = config.recv_maxsz if config.recv_maxsz
18
+ sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
19
+ sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
20
+ sock.identity = config.identity if config.identity
21
+ sock.router_mandatory = true if config.type_name == "router"
22
+ sock
23
+ end
24
+
25
+
26
+ # Bind/connect +sock+ using URL strings from +config.binds+ / +config.connects+.
27
+ #
28
+ def self.attach(sock, config, verbose: false)
29
+ config.binds.each do |url|
30
+ sock.bind(url)
31
+ $stderr.puts "Bound to #{sock.last_endpoint}" if verbose
32
+ end
33
+ config.connects.each do |url|
34
+ sock.connect(url)
35
+ $stderr.puts "Connecting to #{url}" if verbose
36
+ end
37
+ end
38
+
39
+
40
+ # Bind/connect +sock+ from an Array of Endpoint objects.
41
+ # Used by PipeRunner, which works with structured endpoint lists.
42
+ #
43
+ def self.attach_endpoints(sock, endpoints)
44
+ endpoints.each { |ep| ep.bind? ? sock.bind(ep.url) : sock.connect(ep.url) }
45
+ end
46
+
47
+
48
+ # Subscribe or join groups on +sock+ according to +config+.
49
+ #
50
+ def self.setup_subscriptions(sock, config)
51
+ case config.type_name
52
+ when "sub"
53
+ prefixes = config.subscribes.empty? ? [""] : config.subscribes
54
+ prefixes.each { |p| sock.subscribe(p) }
55
+ when "dish"
56
+ config.joins.each { |g| sock.join(g) }
57
+ end
58
+ end
59
+
60
+
61
+ # Configure CURVE encryption on +sock+ using +config+ and env vars.
62
+ #
63
+ def self.setup_curve(sock, config)
64
+ server_key_z85 = config.curve_server_key || ENV["OMQ_SERVER_KEY"]
65
+ server_mode = config.curve_server || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
66
+
67
+ return unless server_key_z85 || server_mode
68
+
69
+ crypto = CLI.load_curve_crypto(config.curve_crypto || ENV["OMQ_CURVE_CRYPTO"], verbose: config.verbose)
70
+ require "protocol/zmtp/mechanism/curve"
71
+
72
+ if server_key_z85
73
+ server_key = Protocol::ZMTP::Z85.decode(server_key_z85)
74
+ client_key = crypto::PrivateKey.generate
75
+ sock.mechanism = Protocol::ZMTP::Mechanism::Curve.client(
76
+ public_key: client_key.public_key.to_s,
77
+ secret_key: client_key.to_s,
78
+ server_key: server_key,
79
+ crypto: crypto
80
+ )
81
+ elsif server_mode
82
+ if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
83
+ server_pub = Protocol::ZMTP::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
84
+ server_sec = Protocol::ZMTP::Z85.decode(ENV["OMQ_SERVER_SECRET"])
85
+ else
86
+ key = crypto::PrivateKey.generate
87
+ server_pub = key.public_key.to_s
88
+ server_sec = key.to_s
89
+ end
90
+ sock.mechanism = Protocol::ZMTP::Mechanism::Curve.server(
91
+ public_key: server_pub,
92
+ secret_key: server_sec,
93
+ crypto: crypto
94
+ )
95
+ $stderr.puts "OMQ_SERVER_KEY='#{Protocol::ZMTP::Z85.encode(server_pub)}'"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Monitors peer-disconnect events for --transient mode.
6
+ #
7
+ # Starts an async task that waits until #ready! is called (signalling
8
+ # that at least one message has been exchanged), then waits for all
9
+ # peers to disconnect, disables reconnection, and either stops the
10
+ # task (send-only) or closes the read side of the socket (recv side).
11
+ #
12
+ class TransientMonitor
13
+ # @param sock [Object] the OMQ socket to monitor
14
+ # @param config [Config] frozen CLI configuration
15
+ # @param task [Async::Task] parent async task
16
+ # @param log_fn [Method] callable for verbose logging
17
+ def initialize(sock, config, task, log_fn)
18
+ @barrier = Async::Promise.new
19
+ task.async do
20
+ @barrier.wait
21
+ sock.all_peers_gone.wait unless sock.connection_count == 0
22
+ log_fn.call("All peers disconnected, exiting")
23
+ sock.reconnect_enabled = false
24
+ if config.send_only?
25
+ task.stop
26
+ else
27
+ sock.close_read
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ # Signal that the first message has been sent or received.
34
+ # Idempotent — safe to call multiple times.
35
+ #
36
+ def ready!
37
+ @barrier.resolve(true) unless @barrier.resolved?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end