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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +158 -0
- data/README.md +14 -9
- data/lib/omq/cli/base_runner.rb +152 -210
- data/lib/omq/cli/cli_parser.rb +470 -0
- data/lib/omq/cli/client_server.rb +32 -59
- data/lib/omq/cli/config.rb +10 -0
- data/lib/omq/cli/expression_evaluator.rb +156 -0
- data/lib/omq/cli/formatter.rb +27 -1
- data/lib/omq/cli/pair.rb +9 -7
- data/lib/omq/cli/parallel_recv_runner.rb +150 -0
- data/lib/omq/cli/pipe.rb +175 -156
- data/lib/omq/cli/pub_sub.rb +5 -1
- data/lib/omq/cli/push_pull.rb +5 -1
- data/lib/omq/cli/radio_dish.rb +5 -1
- data/lib/omq/cli/req_rep.rb +44 -49
- data/lib/omq/cli/router_dealer.rb +13 -46
- data/lib/omq/cli/routing_helper.rb +95 -0
- data/lib/omq/cli/scatter_gather.rb +5 -1
- data/lib/omq/cli/socket_setup.rb +100 -0
- data/lib/omq/cli/transient_monitor.rb +41 -0
- data/lib/omq/cli/version.rb +1 -1
- data/lib/omq/cli.rb +72 -426
- metadata +94 -6
- data/lib/omq/cli/channel.rb +0 -8
- data/lib/omq/cli/peer.rb +0 -8
|
@@ -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)
|
|
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
|