asterism 0.8.0 → 0.10.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 +71 -0
- data/README.md +193 -21
- data/lib/asterism/cli/complete.rb +283 -0
- data/lib/asterism/cli/hz.rb +77 -0
- data/lib/asterism/cli/mcp.rb +56 -1
- data/lib/asterism/cli/nouns.rb +493 -0
- data/lib/asterism/cli/survey.rb +6 -4
- data/lib/asterism/cli/text.rb +256 -0
- data/lib/asterism/cli/values.rb +216 -0
- data/lib/asterism/cli.rb +389 -98
- data/lib/asterism/version.rb +1 -1
- metadata +7 -2
data/lib/asterism/cli.rb
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# The command line of the gem (exe/asterism): look at an Asterism network
|
|
2
|
-
# from CRuby, and call an object when asked to.
|
|
2
|
+
# from CRuby, and call an object when asked to. Its commands are a noun and
|
|
3
|
+
# a verb, as ros2's are:
|
|
3
4
|
#
|
|
4
|
-
# asterism
|
|
5
|
-
# asterism
|
|
6
|
-
# asterism
|
|
7
|
-
# asterism
|
|
8
|
-
# asterism
|
|
9
|
-
# asterism
|
|
10
|
-
# asterism
|
|
5
|
+
# asterism node list --router tcp/192.0.2.2:7447
|
|
6
|
+
# asterism node info games --credentials admin.tar
|
|
7
|
+
# asterism app list / app info games/games
|
|
8
|
+
# asterism object list 'games/games/*' / object info games/games/othello1
|
|
9
|
+
# asterism object find join # the objects that have join
|
|
10
|
+
# asterism status get games/games # echo: as it changes, hz: how often
|
|
11
|
+
# asterism object get games/games/othello1 state # the next put there
|
|
12
|
+
# asterism call games/games/othello1 view '"<token>"'
|
|
13
|
+
# asterism ping / watch / tree / mcp # as before
|
|
14
|
+
# asterism completion bash # the shell's Tab completion
|
|
15
|
+
#
|
|
16
|
+
# The older commands (nodes, objects, echo) stay, with the same output.
|
|
17
|
+
# `-o json` / `-o yaml` (and --json, as before) on every command that reads.
|
|
11
18
|
#
|
|
12
19
|
# It is one more Asterism node: it connects with Asterism.connect (as the
|
|
13
20
|
# certificate's name with --credentials, as cli-<6 hex digits> otherwise)
|
|
@@ -21,56 +28,153 @@ require "securerandom"
|
|
|
21
28
|
require_relative "cli/literal"
|
|
22
29
|
require_relative "cli/survey"
|
|
23
30
|
require_relative "cli/allow"
|
|
31
|
+
require_relative "cli/text"
|
|
32
|
+
require_relative "cli/hz"
|
|
33
|
+
require_relative "cli/complete"
|
|
34
|
+
require_relative "cli/values"
|
|
24
35
|
|
|
25
36
|
module Asterism
|
|
26
37
|
class CLI
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
38
|
+
# The nouns and their verbs: verb => [its arguments, what it does].
|
|
39
|
+
NOUNS = {
|
|
40
|
+
"node" => {
|
|
41
|
+
"list" => ["", "the nodes, a name a line"],
|
|
42
|
+
"info" => ["NODE", "one node: its profile, applications (with their statuses) and objects"]
|
|
43
|
+
},
|
|
44
|
+
"app" => {
|
|
45
|
+
"list" => ["", "the applications, node/app a line"],
|
|
46
|
+
"info" => ["NODE/APP", "one application: its status and its objects"]
|
|
47
|
+
},
|
|
48
|
+
"object" => {
|
|
49
|
+
"list" => ["[PATTERN]", "the objects, node/app/object a line (PATTERN: * and ** for whole parts)"],
|
|
50
|
+
"info" => ["PATH", "one object (node/app/object): its description and its methods"],
|
|
51
|
+
"find" => ["METHOD", "the objects that have the method (* as a wildcard)"],
|
|
52
|
+
"get" => ["PATH [KEY]", "the next value the application puts on PATH/KEY (its own keys; KEY: ** " \
|
|
53
|
+
"by default; --timeout 5.0)"],
|
|
54
|
+
"echo" => ["PATH [KEY]", "the values it puts there as they come (the text: what changed; --once: " \
|
|
55
|
+
"the first; --raw: each whole)"]
|
|
56
|
+
},
|
|
57
|
+
"status" => {
|
|
58
|
+
"get" => ["NODE/APP", "an application's status, now"],
|
|
59
|
+
"echo" => ["NODE/APP", "its status, then each change as it comes (--once: now, once)"],
|
|
60
|
+
"hz" => ["NODE/APP", "how often it changes: the rate, the intervals' min / max / std dev"]
|
|
61
|
+
}
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
# The commands without a noun: name => [its arguments, what it does].
|
|
65
|
+
COMMANDS = {
|
|
66
|
+
"call" => ["PATH METHOD [ARG...]", "calls a method; each ARG is JSON, else a Ruby literal, else text"],
|
|
67
|
+
"ping" => ["PATH", "round trips: PATH node/app/object (its meta) or node (its profile); " \
|
|
68
|
+
"--call METHOD [ARG...] times a call instead"],
|
|
69
|
+
"watch" => ["", "nodes, applications and objects coming and going, and status changes"],
|
|
70
|
+
"tree" => ["", "nodes > applications (with their status) > objects (--methods: their methods)"],
|
|
71
|
+
"mcp" => ["", "an MCP server on stdio (needs the mcp gem): the listings as tools, and the methods " \
|
|
72
|
+
"--allow names"],
|
|
73
|
+
"completion" => ["bash|zsh", "the shell's Tab completion: source <(asterism completion bash)"],
|
|
74
|
+
"version" => ["", "the gem's version"]
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
# The older names, kept with their output.
|
|
78
|
+
ALIASES = {
|
|
79
|
+
"nodes" => "the nodes, their applications (with their statuses) and profiles, in one listing",
|
|
80
|
+
"objects" => "[PATTERN] the objects with their descriptions and methods (object info of each)",
|
|
81
|
+
"echo" => "ping"
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
# The options that take a value (what __complete skips over).
|
|
85
|
+
VALUE_OPTIONS = %w[--router --credentials --peer --node --app -o --output --timeout --settle --duration --count
|
|
86
|
+
--interval --call --kwargs --allow --period --window].freeze
|
|
87
|
+
|
|
88
|
+
def self.usage
|
|
89
|
+
col = 30
|
|
90
|
+
line = lambda do |name, args, what|
|
|
91
|
+
Text.wrap_text(what, 80, first: " #{"#{name} #{args}".strip.ljust(col - 3)} ").join("\n")
|
|
92
|
+
end
|
|
93
|
+
nouns = NOUNS.flat_map { |n, verbs| verbs.map { |v, (a, w)| line.("#{n} #{v}", a, w) } }
|
|
94
|
+
cmds = COMMANDS.map { |c, (a, w)| line.(c, a, w) }
|
|
95
|
+
<<~TEXT
|
|
96
|
+
Usage: asterism NOUN VERB [ARG...] [options] (asterism NOUN: its verbs)
|
|
97
|
+
|
|
98
|
+
Commands (they only read, except call):
|
|
99
|
+
#{nouns.join("\n")}
|
|
100
|
+
|
|
101
|
+
#{cmds.join("\n")}
|
|
102
|
+
|
|
103
|
+
Where the network is (one of them):
|
|
104
|
+
--router LOCATOR a router without TLS, e.g. tcp/192.0.2.2:7447
|
|
105
|
+
--credentials TAR a console's .tar (mutual TLS; its router and name)
|
|
106
|
+
With none of --router, --credentials, --peer, the environment's
|
|
107
|
+
ASTERISM_CREDENTIALS and ASTERISM_ROUTER are used instead.
|
|
108
|
+
|
|
109
|
+
Output: text; -o json or -o yaml (the same content) on the commands that read
|
|
110
|
+
(--json is -o json). On a terminal the text has colours (NO_COLOR=1: none) and
|
|
111
|
+
the terminal's width; otherwise it is plain, 80 columns wide (or COLUMNS).
|
|
112
|
+
|
|
113
|
+
Older names, kept: nodes (all the nodes in one listing), objects [PATTERN]
|
|
114
|
+
(object info of each), echo (ping).
|
|
115
|
+
TEXT
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
USAGE = usage
|
|
119
|
+
|
|
120
|
+
# The verbs of a noun, for `asterism NOUN`.
|
|
121
|
+
def self.noun_usage(noun)
|
|
122
|
+
verbs = NOUNS.fetch(noun).map do |v, (a, w)|
|
|
123
|
+
Text.wrap_text(w, 80, first: " #{"#{noun} #{v} #{a}".strip.ljust(27)} ").join("\n")
|
|
124
|
+
end
|
|
125
|
+
"Usage: asterism #{noun} VERB [ARG...] [options]\n\n#{verbs.join("\n")}\n\n" \
|
|
126
|
+
"asterism #{noun} VERB --help: its options; asterism --help: every command\n"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.start(argv, out: $stdout, err: $stderr, env: ENV)
|
|
130
|
+
new(out: out, err: err, env: env).run(argv.dup)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize(out: $stdout, err: $stderr, env: ENV)
|
|
53
134
|
@out = out
|
|
54
135
|
@err = err
|
|
55
|
-
@
|
|
136
|
+
@env = env
|
|
137
|
+
@lock = Mutex.new
|
|
138
|
+
@last_status = {}
|
|
139
|
+
@opts = { timeout: 2.0, settle: 1.5, format: nil, count: 5, interval: 0.2, allow: [], period: 1.0,
|
|
140
|
+
window: 1000 }
|
|
56
141
|
end
|
|
57
142
|
|
|
58
143
|
def run(argv)
|
|
59
144
|
cmd = argv.shift
|
|
60
145
|
case cmd
|
|
61
146
|
when nil, "help", "-h", "--help"
|
|
147
|
+
if cmd == "help" && NOUNS.key?(argv.first)
|
|
148
|
+
@out.puts CLI.noun_usage(argv.first)
|
|
149
|
+
return 0
|
|
150
|
+
end
|
|
62
151
|
@out.puts USAGE
|
|
63
152
|
return cmd.nil? ? 2 : 0
|
|
64
153
|
when "version", "--version", "-v"
|
|
65
154
|
@out.puts "asterism #{::Asterism::VERSION}"
|
|
66
155
|
return 0
|
|
156
|
+
when "__complete"
|
|
157
|
+
return cmd_complete(argv)
|
|
67
158
|
end
|
|
68
|
-
|
|
159
|
+
key = cmd
|
|
160
|
+
if NOUNS.key?(cmd)
|
|
161
|
+
verb = argv.first
|
|
162
|
+
if verb.nil? || %w[-h --help].include?(verb) || verb.start_with?("-")
|
|
163
|
+
@out.puts CLI.noun_usage(cmd)
|
|
164
|
+
return %w[-h --help].include?(verb) ? 0 : 2
|
|
165
|
+
end
|
|
166
|
+
unless NOUNS[cmd].key?(verb)
|
|
167
|
+
@err.puts "asterism: #{cmd} has no verb #{verb.inspect}\n\n#{CLI.noun_usage(cmd)}"
|
|
168
|
+
return 2
|
|
169
|
+
end
|
|
170
|
+
argv.shift
|
|
171
|
+
key = "#{cmd} #{verb}"
|
|
172
|
+
elsif !(COMMANDS.key?(cmd) || ALIASES.key?(cmd))
|
|
69
173
|
@err.puts "asterism: unknown command #{cmd.inspect}\n\n#{USAGE}"
|
|
70
174
|
return 2
|
|
71
175
|
end
|
|
72
|
-
rest = parser(
|
|
73
|
-
send("cmd_#{
|
|
176
|
+
rest = parser(key).parse(argv)
|
|
177
|
+
send("cmd_#{key == 'echo' ? 'ping' : key.tr(' ', '_')}", rest)
|
|
74
178
|
rescue OptionParser::ParseError, ArgumentError => e
|
|
75
179
|
@err.puts "asterism: #{e.message}"
|
|
76
180
|
2
|
|
@@ -84,18 +188,33 @@ module Asterism
|
|
|
84
188
|
0
|
|
85
189
|
end
|
|
86
190
|
|
|
87
|
-
|
|
191
|
+
# The options of a command (key: "tree", "node info", ...).
|
|
192
|
+
def parser(key)
|
|
88
193
|
OptionParser.new do |o|
|
|
89
|
-
|
|
90
|
-
o.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
194
|
+
args = (NOUNS.dig(*key.split(" ")) || COMMANDS[key] || [""])[0]
|
|
195
|
+
o.banner = "Usage: asterism #{key} #{args} [options]".squeeze(" ")
|
|
196
|
+
unless key == "completion"
|
|
197
|
+
o.on("--router LOCATOR", "a router (tcp/HOST:7447)") { @opts[:router] = _1 }
|
|
198
|
+
o.on("--credentials TAR", "a console's .tar") { @opts[:credentials] = _1 }
|
|
199
|
+
o.on("--peer LOCATOR", "a direct peer link to a node listening there (no router)") { @opts[:peer] = _1 }
|
|
200
|
+
o.on("--node NAME", "this node's ID (default: the certificate's name, or cli-<6 hex>)") { @opts[:node] = _1 }
|
|
201
|
+
o.on("--app NAME", "this application's name (default: cli-<4 hex>)") { @opts[:app] = _1 }
|
|
202
|
+
unless key == "mcp"
|
|
203
|
+
o.on("-o", "--output FORMAT", %w[json yaml], "json or yaml instead of text (the same content)") do
|
|
204
|
+
@opts[:format] = _1.to_sym
|
|
205
|
+
end
|
|
206
|
+
o.on("--json", "JSON instead of text (-o json)") { @opts[:format] = :json }
|
|
207
|
+
end
|
|
208
|
+
if key == "object get"
|
|
209
|
+
o.on("--timeout SECONDS", Float, "how long to wait for a put (5.0)") { @opts[:put_timeout] = _1 }
|
|
210
|
+
else
|
|
211
|
+
o.on("--timeout SECONDS", Float, "time limit of each get or call (2.0)") { @opts[:timeout] = _1 }
|
|
212
|
+
end
|
|
213
|
+
o.on("--settle SECONDS", Float, "how long to wait for the tokens at connect (1.5)") { @opts[:settle] = _1 }
|
|
214
|
+
end
|
|
215
|
+
case key
|
|
216
|
+
when "tree"
|
|
217
|
+
o.on("--methods", "list each object's methods (else how many)") { @opts[:methods] = true }
|
|
99
218
|
when "watch"
|
|
100
219
|
o.on("--duration SECONDS", Float, "stop after this long (default: until Ctrl-C)") { @opts[:duration] = _1 }
|
|
101
220
|
when "ping", "echo"
|
|
@@ -107,20 +226,82 @@ module Asterism
|
|
|
107
226
|
when "mcp"
|
|
108
227
|
o.on("--allow PATTERN", "methods the MCP client may call: [node/][app/]object.method, * as a wildcard " \
|
|
109
228
|
"(repeatable)") { @opts[:allow] << _1 }
|
|
229
|
+
when "object echo"
|
|
230
|
+
o.on("--once", "the first put, then stop") { @opts[:once] = true }
|
|
231
|
+
o.on("--raw", "each value whole (the text; else what changed)") { @opts[:raw] = true }
|
|
232
|
+
o.on("--duration SECONDS", Float, "stop after this long (default: until Ctrl-C)") { @opts[:duration] = _1 }
|
|
233
|
+
when "status echo"
|
|
234
|
+
o.on("--once", "the status now, once, then stop") { @opts[:once] = true }
|
|
235
|
+
o.on("--duration SECONDS", Float, "stop after this long (default: until Ctrl-C)") { @opts[:duration] = _1 }
|
|
236
|
+
when "status hz"
|
|
237
|
+
o.on("--period SECONDS", Float, "how often to print (1.0)") { @opts[:period] = _1 }
|
|
238
|
+
o.on("--window N", Integer, "how many of the last intervals count (1000)") { @opts[:window] = _1 }
|
|
239
|
+
o.on("--duration SECONDS", Float, "stop after this long (default: until Ctrl-C)") { @opts[:duration] = _1 }
|
|
110
240
|
end
|
|
111
241
|
end
|
|
112
242
|
end
|
|
113
243
|
|
|
244
|
+
# ------------------------------------------------------------ the tables, for __complete
|
|
245
|
+
|
|
246
|
+
def noun?(w)
|
|
247
|
+
NOUNS.key?(w)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def nouns
|
|
251
|
+
NOUNS.keys
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def verbs(noun)
|
|
255
|
+
NOUNS.fetch(noun).keys
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def command_words
|
|
259
|
+
NOUNS.keys + COMMANDS.keys + ALIASES.keys + ["help"]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# [key, its arguments] of the positional words (nil when they name no
|
|
263
|
+
# command).
|
|
264
|
+
def command_key(pos)
|
|
265
|
+
return [nil, []] if pos.empty?
|
|
266
|
+
if NOUNS.key?(pos[0])
|
|
267
|
+
return [nil, []] unless pos[1] && NOUNS[pos[0]].key?(pos[1])
|
|
268
|
+
return ["#{pos[0]} #{pos[1]}", pos[2..]]
|
|
269
|
+
end
|
|
270
|
+
return [pos[0], pos[1..]] if COMMANDS.key?(pos[0]) || ALIASES.key?(pos[0]) || pos[0] == "help"
|
|
271
|
+
[nil, []]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def option_takes_value?(w)
|
|
275
|
+
VALUE_OPTIONS.include?(w)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def options_for(key)
|
|
279
|
+
return [] if key == "help"
|
|
280
|
+
parser(key).top.list.flat_map { |sw| sw.respond_to?(:long) ? sw.long + sw.short : [] } + ["--help"]
|
|
281
|
+
end
|
|
282
|
+
|
|
114
283
|
# ------------------------------------------------------------ connecting
|
|
115
284
|
|
|
285
|
+
# [router, credentials, peer]: the command line's, or, when it names
|
|
286
|
+
# none of them, ASTERISM_ROUTER and ASTERISM_CREDENTIALS from the
|
|
287
|
+
# environment (all or nothing, so a router given on the line never meets
|
|
288
|
+
# a .tar from the environment).
|
|
289
|
+
def network(env = @env)
|
|
290
|
+
given = [@opts[:router], @opts[:credentials], @opts[:peer]]
|
|
291
|
+
return given if given.any?
|
|
292
|
+
|
|
293
|
+
router = env["ASTERISM_ROUTER"].to_s
|
|
294
|
+
creds = env["ASTERISM_CREDENTIALS"].to_s
|
|
295
|
+
[router.empty? ? nil : router, creds.empty? ? nil : creds, nil]
|
|
296
|
+
end
|
|
297
|
+
|
|
116
298
|
# Connects, starts the receiving thread, waits for the tokens and yields
|
|
117
299
|
# the Net and a Survey.
|
|
118
300
|
def connect(app_prefix: "cli")
|
|
119
|
-
router =
|
|
120
|
-
creds = @opts[:credentials]
|
|
121
|
-
peer = @opts[:peer]
|
|
301
|
+
router, creds, peer = network
|
|
122
302
|
if router.nil? && creds.nil? && peer.nil?
|
|
123
|
-
raise ArgumentError, "where is the network? --router tcp/HOST:7447 or --credentials FILE.tar"
|
|
303
|
+
raise ArgumentError, "where is the network? --router tcp/HOST:7447 or --credentials FILE.tar " \
|
|
304
|
+
"(or ASTERISM_CREDENTIALS / ASTERISM_ROUTER)"
|
|
124
305
|
end
|
|
125
306
|
node = @opts[:node]
|
|
126
307
|
node ||= "#{app_prefix}-#{SecureRandom.hex(3)}" unless creds
|
|
@@ -142,8 +323,26 @@ module Asterism
|
|
|
142
323
|
end
|
|
143
324
|
end
|
|
144
325
|
|
|
145
|
-
|
|
146
|
-
|
|
326
|
+
# -o json or -o yaml (or --json) given: the data instead of the text.
|
|
327
|
+
def structured?
|
|
328
|
+
!@opts[:format].nil?
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# The data as -o asks: JSON (pretty, as --json always was) or YAML (the
|
|
332
|
+
# same content: what the JSON holds, as YAML).
|
|
333
|
+
def emit(v)
|
|
334
|
+
@opts[:format] == :yaml ? @out.write(yaml(v)) : @out.puts(JSON.pretty_generate(v))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# One record of a stream (watch, status echo / hz, call): a JSON line,
|
|
338
|
+
# or a YAML document ("---" before each).
|
|
339
|
+
def emit_line(v)
|
|
340
|
+
@opts[:format] == :yaml ? @out.write(yaml(v)) : @out.puts(JSON.generate(v))
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def yaml(v)
|
|
344
|
+
require "yaml"
|
|
345
|
+
YAML.dump(JSON.parse(JSON.generate(v)))
|
|
147
346
|
end
|
|
148
347
|
|
|
149
348
|
# ------------------------------------------------------------ nodes
|
|
@@ -151,16 +350,17 @@ module Asterism
|
|
|
151
350
|
def cmd_nodes(_rest)
|
|
152
351
|
connect do |_net, sv|
|
|
153
352
|
tree = sv.tree(meta: false)
|
|
154
|
-
if
|
|
155
|
-
|
|
353
|
+
if structured?
|
|
354
|
+
emit(tree.map { |n| n.merge("apps" => n["apps"].map { |a| a.except("objects") }) })
|
|
156
355
|
elsif tree.empty?
|
|
157
356
|
@out.puts "(no other node is visible)"
|
|
158
357
|
else
|
|
159
358
|
tree.each do |n|
|
|
160
|
-
@out.puts
|
|
359
|
+
@out.puts node_line(n)
|
|
161
360
|
n["apps"].each do |a|
|
|
162
|
-
|
|
163
|
-
|
|
361
|
+
k = a["objects"].size
|
|
362
|
+
@out.puts " #{paint(a['app'], :app)}#{paint(" #{count(k, 'object')}", :note) unless k.zero?}"
|
|
363
|
+
status_block(a, 4)
|
|
164
364
|
end
|
|
165
365
|
end
|
|
166
366
|
end
|
|
@@ -175,8 +375,8 @@ module Asterism
|
|
|
175
375
|
connect do |_net, sv|
|
|
176
376
|
paths = sv.objects(pattern)
|
|
177
377
|
metas = sv.metas(paths)
|
|
178
|
-
if
|
|
179
|
-
|
|
378
|
+
if structured?
|
|
379
|
+
emit(paths.map do |p|
|
|
180
380
|
h = { "path" => p, "methods" => metas[p] }
|
|
181
381
|
h["doc"] = sv.object_doc(p) if sv.object_doc(p)
|
|
182
382
|
h
|
|
@@ -184,14 +384,7 @@ module Asterism
|
|
|
184
384
|
elsif paths.empty?
|
|
185
385
|
@out.puts "(no object matches #{pattern})"
|
|
186
386
|
else
|
|
187
|
-
paths.each
|
|
188
|
-
@out.puts "#{p} #{metas.key?(p) ? methods_text(metas[p]) : '(no meta: not answered in time, or not allowed)'}"
|
|
189
|
-
@out.puts " #{sv.object_doc(p)}" if sv.object_doc(p)
|
|
190
|
-
# The methods that tell what they do, one a line (doc's first line).
|
|
191
|
-
(metas[p] || []).each do |m|
|
|
192
|
-
@out.puts " #{signature(m)} #{m['doc'].lines.first.strip}" if m["doc"]
|
|
193
|
-
end
|
|
194
|
-
end
|
|
387
|
+
paths.each { |p| object_entry(p, metas[p], sv.object_doc(p)) }
|
|
195
388
|
end
|
|
196
389
|
end
|
|
197
390
|
0
|
|
@@ -202,17 +395,20 @@ module Asterism
|
|
|
202
395
|
def cmd_tree(_rest)
|
|
203
396
|
connect do |_net, sv|
|
|
204
397
|
tree = sv.tree
|
|
205
|
-
if
|
|
206
|
-
|
|
398
|
+
if structured?
|
|
399
|
+
emit(tree)
|
|
207
400
|
elsif tree.empty?
|
|
208
401
|
@out.puts "(no other node is visible)"
|
|
209
402
|
else
|
|
210
403
|
tree.each do |n|
|
|
211
|
-
@out.puts
|
|
404
|
+
@out.puts node_line(n)
|
|
212
405
|
n["apps"].each do |a|
|
|
213
|
-
@out.puts " #{a['app']
|
|
406
|
+
@out.puts " #{paint(a['app'], :app)}"
|
|
407
|
+
status_block(a, 4)
|
|
214
408
|
a["objects"].each do |o|
|
|
215
|
-
|
|
409
|
+
ms = o["methods"]
|
|
410
|
+
@out.puts " #{paint(o['name'], :object)} #{paint(ms ? count(ms.size, 'method') : '(no meta)', :note)}"
|
|
411
|
+
method_list(ms, 6) if ms && @opts[:methods] && !ms.empty?
|
|
216
412
|
end
|
|
217
413
|
end
|
|
218
414
|
end
|
|
@@ -252,7 +448,7 @@ module Asterism
|
|
|
252
448
|
end
|
|
253
449
|
sleep 0.3
|
|
254
450
|
@watch_started = true
|
|
255
|
-
@err.puts "asterism: watching (Ctrl-C to stop)" unless
|
|
451
|
+
@err.puts "asterism: watching (Ctrl-C to stop)" unless structured?
|
|
256
452
|
deadline = @opts[:duration] && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + @opts[:duration])
|
|
257
453
|
loop do
|
|
258
454
|
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
@@ -265,25 +461,63 @@ module Asterism
|
|
|
265
461
|
|
|
266
462
|
def event(kind, path, what: nil, status: nil, profile: nil, now: false)
|
|
267
463
|
t = Time.now
|
|
268
|
-
if
|
|
464
|
+
if structured?
|
|
269
465
|
h = { "t" => t.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ"), "event" => kind, "path" => path }
|
|
270
466
|
h["what"] = what if what
|
|
271
467
|
h["status"] = status if status
|
|
272
468
|
h["profile"] = profile if profile
|
|
273
469
|
h["now"] = true if now
|
|
274
|
-
@
|
|
470
|
+
@lock.synchronize { emit_line(h) }
|
|
275
471
|
else
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
else "#{what} #{path}"
|
|
281
|
-
end
|
|
282
|
-
@out.puts "#{t.strftime('%H:%M:%S.%L')} #{mark} #{text}"
|
|
472
|
+
@lock.synchronize do
|
|
473
|
+
text = event_text(t, kind, path, what, status, profile, now)
|
|
474
|
+
@out.puts text if text
|
|
475
|
+
end
|
|
283
476
|
end
|
|
284
477
|
@out.flush
|
|
285
478
|
end
|
|
286
479
|
|
|
480
|
+
# The lines of one event (nil: nothing to tell). A status is told whole
|
|
481
|
+
# (indented under the event) the first time it is seen, else only what
|
|
482
|
+
# changed, a line for each change. An application or a node that leaves
|
|
483
|
+
# takes its statuses along, so they are told whole when they are back.
|
|
484
|
+
def event_text(t, kind, path, what, status, profile, now)
|
|
485
|
+
head = "#{paint(t.strftime('%H:%M:%S.%L'), :time)} #{paint(EVENT_MARKS[kind], kind.to_sym)} "
|
|
486
|
+
case kind
|
|
487
|
+
when "status"
|
|
488
|
+
status = json_safe(status)
|
|
489
|
+
old = @last_status[path]
|
|
490
|
+
@last_status[path] = status
|
|
491
|
+
label = "status #{paint(path, :app)}"
|
|
492
|
+
if old.nil? || !old.is_a?(Hash) || !status.is_a?(Hash)
|
|
493
|
+
lines = Text.status_lines(status, style.width, Text.width(head))
|
|
494
|
+
return "#{head}#{label}#{paint(' (now)', :note) if now}\n#{lines.join("\n")}"
|
|
495
|
+
end
|
|
496
|
+
changes = Text.status_diff(old, status)
|
|
497
|
+
return nil if changes.empty?
|
|
498
|
+
changes.map { |c| "#{head}#{label} #{change_text(c)}" }.join("\n")
|
|
499
|
+
when "profile" then "#{head}profile #{paint(path, :node)}#{profile_text(profile)}"
|
|
500
|
+
else
|
|
501
|
+
forget_statuses(path) if kind == "leave"
|
|
502
|
+
"#{head}#{what} #{paint(path, WHAT_ROLES.fetch(what, :object))}"
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
EVENT_MARKS = { "join" => "+", "leave" => "-", "status" => "~", "profile" => "*" }.freeze
|
|
507
|
+
WHAT_ROLES = { "node" => :node, "app" => :app, "object" => :object }.freeze
|
|
508
|
+
|
|
509
|
+
def change_text(c)
|
|
510
|
+
case c[0]
|
|
511
|
+
when "+" then paint(c, :join)
|
|
512
|
+
when "-" then paint(c, :leave)
|
|
513
|
+
else c
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def forget_statuses(path)
|
|
518
|
+
@last_status.delete_if { |k, _| k == path || k.start_with?("#{path}/") }
|
|
519
|
+
end
|
|
520
|
+
|
|
287
521
|
# ------------------------------------------------------------ ping
|
|
288
522
|
|
|
289
523
|
# Round trips to PATH: the meta of an object (no method runs), the
|
|
@@ -313,11 +547,11 @@ module Asterism
|
|
|
313
547
|
end
|
|
314
548
|
dt = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000
|
|
315
549
|
times << dt if ok
|
|
316
|
-
@out.puts "#{path} #{what}: #{ok ? format('%.1f ms', dt) : 'no answer'}" unless
|
|
550
|
+
@out.puts "#{path} #{what}: #{ok ? format('%.1f ms', dt) : 'no answer'}" unless structured?
|
|
317
551
|
end
|
|
318
552
|
end
|
|
319
|
-
if
|
|
320
|
-
|
|
553
|
+
if structured?
|
|
554
|
+
emit({ "path" => path, "what" => what, "count" => @opts[:count], "answered" => times.size,
|
|
321
555
|
"ms" => times.map { _1.round(2) } })
|
|
322
556
|
elsif times.any?
|
|
323
557
|
@out.puts format("%d of %d answered: min %.1f / avg %.1f / max %.1f ms", times.size, @opts[:count],
|
|
@@ -353,10 +587,10 @@ module Asterism
|
|
|
353
587
|
px = ::Asterism[path, timeout: @opts[:timeout]]
|
|
354
588
|
begin
|
|
355
589
|
v = px.asterism_call(method, *args, **kwargs)
|
|
356
|
-
|
|
590
|
+
structured? ? emit_line({ "value" => json_safe(v) }) : @out.puts(v.inspect)
|
|
357
591
|
rescue ::Asterism::RemoteError => e
|
|
358
|
-
if
|
|
359
|
-
|
|
592
|
+
if structured?
|
|
593
|
+
emit_line({ "error" => { "kind" => e.remote_kind, "class" => e.remote_class,
|
|
360
594
|
"message" => e.remote_message } })
|
|
361
595
|
else
|
|
362
596
|
@err.puts "asterism: #{path}.#{method}: #{e.remote_kind || 'error'}: #{e.message}"
|
|
@@ -390,27 +624,83 @@ module Asterism
|
|
|
390
624
|
|
|
391
625
|
# ------------------------------------------------------------ text
|
|
392
626
|
|
|
627
|
+
def style
|
|
628
|
+
@style ||= Style.new(@out, @env)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def paint(text, *styles)
|
|
632
|
+
style.paint(text, *styles)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def count(n, what)
|
|
636
|
+
"#{n} #{what}#{'s' unless n == 1}"
|
|
637
|
+
end
|
|
638
|
+
|
|
393
639
|
def profile_text(pr)
|
|
394
640
|
return "" unless pr
|
|
395
641
|
parts = [pr["kind"], pr["host"]].compact.join("/")
|
|
396
642
|
name = pr["name"] ? " \"#{pr['name']}\"" : ""
|
|
397
|
-
" (#{parts})#{name}"
|
|
643
|
+
" #{paint("(#{parts})", :note)}#{name}"
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def node_line(n)
|
|
647
|
+
"#{paint(n['node'], :node)}#{profile_text(n['profile'])}"
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# An application's status (when it has one) under "status:", indented.
|
|
651
|
+
def status_block(app, indent)
|
|
652
|
+
return unless app.key?("status")
|
|
653
|
+
@out.puts "#{' ' * indent}#{paint('status:', :note)}"
|
|
654
|
+
Text.status_lines(json_safe(app["status"]), style.width, indent + 2).each { @out.puts _1 }
|
|
398
655
|
end
|
|
399
656
|
|
|
400
|
-
|
|
401
|
-
|
|
657
|
+
# The methods' signatures wrapped to the width, indented.
|
|
658
|
+
def method_list(list, indent)
|
|
659
|
+
sigs = list.map { paint(signature(_1), :method) }
|
|
660
|
+
Text.wrap(sigs, style.width, first: " " * indent, sep: " ").each { @out.puts _1 }
|
|
402
661
|
end
|
|
403
662
|
|
|
663
|
+
# One object of `objects`: its path, its description, then its methods:
|
|
664
|
+
# one a line with their descriptions, the descriptions in one column and
|
|
665
|
+
# wrapped (a signature too long for the column has its description on
|
|
666
|
+
# the next line); or, when none has one, the signatures wrapped.
|
|
667
|
+
def object_entry(path, methods, doc)
|
|
668
|
+
@out.puts paint(path, :object)
|
|
669
|
+
Text.wrap_text(doc, style.width, first: " ").each { @out.puts _1 } if doc
|
|
670
|
+
unless methods
|
|
671
|
+
@out.puts " #{paint('(no meta: not answered in time, or not allowed)', :note)}"
|
|
672
|
+
return
|
|
673
|
+
end
|
|
674
|
+
return method_list(methods, 2) unless methods.any? { _1["doc"] }
|
|
675
|
+
|
|
676
|
+
sigs = methods.map { signature(_1) }
|
|
677
|
+
col = 2 + [sigs.map { Text.width(_1) }.max, SIGNATURE_COLUMN].min + 2
|
|
678
|
+
methods.zip(sigs).each do |m, sig|
|
|
679
|
+
head = " #{paint(sig, :method)}"
|
|
680
|
+
unless m["doc"]
|
|
681
|
+
@out.puts head
|
|
682
|
+
next
|
|
683
|
+
end
|
|
684
|
+
sw = 2 + Text.width(sig)
|
|
685
|
+
if sw + 2 > col
|
|
686
|
+
@out.puts head
|
|
687
|
+
head = " " * col
|
|
688
|
+
else
|
|
689
|
+
head += " " * (col - sw)
|
|
690
|
+
end
|
|
691
|
+
Text.wrap_text(m["doc"], style.width, first: head, rest: " " * col).each { @out.puts _1 }
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# The widest signature the description column makes room for.
|
|
696
|
+
SIGNATURE_COLUMN = 24
|
|
697
|
+
|
|
404
698
|
# name(arg, arg) with the names the meta tells, else name(2) / name(*).
|
|
405
699
|
def signature(m)
|
|
406
700
|
return "#{m['name']}(#{m['args'].join(', ')}#{', *' if m['arity'].negative?})" if m["args"]
|
|
407
701
|
m["arity"].negative? ? "#{m['name']}(*)" : "#{m['name']}(#{m['arity']})"
|
|
408
702
|
end
|
|
409
703
|
|
|
410
|
-
def compact(v)
|
|
411
|
-
JSON.generate(json_safe(v))
|
|
412
|
-
end
|
|
413
|
-
|
|
414
704
|
# A value as JSON can hold it (bytes as a note).
|
|
415
705
|
def json_safe(v)
|
|
416
706
|
case v
|
|
@@ -422,3 +712,4 @@ module Asterism
|
|
|
422
712
|
end
|
|
423
713
|
end
|
|
424
714
|
end
|
|
715
|
+
require_relative "cli/nouns"
|
data/lib/asterism/version.rb
CHANGED