nnq-cli 0.2.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 +7 -0
- data/CHANGELOG.md +78 -0
- data/LICENSE +15 -0
- data/README.md +391 -0
- data/exe/nnq +10 -0
- data/lib/nnq/cli/base_runner.rb +448 -0
- data/lib/nnq/cli/bus.rb +33 -0
- data/lib/nnq/cli/cli_parser.rb +485 -0
- data/lib/nnq/cli/config.rb +59 -0
- data/lib/nnq/cli/expression_evaluator.rb +142 -0
- data/lib/nnq/cli/formatter.rb +140 -0
- data/lib/nnq/cli/pair.rb +33 -0
- data/lib/nnq/cli/pipe.rb +206 -0
- data/lib/nnq/cli/pipe_worker.rb +138 -0
- data/lib/nnq/cli/pub_sub.rb +16 -0
- data/lib/nnq/cli/push_pull.rb +19 -0
- data/lib/nnq/cli/ractor_helpers.rb +81 -0
- data/lib/nnq/cli/req_rep.rb +105 -0
- data/lib/nnq/cli/socket_setup.rb +93 -0
- data/lib/nnq/cli/surveyor_respondent.rb +112 -0
- data/lib/nnq/cli/term.rb +86 -0
- data/lib/nnq/cli/transient_monitor.rb +41 -0
- data/lib/nnq/cli/version.rb +7 -0
- data/lib/nnq/cli.rb +190 -0
- metadata +110 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Runner for REQ sockets (synchronous request-reply client).
|
|
6
|
+
#
|
|
7
|
+
# nnq's REQ is cooked: #send_request takes the request body and
|
|
8
|
+
# blocks until the matching reply arrives, returning the reply body.
|
|
9
|
+
class ReqRunner < BaseRunner
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_loop(task)
|
|
14
|
+
n = config.count
|
|
15
|
+
i = 0
|
|
16
|
+
sleep(config.delay) if config.delay
|
|
17
|
+
loop do
|
|
18
|
+
msg = read_next
|
|
19
|
+
break unless msg
|
|
20
|
+
msg = eval_send_expr(msg)
|
|
21
|
+
next unless msg
|
|
22
|
+
reply = request_and_receive(msg)
|
|
23
|
+
break if reply.nil?
|
|
24
|
+
output(eval_recv_expr(reply))
|
|
25
|
+
i += 1
|
|
26
|
+
break if n && n > 0 && i >= n
|
|
27
|
+
break if !config.interval && (config.data || config.file)
|
|
28
|
+
wait_for_interval if config.interval
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def request_and_receive(msg)
|
|
34
|
+
return nil if msg.empty?
|
|
35
|
+
msg = [Marshal.dump(msg.first)] if config.format == :marshal
|
|
36
|
+
msg = @fmt.compress(msg)
|
|
37
|
+
reply_body = @sock.send_request(msg.first)
|
|
38
|
+
transient_ready!
|
|
39
|
+
return nil if reply_body.nil?
|
|
40
|
+
reply = @fmt.decompress([reply_body])
|
|
41
|
+
reply = [Marshal.load(reply.first)] if config.format == :marshal
|
|
42
|
+
reply
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def wait_for_interval
|
|
47
|
+
wait = config.interval - (Time.now.to_f % config.interval)
|
|
48
|
+
sleep(wait) if wait > 0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Runner for REP sockets (synchronous request-reply server).
|
|
54
|
+
#
|
|
55
|
+
# nnq's REP enforces strict alternation: #receive then #send_reply.
|
|
56
|
+
# There is no #send at all, so we bypass BaseRunner's send helpers.
|
|
57
|
+
class RepRunner < BaseRunner
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_loop(task)
|
|
62
|
+
n = config.count
|
|
63
|
+
i = 0
|
|
64
|
+
loop do
|
|
65
|
+
msg = recv_msg
|
|
66
|
+
break if msg.nil?
|
|
67
|
+
break unless handle_rep_request(msg)
|
|
68
|
+
i += 1
|
|
69
|
+
break if n && n > 0 && i >= n
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def handle_rep_request(msg)
|
|
75
|
+
if config.recv_expr || @recv_eval_proc
|
|
76
|
+
reply = eval_recv_expr(msg)
|
|
77
|
+
unless reply.equal?(SENT)
|
|
78
|
+
output(reply)
|
|
79
|
+
send_reply(reply || [""])
|
|
80
|
+
end
|
|
81
|
+
elsif config.echo
|
|
82
|
+
output(msg)
|
|
83
|
+
send_reply(msg)
|
|
84
|
+
elsif config.data || config.file || !config.stdin_is_tty
|
|
85
|
+
reply = read_next
|
|
86
|
+
return false unless reply
|
|
87
|
+
output(msg)
|
|
88
|
+
send_reply(reply)
|
|
89
|
+
else
|
|
90
|
+
abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
|
|
91
|
+
end
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def send_reply(msg)
|
|
97
|
+
return if msg.empty?
|
|
98
|
+
msg = [Marshal.dump(msg.first)] if config.format == :marshal
|
|
99
|
+
msg = @fmt.compress(msg)
|
|
100
|
+
@sock.send_reply(msg.first)
|
|
101
|
+
transient_ready!
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Stateless helper for socket construction and configuration.
|
|
6
|
+
# All methods are module-level so callers compose rather than inherit.
|
|
7
|
+
module SocketSetup
|
|
8
|
+
# Default high water mark applied when the user does not pass
|
|
9
|
+
# --hwm. 64 matches the send pump's per-fairness-batch limit
|
|
10
|
+
# (one batch exactly fills a full queue).
|
|
11
|
+
DEFAULT_HWM = 64
|
|
12
|
+
|
|
13
|
+
# Default max inbound message size (1 MiB) so a misconfigured or
|
|
14
|
+
# malicious peer can't force arbitrary memory allocation on a
|
|
15
|
+
# terminal user. Users can raise it with --recv-maxsz N, or
|
|
16
|
+
# disable it entirely with --recv-maxsz 0.
|
|
17
|
+
DEFAULT_RECV_MAXSZ = 1 << 20
|
|
18
|
+
|
|
19
|
+
# Apply post-construction socket options from +config+ to +sock+.
|
|
20
|
+
# send_hwm and linger are construction-time kwargs (see {.build});
|
|
21
|
+
# the rest of the options are set here and read later by the
|
|
22
|
+
# engine/transports.
|
|
23
|
+
def self.apply_options(sock, config)
|
|
24
|
+
sock.options.read_timeout = config.timeout if config.timeout
|
|
25
|
+
sock.options.write_timeout = config.timeout if config.timeout
|
|
26
|
+
sock.options.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
27
|
+
sock.options.max_message_size =
|
|
28
|
+
case config.recv_maxsz
|
|
29
|
+
when nil then DEFAULT_RECV_MAXSZ
|
|
30
|
+
when 0 then nil
|
|
31
|
+
else config.recv_maxsz
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Create and fully configure a socket from +klass+ and +config+.
|
|
37
|
+
# nnq's Socket constructor takes linger + send_hwm directly
|
|
38
|
+
# (send_hwm is captured during routing init and can't be changed
|
|
39
|
+
# after the fact), so we pass them there.
|
|
40
|
+
def self.build(klass, config)
|
|
41
|
+
sock = klass.new(
|
|
42
|
+
linger: config.linger,
|
|
43
|
+
send_hwm: config.send_hwm || DEFAULT_HWM,
|
|
44
|
+
)
|
|
45
|
+
apply_options(sock, config)
|
|
46
|
+
sock
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Bind/connect +sock+ using URL strings from +config.binds+ / +config.connects+.
|
|
51
|
+
# +verbose+ is the integer verbosity level (0 = silent).
|
|
52
|
+
def self.attach(sock, config, verbose: 0)
|
|
53
|
+
config.binds.each do |url|
|
|
54
|
+
sock.bind(url)
|
|
55
|
+
CLI::Term.write_attach(:bind, sock.last_endpoint, verbose) if verbose >= 1
|
|
56
|
+
end
|
|
57
|
+
config.connects.each do |url|
|
|
58
|
+
sock.connect(url)
|
|
59
|
+
CLI::Term.write_attach(:connect, url, verbose) if verbose >= 1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Bind/connect +sock+ from an Array of Endpoint objects.
|
|
65
|
+
# Used by PipeRunner, which works with structured endpoint lists.
|
|
66
|
+
# +verbose+ is the integer verbosity level (0 = silent).
|
|
67
|
+
def self.attach_endpoints(sock, endpoints, verbose: 0)
|
|
68
|
+
endpoints.each do |ep|
|
|
69
|
+
if ep.bind?
|
|
70
|
+
sock.bind(ep.url)
|
|
71
|
+
CLI::Term.write_attach(:bind, sock.last_endpoint, verbose) if verbose >= 1
|
|
72
|
+
else
|
|
73
|
+
sock.connect(ep.url)
|
|
74
|
+
CLI::Term.write_attach(:connect, ep.url, verbose) if verbose >= 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Subscribe to prefixes on a SUB socket.
|
|
81
|
+
#
|
|
82
|
+
# Unlike ZeroMQ, nng's sub0 starts with an empty subscription set,
|
|
83
|
+
# meaning *no* messages match. If the user passed no `--subscribe`
|
|
84
|
+
# flags, default to subscribing to the empty prefix so the CLI
|
|
85
|
+
# feels like `nngcat` / `omq sub`: receive everything by default.
|
|
86
|
+
def self.setup_subscriptions(sock, config)
|
|
87
|
+
return unless config.type_name == "sub"
|
|
88
|
+
prefixes = config.subscribes.empty? ? [""] : config.subscribes
|
|
89
|
+
prefixes.each { |p| sock.subscribe(p) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Runner for SURVEYOR sockets (broadcast survey, collect replies).
|
|
6
|
+
#
|
|
7
|
+
# Sends each input line as a survey, then collects replies until
|
|
8
|
+
# the survey window expires.
|
|
9
|
+
class SurveyorRunner < BaseRunner
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_loop(task)
|
|
14
|
+
n = config.count
|
|
15
|
+
i = 0
|
|
16
|
+
sleep(config.delay) if config.delay
|
|
17
|
+
loop do
|
|
18
|
+
msg = read_next
|
|
19
|
+
break unless msg
|
|
20
|
+
msg = eval_send_expr(msg)
|
|
21
|
+
next unless msg
|
|
22
|
+
survey_and_collect(msg)
|
|
23
|
+
i += 1
|
|
24
|
+
break if n && n > 0 && i >= n
|
|
25
|
+
break if !config.interval && (config.data || config.file)
|
|
26
|
+
wait_for_interval if config.interval
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def survey_and_collect(msg)
|
|
32
|
+
return if msg.empty?
|
|
33
|
+
msg = [Marshal.dump(msg.first)] if config.format == :marshal
|
|
34
|
+
msg = @fmt.compress(msg)
|
|
35
|
+
@sock.send_survey(msg.first)
|
|
36
|
+
transient_ready!
|
|
37
|
+
collect_replies
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def collect_replies
|
|
42
|
+
loop do
|
|
43
|
+
body = @sock.receive
|
|
44
|
+
break if body.nil?
|
|
45
|
+
reply = @fmt.decompress([body])
|
|
46
|
+
reply = [Marshal.load(reply.first)] if config.format == :marshal
|
|
47
|
+
output(eval_recv_expr(reply))
|
|
48
|
+
rescue NNQ::TimedOut
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def wait_for_interval
|
|
55
|
+
wait = config.interval - (Time.now.to_f % config.interval)
|
|
56
|
+
sleep(wait) if wait > 0
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Runner for RESPONDENT sockets (receive surveys, send replies).
|
|
62
|
+
#
|
|
63
|
+
# Mirrors REP: strict alternation of #receive then #send_reply.
|
|
64
|
+
class RespondentRunner < BaseRunner
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_loop(task)
|
|
69
|
+
n = config.count
|
|
70
|
+
i = 0
|
|
71
|
+
loop do
|
|
72
|
+
msg = recv_msg
|
|
73
|
+
break if msg.nil?
|
|
74
|
+
break unless handle_survey(msg)
|
|
75
|
+
i += 1
|
|
76
|
+
break if n && n > 0 && i >= n
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def handle_survey(msg)
|
|
82
|
+
if config.recv_expr || @recv_eval_proc
|
|
83
|
+
reply = eval_recv_expr(msg)
|
|
84
|
+
unless reply.equal?(SENT)
|
|
85
|
+
output(reply)
|
|
86
|
+
send_reply(reply || [""])
|
|
87
|
+
end
|
|
88
|
+
elsif config.echo
|
|
89
|
+
output(msg)
|
|
90
|
+
send_reply(msg)
|
|
91
|
+
elsif config.data || config.file || !config.stdin_is_tty
|
|
92
|
+
reply = read_next
|
|
93
|
+
return false unless reply
|
|
94
|
+
output(msg)
|
|
95
|
+
send_reply(reply)
|
|
96
|
+
else
|
|
97
|
+
abort "RESPONDENT needs a reply source: --echo, --data, --file, -e, or stdin pipe"
|
|
98
|
+
end
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def send_reply(msg)
|
|
104
|
+
return if msg.empty?
|
|
105
|
+
msg = [Marshal.dump(msg.first)] if config.format == :marshal
|
|
106
|
+
msg = @fmt.compress(msg)
|
|
107
|
+
@sock.send_reply(msg.first)
|
|
108
|
+
transient_ready!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/nnq/cli/term.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Stateless terminal formatting and stderr writing helpers shared
|
|
6
|
+
# by every code path that emits verbose-driven log lines (event
|
|
7
|
+
# monitor callbacks in BaseRunner / PipeRunner, SocketSetup attach
|
|
8
|
+
# helpers, parallel/pipe Ractor workers).
|
|
9
|
+
#
|
|
10
|
+
# Pure module functions: no state, no instance, safe to call from
|
|
11
|
+
# any thread or Ractor.
|
|
12
|
+
#
|
|
13
|
+
module Term
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Returns a stderr log line prefix. At verbose >= 4, prepends an
|
|
18
|
+
# ISO8601 UTC timestamp with µs precision so log traces become
|
|
19
|
+
# time-correlatable. Otherwise returns the empty string.
|
|
20
|
+
#
|
|
21
|
+
# @param verbose [Integer]
|
|
22
|
+
# @return [String]
|
|
23
|
+
def log_prefix(verbose)
|
|
24
|
+
return "" unless verbose && verbose >= 4
|
|
25
|
+
"#{Time.now.utc.strftime("%FT%T.%6N")}Z "
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Formats one NNQ::MonitorEvent into a single log line (no
|
|
30
|
+
# trailing newline).
|
|
31
|
+
#
|
|
32
|
+
# @param event [NNQ::MonitorEvent]
|
|
33
|
+
# @param verbose [Integer]
|
|
34
|
+
# @return [String]
|
|
35
|
+
def format_event(event, verbose)
|
|
36
|
+
prefix = log_prefix(verbose)
|
|
37
|
+
case event.type
|
|
38
|
+
when :message_sent
|
|
39
|
+
"#{prefix}nnq: >> #{Formatter.preview([event.detail[:body]])}"
|
|
40
|
+
when :message_received
|
|
41
|
+
"#{prefix}nnq: << #{Formatter.preview([event.detail[:body]])}"
|
|
42
|
+
else
|
|
43
|
+
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
44
|
+
detail = event.detail ? " #{event.detail}" : ""
|
|
45
|
+
"#{prefix}nnq: #{event.type}#{ep}#{detail}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Formats an "attached endpoint" log line (Bound to / Connecting to).
|
|
51
|
+
#
|
|
52
|
+
# @param kind [:bind, :connect]
|
|
53
|
+
# @param url [String]
|
|
54
|
+
# @param verbose [Integer]
|
|
55
|
+
# @return [String]
|
|
56
|
+
def format_attach(kind, url, verbose)
|
|
57
|
+
verb = kind == :bind ? "Bound to" : "Connecting to"
|
|
58
|
+
"#{log_prefix(verbose)}nnq: #{verb} #{url}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Writes one formatted event line to +io+ (default $stderr).
|
|
63
|
+
#
|
|
64
|
+
# @param event [NNQ::MonitorEvent]
|
|
65
|
+
# @param verbose [Integer]
|
|
66
|
+
# @param io [#write] writable sink, default $stderr
|
|
67
|
+
# @return [void]
|
|
68
|
+
def write_event(event, verbose, io: $stderr)
|
|
69
|
+
io.write("#{format_event(event, verbose)}\n")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Writes one "Bound to / Connecting to" line to +io+
|
|
74
|
+
# (default $stderr).
|
|
75
|
+
#
|
|
76
|
+
# @param kind [:bind, :connect]
|
|
77
|
+
# @param url [String]
|
|
78
|
+
# @param verbose [Integer]
|
|
79
|
+
# @param io [#write]
|
|
80
|
+
# @return [void]
|
|
81
|
+
def write_attach(kind, url, verbose, io: $stderr)
|
|
82
|
+
io.write("#{format_attach(kind, url, verbose)}\n")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
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 NNQ 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
|
data/lib/nnq/cli.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "cli/version"
|
|
5
|
+
require_relative "cli/config"
|
|
6
|
+
require_relative "cli/cli_parser"
|
|
7
|
+
require_relative "cli/formatter"
|
|
8
|
+
require_relative "cli/expression_evaluator"
|
|
9
|
+
require_relative "cli/socket_setup"
|
|
10
|
+
require_relative "cli/term"
|
|
11
|
+
require_relative "cli/transient_monitor"
|
|
12
|
+
require_relative "cli/base_runner"
|
|
13
|
+
require_relative "cli/push_pull"
|
|
14
|
+
require_relative "cli/pub_sub"
|
|
15
|
+
require_relative "cli/req_rep"
|
|
16
|
+
require_relative "cli/pair"
|
|
17
|
+
require_relative "cli/bus"
|
|
18
|
+
require_relative "cli/surveyor_respondent"
|
|
19
|
+
require_relative "cli/ractor_helpers"
|
|
20
|
+
require_relative "cli/pipe_worker"
|
|
21
|
+
require_relative "cli/pipe"
|
|
22
|
+
|
|
23
|
+
module NNQ
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# @return [Proc, nil] registered outgoing message transform
|
|
27
|
+
attr_reader :outgoing_proc
|
|
28
|
+
# @return [Proc, nil] registered incoming message transform
|
|
29
|
+
attr_reader :incoming_proc
|
|
30
|
+
|
|
31
|
+
# Registers an outgoing message transform (used by -r scripts).
|
|
32
|
+
#
|
|
33
|
+
# @yield [Array<String>] 1-element message array before sending
|
|
34
|
+
# @return [Proc]
|
|
35
|
+
def outgoing(&block) = @outgoing_proc = block
|
|
36
|
+
|
|
37
|
+
# Registers an incoming message transform (used by -r scripts).
|
|
38
|
+
#
|
|
39
|
+
# @yield [Array<String>] 1-element message array after receiving
|
|
40
|
+
# @return [Proc]
|
|
41
|
+
def incoming(&block) = @incoming_proc = block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Command-line interface for NNQ socket operations.
|
|
46
|
+
module CLI
|
|
47
|
+
SOCKET_TYPE_NAMES = %w[req rep pub sub push pull pair bus surveyor respondent pipe].freeze
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
RUNNER_MAP = {
|
|
51
|
+
"push" => [PushRunner, :PUSH0],
|
|
52
|
+
"pull" => [PullRunner, :PULL0],
|
|
53
|
+
"pub" => [PubRunner, :PUB0],
|
|
54
|
+
"sub" => [SubRunner, :SUB0],
|
|
55
|
+
"req" => [ReqRunner, :REQ0],
|
|
56
|
+
"rep" => [RepRunner, :REP0],
|
|
57
|
+
"pair" => [PairRunner, :PAIR0],
|
|
58
|
+
"bus" => [BusRunner, :BUS0],
|
|
59
|
+
"surveyor" => [SurveyorRunner, :SURVEYOR0],
|
|
60
|
+
"respondent" => [RespondentRunner, :RESPONDENT0],
|
|
61
|
+
"pipe" => [PipeRunner, nil],
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
module_function
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Displays text through the system pager, or prints directly
|
|
69
|
+
# when stdout is not a terminal.
|
|
70
|
+
#
|
|
71
|
+
def page(text)
|
|
72
|
+
if $stdout.tty?
|
|
73
|
+
if ENV["PAGER"]
|
|
74
|
+
pager = ENV["PAGER"]
|
|
75
|
+
else
|
|
76
|
+
ENV["LESS"] ||= "-FR"
|
|
77
|
+
pager = "less"
|
|
78
|
+
end
|
|
79
|
+
IO.popen(pager, "w") { |io| io.puts text }
|
|
80
|
+
else
|
|
81
|
+
puts text
|
|
82
|
+
end
|
|
83
|
+
rescue Errno::ENOENT
|
|
84
|
+
puts text
|
|
85
|
+
rescue Errno::EPIPE
|
|
86
|
+
# user quit pager early
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Main entry point.
|
|
91
|
+
#
|
|
92
|
+
# @param argv [Array<String>] command-line arguments
|
|
93
|
+
# @return [void]
|
|
94
|
+
def run(argv = ARGV)
|
|
95
|
+
run_socket(argv)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Parses CLI arguments, validates options, and runs the main
|
|
100
|
+
# event loop inside an Async reactor.
|
|
101
|
+
#
|
|
102
|
+
def run_socket(argv)
|
|
103
|
+
config = build_config(argv)
|
|
104
|
+
|
|
105
|
+
require "nnq"
|
|
106
|
+
require "async"
|
|
107
|
+
require "json"
|
|
108
|
+
require "console"
|
|
109
|
+
|
|
110
|
+
CliParser.validate_gems!(config)
|
|
111
|
+
trap("INT") { Process.exit!(0) }
|
|
112
|
+
trap("TERM") { Process.exit!(0) }
|
|
113
|
+
|
|
114
|
+
Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose >= 1
|
|
115
|
+
|
|
116
|
+
debug_ep = nil
|
|
117
|
+
|
|
118
|
+
if ENV["NNQ_DEBUG_URI"]
|
|
119
|
+
begin
|
|
120
|
+
require "async/debug"
|
|
121
|
+
debug_ep = Async::HTTP::Endpoint.parse(ENV["NNQ_DEBUG_URI"])
|
|
122
|
+
if debug_ep.scheme == "https"
|
|
123
|
+
require "localhost"
|
|
124
|
+
debug_ep = Async::HTTP::Endpoint.parse(ENV["NNQ_DEBUG_URI"],
|
|
125
|
+
ssl_context: Localhost::Authority.fetch.server_context)
|
|
126
|
+
end
|
|
127
|
+
rescue LoadError
|
|
128
|
+
abort "NNQ_DEBUG_URI requires the async-debug gem: gem install async-debug"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if config.type_name.nil?
|
|
133
|
+
Process.setproctitle("nnq script")
|
|
134
|
+
Object.include(NNQ) unless Object.include?(NNQ)
|
|
135
|
+
Async annotation: 'nnq' do
|
|
136
|
+
Async::Debug.serve(endpoint: debug_ep) if debug_ep
|
|
137
|
+
config.scripts.each { |s| load_script(s) }
|
|
138
|
+
rescue => e
|
|
139
|
+
$stderr.puts "nnq: #{e.message}"
|
|
140
|
+
exit 1
|
|
141
|
+
end
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
|
|
146
|
+
|
|
147
|
+
Async annotation: "nnq #{config.type_name}" do |task|
|
|
148
|
+
Async::Debug.serve(endpoint: debug_ep) if debug_ep
|
|
149
|
+
config.scripts.each { |s| load_script(s) }
|
|
150
|
+
runner = if socket_sym
|
|
151
|
+
runner_class.new(config, NNQ.const_get(socket_sym))
|
|
152
|
+
else
|
|
153
|
+
runner_class.new(config)
|
|
154
|
+
end
|
|
155
|
+
runner.call(task)
|
|
156
|
+
rescue DecompressError => e
|
|
157
|
+
$stderr.puts "nnq: #{e.message}"
|
|
158
|
+
exit 1
|
|
159
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
160
|
+
$stderr.puts "nnq: timeout" unless config.quiet
|
|
161
|
+
exit 2
|
|
162
|
+
rescue ::Socket::ResolutionError => e
|
|
163
|
+
$stderr.puts "nnq: #{e.message}"
|
|
164
|
+
exit 1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def load_script(s)
|
|
170
|
+
if s == :stdin
|
|
171
|
+
eval($stdin.read, TOPLEVEL_BINDING, "(stdin)", 1) # rubocop:disable Security/Eval
|
|
172
|
+
else
|
|
173
|
+
require s
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
private_class_method :load_script
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Builds a frozen Config from command-line arguments.
|
|
180
|
+
#
|
|
181
|
+
def build_config(argv)
|
|
182
|
+
opts = CliParser.parse(argv)
|
|
183
|
+
CliParser.validate!(opts)
|
|
184
|
+
|
|
185
|
+
opts[:stdin_is_tty] = $stdin.tty?
|
|
186
|
+
|
|
187
|
+
Ractor.make_shareable(Config.new(**opts))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|