asterism 0.7.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 +22 -0
- data/README.md +44 -5
- data/lib/asterism/cruby/credentials.rb +152 -0
- data/lib/asterism/cruby/objects.rb +12 -3
- data/lib/asterism/cruby/ros.rb +1 -1
- data/lib/asterism/cruby/zenoh.rb +13 -2
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +1 -1
- data/mrblib/asterism.rb +39 -4
- data/mrblib/profile.rb +1 -1
- 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,28 @@ 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
|
+
|
|
9
31
|
## 0.7.0
|
|
10
32
|
|
|
11
33
|
**This release breaks compatibility: it speaks wire protocol 1 only.** A
|
data/README.md
CHANGED
|
@@ -50,6 +50,9 @@ together. It adds who called (`Asterism.current_call`, `expose(...,
|
|
|
50
50
|
caller: :from)`; [Who called](#who-called)), the kind of a remote error
|
|
51
51
|
(`RemoteError#remote_kind`) and `proxy.asterism_call`.
|
|
52
52
|
|
|
53
|
+
A mechanism-by-mechanism comparison with dRuby and Rinda is in
|
|
54
|
+
[docs/comparison.md](docs/comparison.md).
|
|
55
|
+
|
|
53
56
|
## Which API?
|
|
54
57
|
|
|
55
58
|
Asterism has two APIs over the same layers. Both are in the CRuby gem;
|
|
@@ -84,7 +87,7 @@ time is taken, on both APIs and every Ruby; giving one twice raises
|
|
|
84
87
|
CRuby (needs CRuby 3.2+ and a C compiler):
|
|
85
88
|
|
|
86
89
|
```
|
|
87
|
-
gem install asterism # also installs asterism-zenoh (
|
|
90
|
+
gem install asterism # also installs asterism-zenoh (>= 0.4.1, < 1.0) and msgpack
|
|
88
91
|
ruby -e 'require "asterism"'
|
|
89
92
|
```
|
|
90
93
|
|
|
@@ -97,7 +100,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
97
100
|
|
|
98
101
|
```
|
|
99
102
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
100
|
-
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
|
|
101
104
|
```
|
|
102
105
|
|
|
103
106
|
From the working trees, without installing:
|
|
@@ -290,6 +293,28 @@ Asterism.connect("tls/router.example.org:7448", node: "cruby", app: "demo", conf
|
|
|
290
293
|
end
|
|
291
294
|
```
|
|
292
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
|
+
|
|
293
318
|
The router checks the certificate against its CA, and its ACL can tell the
|
|
294
319
|
nodes apart by the certificate's common name. The name in the locator must
|
|
295
320
|
be one the router's certificate is valid for (zenoh checks it; it is
|
|
@@ -427,7 +452,7 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
427
452
|
|
|
428
453
|
| Call | Returns | Raises / notes |
|
|
429
454
|
|---|---|---|
|
|
430
|
-
| `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). |
|
|
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). |
|
|
431
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)). |
|
|
432
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)). |
|
|
433
458
|
| `Asterism.unexpose(name)` | true / false | |
|
|
@@ -444,6 +469,7 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
444
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. |
|
|
445
470
|
| `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
|
|
446
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)). |
|
|
447
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. |
|
|
448
474
|
| `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
|
|
449
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). |
|
|
@@ -666,8 +692,21 @@ a field, as before.
|
|
|
666
692
|
|
|
667
693
|
### When the connection is lost
|
|
668
694
|
|
|
669
|
-
|
|
670
|
-
(
|
|
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
|
|
671
710
|
`Asterism.poll` returns `false`, `connected?` is false, `lost_reason` says
|
|
672
711
|
why, and calls raise `Asterism::Disconnected`. **`Asterism::Zenoh::Error`
|
|
673
712
|
never comes out of Asterism; it is always wrapped in `Disconnected`** (the
|
|
@@ -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,6 +156,7 @@ 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
|
data/lib/asterism/cruby/ros.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Asterism
|
|
|
27
27
|
# Opens a Zenoh session and makes a Connection for nodes on it. domain:
|
|
28
28
|
# the ROS domain of the nodes made by ros.node. type_path: directories
|
|
29
29
|
# with generated types to add to Asterism::ROS::TYPE_PATH. The other
|
|
30
|
-
# options go to Asterism::Zenoh
|
|
30
|
+
# options (credentials: among them) go to Asterism::Zenoh.open. With a block: yields the
|
|
31
31
|
# Connection, closes it when the block ends and returns its value.
|
|
32
32
|
def self.connect(locator = nil, domain: 0, type_path: nil, interval: Runner::DEFAULT_INTERVAL, **opts)
|
|
33
33
|
Array(type_path).each { |d| TYPE_PATH << File.expand_path(d) unless TYPE_PATH.include?(File.expand_path(d)) }
|
data/lib/asterism/cruby/zenoh.rb
CHANGED
|
@@ -69,8 +69,13 @@ module Asterism
|
|
|
69
69
|
|
|
70
70
|
# Opens a session (the arguments of Session.open) and wraps it in a
|
|
71
71
|
# Connection. With a block: yields it, closes it when the block ends
|
|
72
|
-
# (also on an exception) and returns the block's value.
|
|
73
|
-
|
|
72
|
+
# (also on an exception) and returns the block's value. credentials: a
|
|
73
|
+
# .tar from an Asterism console (Asterism::Credentials): mutual TLS with
|
|
74
|
+
# its certificates, to its router unless a locator is given.
|
|
75
|
+
def self.open(locator = nil, interval: Runner::DEFAULT_INTERVAL, credentials: nil, **opts)
|
|
76
|
+
if credentials
|
|
77
|
+
locator, opts[:config], = ::Asterism::Credentials.merge(credentials, locator, opts[:config])
|
|
78
|
+
end
|
|
74
79
|
conn = Connection.new(Session.open(locator, **opts), interval: interval)
|
|
75
80
|
return conn unless block_given?
|
|
76
81
|
begin
|
|
@@ -358,6 +363,12 @@ module Asterism
|
|
|
358
363
|
@session.closed?
|
|
359
364
|
end
|
|
360
365
|
|
|
366
|
+
# True while the router is away and zenoh-c opens the connection
|
|
367
|
+
# again (a client session; what was declared comes back with it).
|
|
368
|
+
def reconnecting?
|
|
369
|
+
@session.reconnecting?
|
|
370
|
+
end
|
|
371
|
+
|
|
361
372
|
def zid
|
|
362
373
|
@session.zid
|
|
363
374
|
end
|
data/lib/asterism/version.rb
CHANGED
data/lib/asterism.rb
CHANGED
|
@@ -18,6 +18,6 @@ end
|
|
|
18
18
|
require_relative "asterism/cruby"
|
|
19
19
|
# The CRuby API on top (blocks, a receiving thread, Enumerators,
|
|
20
20
|
# pattern matching): CRuby only, see README "The CRuby API".
|
|
21
|
-
%w[runner zenoh objects ros].each do |f|
|
|
21
|
+
%w[runner credentials zenoh objects ros].each do |f|
|
|
22
22
|
require_relative "asterism/cruby/#{f}"
|
|
23
23
|
end
|
data/mrblib/asterism.rb
CHANGED
|
@@ -289,6 +289,7 @@ module Asterism
|
|
|
289
289
|
@known_set = {} # the same, as node => true
|
|
290
290
|
@node_list = [node] # what nodes returns (this node first), rebuilt on a change
|
|
291
291
|
@nodes_changed = false # liveliness changed since tell_nodes last compared
|
|
292
|
+
@was_reconnecting = false
|
|
292
293
|
tell_profile # for those who follow the profiles (the reply is the queryable's)
|
|
293
294
|
self
|
|
294
295
|
end
|
|
@@ -320,6 +321,20 @@ module Asterism
|
|
|
320
321
|
true
|
|
321
322
|
end
|
|
322
323
|
|
|
324
|
+
# True while the connection is lost and the Zenoh binding opens it again
|
|
325
|
+
# by itself (a client of a router: zenoh-pico built with reconnection on
|
|
326
|
+
# the boards, zenoh-c on CRuby). connected? stays true meanwhile, and once
|
|
327
|
+
# the router is back everything declared before is there again: the
|
|
328
|
+
# application token, the exposed objects, the liveliness watch, and what
|
|
329
|
+
# the application declared on Asterism.session. Meanwhile the other nodes
|
|
330
|
+
# leave (on_leave) and join again after it, calls raise Disconnected or
|
|
331
|
+
# time out, and puts reach nobody.
|
|
332
|
+
def self.reconnecting?
|
|
333
|
+
s = @session
|
|
334
|
+
return false if s.nil?
|
|
335
|
+
s.respond_to?(:reconnecting?) ? s.reconnecting? : false
|
|
336
|
+
end
|
|
337
|
+
|
|
323
338
|
def self.node_id
|
|
324
339
|
@node
|
|
325
340
|
end
|
|
@@ -594,13 +609,33 @@ module Asterism
|
|
|
594
609
|
lost("the connection was lost")
|
|
595
610
|
return false
|
|
596
611
|
end
|
|
612
|
+
back_again(s)
|
|
597
613
|
answer_calls
|
|
598
614
|
answer_profile
|
|
599
615
|
follow_liveliness
|
|
600
616
|
!@session.nil?
|
|
601
617
|
rescue Asterism::Zenoh::Error => e
|
|
602
|
-
|
|
603
|
-
|
|
618
|
+
lost_if_closed(e.message)
|
|
619
|
+
!@session.nil?
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# After the binding opened a lost connection again: tell the profile
|
|
623
|
+
# again (a put made while the router was away reached nobody).
|
|
624
|
+
# @api private
|
|
625
|
+
def self.back_again(s)
|
|
626
|
+
now = s.respond_to?(:reconnecting?) && s.reconnecting?
|
|
627
|
+
was = @was_reconnecting
|
|
628
|
+
@was_reconnecting = now
|
|
629
|
+
tell_profile if was && !now
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# A Zenoh error during a call or a poll: the connection is gone only when
|
|
633
|
+
# the session says it is closed. While the binding reconnects, the
|
|
634
|
+
# session (and everything declared on it) stays.
|
|
635
|
+
# @api private
|
|
636
|
+
def self.lost_if_closed(reason)
|
|
637
|
+
s = @session
|
|
638
|
+
lost(reason) if s.nil? || s.closed?
|
|
604
639
|
end
|
|
605
640
|
|
|
606
641
|
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
@@ -764,7 +799,7 @@ module Asterism
|
|
|
764
799
|
# The caller's node ID is the key's last chunk (docs/protocol.md 4.1).
|
|
765
800
|
g = s.get("#{ROOT}/#{path}/call/#{@node}", timeout_ms: timeout_ms, payload: payload)
|
|
766
801
|
rescue Asterism::Zenoh::Error => e
|
|
767
|
-
|
|
802
|
+
lost_if_closed(e.message)
|
|
768
803
|
raise Disconnected, e.message
|
|
769
804
|
end
|
|
770
805
|
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
@@ -797,7 +832,7 @@ module Asterism
|
|
|
797
832
|
begin
|
|
798
833
|
g = session!.get("#{ROOT}/#{path}/meta", timeout_ms: timeout_ms)
|
|
799
834
|
rescue Asterism::Zenoh::Error => e
|
|
800
|
-
|
|
835
|
+
lost_if_closed(e.message)
|
|
801
836
|
raise Disconnected, e.message
|
|
802
837
|
end
|
|
803
838
|
f = Future.new(path, "meta", g, now_ms + timeout_ms, timeout_ms)
|
data/mrblib/profile.rb
CHANGED
|
@@ -282,7 +282,7 @@ module Asterism
|
|
|
282
282
|
begin
|
|
283
283
|
g = session!.get("#{ROOT}/#{path}", timeout_ms: ms, params: icon ? nil : "icon=0")
|
|
284
284
|
rescue Asterism::Zenoh::Error => e
|
|
285
|
-
|
|
285
|
+
lost_if_closed(e.message)
|
|
286
286
|
raise Disconnected, e.message
|
|
287
287
|
end
|
|
288
288
|
Future.new(path, "profile", g, now_ms + ms, ms)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asterism
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katsuhiko Kageyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asterism-zenoh
|
|
@@ -16,14 +16,20 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.4
|
|
19
|
+
version: '0.4'
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.4.1
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
27
|
- - "~>"
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.4
|
|
29
|
+
version: '0.4'
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.4.1
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: msgpack
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -140,6 +146,7 @@ files:
|
|
|
140
146
|
- data/msgs/visualization_msgs/msg/UVCoordinate.rb
|
|
141
147
|
- lib/asterism.rb
|
|
142
148
|
- lib/asterism/cruby.rb
|
|
149
|
+
- lib/asterism/cruby/credentials.rb
|
|
143
150
|
- lib/asterism/cruby/objects.rb
|
|
144
151
|
- lib/asterism/cruby/ros.rb
|
|
145
152
|
- lib/asterism/cruby/runner.rb
|