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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +184 -0
- data/README.md +21 -19
- data/exe/omq +6 -0
- data/lib/omq/cli/base_runner.rb +423 -0
- data/lib/omq/cli/channel.rb +8 -0
- data/lib/omq/cli/client_server.rb +106 -0
- data/lib/omq/cli/config.rb +51 -0
- data/lib/omq/cli/formatter.rb +75 -0
- data/lib/omq/cli/pair.rb +31 -0
- data/lib/omq/cli/peer.rb +8 -0
- data/lib/omq/cli/pipe.rb +249 -0
- data/lib/omq/cli/pub_sub.rb +14 -0
- data/lib/omq/cli/push_pull.rb +14 -0
- data/lib/omq/cli/radio_dish.rb +27 -0
- data/lib/omq/cli/req_rep.rb +77 -0
- data/lib/omq/cli/router_dealer.rb +70 -0
- data/lib/omq/cli/scatter_gather.rb +14 -0
- data/lib/omq/cli.rb +468 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/socket.rb +74 -27
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/connection.rb +24 -3
- data/lib/omq/zmtp/engine.rb +179 -17
- data/lib/omq/zmtp/options.rb +4 -3
- data/lib/omq/zmtp/reactor.rb +10 -5
- data/lib/omq/zmtp/routing/channel.rb +8 -2
- data/lib/omq/zmtp/routing/fan_out.rb +38 -8
- data/lib/omq/zmtp/routing/pair.rb +8 -2
- data/lib/omq/zmtp/routing/peer.rb +7 -1
- data/lib/omq/zmtp/routing/push.rb +14 -7
- data/lib/omq/zmtp/routing/radio.rb +32 -11
- data/lib/omq/zmtp/routing/rep.rb +11 -7
- data/lib/omq/zmtp/routing/req.rb +1 -2
- data/lib/omq/zmtp/routing/round_robin.rb +35 -1
- data/lib/omq/zmtp/routing/router.rb +7 -1
- data/lib/omq/zmtp/routing/scatter.rb +16 -3
- data/lib/omq/zmtp/routing/server.rb +7 -1
- data/lib/omq/zmtp/routing/xsub.rb +7 -1
- data/lib/omq/zmtp/transport/inproc.rb +40 -5
- data/lib/omq/zmtp/transport/ipc.rb +9 -7
- data/lib/omq/zmtp/transport/tcp.rb +14 -7
- data/lib/omq/zmtp/writable.rb +21 -4
- data/lib/omq.rb +7 -0
- metadata +18 -3
- 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
|
-
|
|
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
|
data/lib/omq/zmtp/writable.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
-
|
|
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/
|
|
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
|