asterism 0.6.0 → 0.7.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 +94 -0
- data/README.md +140 -32
- data/lib/asterism/cruby/credentials.rb +152 -0
- data/lib/asterism/cruby/objects.rb +19 -6
- data/lib/asterism/cruby/ros.rb +1 -1
- data/lib/asterism/cruby/zenoh.rb +13 -2
- data/lib/asterism/cruby.rb +51 -7
- data/lib/asterism/version.rb +4 -3
- data/lib/asterism.rb +1 -1
- data/mrblib/asterism.rb +248 -62
- data/mrblib/future.rb +11 -5
- data/mrblib/profile.rb +17 -4
- data/mrblib/proxy.rb +23 -5
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d729d6c233d712c647e7fb1168f2587fa0e81eb1b176a11f12ac7c349f61a592
|
|
4
|
+
data.tar.gz: fce6e1102b0e4e2c404adf28a299c77efd548f6a07e92dc81a7b54a9efe1951e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2050d9890925ad4b0910f3e8063a7d2b6e9ef1e8f68ea3346bea26c02674cce7a6b0c2ee798d2940f2171d7c763c49dd8160a44b3d3204b13ccea813936430c
|
|
7
|
+
data.tar.gz: 862531b730d5bc6f00c673271fb4d1d8db6ecaaab11969a4be95234c09be4ba6eceed245b24102f490fda6608f946b5d30e8ce3b960255b197b95bcd5a4aa550
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,100 @@ 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.7.1
|
|
10
|
+
|
|
11
|
+
Needs asterism-zenoh 0.4.1 or later (`Session#reconnecting?`).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
- `credentials:` on CRuby (`Asterism.connect`, `Asterism::Zenoh.open`,
|
|
15
|
+
`Asterism::ROS.connect`): the one `.tar` an Asterism console's
|
|
16
|
+
certificate page saves (key, certificate, CA, `asterism.json`). It is read
|
|
17
|
+
in memory (`Asterism::Credentials`, no temporary files) and handed to
|
|
18
|
+
zenoh as `transport/link/tls/*_base64` with `enable_mtls`; the locator and
|
|
19
|
+
`node:` default to the router and the name in `asterism.json` (given ones
|
|
20
|
+
win). A tar the group or others may read is warned about. `rake
|
|
21
|
+
test:credentials` (docker) connects two nodes to a mutual-TLS zenohd with
|
|
22
|
+
a tar each.
|
|
23
|
+
|
|
24
|
+
- A client of a router keeps its connection across a router restart: the
|
|
25
|
+
bindings reopen it and declare everything again, and Asterism no longer
|
|
26
|
+
closes the connection on a Zenoh error while the session is reconnecting.
|
|
27
|
+
`Asterism.reconnecting?` (and `net.reconnecting?`) is true meanwhile; the
|
|
28
|
+
profile is told again once it is back. `rake test:reconnect` (docker)
|
|
29
|
+
restarts a zenohd under a CRuby and an mruby node.
|
|
30
|
+
|
|
31
|
+
## 0.7.0
|
|
32
|
+
|
|
33
|
+
**This release breaks compatibility: it speaks wire protocol 1 only.** A
|
|
34
|
+
node of 0.7.0 and a node of 0.6.0 or earlier (protocol 0) do not see each
|
|
35
|
+
other's objects, and a call between them fails: a protocol 0 call to a
|
|
36
|
+
0.7.0 node is refused with the error kind `protocol`, a 0.7.0 call to an
|
|
37
|
+
older node is not answered, and their replies are malformed answers to
|
|
38
|
+
each other. Move every node of a network to 0.7.0 together (boards,
|
|
39
|
+
browsers, consoles, CRuby programs). The Ruby API stays as it was, with
|
|
40
|
+
the additions below. Needs asterism-zenoh / picoruby-asterism-zenoh 0.4.0
|
|
41
|
+
(unchanged), and on mruby a MessagePack that reads and writes bin (README,
|
|
42
|
+
"Using it").
|
|
43
|
+
|
|
44
|
+
Protocol 1 (`docs/protocol.md`, normative; `docs/protocol_review.md` says
|
|
45
|
+
what was decided and why):
|
|
46
|
+
|
|
47
|
+
- A call is a get on `asterism/<node>/<app>/<object>/call/<caller>`, where
|
|
48
|
+
`<caller>` is the calling node's ID. Its payload is a map
|
|
49
|
+
`{"method" => name, "args" => [...], "kwargs" => {...}}` (args and
|
|
50
|
+
kwargs left out when empty) instead of the array `[method, args,
|
|
51
|
+
kwargs]`.
|
|
52
|
+
- A reply is `{"value" => value}` or `{"error" => {"kind" => ..., "class"
|
|
53
|
+
=> ..., "message" => ...}}` instead of `["ok", value]` /
|
|
54
|
+
`["error", class, message]`. The kinds: `raised`, `not_exposed`,
|
|
55
|
+
`no_object`, `arguments`, `malformed`, `unsendable`, `nesting`,
|
|
56
|
+
`protocol`.
|
|
57
|
+
- The meta is `{"methods" => [{"name" => name, "arity" => n}, ...]}`.
|
|
58
|
+
- Binary Strings go as MessagePack bin (CRuby: ASCII-8BIT Strings, no more
|
|
59
|
+
compatibility mode; mruby: `String#b`), and a bin arrives as a binary
|
|
60
|
+
String. The profile's icon is a bin.
|
|
61
|
+
- The profile has `"protocol" => 1`.
|
|
62
|
+
- Each application declares its own liveliness token
|
|
63
|
+
`asterism/<node>/<app>`; the node token `asterism/<node>`, which the
|
|
64
|
+
applications of a node shared, is gone. A node is present while any of
|
|
65
|
+
its applications or objects is.
|
|
66
|
+
- A payload is exactly one value: bytes after it, ext types and integers
|
|
67
|
+
outside the signed 64-bit range make it malformed. CRuby raises
|
|
68
|
+
`EncodeError` before sending an Integer outside int64 (it raised
|
|
69
|
+
`RangeError` before).
|
|
70
|
+
- A protocol 0 call (on the five-chunk `.../call` key) is answered with the
|
|
71
|
+
error kind `protocol`.
|
|
72
|
+
|
|
73
|
+
Added
|
|
74
|
+
|
|
75
|
+
- `Asterism.current_call` while an exposed method runs for a call (per
|
|
76
|
+
thread on CRuby, `net.current_call` too): `from` (the calling node),
|
|
77
|
+
`verified` (`:claimed`; `:local` for a call from the same application),
|
|
78
|
+
`object`, `method`. `Asterism::CurrentCall`.
|
|
79
|
+
- `Asterism.expose(name, obj, methods:, caller: :from)` (and
|
|
80
|
+
`net.expose`): passes the calling node's ID as the keyword `from:` and
|
|
81
|
+
refuses a call that brings `from:` itself.
|
|
82
|
+
- `RemoteError#remote_kind`: the error's kind, as above (nil when the
|
|
83
|
+
answer did not say).
|
|
84
|
+
- `proxy.asterism_call(name, *args, **kw)` and `proxy.async.asterism_call`:
|
|
85
|
+
a call by the method's name, for names a proxy cannot forward (`display`,
|
|
86
|
+
`hash`, `class`, `fail`, ...) and names that are not Ruby identifiers.
|
|
87
|
+
- `Asterism::PROTOCOL` (1).
|
|
88
|
+
- The conformance tests (`test/protocol/`) are protocol 1's: 55 values, 54
|
|
89
|
+
incoming queries, 24 outgoing steps; CRuby and mruby pass them all.
|
|
90
|
+
|
|
91
|
+
Changed
|
|
92
|
+
|
|
93
|
+
- `proxy.asterism_meta` returns the new meta form.
|
|
94
|
+
- `Asterism.connect`'s duplicate check also finds a copy of the
|
|
95
|
+
application that exposes nothing (it has its application token now).
|
|
96
|
+
- `host_test/msgpack.rb` (the mruby test's MessagePack) writes a binary
|
|
97
|
+
String as bin and reads a bin as one; the mruby test build adds
|
|
98
|
+
mruby-encoding (to tell a binary String).
|
|
99
|
+
- The documentation of the wire protocol (docs/protocol.md,
|
|
100
|
+
docs/protocol_review.md) and the conformance tests (X0, unreleased in
|
|
101
|
+
0.6.0) are rewritten for protocol 1.
|
|
102
|
+
|
|
9
103
|
## 0.6.0
|
|
10
104
|
|
|
11
105
|
One connection for everything and a lighter `Asterism.poll`; every 0.5.x
|
data/README.md
CHANGED
|
@@ -14,6 +14,7 @@ repository holds the pure Ruby layers, written once for every Ruby:
|
|
|
14
14
|
| `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0; tf2_msgs BSD-3-Clause; see NOTICE) |
|
|
15
15
|
| `tools/` | The type generator `asterism_msggen.rb`, the Jazzy definitions it reads, `ros2_types.rb` (refreshes them from a ROS 2 image) |
|
|
16
16
|
| `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_profile.rb` (node profiles), `test_session.rb` (`Asterism.session`, `on_join` / `on_leave`), `test_api_*.rb` (the CRuby API), `bench_poll.rb` (`rake bench:poll`) |
|
|
17
|
+
| `test/protocol/` | the conformance samples of the wire protocol (`samples.json`), the driver that checks an implementation against them, and the Ruby node it checks (`rake test:protocol`, `rake test:protocol_mruby`; [docs/protocol.md](docs/protocol.md)) |
|
|
17
18
|
| `host_test/` | `rake test:mruby`: the shared layer on mruby (profiles between two mruby processes; `Asterism.session`, `on_join` / `on_leave` and the time of a poll against `peer.rb`) |
|
|
18
19
|
| `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the portable API of the boards), `ros2_talker.rb` (topics, a timer, services with ROS 2), `ros2_rover.rb` (drive a ROS 2 robot: `/cmd_vel`, `/odom`, `/imu`), `zenoh.rb` (plain Zenoh), `mujoco/` (MuJoCo run from CRuby through Fiddle, its robot exposed as Asterism objects; see its README) |
|
|
19
20
|
|
|
@@ -42,7 +43,15 @@ profiles ([Profiles](#profiles): what a node runs on, where, a name and an
|
|
|
42
43
|
icon); 0.6.0 adds `Asterism.session`
|
|
43
44
|
([One connection](#one-connection-asterismsession): the object layer's
|
|
44
45
|
session for plain Zenoh and ROS 2 too) and makes `Asterism.poll` cheap when
|
|
45
|
-
nothing changed.
|
|
46
|
+
nothing changed. **0.7.0 speaks wire protocol 1 only**
|
|
47
|
+
([docs/protocol.md](docs/protocol.md)): it does not talk to the object
|
|
48
|
+
layer of earlier versions, so every node of a network moves to 0.7.0
|
|
49
|
+
together. It adds who called (`Asterism.current_call`, `expose(...,
|
|
50
|
+
caller: :from)`; [Who called](#who-called)), the kind of a remote error
|
|
51
|
+
(`RemoteError#remote_kind`) and `proxy.asterism_call`.
|
|
52
|
+
|
|
53
|
+
A mechanism-by-mechanism comparison with dRuby and Rinda is in
|
|
54
|
+
[docs/comparison.md](docs/comparison.md).
|
|
46
55
|
|
|
47
56
|
## Which API?
|
|
48
57
|
|
|
@@ -78,7 +87,7 @@ time is taken, on both APIs and every Ruby; giving one twice raises
|
|
|
78
87
|
CRuby (needs CRuby 3.2+ and a C compiler):
|
|
79
88
|
|
|
80
89
|
```
|
|
81
|
-
gem install asterism # also installs asterism-zenoh (
|
|
90
|
+
gem install asterism # also installs asterism-zenoh (>= 0.4.1, < 1.0) and msgpack
|
|
82
91
|
ruby -e 'require "asterism"'
|
|
83
92
|
```
|
|
84
93
|
|
|
@@ -91,7 +100,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
91
100
|
|
|
92
101
|
```
|
|
93
102
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
94
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.
|
|
103
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.1.gem pkg/asterism-0.7.1.gem
|
|
95
104
|
```
|
|
96
105
|
|
|
97
106
|
From the working trees, without installing:
|
|
@@ -101,12 +110,14 @@ ruby -I lib -I ../asterism-zenoh/lib your_script.rb # after `rake compile` in
|
|
|
101
110
|
```
|
|
102
111
|
|
|
103
112
|
`Asterism::ROS::TYPE_PATH` is `data/msgs` there; add your own directories.
|
|
104
|
-
MessagePack sends
|
|
105
|
-
|
|
113
|
+
MessagePack sends a binary (ASCII-8BIT) String as bin and any other String
|
|
114
|
+
as str (protocol 1; [Values](#values)).
|
|
106
115
|
|
|
107
116
|
mruby / PicoRuby: add this repository and picoruby-asterism-zenoh as gems
|
|
108
117
|
(`conf.gem "path/to/asterism"`; a `MessagePack` module with `pack` /
|
|
109
|
-
`unpack` is needed too
|
|
118
|
+
`unpack` is needed too: for protocol 1 it must write a binary String
|
|
119
|
+
(`String#b`) as bin and read a bin as one, and refuse ext types, integers
|
|
120
|
+
outside int64 and bytes after the value), and put `data/msgs` on the device's file system at
|
|
110
121
|
`/usr/share/asterism/msgs` (or add the place to
|
|
111
122
|
`Asterism::ROS::TYPE_PATH`). Family mruby does this from pinned commits of
|
|
112
123
|
these repositories.
|
|
@@ -120,6 +131,8 @@ rake test:objects # objects and Asterism::ROS between CRuby sessions over a
|
|
|
120
131
|
rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
|
|
121
132
|
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
|
|
122
133
|
rake test:mruby # the shared layer on mruby (builds mruby; not part of `rake`)
|
|
134
|
+
rake test:protocol # the wire protocol's conformance samples and driver against CRuby (docs/protocol.md)
|
|
135
|
+
rake test:protocol_mruby # the same driver against mruby (builds mruby; not part of `rake`)
|
|
123
136
|
rake bench:poll # the time and allocations of one Asterism.poll by nodes and objects (prints a table)
|
|
124
137
|
```
|
|
125
138
|
|
|
@@ -251,7 +264,8 @@ end
|
|
|
251
264
|
|---|---|
|
|
252
265
|
| `Asterism.connect(...) { \|net\| }` | yields an `Asterism::Net`, `Asterism.close` when the block ends; returns the block's value. Without a block, as before |
|
|
253
266
|
| `Asterism.net` | the `Net` of the connection made without a block (nil when not connected) |
|
|
254
|
-
| `net.expose` / `unexpose` / `exposed` | as `Asterism.expose
|
|
267
|
+
| `net.expose` / `unexpose` / `exposed` | as `Asterism.expose` (also `caller: :from`). The methods run on the receiving thread, in `run`, or in whatever polls |
|
|
268
|
+
| `net.current_call` | `Asterism.current_call`: the call being answered on this thread (the receiving thread while it answers), or nil |
|
|
255
269
|
| `net[path, timeout: 2.0]` | a proxy (`timeout` in seconds, or `timeout_ms:`) |
|
|
256
270
|
| `net.each(pattern = "**")` | the proxies alive now; an Enumerator without a block. With a block it returns their number (1.0: `self`, as Ruby's `each` does; `net.count` is the number) |
|
|
257
271
|
| `net.on_join { \|node\| }` / `net.on_leave { \|node\| }` | `Asterism.on_join` / `on_leave` of the portable API (the same blocks, told from `Asterism.poll`), here on the receiving thread; what they raise goes to `on_error`. `net.off_join(proc)` / `off_leave(proc)` remove one |
|
|
@@ -279,6 +293,28 @@ Asterism.connect("tls/router.example.org:7448", node: "cruby", app: "demo", conf
|
|
|
279
293
|
end
|
|
280
294
|
```
|
|
281
295
|
|
|
296
|
+
With an Asterism console, the certificate page saves everything as one
|
|
297
|
+
file ("Save all (.tar)": the key, the certificate, the CA, an example
|
|
298
|
+
configuration and `asterism.json` with the name and the router). Connect
|
|
299
|
+
with that one file:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
Asterism.connect(credentials: "alice.tar", app: "game") do |net|
|
|
303
|
+
net["fmruby-aaaaaa/game/board"].state
|
|
304
|
+
end
|
|
305
|
+
Asterism::Zenoh.open(credentials: "alice.tar") { |s| s.put("asterism/alice/game/hello", "hi") }
|
|
306
|
+
Asterism::ROS.connect(credentials: "alice.tar") { |ros| ros.node("ruby_talker") }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The gem reads the tar in memory (nothing is written; `Asterism::Credentials`)
|
|
310
|
+
and gives zenoh the CA, the certificate and the key as
|
|
311
|
+
`transport/link/tls/root_ca_certificate_base64`,
|
|
312
|
+
`connect_certificate_base64` and `connect_private_key_base64`, with
|
|
313
|
+
`enable_mtls`. The locator defaults to the router in `asterism.json` and
|
|
314
|
+
`node:` to the certificate's name; give either to use another. `config:`
|
|
315
|
+
(a Hash) is laid over these keys. A tar that the group or others may read
|
|
316
|
+
is warned about, as ssh does with a key: `chmod 600 alice.tar`.
|
|
317
|
+
|
|
282
318
|
The router checks the certificate against its CA, and its ACL can tell the
|
|
283
319
|
nodes apart by the certificate's common name. The name in the locator must
|
|
284
320
|
be one the router's certificate is valid for (zenoh checks it; it is
|
|
@@ -416,21 +452,24 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
416
452
|
|
|
417
453
|
| Call | Returns | Raises / notes |
|
|
418
454
|
|---|---|---|
|
|
419
|
-
| `Asterism.connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5)` | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given, CRuby only: TLS and the rest of zenoh's configuration; the boards raise `ArgumentError`) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
|
|
420
|
-
| `Asterism.expose(name, obj, methods:)` | `"<node>/<app>/<name>"` | `methods:` is an Array of names, or a Hash `name => number of arguments` (checked before the call; `-1` or the Array form: any). `ArgumentError` when `obj` has no such public method. Exposing a name again replaces it. |
|
|
455
|
+
| `Asterism.connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5)` (CRuby also `credentials:`, see [Over TLS](#over-tls-a-router-on-the-internet)) | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given, CRuby only: TLS and the rest of zenoh's configuration; the boards raise `ArgumentError`) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
|
|
456
|
+
| `Asterism.expose(name, obj, methods:, caller: nil)` | `"<node>/<app>/<name>"` | `methods:` is an Array of names, or a Hash `name => number of arguments` (checked before the call; `-1` or the Array form: any). `ArgumentError` when `obj` has no such public method. Exposing a name again replaces it. `caller: :from` passes the calling node's ID to every method as the keyword `from:` and refuses a call that brings `from:` itself ([Who called](#who-called)). |
|
|
457
|
+
| `Asterism.current_call` | `Asterism::CurrentCall` or nil | While an exposed method runs for a call: `from` (the calling node's ID), `verified` (`:claimed`, or `:local` for a call from this application), `object`, `method`. nil outside a call ([Who called](#who-called)). |
|
|
421
458
|
| `Asterism.unexpose(name)` | true / false | |
|
|
422
459
|
| `Asterism.poll` | true / false | Call from the update loop: polls Zenoh, answers the calls that came in, follows who is alive and calls the `on_join` / `on_leave` blocks. `false` once the connection is closed or lost. Cheap when nothing came (0.6.0): each queue is looked at by its count, and the list of nodes is made again only after a liveliness change. |
|
|
423
|
-
| `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (
|
|
460
|
+
| `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (one of its applications or objects) / is gone (no application and no object left). The nodes there already join on the first polls after connecting; when the connection is lost, every node known until then leaves (on the next `Asterism.poll`). The blocks run from `Asterism.poll` only, never from the polling inside a waiting call; what they raise comes out of `Asterism.poll`. Each call adds a block; `Asterism.off_join(proc)` / `off_leave(proc)` remove one; `Asterism.close` forgets them (a lost connection does not). |
|
|
424
461
|
| `Asterism[path, timeout: 2.0]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). `timeout:` (seconds) or `timeout_ms:` is the time limit of its calls. Nothing is sent until a method is called. |
|
|
425
|
-
| `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (the proxy's time limit). `RemoteError` when it raised there or could not be called (not exposed
|
|
462
|
+
| `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (the proxy's time limit). `RemoteError` when it raised there or could not be called (`remote_kind` says which: `raised`, or not exposed `not_exposed` (`NoMethodError`), wrong number of arguments `arguments` (`ArgumentError`), no such object `no_object` (`NameError`); [Errors](#errors)), `TimeoutError` when no answer came in time (also at once when nobody answers that key), `Disconnected`, `EncodeError` (before anything is sent) for a value MessagePack cannot carry. A block cannot be sent (`ArgumentError`). |
|
|
463
|
+
| `proxy.asterism_call(name, *args, **kw)` | the remote return value | The same call by the method's name (a String or Symbol): for names a proxy cannot forward (those `Object` and `Kernel` have: `display`, `hash`, `class`, `fail`, ...) and names that are not Ruby identifiers. `proxy.async.asterism_call(...)` returns a `Future`. |
|
|
426
464
|
| `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
|
|
427
465
|
| `future.done?` | true / false | Never waits (`Asterism.poll` moves it on). True once the answer came, the time ran out or the connection closed. |
|
|
428
466
|
| `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took` (seconds) / `took_ms`: time to the answer. |
|
|
429
467
|
| `proxy.respond_to?(name)` / `proxy.remote_methods` | true / false, `[Symbol]` | From the object's meta (fetched once, `proxy.asterism_refresh` forgets it). `respond_to?` is false when the object does not answer; `remote_methods` raises then. (`proxy.methods` returns the same with a deprecation warning; from 1.0 it is `Object#methods`.) |
|
|
430
|
-
| `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [
|
|
468
|
+
| `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [{"name" => name, "arity" => n}, ...]}` |
|
|
431
469
|
| `Asterism.each(pattern = "**") { \|proxy\| }` | count (Array without a block) | The exposed objects alive now (this application's own included) whose `<node>/<app>/<object>` matches; `*` is one chunk, `**` any number. 1.0 returns the proxies from the block form instead of their number (Ruby's `each` returns its receiver); write `Asterism.each(pattern).size` for the count. |
|
|
432
470
|
| `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
|
|
433
471
|
| `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
|
|
472
|
+
| `Asterism.reconnecting?` | true / false | True while the router is away and the binding opens the connection again ([When the connection is lost](#when-the-connection-is-lost)). |
|
|
434
473
|
| `Asterism.session` | `Asterism::Zenoh::Session` | The connection's own session, for put / subscribe / a ROS 2 node on the same connection ([One connection](#one-connection-asterismsession)). The same object until the connection closes. `Disconnected` when not connected. Its `close` does nothing (warns once); `Asterism.close` closes it. |
|
|
435
474
|
| `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
|
|
436
475
|
| `Asterism.profile(kind:, name:, host:, icon:)` / `Asterism.profile` | Hash | This node's profile: sets the fields given and returns it; without arguments, returns it. See [Profiles](#profiles). |
|
|
@@ -444,19 +483,20 @@ and an icon. A console draws it with that; anyone can ask.
|
|
|
444
483
|
```ruby
|
|
445
484
|
Asterism.profile(name: "Rover", icon: File.binread("rover.png")) # this node's (any time)
|
|
446
485
|
Asterism.profile
|
|
447
|
-
# => {"kind" => "cruby", "name" => "Rover", "icon" => "\x89PNG...", "icon_hash" => "2a8c0626"}
|
|
486
|
+
# => {"protocol" => 1, "kind" => "cruby", "name" => "Rover", "icon" => "\x89PNG...", "icon_hash" => "2a8c0626"}
|
|
448
487
|
Asterism.profile_of("fmruby-aaaaaa")
|
|
449
|
-
# => {"kind" => "family_mruby", "host" => "atoms3r", "icon" => "\x89PNG...", "icon_hash" => "..."}
|
|
488
|
+
# => {"protocol" => 1, "kind" => "family_mruby", "host" => "atoms3r", "icon" => "\x89PNG...", "icon_hash" => "..."}
|
|
450
489
|
Asterism.profile_of("fmruby-aaaaaa", icon: false) # the same without the icon bytes
|
|
451
490
|
Asterism.profile_of("an-older-node") # => nil
|
|
452
491
|
```
|
|
453
492
|
|
|
454
493
|
| Field | Value | Default |
|
|
455
494
|
|---|---|---|
|
|
495
|
+
| `protocol` | set by Asterism, never given: the wire protocol the node speaks (`Asterism::PROTOCOL`, 1). A node whose profile has none speaks protocol 0 (asterism before 0.7.0), which 0.7.0 does not talk to | |
|
|
456
496
|
| `kind` | what Ruby runs the node: `family_mruby`, `picoruby`, `cruby`, `console` (a lower-case word: `a-z`, `0-9`, `_`, up to 32 bytes; other words are allowed) | `cruby` on CRuby, `picoruby` on mruby / PicoRuby; Family mruby's firmware sets `family_mruby` |
|
|
457
497
|
| `host` | where it runs, e.g. `atoms3r`, `tab5`, `p4nano`, `browser`, `sim` (up to 32 bytes) | none; Family mruby's firmware sets it from the board |
|
|
458
498
|
| `name` | a display name (up to 64 bytes) | none |
|
|
459
|
-
| `icon` | the bytes of a PNG: at most 8 KB and 256x256 (32x32 is the size meant). PNG only: an SVG can carry scripts | the file at `Asterism.default_icon_path` when it is there: on CRuby the gem's `data/icon.png` (a ruby), on the boards `/usr/share/asterism/icon.png` |
|
|
499
|
+
| `icon` | the bytes of a PNG (a binary String; sent as a MessagePack bin): at most 8 KB and 256x256 (32x32 is the size meant). PNG only: an SVG can carry scripts | the file at `Asterism.default_icon_path` when it is there: on CRuby the gem's `data/icon.png` (a ruby), on the boards `/usr/share/asterism/icon.png` |
|
|
460
500
|
| `icon_hash` | set by Asterism, never given: the icon's summary (8 hex digits: the Adler-32 of the PNG's chunk CRCs, so a board computes it from a few bytes) | |
|
|
461
501
|
|
|
462
502
|
- **Setting.** `Asterism.profile(**fields)` changes the fields given and keeps
|
|
@@ -536,7 +576,7 @@ end
|
|
|
536
576
|
another. Your own `put` does not come back to your own subscriber on the
|
|
537
577
|
same session.
|
|
538
578
|
- **Keys.** Keep your keys out of the object layer's: `asterism/<node>/<app>/**`
|
|
539
|
-
is answered by its queryable (`get` there is a call), `asterism/<node>` and
|
|
579
|
+
is answered by its queryable (`get` there is a call), `asterism/<node>/<app>` and
|
|
540
580
|
`asterism/<node>/<app>/<object>` are its liveliness tokens. Putting under
|
|
541
581
|
`asterism/<node>/<app>/<object>/<anything>` (a state, a log) is fine:
|
|
542
582
|
puts and subscriptions do not meet queryables or liveliness.
|
|
@@ -553,12 +593,18 @@ Asterism::Error (the Zenoh bindings define it)
|
|
|
553
593
|
├── Asterism::TimeoutError (no answer in time; old name Asterism::Timeout)
|
|
554
594
|
│ └── Asterism::ROS::TimeoutError (old name Asterism::ROS::Timeout)
|
|
555
595
|
├── Asterism::EncodeError
|
|
556
|
-
├── Asterism::RemoteError (remote_class, remote_message)
|
|
596
|
+
├── Asterism::RemoteError (remote_kind, remote_class, remote_message)
|
|
557
597
|
├── Asterism::ROS::UnknownType
|
|
558
598
|
├── Asterism::CDR::DecodeError
|
|
559
599
|
└── Asterism::DeprecationError (only with Asterism.deprecations = :raise)
|
|
560
600
|
```
|
|
561
601
|
|
|
602
|
+
`RemoteError#remote_kind` says what happened on the other side, as a word
|
|
603
|
+
of the protocol (docs/protocol.md 4.4): `raised` (the method raised;
|
|
604
|
+
`remote_class` is its class there), `not_exposed`, `no_object`,
|
|
605
|
+
`arguments`, `malformed`, `unsendable`, `nesting`, `protocol` (the other
|
|
606
|
+
side speaks a different protocol); nil when the answer did not say.
|
|
607
|
+
|
|
562
608
|
The old names `Asterism::Timeout` and `Asterism::ROS::Timeout` still work
|
|
563
609
|
(they warn once). Inside `module Asterism`, `Timeout` is Ruby's `Timeout`
|
|
564
610
|
module again. The ROS layer raises `Zenoh::ClosedError` when its session
|
|
@@ -646,8 +692,21 @@ a field, as before.
|
|
|
646
692
|
|
|
647
693
|
### When the connection is lost
|
|
648
694
|
|
|
649
|
-
|
|
650
|
-
(
|
|
695
|
+
A client of a router keeps its connection when the router goes away for a
|
|
696
|
+
while (it restarts, the line drops): the Zenoh binding opens the connection
|
|
697
|
+
again by itself (zenoh-c on CRuby; zenoh-pico built with reconnection on
|
|
698
|
+
the boards) and declares again everything that was declared, so the
|
|
699
|
+
application token, the exposed objects, the liveliness watch and what the
|
|
700
|
+
application declared on `Asterism.session` are all back without the
|
|
701
|
+
application doing anything. Meanwhile `Asterism.reconnecting?` is true,
|
|
702
|
+
`connected?` stays true, the other nodes leave (`on_leave`) and join again
|
|
703
|
+
after it (`on_join`), calls raise `Asterism::Disconnected` or time out, and
|
|
704
|
+
puts reach nobody. The profile is told again once it is back. Measured
|
|
705
|
+
with zenohd 1.10.1 restarted by `docker restart` (`rake test:reconnect`,
|
|
706
|
+
CRuby and mruby): back about 1 s after the router.
|
|
707
|
+
|
|
708
|
+
A peer connection (or a build without reconnection) is closed when the
|
|
709
|
+
peer (or the router) goes away, and is not reopened by itself. From then on
|
|
651
710
|
`Asterism.poll` returns `false`, `connected?` is false, `lost_reason` says
|
|
652
711
|
why, and calls raise `Asterism::Disconnected`. **`Asterism::Zenoh::Error`
|
|
653
712
|
never comes out of Asterism; it is always wrapped in `Disconnected`** (the
|
|
@@ -658,22 +717,71 @@ session is closed, its entities raise `Zenoh::ClosedError`).
|
|
|
658
717
|
|
|
659
718
|
## Values
|
|
660
719
|
|
|
661
|
-
Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (
|
|
662
|
-
`Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
720
|
+
Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (signed
|
|
721
|
+
64 bit), `Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a
|
|
722
|
+
`String` and arrives as one (also as a Hash key; keyword names become
|
|
723
|
+
Symbols again). A binary String (`String#b`: ASCII-8BIT on CRuby, the
|
|
724
|
+
binary flag on mruby) goes as a MessagePack bin and arrives as a binary
|
|
725
|
+
String; any other String goes as text (str). Anything else in the
|
|
726
|
+
arguments, or an Integer outside the signed 64-bit range, raises
|
|
727
|
+
`EncodeError` before sending; in a return value the caller gets
|
|
728
|
+
`RemoteError` (`unsendable`). Nesting is limited to 16 levels.
|
|
729
|
+
|
|
730
|
+
- Hash keys: use String (or Symbol) and Integer keys for anything another
|
|
731
|
+
language reads (Python and JavaScript refuse others by default).
|
|
732
|
+
- Times: send an Integer of nanoseconds since the epoch (or name the unit:
|
|
733
|
+
`timeout_ms`); there is no time type.
|
|
734
|
+
- A keyword given as `nil` and one left out differ: the callee's default
|
|
735
|
+
applies only to the one left out.
|
|
736
|
+
- A JavaScript caller's `1.0` arrives as the Integer `1`.
|
|
737
|
+
|
|
738
|
+
## Who called
|
|
739
|
+
|
|
740
|
+
The caller's node ID travels in the call's key (`.../call/<caller>`). An
|
|
741
|
+
exposed method can read it:
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
class Rover
|
|
745
|
+
def stop
|
|
746
|
+
c = Asterism.current_call # nil outside a call
|
|
747
|
+
log("stop from #{c.from}") # c.verified: :claimed (:local from this app)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def drive(v, w, from:) # caller: :from fills from: in
|
|
751
|
+
log("drive from #{from}")
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
Asterism.expose("rover", Rover.new, methods: { stop: 0 })
|
|
755
|
+
Asterism.expose("wheels", Rover.new, methods: { drive: 2 }, caller: :from)
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- `Asterism.current_call` is set only while a method runs for a call (per
|
|
759
|
+
thread on CRuby). Calls answered while that method waits for its own
|
|
760
|
+
call come on top of it and go when they end.
|
|
761
|
+
- With `caller: :from`, a call that passes `from:` itself is refused
|
|
762
|
+
(`RemoteError`, `remote_kind` `arguments`): `from` is always the one
|
|
763
|
+
Asterism took from the call.
|
|
764
|
+
- **It is a claim.** Any client can call with any name. A router's access
|
|
765
|
+
control makes it binding: with mutual TLS, zenohd's ACL can let each
|
|
766
|
+
certificate query only keys ending in its own name
|
|
767
|
+
(`asterism/*/*/*/call/<name>`; docs/protocol.md 4.7). Without such a
|
|
768
|
+
router, use `from` to avoid mistakes, not to decide who may do what.
|
|
667
769
|
|
|
668
770
|
## Keys and encoding
|
|
669
771
|
|
|
772
|
+
Wire protocol 1. The full description, for implementations in other
|
|
773
|
+
languages, is [docs/protocol.md](docs/protocol.md) (with the conformance
|
|
774
|
+
tests); why it has this shape (the review of protocol 0's Ruby-shaped
|
|
775
|
+
parts, and what was decided) is
|
|
776
|
+
[docs/protocol_review.md](docs/protocol_review.md).
|
|
777
|
+
|
|
670
778
|
| Key | Zenoh | Payload |
|
|
671
779
|
|---|---|---|
|
|
672
|
-
| `asterism/<node>/<app>/<object>/call
|
|
673
|
-
| `asterism/<node>/<app>/<object>/meta` | get / queryable | reply: `{"methods" => [
|
|
674
|
-
| `asterism/<node>` | liveliness | the
|
|
780
|
+
| `asterism/<node>/<app>/<object>/call/<caller>` | get / queryable | query: `{"method" => name, "args" => [...], "kwargs" => {...}}` (args and kwargs left out when empty); reply: `{"value" => value}` or `{"error" => {"kind" => ..., "class" => ..., "message" => ...}}`. `<caller>` is the calling node's ID |
|
|
781
|
+
| `asterism/<node>/<app>/<object>/meta` | get / queryable | reply: `{"methods" => [{"name" => name, "arity" => n}, ...]}` (arity `-1` when not declared). The object may be `*` in the query: one reply per object |
|
|
782
|
+
| `asterism/<node>/<app>` | liveliness | the application is connected (a node is there while any of its applications or objects is) |
|
|
675
783
|
| `asterism/<node>/<app>/<object>` | liveliness | the object is exposed |
|
|
676
|
-
| `asterism/<node>/@profile` | get / queryable, and put | query (parameter `icon=0` for no icon): reply the node's profile, a Hash (`kind`, `host`, `name`, `icon`, `icon_hash`; [Profiles](#profiles)). The node also puts the profile without the icon at connect and on each change. `@profile` is a verbatim chunk, so `asterism/**` does not match it: the object layer's wildcards never see it. An application's own profile would go at `asterism/<node>/<app>/@profile` (not
|
|
784
|
+
| `asterism/<node>/@profile` | get / queryable, and put | query (parameter `icon=0` for no icon): reply the node's profile, a Hash (`kind`, `host`, `name`, `icon`, `icon_hash`; [Profiles](#profiles)). The node also puts the profile without the icon at connect and on each change. `@profile` is a verbatim chunk, so `asterism/**` does not match it: the object layer's wildcards never see it. An application's own profile would go at `asterism/<node>/<app>/@profile` (not yet) |
|
|
677
785
|
|
|
678
786
|
One queryable per application (`asterism/<node>/<app>/**`), and one for the
|
|
679
787
|
profile (`asterism/<node>/@profile`). A call whose
|
|
@@ -728,9 +836,9 @@ nested type (`geometry_msgs/msg/Twist`) at start-up took 0.6 KB more.
|
|
|
728
836
|
|
|
729
837
|
- Objects cannot be passed by reference (a return value is a copy), blocks
|
|
730
838
|
cannot be sent, there are no events (later stages).
|
|
731
|
-
- The
|
|
732
|
-
|
|
733
|
-
|
|
839
|
+
- The wire protocol is protocol 1 only: nodes of asterism before 0.7.0
|
|
840
|
+
(protocol 0) do not see 0.7.0's objects and a call between the two
|
|
841
|
+
fails (a protocol 0 call is refused with `remote_kind` `protocol`).
|
|
734
842
|
- No authentication by default (plain TCP): a trusted LAN is assumed, or
|
|
735
843
|
TLS through a router (CRuby; [Over TLS](#over-tls-a-router-on-the-internet)).
|
|
736
844
|
- One object-layer connection per process (per VM on the boards).
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# The credentials of one node or router as one file (CRuby only): the .tar
|
|
2
|
+
# an Asterism console hands out when it issues a certificate ("Save all
|
|
3
|
+
# (.tar)" on the certificate page; the archive is made in the browser that
|
|
4
|
+
# made the key). It holds, at its top or in one directory:
|
|
5
|
+
#
|
|
6
|
+
# <name>.key the private key (PKCS#8 PEM)
|
|
7
|
+
# <name>.pem the certificate (its common name is <name>)
|
|
8
|
+
# ca.pem the CA that signed it (and the router's certificate)
|
|
9
|
+
# zenoh.json5 an example configuration (for zenohd or another client)
|
|
10
|
+
# asterism.json {"name": ..., "router": "tls/host:port", "not_after": ..., ...}
|
|
11
|
+
#
|
|
12
|
+
# Asterism.connect(credentials: "alice.tar", app: "game") { |net| ... }
|
|
13
|
+
# Asterism::Zenoh.open(credentials: "alice.tar") { |s| ... }
|
|
14
|
+
# Asterism::ROS.connect(credentials: "alice.tar") { |ros| ... }
|
|
15
|
+
#
|
|
16
|
+
# The file is read into memory (Gem::Package::TarReader, the standard
|
|
17
|
+
# library): nothing is written anywhere. The certificates go to zenoh as
|
|
18
|
+
# base64 (transport/link/tls/*_base64) with enable_mtls, the router's
|
|
19
|
+
# locator and the node ID come from asterism.json unless given, and a file
|
|
20
|
+
# that others may read is warned about, as ssh does with a key.
|
|
21
|
+
require "rubygems/package"
|
|
22
|
+
require "json"
|
|
23
|
+
require "stringio"
|
|
24
|
+
|
|
25
|
+
module Asterism
|
|
26
|
+
class Credentials
|
|
27
|
+
# What a credentials file is missing or holds wrong.
|
|
28
|
+
class Error < ::Asterism::Error; end
|
|
29
|
+
|
|
30
|
+
TLS = "transport/link/tls"
|
|
31
|
+
# The largest file read (the real ones are a few KB).
|
|
32
|
+
MAX_SIZE = 1024 * 1024
|
|
33
|
+
|
|
34
|
+
attr_reader :path, :name, :kind, :router, :not_after, :key_pem, :cert_pem, :ca_pem, :info
|
|
35
|
+
|
|
36
|
+
# Reads path (a String or Pathname of a .tar). warn: false skips the
|
|
37
|
+
# check of who may read the file.
|
|
38
|
+
def self.load(path, warn: true)
|
|
39
|
+
new(path, warn: warn)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A credentials file (a path or a Credentials) and the arguments of a
|
|
43
|
+
# connection -> [locator, config Hash, credentials]. The locator given
|
|
44
|
+
# wins over asterism.json's router; config given (a Hash) is laid over
|
|
45
|
+
# the TLS keys. @api private
|
|
46
|
+
def self.merge(credentials, locator, config)
|
|
47
|
+
creds = credentials.is_a?(Credentials) ? credentials : load(credentials)
|
|
48
|
+
if config.is_a?(String)
|
|
49
|
+
raise ArgumentError, "credentials: and a config String cannot be combined; pass config: as a Hash"
|
|
50
|
+
end
|
|
51
|
+
merged = creds.zenoh_config.merge(config || {})
|
|
52
|
+
[locator || creds.router, merged, creds]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(path, warn: true)
|
|
56
|
+
@path = path.to_s
|
|
57
|
+
check_mode if warn
|
|
58
|
+
files = read_tar
|
|
59
|
+
@info = parse_info(files["asterism.json"])
|
|
60
|
+
keys = files.keys.select { _1.end_with?(".key") }
|
|
61
|
+
raise Error, "#{@path}: no private key (<name>.key) in it" if keys.empty?
|
|
62
|
+
raise Error, "#{@path}: more than one private key in it (#{keys.sort.join(', ')})" if keys.size > 1
|
|
63
|
+
@name = @info["name"].to_s
|
|
64
|
+
@name = File.basename(keys.first, ".key") if @name.empty?
|
|
65
|
+
@key_pem = files["#{@name}.key"] or raise Error, "#{@path}: the key is not named after #{@name} (#{keys.first})"
|
|
66
|
+
@cert_pem = files["#{@name}.pem"] or raise Error, "#{@path}: no certificate #{@name}.pem in it"
|
|
67
|
+
@ca_pem = files["ca.pem"] or raise Error, "#{@path}: no ca.pem in it"
|
|
68
|
+
@kind = @info["kind"]
|
|
69
|
+
@router = @info["router"].to_s.empty? ? nil : @info["router"].to_s
|
|
70
|
+
@not_after = @info["not_after"]
|
|
71
|
+
check_pair
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The zenoh configuration keys (a Hash for Session.open's config:):
|
|
75
|
+
# the CA, the certificate and the key as base64, and mutual TLS.
|
|
76
|
+
def zenoh_config
|
|
77
|
+
{
|
|
78
|
+
"#{TLS}/root_ca_certificate_base64" => b64(@ca_pem),
|
|
79
|
+
"#{TLS}/connect_certificate_base64" => b64(@cert_pem),
|
|
80
|
+
"#{TLS}/connect_private_key_base64" => b64(@key_pem),
|
|
81
|
+
"#{TLS}/enable_mtls" => true
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def inspect
|
|
86
|
+
"#<Asterism::Credentials #{@name} #{@router || '(no router)'}#{@not_after ? " until #{@not_after}" : ''}>"
|
|
87
|
+
end
|
|
88
|
+
alias to_s inspect
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def b64(pem)
|
|
93
|
+
[pem].pack("m0")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Like ssh with a private key: a file that the group or others may read
|
|
97
|
+
# is told about (it still works).
|
|
98
|
+
def check_mode
|
|
99
|
+
st = File.stat(@path)
|
|
100
|
+
return if Gem.win_platform? || (st.mode & 0o077).zero?
|
|
101
|
+
Kernel.warn("asterism: #{@path} holds a private key and can be read by others " \
|
|
102
|
+
"(mode #{format('%04o', st.mode & 0o7777)}); chmod 600 #{@path}")
|
|
103
|
+
rescue SystemCallError => e
|
|
104
|
+
raise Error, "#{@path}: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# {basename => content} of the regular files, at the top or in one
|
|
108
|
+
# directory.
|
|
109
|
+
def read_tar
|
|
110
|
+
data = File.binread(@path)
|
|
111
|
+
raise Error, "#{@path}: too large for a credentials file" if data.bytesize > MAX_SIZE
|
|
112
|
+
files = {}
|
|
113
|
+
Gem::Package::TarReader.new(StringIO.new(data)) do |tar|
|
|
114
|
+
tar.each do |entry|
|
|
115
|
+
next unless entry.file?
|
|
116
|
+
base = File.basename(entry.full_name)
|
|
117
|
+
raise Error, "#{@path}: #{base} is in it twice" if files.key?(base)
|
|
118
|
+
files[base] = entry.read.to_s
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
raise Error, "#{@path}: not a tar file with credentials in it" if files.empty?
|
|
122
|
+
files
|
|
123
|
+
rescue SystemCallError => e
|
|
124
|
+
raise Error, "#{@path}: #{e.message}"
|
|
125
|
+
rescue Gem::Package::TarInvalidError, ArgumentError => e
|
|
126
|
+
raise Error, "#{@path}: not a tar file (#{e.message})"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_info(text)
|
|
130
|
+
return {} if text.nil?
|
|
131
|
+
h = JSON.parse(text)
|
|
132
|
+
raise Error, "#{@path}: asterism.json is not an object" unless h.is_a?(Hash)
|
|
133
|
+
h
|
|
134
|
+
rescue JSON::ParserError => e
|
|
135
|
+
raise Error, "#{@path}: asterism.json: #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The key belongs to the certificate, and the certificate has not
|
|
139
|
+
# expired (a warning: zenoh refuses it anyway). Skipped without openssl.
|
|
140
|
+
def check_pair
|
|
141
|
+
require "openssl"
|
|
142
|
+
cert = OpenSSL::X509::Certificate.new(@cert_pem)
|
|
143
|
+
key = OpenSSL::PKey.read(@key_pem)
|
|
144
|
+
raise Error, "#{@path}: #{@name}.key is not the key of #{@name}.pem" unless cert.check_private_key(key)
|
|
145
|
+
Kernel.warn("asterism: #{@path}: the certificate expired at #{cert.not_after.utc}") if cert.not_after < Time.now
|
|
146
|
+
rescue LoadError
|
|
147
|
+
nil
|
|
148
|
+
rescue OpenSSL::OpenSSLError => e
|
|
149
|
+
raise Error, "#{@path}: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -26,7 +26,7 @@ module Asterism
|
|
|
26
26
|
# The portable API, entered under LOCK. Prepended to the Asterism module's
|
|
27
27
|
# singleton class, so the shared methods themselves are not changed.
|
|
28
28
|
module Locked
|
|
29
|
-
%i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave off_join off_leave
|
|
29
|
+
%i[expose unexpose poll nodes [] call_async meta connected? reconnecting? exposed on_join on_leave off_join off_leave
|
|
30
30
|
profile profile_async session].each do |m|
|
|
31
31
|
define_method(m) do |*args, **kw, &blk|
|
|
32
32
|
::Asterism::LOCK.synchronize { super(*args, **kw, &blk) }
|
|
@@ -36,8 +36,16 @@ module Asterism
|
|
|
36
36
|
# With a block: yields an Asterism::Net and closes when the block ends
|
|
37
37
|
# (also on an exception); returns the block's value. Without a block,
|
|
38
38
|
# as before (returns Asterism).
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
#
|
|
40
|
+
# credentials: a .tar from an Asterism console (Asterism::Credentials):
|
|
41
|
+
# its certificates go to zenoh (mutual TLS), and the locator and node:
|
|
42
|
+
# default to its router and name (given ones win).
|
|
43
|
+
def connect(locator = nil, node: nil, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil,
|
|
44
|
+
check_timeout: nil, credentials: nil, &blk)
|
|
45
|
+
if credentials
|
|
46
|
+
locator, config, creds = ::Asterism::Credentials.merge(credentials, locator, config)
|
|
47
|
+
node ||= creds.name
|
|
48
|
+
end
|
|
41
49
|
::Asterism::LOCK.synchronize do
|
|
42
50
|
super(locator, node: node, app: app, mode: mode, listen: listen, config: config,
|
|
43
51
|
connect_timeout: connect_timeout, check_timeout: check_timeout)
|
|
@@ -148,17 +156,22 @@ module Asterism
|
|
|
148
156
|
def node_id = ::Asterism.node_id
|
|
149
157
|
def app = ::Asterism.app
|
|
150
158
|
def connected? = ::Asterism.connected?
|
|
159
|
+
def reconnecting? = ::Asterism.reconnecting?
|
|
151
160
|
def lost_reason = ::Asterism.lost_reason
|
|
152
161
|
def nodes = ::Asterism.nodes
|
|
153
162
|
def exposed = ::Asterism.exposed
|
|
154
163
|
|
|
155
164
|
# Asterism.expose: methods: an Array of names or a Hash name => number
|
|
156
|
-
# of arguments
|
|
165
|
+
# of arguments; caller: :from passes the calling node's ID as that
|
|
166
|
+
# keyword. The object's methods run on the receiving thread (or in
|
|
157
167
|
# run, or in whatever polls).
|
|
158
|
-
def expose(name, obj, methods:)
|
|
159
|
-
::Asterism.expose(name, obj, methods: methods)
|
|
168
|
+
def expose(name, obj, methods:, caller: nil)
|
|
169
|
+
::Asterism.expose(name, obj, methods: methods, caller: caller)
|
|
160
170
|
end
|
|
161
171
|
|
|
172
|
+
# Asterism.current_call: the call being answered on this thread, or nil.
|
|
173
|
+
def current_call = ::Asterism.current_call
|
|
174
|
+
|
|
162
175
|
def unexpose(name)
|
|
163
176
|
::Asterism.unexpose(name)
|
|
164
177
|
end
|