asterism 0.8.0 → 0.9.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 +44 -0
- data/README.md +143 -21
- data/lib/asterism/cli/complete.rb +282 -0
- data/lib/asterism/cli/hz.rb +77 -0
- data/lib/asterism/cli/nouns.rb +396 -0
- data/lib/asterism/cli/survey.rb +6 -4
- data/lib/asterism/cli/text.rb +256 -0
- data/lib/asterism/cli.rb +375 -98
- data/lib/asterism/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c3c929e3018c538fab3983c9fffce4fd997bfa8b37cad43fbaa6c0eb5890d65
|
|
4
|
+
data.tar.gz: b4fc43d2d5d286139eb700c5711786827aab41aa8a7628686b45270a321f1807
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f971a84780f4a1ce1e24554530201f6bccaba7f8de6ec03c5b2dc6742c6c370e2567af5c82e4609fd82f582d32cbe314f82bc5c60d5815b5c33e805980b14a58
|
|
7
|
+
data.tar.gz: c355695ad9b52f7513f791191eeae8ded62011cdc011c50d1e921f6fa1e58190f491cc3adc0b367fa0087def3cfd6cd03729a43143b19f608b3f3a5603879f3c
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,50 @@ the earlier ones are only in this repository. From 0.4.0 the two Zenoh
|
|
|
6
6
|
bindings (asterism-zenoh, picoruby-asterism-zenoh) carry the same minor
|
|
7
7
|
version (a patch release of one may come without the other).
|
|
8
8
|
|
|
9
|
+
## 0.9.0
|
|
10
|
+
|
|
11
|
+
The command line only; the library and the wire are as in 0.8.0 (0.8.1
|
|
12
|
+
was not published: its changes are here).
|
|
13
|
+
|
|
14
|
+
- The command line's commands are a noun and a verb, as ros2's are:
|
|
15
|
+
`node list` / `node info NODE`, `app list` / `app info NODE/APP`,
|
|
16
|
+
`object list [PATTERN]` / `object info PATH` / `object find METHOD`
|
|
17
|
+
(the objects that have a method, `*` as a wildcard), `status get` /
|
|
18
|
+
`status echo` / `status hz NODE/APP`. The lists print the names only, a
|
|
19
|
+
name a line, sorted (for grep and xargs). `asterism NOUN` lists its
|
|
20
|
+
verbs; `asterism --help` is grouped by noun. `call`, `ping`, `watch`,
|
|
21
|
+
`tree` and `mcp` are as they were, and the older `nodes`, `objects` and
|
|
22
|
+
`echo` stay with the same output. README "The command line".
|
|
23
|
+
- `status echo NODE/APP`: the status, then each change as it comes (the
|
|
24
|
+
text tells what changed, as `watch` does; `--once`: the status now,
|
|
25
|
+
once). `status hz NODE/APP`: how often the status changes, as
|
|
26
|
+
`ros2 topic hz` (the average rate and the intervals' min, max and
|
|
27
|
+
standard deviation, every `--period`; `--window`).
|
|
28
|
+
- `-o json` / `-o yaml` on every command that reads (YAML with the
|
|
29
|
+
standard library, the same content as the JSON). `--json` is `-o json`;
|
|
30
|
+
its output is as it was, byte for byte.
|
|
31
|
+
- Tab completion: `asterism completion bash` / `zsh` prints a script
|
|
32
|
+
(`source <(asterism completion bash)`). The commands, verbs and options
|
|
33
|
+
complete without the network; the nodes, `node/app`, `node/app/object`
|
|
34
|
+
(a part at a time) and an object's methods come from the network named
|
|
35
|
+
on the line or in the environment, asked for 1.5 s at most and
|
|
36
|
+
remembered for 20 s in `$XDG_CACHE_HOME/asterism/` (names only; never a
|
|
37
|
+
key or a certificate). README "Tab completion".
|
|
38
|
+
- The command line's text is easier to read (`--json` is unchanged): a
|
|
39
|
+
status is shown as indented `key=value` lines instead of one line of
|
|
40
|
+
JSON (`tree`, `nodes`, the first status of `watch`); `tree` shows how
|
|
41
|
+
many methods each object has and `tree --methods` lists them, wrapped to
|
|
42
|
+
the width; `objects` wraps the objects' and methods' descriptions to the
|
|
43
|
+
width in one column, the signatures in bold; `watch` shows only what
|
|
44
|
+
changed in a status (`~ status games/games mahjong1.members 0→1`, keys
|
|
45
|
+
that came with `+`, keys that went with `-`). On a terminal the text has
|
|
46
|
+
colours and the terminal's width; to a pipe or a file it is plain and 80
|
|
47
|
+
columns wide (or `COLUMNS`); `NO_COLOR` turns the colours off. Standard
|
|
48
|
+
library only. README "The command line".
|
|
49
|
+
- The command line takes `ASTERISM_CREDENTIALS` and `ASTERISM_ROUTER`
|
|
50
|
+
from the environment when none of `--router`, `--credentials`, `--peer`
|
|
51
|
+
is given (all or nothing). README "The command line".
|
|
52
|
+
|
|
9
53
|
## 0.8.0
|
|
10
54
|
|
|
11
55
|
Needs asterism-zenoh 0.4.1 or later, as 0.7.1. Wire protocol 1 as before:
|
data/README.md
CHANGED
|
@@ -101,7 +101,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
101
101
|
|
|
102
102
|
```
|
|
103
103
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
104
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.1.gem pkg/asterism-0.
|
|
104
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.1.gem pkg/asterism-0.9.0.gem
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
From the working trees, without installing:
|
|
@@ -130,7 +130,7 @@ rake # every suite
|
|
|
130
130
|
rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
|
|
131
131
|
rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
|
|
132
132
|
rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
|
|
133
|
-
rake test:cli # the command line (exe/asterism) and its MCP bridge (the mcp gem's client; skipped without it)
|
|
133
|
+
rake test:cli # the command line (exe/asterism), its Tab completion and its MCP bridge (the mcp gem's client; skipped without it)
|
|
134
134
|
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
|
|
135
135
|
rake test:mruby # the shared layer on mruby (builds mruby; not part of `rake`)
|
|
136
136
|
rake test:protocol # the wire protocol's conformance samples and driver against CRuby (docs/protocol.md)
|
|
@@ -415,31 +415,101 @@ What may be shared between threads:
|
|
|
415
415
|
The gem has a command, `asterism`, that looks at a network from a terminal
|
|
416
416
|
(CRuby). It is one more node: it connects, reads the liveliness tokens,
|
|
417
417
|
the meta, the profiles and the statuses, and calls a method only when told
|
|
418
|
-
to with `call`.
|
|
418
|
+
to with `call`. Its commands are a noun and a verb, as ros2's are
|
|
419
|
+
(`asterism node` alone lists the node's verbs, `asterism --help` all of
|
|
420
|
+
them):
|
|
419
421
|
|
|
420
422
|
```
|
|
421
|
-
asterism
|
|
422
|
-
asterism
|
|
423
|
-
asterism
|
|
424
|
-
asterism
|
|
425
|
-
asterism
|
|
426
|
-
asterism
|
|
427
|
-
asterism
|
|
428
|
-
asterism
|
|
423
|
+
asterism node list --router tcp/192.0.2.2:7447 # the nodes, a name a line
|
|
424
|
+
asterism node info games # one node: profile, apps, statuses, objects
|
|
425
|
+
asterism app list # node/app, a line each
|
|
426
|
+
asterism app info games/games # its status and its objects
|
|
427
|
+
asterism object list 'games/games/*' # node/app/object, a line each
|
|
428
|
+
asterism object info games/games/othello1 # its description and its methods
|
|
429
|
+
asterism object find join # the objects that have join (* as a wildcard)
|
|
430
|
+
asterism status get games/games # the status now
|
|
431
|
+
asterism status echo games/games # the status, then each change (--once: now, once)
|
|
432
|
+
asterism status hz games/games # how often it changes
|
|
433
|
+
asterism call games/games/othello1 join '"Alice"'
|
|
434
|
+
asterism call linux/demo/screen say hello --kwargs '{color: 3}'
|
|
435
|
+
asterism ping games/games/othello1 # round trips (its meta; no method runs)
|
|
436
|
+
asterism ping games/games/othello1 --call view '"<token>"' # round trips of a call
|
|
437
|
+
asterism watch # nodes, apps and objects coming and going, statuses
|
|
438
|
+
asterism tree --credentials admin.tar # nodes > apps > objects (--methods: the methods)
|
|
439
|
+
asterism completion bash # Tab completion (below)
|
|
429
440
|
```
|
|
430
441
|
|
|
442
|
+
The older commands stay, with the same output: `nodes` (every node with
|
|
443
|
+
its applications and statuses, in one listing), `objects [PATTERN]` (each
|
|
444
|
+
object with its description and methods) and `echo` (`ping`).
|
|
445
|
+
|
|
431
446
|
```
|
|
432
|
-
$
|
|
447
|
+
$ export ASTERISM_ROUTER=tcp/192.0.2.2:7447
|
|
448
|
+
$ asterism node list
|
|
449
|
+
games
|
|
450
|
+
linux
|
|
451
|
+
$ asterism object list
|
|
452
|
+
games/games/mahjong1
|
|
453
|
+
games/games/othello1
|
|
454
|
+
linux/demo/screen
|
|
455
|
+
$ asterism app info games/games
|
|
456
|
+
games/games
|
|
457
|
+
status:
|
|
458
|
+
mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
|
|
459
|
+
othello1: st=wait members=0 humans=0 cpus=0 away=0
|
|
460
|
+
objects:
|
|
461
|
+
mahjong1 18 methods
|
|
462
|
+
A riichi mahjong table (four seats). From a program: call describe first,
|
|
463
|
+
then join, sit, and look / choose in a loop.
|
|
464
|
+
othello1 11 methods
|
|
465
|
+
An othello table (two seats). join, sit, then view / move in a loop.
|
|
466
|
+
$ asterism object info games/games/othello1
|
|
467
|
+
games/games/othello1
|
|
468
|
+
An othello table (two seats). join, sit, then view / move in a loop.
|
|
469
|
+
join(name) Joins the table as name (or gets this node's
|
|
470
|
+
membership back). Returns {"token", ...}: every later
|
|
471
|
+
call brings the token. {"error" => why} when the table
|
|
472
|
+
is full.
|
|
473
|
+
mine() This node's membership ({"token", ...}), or nil when
|
|
474
|
+
it has not joined.
|
|
475
|
+
...
|
|
476
|
+
$ asterism object find join
|
|
477
|
+
games/games/mahjong1
|
|
478
|
+
games/games/othello1
|
|
479
|
+
$ asterism status get games/games -o yaml
|
|
480
|
+
---
|
|
481
|
+
mahjong1:
|
|
482
|
+
st: wait
|
|
483
|
+
members: 0
|
|
484
|
+
...
|
|
485
|
+
$ asterism status echo games/games
|
|
486
|
+
12:00:01.204 games/games (now)
|
|
487
|
+
mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
|
|
488
|
+
othello1: st=wait members=0 humans=0 cpus=0 away=0
|
|
489
|
+
12:00:05.120 mahjong1.members 0→1
|
|
490
|
+
12:00:05.133 mahjong1.humans 0→1
|
|
491
|
+
$ asterism status hz games/games
|
|
492
|
+
average rate: 0.409 Hz
|
|
493
|
+
min: 0.013 s max: 4.876 s std dev: 2.432 s window: 2
|
|
494
|
+
$ asterism tree
|
|
433
495
|
games (cruby) "asterism-games"
|
|
434
|
-
games
|
|
435
|
-
|
|
496
|
+
games
|
|
497
|
+
status:
|
|
498
|
+
mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
|
|
499
|
+
othello1: st=wait members=0 humans=0 cpus=0 away=0
|
|
500
|
+
mahjong1 18 methods
|
|
501
|
+
othello1 11 methods
|
|
436
502
|
linux (family_mruby/sim)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
503
|
+
demo
|
|
504
|
+
screen 3 methods
|
|
505
|
+
$ asterism watch
|
|
506
|
+
12:00:01.204 ~ status games/games (now)
|
|
507
|
+
mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
|
|
508
|
+
othello1: st=wait members=0 humans=0 cpus=0 away=0
|
|
440
509
|
12:00:04.881 + node wasm-1a2b3c
|
|
441
510
|
12:00:04.881 + app wasm-1a2b3c/game
|
|
442
|
-
12:00:05.120 ~ status games/games
|
|
511
|
+
12:00:05.120 ~ status games/games mahjong1.members 0→1
|
|
512
|
+
12:00:05.133 ~ status games/games mahjong1.humans 0→1
|
|
443
513
|
```
|
|
444
514
|
|
|
445
515
|
- **Where the network is.** `--router tcp/HOST:7447` (a router without TLS),
|
|
@@ -447,7 +517,12 @@ $ asterism watch --router tcp/192.0.2.2:7447
|
|
|
447
517
|
and its name; `--router` overrides the router). `--peer LOCATOR` links
|
|
448
518
|
straight to a node listening there (no router). The node ID is the
|
|
449
519
|
certificate's name, otherwise `cli-` and 6 hex digits (`--node`); the
|
|
450
|
-
application `cli-` and 4 (`--app`).
|
|
520
|
+
application `cli-` and 4 (`--app`). With none of `--router`,
|
|
521
|
+
`--credentials`, `--peer` on the line, `ASTERISM_CREDENTIALS` and
|
|
522
|
+
`ASTERISM_ROUTER` from the environment are used instead (all or nothing:
|
|
523
|
+
a router on the line never meets a `.tar` from the environment), so
|
|
524
|
+
`export ASTERISM_CREDENTIALS=~/me.tar` makes `asterism tree` enough, and
|
|
525
|
+
an MCP client can pass it in the server's environment.
|
|
451
526
|
- **What it sees** is what the router lets it see. With a console's `.tar`
|
|
452
527
|
that is the row's roles: a role for looking only is in the console's
|
|
453
528
|
docs/g1_5.md ("Looking only: a viewer role"): it reads every node, object,
|
|
@@ -458,13 +533,60 @@ $ asterism watch --router tcp/192.0.2.2:7447
|
|
|
458
533
|
literal (nil, true, false, numbers, strings, symbols, arrays, hashes; read
|
|
459
534
|
with Ripper, nothing is evaluated), else the text as written. `--kwargs`
|
|
460
535
|
is a JSON object or a Ruby Hash.
|
|
461
|
-
- **
|
|
462
|
-
the
|
|
536
|
+
- **Lists** (`node list`, `app list`, `object list`, `object find`) print
|
|
537
|
+
the names only, a name a line, sorted, so they go to `grep` and `xargs`
|
|
538
|
+
as they are (on a terminal the parts are coloured; to a pipe they are
|
|
539
|
+
plain).
|
|
540
|
+
- **Output**: text, or `-o json` / `-o yaml` on every command that reads
|
|
541
|
+
(the YAML holds what the JSON does; `--json` is `-o json`, its output as
|
|
542
|
+
it always was). The streams (`watch`, `status echo`, `status hz`) give a
|
|
543
|
+
JSON object a line, or a YAML document each (`---`), with the time. The
|
|
544
|
+
text is for people: on a terminal it has colours (nodes, applications,
|
|
545
|
+
objects, the `+` `-` `~` of watch; `NO_COLOR=1` turns them off) and is
|
|
546
|
+
wrapped to the terminal's width; written to a pipe or a file it is plain
|
|
547
|
+
and 80 columns wide (or `COLUMNS`). A status is shown as `key=value`, a
|
|
548
|
+
line for each level that holds Hashes (a long text or array is cut with
|
|
549
|
+
`…`; `-o json` has it whole). `tree` tells how many methods an object
|
|
550
|
+
has, `tree --methods` lists them; `object info` (and `objects`) lists
|
|
551
|
+
each method with its description. `watch` and `status echo` show a
|
|
552
|
+
status whole the first time (and when its application comes back), then
|
|
553
|
+
only what changed: `a.b 0→1`, `+a.c=…` (a key came), `-a.d` (a key
|
|
554
|
+
went). `--timeout` (2.0 s) is each get's and call's limit, `--settle`
|
|
463
555
|
(1.5 s) how long it waits for the tokens after connecting.
|
|
556
|
+
- **status hz** counts the changes of the status (a put of the same status
|
|
557
|
+
again is not one) and prints, every `--period` (1 s) in which a change
|
|
558
|
+
came, the average rate and the intervals' min, max and standard
|
|
559
|
+
deviation over the last `--window` (1000) intervals, as `ros2 topic hz`
|
|
560
|
+
does. An application puts its status when it changes, so this is how
|
|
561
|
+
often the application has something new to tell.
|
|
464
562
|
- **watch** shows the applications and objects themselves (their tokens),
|
|
465
563
|
not only the nodes: a node with two applications stays while one closes,
|
|
466
564
|
and `watch` shows that one leave.
|
|
467
565
|
|
|
566
|
+
### Tab completion
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
source <(asterism completion bash) # or put this line in ~/.bashrc
|
|
570
|
+
source <(asterism completion zsh) # zsh, after compinit (~/.zshrc)
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The commands, the verbs and the options complete without the network. The
|
|
574
|
+
names on the network complete too: the nodes, `node/app`, `node/app/object`
|
|
575
|
+
a part at a time (`ga<Tab>` gives `games/`, `games/games/<Tab>` the
|
|
576
|
+
objects), and after `call PATH` (or `ping PATH --call`) the object's
|
|
577
|
+
methods. On each Tab the script runs `asterism __complete`, which asks the
|
|
578
|
+
network named on the line (`--router`, `--credentials`, `--peer`) or in
|
|
579
|
+
`ASTERISM_CREDENTIALS` / `ASTERISM_ROUTER`:
|
|
580
|
+
|
|
581
|
+
- briefly: what does not answer within 1.5 s gives no candidates, and the
|
|
582
|
+
shell is not kept waiting;
|
|
583
|
+
- remembered for 20 s, a file per network in `$XDG_CACHE_HOME/asterism/`
|
|
584
|
+
(`~/.cache/asterism/`; the directory 0700, the files 0600), named by a
|
|
585
|
+
digest of where the network is. It holds the names only, never a key or
|
|
586
|
+
a certificate. What did not answer is not remembered;
|
|
587
|
+
- as one more node for a moment (a `cli-` node, or the certificate's name),
|
|
588
|
+
so `asterism watch` shows it come and go.
|
|
589
|
+
|
|
468
590
|
### MCP: Asterism as tools of an AI assistant
|
|
469
591
|
|
|
470
592
|
`asterism mcp` is an MCP server on stdio (it needs the `mcp` gem, which
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# The shell's Tab completion: `asterism completion bash|zsh` prints a
|
|
2
|
+
# script, and the script calls the hidden `asterism __complete` on each
|
|
3
|
+
# Tab, which prints the candidates, one a line.
|
|
4
|
+
#
|
|
5
|
+
# The commands, the verbs and the options come from the command line's own
|
|
6
|
+
# tables (no network). The names on the network (nodes, node/app,
|
|
7
|
+
# node/app/object, an object's methods) come from the network named on the
|
|
8
|
+
# line (--router, --credentials, --peer) or in the environment, asked
|
|
9
|
+
# briefly (Complete::BUDGET: nothing comes back when it does not answer in
|
|
10
|
+
# time, the shell is not kept waiting) and remembered for a few seconds
|
|
11
|
+
# (Cache: a file per network under $XDG_CACHE_HOME/asterism, named by a
|
|
12
|
+
# digest of where the network is; it holds names only, never a key or a
|
|
13
|
+
# certificate). A name with "/" is completed a part at a time:
|
|
14
|
+
# `ga<Tab>` gives `games/`, `games/games/<Tab>` the objects.
|
|
15
|
+
require "json"
|
|
16
|
+
require "digest"
|
|
17
|
+
require "fileutils"
|
|
18
|
+
require "shellwords"
|
|
19
|
+
|
|
20
|
+
module Asterism
|
|
21
|
+
class CLI
|
|
22
|
+
class Complete
|
|
23
|
+
# Seconds the network may take to answer one Tab (connecting, the
|
|
24
|
+
# tokens, a meta get).
|
|
25
|
+
BUDGET = 1.5
|
|
26
|
+
# What a line of the output means "complete a file name here".
|
|
27
|
+
FILES = ":files"
|
|
28
|
+
|
|
29
|
+
# spec: the CLI (its command tables and option lists); source: what
|
|
30
|
+
# the network names (#names, #methods(path), #all_methods), each
|
|
31
|
+
# nil when it did not answer.
|
|
32
|
+
def initialize(spec, source)
|
|
33
|
+
@spec = spec
|
|
34
|
+
@source = source
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The candidates for the last word of words (the words after
|
|
38
|
+
# `asterism`, the last one the word being completed, "" for a new
|
|
39
|
+
# one), those that start with it, sorted.
|
|
40
|
+
def candidates(words)
|
|
41
|
+
words = words.dup
|
|
42
|
+
cur = words.pop || ""
|
|
43
|
+
pos = []
|
|
44
|
+
prev = nil
|
|
45
|
+
words.each do |w|
|
|
46
|
+
if prev
|
|
47
|
+
prev = nil
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
if w.start_with?("-") && w.size > 1
|
|
51
|
+
prev = w if @spec.option_takes_value?(w)
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
pos << w
|
|
55
|
+
end
|
|
56
|
+
key, args = @spec.command_key(pos)
|
|
57
|
+
list =
|
|
58
|
+
if prev then values(prev, key, args)
|
|
59
|
+
elsif cur.start_with?("-") then key ? @spec.options_for(key) : []
|
|
60
|
+
else positional(pos, key, args, cur)
|
|
61
|
+
end
|
|
62
|
+
return list if list == [FILES]
|
|
63
|
+
list.select { _1.start_with?(cur) }.uniq.sort
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def values(opt, key, args)
|
|
69
|
+
case opt
|
|
70
|
+
when "-o", "--output" then %w[json yaml]
|
|
71
|
+
when "--credentials" then [FILES]
|
|
72
|
+
when "--call" then key == "ping" && args[0] ? methods_of(args[0]) : []
|
|
73
|
+
else []
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def positional(pos, key, args, cur)
|
|
78
|
+
return @spec.command_words if pos.empty?
|
|
79
|
+
return @spec.verbs(pos[0]) if pos.size == 1 && @spec.noun?(pos[0])
|
|
80
|
+
return [] unless key
|
|
81
|
+
i = args.size
|
|
82
|
+
case [key, i]
|
|
83
|
+
when ["node info", 0] then nodes
|
|
84
|
+
when ["app info", 0], ["status get", 0], ["status echo", 0], ["status hz", 0] then staged(cur, apps, 2)
|
|
85
|
+
when ["object list", 0], ["object info", 0], ["objects", 0], ["call", 0] then staged(cur, objects, 3)
|
|
86
|
+
when ["ping", 0], ["echo", 0] then (cur.include?("/") ? [] : nodes) + staged(cur, objects, 3)
|
|
87
|
+
when ["object find", 0] then @source.all_methods || []
|
|
88
|
+
when ["call", 1] then methods_of(args[0])
|
|
89
|
+
when ["completion", 0] then %w[bash zsh]
|
|
90
|
+
when ["help", 0] then @spec.nouns
|
|
91
|
+
else []
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def methods_of(path)
|
|
96
|
+
path.count("/") == 2 ? (@source.methods(path) || []) : []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def names
|
|
100
|
+
@names ||= @source.names || { "apps" => [], "objects" => [] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def objects
|
|
104
|
+
names["objects"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The applications, with those that only objects tell of.
|
|
108
|
+
def apps
|
|
109
|
+
(names["apps"] + objects.map { _1.split("/")[0, 2].join("/") }).uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def nodes
|
|
113
|
+
(apps + objects).map { _1.split("/")[0] }.uniq
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The next part of the paths (depth parts each) that start with cur:
|
|
117
|
+
# the parts up to the one cur is in, with "/" after it when more come.
|
|
118
|
+
def staged(cur, paths, depth)
|
|
119
|
+
k = cur.count("/")
|
|
120
|
+
return [] if k >= depth
|
|
121
|
+
paths.filter_map do |p|
|
|
122
|
+
s = p.split("/")
|
|
123
|
+
next if s.size < depth
|
|
124
|
+
c = s[0..k].join("/")
|
|
125
|
+
c += "/" if k + 1 < depth
|
|
126
|
+
c if c.start_with?(cur)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
public
|
|
131
|
+
|
|
132
|
+
# The words of a command line up to the cursor (bash's COMP_LINE cut
|
|
133
|
+
# at COMP_POINT): the words after the command, the last one the word
|
|
134
|
+
# being completed ("" after a space). An open quote is closed.
|
|
135
|
+
def self.split_line(line)
|
|
136
|
+
words = begin
|
|
137
|
+
Shellwords.split(line)
|
|
138
|
+
rescue ArgumentError
|
|
139
|
+
begin
|
|
140
|
+
Shellwords.split("#{line}\"")
|
|
141
|
+
rescue ArgumentError
|
|
142
|
+
Shellwords.split("#{line}'")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
words << "" if line.match?(/\s\z/) || words.empty?
|
|
146
|
+
words.drop(1)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Bash splits the word being completed at ":" and "=" (COMP_WORDBREAKS):
|
|
150
|
+
# the candidates without what comes before the last of them in cur.
|
|
151
|
+
def self.bash_trim(cands, cur)
|
|
152
|
+
cut = cur.rindex(/[:=]/)
|
|
153
|
+
return cands unless cut
|
|
154
|
+
cands.map { _1.start_with?(cur[0..cut]) ? _1[(cut + 1)..] : _1 }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------ the cache
|
|
158
|
+
|
|
159
|
+
# What __complete remembers of a network for a few seconds: a JSON
|
|
160
|
+
# file per network ({entry => {"t" => seconds, "v" => value}}), the
|
|
161
|
+
# directory 0700 and the files 0600. The network's identity is a
|
|
162
|
+
# digest of where it is (router, the credentials file's path, peer);
|
|
163
|
+
# nothing else about it is written.
|
|
164
|
+
class Cache
|
|
165
|
+
TTL = 20
|
|
166
|
+
|
|
167
|
+
def initialize(dir:, network:, ttl: TTL, clock: -> { Time.now.to_f })
|
|
168
|
+
@dir = dir
|
|
169
|
+
@ttl = ttl
|
|
170
|
+
@clock = clock
|
|
171
|
+
id = network.map { _1.to_s }.join("\n")
|
|
172
|
+
@file = File.join(dir, "#{Digest::SHA256.hexdigest(id)[0, 24]}.json")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
attr_reader :file
|
|
176
|
+
|
|
177
|
+
# The cache directory: $XDG_CACHE_HOME/asterism, else
|
|
178
|
+
# ~/.cache/asterism.
|
|
179
|
+
def self.dir(env)
|
|
180
|
+
base = env["XDG_CACHE_HOME"].to_s
|
|
181
|
+
base = File.join(Dir.home, ".cache") if base.empty?
|
|
182
|
+
File.join(base, "asterism")
|
|
183
|
+
rescue ArgumentError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The entry's value when it is fresh, else the block's (kept unless
|
|
188
|
+
# it is nil: what did not answer is asked again next time).
|
|
189
|
+
def fetch(entry)
|
|
190
|
+
data = read
|
|
191
|
+
e = data[entry]
|
|
192
|
+
now = @clock.call
|
|
193
|
+
return e["v"] if e.is_a?(Hash) && e["t"].is_a?(Numeric) && now - e["t"] < @ttl && now >= e["t"]
|
|
194
|
+
v = yield
|
|
195
|
+
return v if v.nil?
|
|
196
|
+
data = read.reject { |_, x| !x.is_a?(Hash) || !x["t"].is_a?(Numeric) || now - x["t"] >= @ttl }
|
|
197
|
+
data[entry] = { "t" => now, "v" => v }
|
|
198
|
+
write(data)
|
|
199
|
+
v
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def read
|
|
205
|
+
h = JSON.parse(File.read(@file))
|
|
206
|
+
h.is_a?(Hash) ? h : {}
|
|
207
|
+
rescue StandardError
|
|
208
|
+
{}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def write(data)
|
|
212
|
+
FileUtils.mkdir_p(@dir, mode: 0o700)
|
|
213
|
+
tmp = "#{@file}.#{Process.pid}.tmp"
|
|
214
|
+
File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC, 0o600) { _1.write(JSON.generate(data)) }
|
|
215
|
+
File.rename(tmp, @file)
|
|
216
|
+
rescue StandardError
|
|
217
|
+
begin
|
|
218
|
+
File.unlink(tmp) if tmp
|
|
219
|
+
rescue StandardError
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------ the scripts
|
|
227
|
+
|
|
228
|
+
BASH = <<~'BASH'
|
|
229
|
+
# asterism's Tab completion for bash: source <(asterism completion bash)
|
|
230
|
+
# (or put that line in ~/.bashrc). Each Tab runs `asterism __complete`;
|
|
231
|
+
# the names on the network come from the network named on the line
|
|
232
|
+
# (--router, --credentials, --peer) or in ASTERISM_CREDENTIALS /
|
|
233
|
+
# ASTERISM_ROUTER, asked briefly and remembered for a few seconds.
|
|
234
|
+
_asterism() {
|
|
235
|
+
local cur=${COMP_WORDS[COMP_CWORD]}
|
|
236
|
+
local IFS=$'\n'
|
|
237
|
+
local out
|
|
238
|
+
out=$(asterism __complete --bash "${COMP_LINE:0:COMP_POINT}" 2>/dev/null)
|
|
239
|
+
if [[ $out == ":files" ]]; then
|
|
240
|
+
compopt -o filenames 2>/dev/null
|
|
241
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
COMPREPLY=($out)
|
|
245
|
+
local c
|
|
246
|
+
for c in "${COMPREPLY[@]}"; do
|
|
247
|
+
if [[ $c == */ ]]; then
|
|
248
|
+
compopt -o nospace 2>/dev/null
|
|
249
|
+
break
|
|
250
|
+
fi
|
|
251
|
+
done
|
|
252
|
+
return 0
|
|
253
|
+
}
|
|
254
|
+
complete -F _asterism asterism
|
|
255
|
+
BASH
|
|
256
|
+
|
|
257
|
+
ZSH = <<~'ZSH'
|
|
258
|
+
# asterism's Tab completion for zsh: source <(asterism completion zsh)
|
|
259
|
+
# after compinit (or put that line in ~/.zshrc). Each Tab runs
|
|
260
|
+
# `asterism __complete`; the names on the network come from the network
|
|
261
|
+
# named on the line (--router, --credentials, --peer) or in
|
|
262
|
+
# ASTERISM_CREDENTIALS / ASTERISM_ROUTER, asked briefly and remembered
|
|
263
|
+
# for a few seconds.
|
|
264
|
+
_asterism() {
|
|
265
|
+
local -a cands slash plain
|
|
266
|
+
cands=("${(@f)$(asterism __complete -- "${(@)words[2,CURRENT]}" 2>/dev/null)}")
|
|
267
|
+
cands=(${cands:#})
|
|
268
|
+
if [[ ${cands[1]} == ":files" ]]; then
|
|
269
|
+
_files
|
|
270
|
+
return
|
|
271
|
+
fi
|
|
272
|
+
slash=(${(M)cands:#*/})
|
|
273
|
+
plain=(${cands:#*/})
|
|
274
|
+
(( ${#plain} )) && compadd -- "${plain[@]}"
|
|
275
|
+
(( ${#slash} )) && compadd -S '' -- "${slash[@]}"
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
compdef _asterism asterism
|
|
279
|
+
ZSH
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# `asterism status hz`: how often an application's status changes, as
|
|
2
|
+
# `ros2 topic hz` tells how often a topic is published. Each change is a
|
|
3
|
+
# time (a monotonic clock, given or Process.clock_gettime); the intervals
|
|
4
|
+
# between the last ones (a window of them) give the average rate and the
|
|
5
|
+
# intervals' min, max and standard deviation. A put that brings the same
|
|
6
|
+
# status again (as after a reconnection) is not a change.
|
|
7
|
+
module Asterism
|
|
8
|
+
class CLI
|
|
9
|
+
class Hz
|
|
10
|
+
attr_reader :count
|
|
11
|
+
|
|
12
|
+
# window: how many of the last intervals count. clock: a callable
|
|
13
|
+
# that returns the time in seconds (monotonic).
|
|
14
|
+
def initialize(window: 1000, clock: nil)
|
|
15
|
+
@window = window
|
|
16
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
17
|
+
@times = []
|
|
18
|
+
@last = nil
|
|
19
|
+
@seeded = false
|
|
20
|
+
@count = 0
|
|
21
|
+
@reported = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The status there at the start: what the first put is compared with
|
|
25
|
+
# (not a change itself).
|
|
26
|
+
def seed(status)
|
|
27
|
+
@last = status
|
|
28
|
+
@seeded = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A put of the status: a change (true) when it differs from the last
|
|
32
|
+
# one seen (the first put without a seed is one).
|
|
33
|
+
def put(status)
|
|
34
|
+
return false if @seeded && status == @last
|
|
35
|
+
@last = status
|
|
36
|
+
@seeded = true
|
|
37
|
+
add(@clock.call)
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A change at time t (seconds).
|
|
42
|
+
def add(t)
|
|
43
|
+
@count += 1
|
|
44
|
+
@times << t
|
|
45
|
+
@times.shift while @times.size > @window + 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# {"rate_hz", "min_s", "max_s", "std_s", "window", "count"} over the
|
|
49
|
+
# intervals in the window, or nil before two changes.
|
|
50
|
+
def stats
|
|
51
|
+
return nil if @times.size < 2
|
|
52
|
+
ints = @times.each_cons(2).map { |a, b| b - a }
|
|
53
|
+
avg = ints.sum / ints.size
|
|
54
|
+
var = ints.sum { (_1 - avg)**2 } / ints.size
|
|
55
|
+
{ "rate_hz" => avg.positive? ? 1.0 / avg : nil, "min_s" => ints.min, "max_s" => ints.max,
|
|
56
|
+
"std_s" => Math.sqrt(var), "window" => ints.size, "count" => @count }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# stats when a change came since the last report (else nil): what a
|
|
60
|
+
# period prints.
|
|
61
|
+
def report
|
|
62
|
+
return nil if @count == @reported
|
|
63
|
+
s = stats
|
|
64
|
+
@reported = @count if s
|
|
65
|
+
s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The lines of a report, as ros2 topic hz prints them.
|
|
69
|
+
def self.text(s)
|
|
70
|
+
rate = s["rate_hz"] ? format("%.3f", s["rate_hz"]) : "inf"
|
|
71
|
+
["average rate: #{rate} Hz",
|
|
72
|
+
format(" min: %.3f s max: %.3f s std dev: %.3f s window: %d", s["min_s"], s["max_s"], s["std_s"],
|
|
73
|
+
s["window"])]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|