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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54a2e8d7f4b96b04dc56b290714d60a4a8d10cdc87d789ccf01bbb2b474f0459
4
- data.tar.gz: be9f2f6c820e67ea513ea19a3d6f88437589cb2020615bc558b553f83e882673
3
+ metadata.gz: d729d6c233d712c647e7fb1168f2587fa0e81eb1b176a11f12ac7c349f61a592
4
+ data.tar.gz: fce6e1102b0e4e2c404adf28a299c77efd548f6a07e92dc81a7b54a9efe1951e
5
5
  SHA512:
6
- metadata.gz: cb166817303dfd933dd1fd6a4444aecd966ce06b66347a4b058759c49dc0b0cd1397bbef60d05753c72ccf511a8f70ea5c49c8ffdfaa591eb2d2cf2255d8b1da
7
- data.tar.gz: d0d0549e336a50af534813aa4ab74c068952bf3d43148cc7c2032c0f4447df0aae73c9d1d813f8aa99819310c13ae3e220d59376276195fed408506b34144399
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 (~> 0.4.0) and msgpack
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.0.gem pkg/asterism-0.7.0.gem
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
- Asterism follows `Asterism::Zenoh`: the connection is closed when the router
670
- (or the only peer) goes away, and is not reopened by itself. From then on
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
- def connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: nil,
40
- &blk)
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
@@ -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::Session.open. With a block: yields the
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)) }
@@ -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
- def self.open(locator = nil, interval: Runner::DEFAULT_INTERVAL, **opts)
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
@@ -3,5 +3,5 @@ module Asterism
3
3
  # is in asterism.gemspec (asterism-zenoh ~> 0.4.0 for 0.5.0 to 0.7.0:
4
4
  # the bindings did not change; see CHANGELOG.md). 0.7.0 speaks wire
5
5
  # protocol 1 only (Asterism::PROTOCOL, docs/protocol.md).
6
- VERSION = "0.7.0"
6
+ VERSION = "0.7.1"
7
7
  end
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
- lost(e.message)
603
- false
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
- lost(e.message)
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
- lost(e.message)
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
- lost(e.message)
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.0
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-12 00:00:00.000000000 Z
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.0
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.0
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