asterism 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +97 -28
- data/lib/asterism/cruby/objects.rb +7 -3
- data/lib/asterism/cruby.rb +51 -7
- data/lib/asterism/version.rb +4 -3
- data/mrblib/asterism.rb +209 -58
- data/mrblib/future.rb +11 -5
- data/mrblib/profile.rb +16 -3
- data/mrblib/proxy.rb +23 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54a2e8d7f4b96b04dc56b290714d60a4a8d10cdc87d789ccf01bbb2b474f0459
|
|
4
|
+
data.tar.gz: be9f2f6c820e67ea513ea19a3d6f88437589cb2020615bc558b553f83e882673
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb166817303dfd933dd1fd6a4444aecd966ce06b66347a4b058759c49dc0b0cd1397bbef60d05753c72ccf511a8f70ea5c49c8ffdfaa591eb2d2cf2255d8b1da
|
|
7
|
+
data.tar.gz: d0d0549e336a50af534813aa4ab74c068952bf3d43148cc7c2032c0f4447df0aae73c9d1d813f8aa99819310c13ae3e220d59376276195fed408506b34144399
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,78 @@ 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.0
|
|
10
|
+
|
|
11
|
+
**This release breaks compatibility: it speaks wire protocol 1 only.** A
|
|
12
|
+
node of 0.7.0 and a node of 0.6.0 or earlier (protocol 0) do not see each
|
|
13
|
+
other's objects, and a call between them fails: a protocol 0 call to a
|
|
14
|
+
0.7.0 node is refused with the error kind `protocol`, a 0.7.0 call to an
|
|
15
|
+
older node is not answered, and their replies are malformed answers to
|
|
16
|
+
each other. Move every node of a network to 0.7.0 together (boards,
|
|
17
|
+
browsers, consoles, CRuby programs). The Ruby API stays as it was, with
|
|
18
|
+
the additions below. Needs asterism-zenoh / picoruby-asterism-zenoh 0.4.0
|
|
19
|
+
(unchanged), and on mruby a MessagePack that reads and writes bin (README,
|
|
20
|
+
"Using it").
|
|
21
|
+
|
|
22
|
+
Protocol 1 (`docs/protocol.md`, normative; `docs/protocol_review.md` says
|
|
23
|
+
what was decided and why):
|
|
24
|
+
|
|
25
|
+
- A call is a get on `asterism/<node>/<app>/<object>/call/<caller>`, where
|
|
26
|
+
`<caller>` is the calling node's ID. Its payload is a map
|
|
27
|
+
`{"method" => name, "args" => [...], "kwargs" => {...}}` (args and
|
|
28
|
+
kwargs left out when empty) instead of the array `[method, args,
|
|
29
|
+
kwargs]`.
|
|
30
|
+
- A reply is `{"value" => value}` or `{"error" => {"kind" => ..., "class"
|
|
31
|
+
=> ..., "message" => ...}}` instead of `["ok", value]` /
|
|
32
|
+
`["error", class, message]`. The kinds: `raised`, `not_exposed`,
|
|
33
|
+
`no_object`, `arguments`, `malformed`, `unsendable`, `nesting`,
|
|
34
|
+
`protocol`.
|
|
35
|
+
- The meta is `{"methods" => [{"name" => name, "arity" => n}, ...]}`.
|
|
36
|
+
- Binary Strings go as MessagePack bin (CRuby: ASCII-8BIT Strings, no more
|
|
37
|
+
compatibility mode; mruby: `String#b`), and a bin arrives as a binary
|
|
38
|
+
String. The profile's icon is a bin.
|
|
39
|
+
- The profile has `"protocol" => 1`.
|
|
40
|
+
- Each application declares its own liveliness token
|
|
41
|
+
`asterism/<node>/<app>`; the node token `asterism/<node>`, which the
|
|
42
|
+
applications of a node shared, is gone. A node is present while any of
|
|
43
|
+
its applications or objects is.
|
|
44
|
+
- A payload is exactly one value: bytes after it, ext types and integers
|
|
45
|
+
outside the signed 64-bit range make it malformed. CRuby raises
|
|
46
|
+
`EncodeError` before sending an Integer outside int64 (it raised
|
|
47
|
+
`RangeError` before).
|
|
48
|
+
- A protocol 0 call (on the five-chunk `.../call` key) is answered with the
|
|
49
|
+
error kind `protocol`.
|
|
50
|
+
|
|
51
|
+
Added
|
|
52
|
+
|
|
53
|
+
- `Asterism.current_call` while an exposed method runs for a call (per
|
|
54
|
+
thread on CRuby, `net.current_call` too): `from` (the calling node),
|
|
55
|
+
`verified` (`:claimed`; `:local` for a call from the same application),
|
|
56
|
+
`object`, `method`. `Asterism::CurrentCall`.
|
|
57
|
+
- `Asterism.expose(name, obj, methods:, caller: :from)` (and
|
|
58
|
+
`net.expose`): passes the calling node's ID as the keyword `from:` and
|
|
59
|
+
refuses a call that brings `from:` itself.
|
|
60
|
+
- `RemoteError#remote_kind`: the error's kind, as above (nil when the
|
|
61
|
+
answer did not say).
|
|
62
|
+
- `proxy.asterism_call(name, *args, **kw)` and `proxy.async.asterism_call`:
|
|
63
|
+
a call by the method's name, for names a proxy cannot forward (`display`,
|
|
64
|
+
`hash`, `class`, `fail`, ...) and names that are not Ruby identifiers.
|
|
65
|
+
- `Asterism::PROTOCOL` (1).
|
|
66
|
+
- The conformance tests (`test/protocol/`) are protocol 1's: 55 values, 54
|
|
67
|
+
incoming queries, 24 outgoing steps; CRuby and mruby pass them all.
|
|
68
|
+
|
|
69
|
+
Changed
|
|
70
|
+
|
|
71
|
+
- `proxy.asterism_meta` returns the new meta form.
|
|
72
|
+
- `Asterism.connect`'s duplicate check also finds a copy of the
|
|
73
|
+
application that exposes nothing (it has its application token now).
|
|
74
|
+
- `host_test/msgpack.rb` (the mruby test's MessagePack) writes a binary
|
|
75
|
+
String as bin and reads a bin as one; the mruby test build adds
|
|
76
|
+
mruby-encoding (to tell a binary String).
|
|
77
|
+
- The documentation of the wire protocol (docs/protocol.md,
|
|
78
|
+
docs/protocol_review.md) and the conformance tests (X0, unreleased in
|
|
79
|
+
0.6.0) are rewritten for protocol 1.
|
|
80
|
+
|
|
9
81
|
## 0.6.0
|
|
10
82
|
|
|
11
83
|
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,12 @@ 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`.
|
|
46
52
|
|
|
47
53
|
## Which API?
|
|
48
54
|
|
|
@@ -91,7 +97,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
91
97
|
|
|
92
98
|
```
|
|
93
99
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
94
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.
|
|
100
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.7.0.gem
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
From the working trees, without installing:
|
|
@@ -101,12 +107,14 @@ ruby -I lib -I ../asterism-zenoh/lib your_script.rb # after `rake compile` in
|
|
|
101
107
|
```
|
|
102
108
|
|
|
103
109
|
`Asterism::ROS::TYPE_PATH` is `data/msgs` there; add your own directories.
|
|
104
|
-
MessagePack sends
|
|
105
|
-
|
|
110
|
+
MessagePack sends a binary (ASCII-8BIT) String as bin and any other String
|
|
111
|
+
as str (protocol 1; [Values](#values)).
|
|
106
112
|
|
|
107
113
|
mruby / PicoRuby: add this repository and picoruby-asterism-zenoh as gems
|
|
108
114
|
(`conf.gem "path/to/asterism"`; a `MessagePack` module with `pack` /
|
|
109
|
-
`unpack` is needed too
|
|
115
|
+
`unpack` is needed too: for protocol 1 it must write a binary String
|
|
116
|
+
(`String#b`) as bin and read a bin as one, and refuse ext types, integers
|
|
117
|
+
outside int64 and bytes after the value), and put `data/msgs` on the device's file system at
|
|
110
118
|
`/usr/share/asterism/msgs` (or add the place to
|
|
111
119
|
`Asterism::ROS::TYPE_PATH`). Family mruby does this from pinned commits of
|
|
112
120
|
these repositories.
|
|
@@ -120,6 +128,8 @@ rake test:objects # objects and Asterism::ROS between CRuby sessions over a
|
|
|
120
128
|
rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
|
|
121
129
|
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
|
|
122
130
|
rake test:mruby # the shared layer on mruby (builds mruby; not part of `rake`)
|
|
131
|
+
rake test:protocol # the wire protocol's conformance samples and driver against CRuby (docs/protocol.md)
|
|
132
|
+
rake test:protocol_mruby # the same driver against mruby (builds mruby; not part of `rake`)
|
|
123
133
|
rake bench:poll # the time and allocations of one Asterism.poll by nodes and objects (prints a table)
|
|
124
134
|
```
|
|
125
135
|
|
|
@@ -251,7 +261,8 @@ end
|
|
|
251
261
|
|---|---|
|
|
252
262
|
| `Asterism.connect(...) { \|net\| }` | yields an `Asterism::Net`, `Asterism.close` when the block ends; returns the block's value. Without a block, as before |
|
|
253
263
|
| `Asterism.net` | the `Net` of the connection made without a block (nil when not connected) |
|
|
254
|
-
| `net.expose` / `unexpose` / `exposed` | as `Asterism.expose
|
|
264
|
+
| `net.expose` / `unexpose` / `exposed` | as `Asterism.expose` (also `caller: :from`). The methods run on the receiving thread, in `run`, or in whatever polls |
|
|
265
|
+
| `net.current_call` | `Asterism.current_call`: the call being answered on this thread (the receiving thread while it answers), or nil |
|
|
255
266
|
| `net[path, timeout: 2.0]` | a proxy (`timeout` in seconds, or `timeout_ms:`) |
|
|
256
267
|
| `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
268
|
| `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 |
|
|
@@ -417,17 +428,19 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
417
428
|
| Call | Returns | Raises / notes |
|
|
418
429
|
|---|---|---|
|
|
419
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). |
|
|
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. |
|
|
431
|
+
| `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
|
+
| `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
433
|
| `Asterism.unexpose(name)` | true / false | |
|
|
422
434
|
| `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 (
|
|
435
|
+
| `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
436
|
| `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
|
|
437
|
+
| `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`). |
|
|
438
|
+
| `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
439
|
| `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
|
|
427
440
|
| `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
441
|
| `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
442
|
| `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" => [
|
|
443
|
+
| `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [{"name" => name, "arity" => n}, ...]}` |
|
|
431
444
|
| `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
445
|
| `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
|
|
433
446
|
| `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
|
|
@@ -444,19 +457,20 @@ and an icon. A console draws it with that; anyone can ask.
|
|
|
444
457
|
```ruby
|
|
445
458
|
Asterism.profile(name: "Rover", icon: File.binread("rover.png")) # this node's (any time)
|
|
446
459
|
Asterism.profile
|
|
447
|
-
# => {"kind" => "cruby", "name" => "Rover", "icon" => "\x89PNG...", "icon_hash" => "2a8c0626"}
|
|
460
|
+
# => {"protocol" => 1, "kind" => "cruby", "name" => "Rover", "icon" => "\x89PNG...", "icon_hash" => "2a8c0626"}
|
|
448
461
|
Asterism.profile_of("fmruby-aaaaaa")
|
|
449
|
-
# => {"kind" => "family_mruby", "host" => "atoms3r", "icon" => "\x89PNG...", "icon_hash" => "..."}
|
|
462
|
+
# => {"protocol" => 1, "kind" => "family_mruby", "host" => "atoms3r", "icon" => "\x89PNG...", "icon_hash" => "..."}
|
|
450
463
|
Asterism.profile_of("fmruby-aaaaaa", icon: false) # the same without the icon bytes
|
|
451
464
|
Asterism.profile_of("an-older-node") # => nil
|
|
452
465
|
```
|
|
453
466
|
|
|
454
467
|
| Field | Value | Default |
|
|
455
468
|
|---|---|---|
|
|
469
|
+
| `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
470
|
| `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
471
|
| `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
472
|
| `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` |
|
|
473
|
+
| `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
474
|
| `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
475
|
|
|
462
476
|
- **Setting.** `Asterism.profile(**fields)` changes the fields given and keeps
|
|
@@ -536,7 +550,7 @@ end
|
|
|
536
550
|
another. Your own `put` does not come back to your own subscriber on the
|
|
537
551
|
same session.
|
|
538
552
|
- **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
|
|
553
|
+
is answered by its queryable (`get` there is a call), `asterism/<node>/<app>` and
|
|
540
554
|
`asterism/<node>/<app>/<object>` are its liveliness tokens. Putting under
|
|
541
555
|
`asterism/<node>/<app>/<object>/<anything>` (a state, a log) is fine:
|
|
542
556
|
puts and subscriptions do not meet queryables or liveliness.
|
|
@@ -553,12 +567,18 @@ Asterism::Error (the Zenoh bindings define it)
|
|
|
553
567
|
├── Asterism::TimeoutError (no answer in time; old name Asterism::Timeout)
|
|
554
568
|
│ └── Asterism::ROS::TimeoutError (old name Asterism::ROS::Timeout)
|
|
555
569
|
├── Asterism::EncodeError
|
|
556
|
-
├── Asterism::RemoteError (remote_class, remote_message)
|
|
570
|
+
├── Asterism::RemoteError (remote_kind, remote_class, remote_message)
|
|
557
571
|
├── Asterism::ROS::UnknownType
|
|
558
572
|
├── Asterism::CDR::DecodeError
|
|
559
573
|
└── Asterism::DeprecationError (only with Asterism.deprecations = :raise)
|
|
560
574
|
```
|
|
561
575
|
|
|
576
|
+
`RemoteError#remote_kind` says what happened on the other side, as a word
|
|
577
|
+
of the protocol (docs/protocol.md 4.4): `raised` (the method raised;
|
|
578
|
+
`remote_class` is its class there), `not_exposed`, `no_object`,
|
|
579
|
+
`arguments`, `malformed`, `unsendable`, `nesting`, `protocol` (the other
|
|
580
|
+
side speaks a different protocol); nil when the answer did not say.
|
|
581
|
+
|
|
562
582
|
The old names `Asterism::Timeout` and `Asterism::ROS::Timeout` still work
|
|
563
583
|
(they warn once). Inside `module Asterism`, `Timeout` is Ruby's `Timeout`
|
|
564
584
|
module again. The ROS layer raises `Zenoh::ClosedError` when its session
|
|
@@ -658,22 +678,71 @@ session is closed, its entities raise `Zenoh::ClosedError`).
|
|
|
658
678
|
|
|
659
679
|
## Values
|
|
660
680
|
|
|
661
|
-
Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (
|
|
662
|
-
`Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
681
|
+
Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (signed
|
|
682
|
+
64 bit), `Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a
|
|
683
|
+
`String` and arrives as one (also as a Hash key; keyword names become
|
|
684
|
+
Symbols again). A binary String (`String#b`: ASCII-8BIT on CRuby, the
|
|
685
|
+
binary flag on mruby) goes as a MessagePack bin and arrives as a binary
|
|
686
|
+
String; any other String goes as text (str). Anything else in the
|
|
687
|
+
arguments, or an Integer outside the signed 64-bit range, raises
|
|
688
|
+
`EncodeError` before sending; in a return value the caller gets
|
|
689
|
+
`RemoteError` (`unsendable`). Nesting is limited to 16 levels.
|
|
690
|
+
|
|
691
|
+
- Hash keys: use String (or Symbol) and Integer keys for anything another
|
|
692
|
+
language reads (Python and JavaScript refuse others by default).
|
|
693
|
+
- Times: send an Integer of nanoseconds since the epoch (or name the unit:
|
|
694
|
+
`timeout_ms`); there is no time type.
|
|
695
|
+
- A keyword given as `nil` and one left out differ: the callee's default
|
|
696
|
+
applies only to the one left out.
|
|
697
|
+
- A JavaScript caller's `1.0` arrives as the Integer `1`.
|
|
698
|
+
|
|
699
|
+
## Who called
|
|
700
|
+
|
|
701
|
+
The caller's node ID travels in the call's key (`.../call/<caller>`). An
|
|
702
|
+
exposed method can read it:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
class Rover
|
|
706
|
+
def stop
|
|
707
|
+
c = Asterism.current_call # nil outside a call
|
|
708
|
+
log("stop from #{c.from}") # c.verified: :claimed (:local from this app)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def drive(v, w, from:) # caller: :from fills from: in
|
|
712
|
+
log("drive from #{from}")
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
Asterism.expose("rover", Rover.new, methods: { stop: 0 })
|
|
716
|
+
Asterism.expose("wheels", Rover.new, methods: { drive: 2 }, caller: :from)
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
- `Asterism.current_call` is set only while a method runs for a call (per
|
|
720
|
+
thread on CRuby). Calls answered while that method waits for its own
|
|
721
|
+
call come on top of it and go when they end.
|
|
722
|
+
- With `caller: :from`, a call that passes `from:` itself is refused
|
|
723
|
+
(`RemoteError`, `remote_kind` `arguments`): `from` is always the one
|
|
724
|
+
Asterism took from the call.
|
|
725
|
+
- **It is a claim.** Any client can call with any name. A router's access
|
|
726
|
+
control makes it binding: with mutual TLS, zenohd's ACL can let each
|
|
727
|
+
certificate query only keys ending in its own name
|
|
728
|
+
(`asterism/*/*/*/call/<name>`; docs/protocol.md 4.7). Without such a
|
|
729
|
+
router, use `from` to avoid mistakes, not to decide who may do what.
|
|
667
730
|
|
|
668
731
|
## Keys and encoding
|
|
669
732
|
|
|
733
|
+
Wire protocol 1. The full description, for implementations in other
|
|
734
|
+
languages, is [docs/protocol.md](docs/protocol.md) (with the conformance
|
|
735
|
+
tests); why it has this shape (the review of protocol 0's Ruby-shaped
|
|
736
|
+
parts, and what was decided) is
|
|
737
|
+
[docs/protocol_review.md](docs/protocol_review.md).
|
|
738
|
+
|
|
670
739
|
| Key | Zenoh | Payload |
|
|
671
740
|
|---|---|---|
|
|
672
|
-
| `asterism/<node>/<app>/<object>/call
|
|
673
|
-
| `asterism/<node>/<app>/<object>/meta` | get / queryable | reply: `{"methods" => [
|
|
674
|
-
| `asterism/<node>` | liveliness | the
|
|
741
|
+
| `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 |
|
|
742
|
+
| `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 |
|
|
743
|
+
| `asterism/<node>/<app>` | liveliness | the application is connected (a node is there while any of its applications or objects is) |
|
|
675
744
|
| `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
|
|
745
|
+
| `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
746
|
|
|
678
747
|
One queryable per application (`asterism/<node>/<app>/**`), and one for the
|
|
679
748
|
profile (`asterism/<node>/@profile`). A call whose
|
|
@@ -728,9 +797,9 @@ nested type (`geometry_msgs/msg/Twist`) at start-up took 0.6 KB more.
|
|
|
728
797
|
|
|
729
798
|
- Objects cannot be passed by reference (a return value is a copy), blocks
|
|
730
799
|
cannot be sent, there are no events (later stages).
|
|
731
|
-
- The
|
|
732
|
-
|
|
733
|
-
|
|
800
|
+
- The wire protocol is protocol 1 only: nodes of asterism before 0.7.0
|
|
801
|
+
(protocol 0) do not see 0.7.0's objects and a call between the two
|
|
802
|
+
fails (a protocol 0 call is refused with `remote_kind` `protocol`).
|
|
734
803
|
- No authentication by default (plain TCP): a trusted LAN is assumed, or
|
|
735
804
|
TLS through a router (CRuby; [Over TLS](#over-tls-a-router-on-the-internet)).
|
|
736
805
|
- One object-layer connection per process (per VM on the boards).
|
|
@@ -153,12 +153,16 @@ module Asterism
|
|
|
153
153
|
def exposed = ::Asterism.exposed
|
|
154
154
|
|
|
155
155
|
# Asterism.expose: methods: an Array of names or a Hash name => number
|
|
156
|
-
# of arguments
|
|
156
|
+
# of arguments; caller: :from passes the calling node's ID as that
|
|
157
|
+
# keyword. The object's methods run on the receiving thread (or in
|
|
157
158
|
# run, or in whatever polls).
|
|
158
|
-
def expose(name, obj, methods:)
|
|
159
|
-
::Asterism.expose(name, obj, methods: methods)
|
|
159
|
+
def expose(name, obj, methods:, caller: nil)
|
|
160
|
+
::Asterism.expose(name, obj, methods: methods, caller: caller)
|
|
160
161
|
end
|
|
161
162
|
|
|
163
|
+
# Asterism.current_call: the call being answered on this thread, or nil.
|
|
164
|
+
def current_call = ::Asterism.current_call
|
|
165
|
+
|
|
162
166
|
def unexpose(name)
|
|
163
167
|
::Asterism.unexpose(name)
|
|
164
168
|
end
|
data/lib/asterism/cruby.rb
CHANGED
|
@@ -9,12 +9,53 @@ module Asterism
|
|
|
9
9
|
ROS::TYPE_PATH.replace([MSGS_DIR])
|
|
10
10
|
|
|
11
11
|
module Codec
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
12
|
+
INT_MIN = -(2**63)
|
|
13
|
+
INT_MAX = 2**63 - 1
|
|
14
|
+
|
|
15
|
+
# msgpack-ruby packs a binary (ASCII-8BIT) String, such as a payload
|
|
16
|
+
# that came from Zenoh or File.binread, as a bin and any other String
|
|
17
|
+
# as a str (docs/protocol.md 3.1): no compatibility mode.
|
|
16
18
|
def self.pack(v)
|
|
17
|
-
::MessagePack.pack(check(v)
|
|
19
|
+
::MessagePack.pack(check(v))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
alias_method :check_portable, :check
|
|
24
|
+
|
|
25
|
+
# An Integer outside the signed 64-bit range cannot be sent (the
|
|
26
|
+
# other implementations cannot hold it): EncodeError, before sending.
|
|
27
|
+
def check(v, depth = 0)
|
|
28
|
+
if v.is_a?(Integer) && (v < INT_MIN || v > INT_MAX)
|
|
29
|
+
raise ::Asterism::EncodeError, "Integer #{v} is outside the signed 64-bit range"
|
|
30
|
+
end
|
|
31
|
+
return v if v.is_a?(Integer)
|
|
32
|
+
check_portable(v, depth)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias_method :unpack_portable, :unpack
|
|
36
|
+
|
|
37
|
+
# Also nil for an integer above 2^63-1 (msgpack-ruby reads a uint64;
|
|
38
|
+
# the protocol carries signed 64-bit integers only).
|
|
39
|
+
def unpack(bytes)
|
|
40
|
+
v = unpack_portable(bytes)
|
|
41
|
+
return v unless bytes&.b&.include?("\xcf".b)
|
|
42
|
+
int64_only?(v) ? v : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def int64_only?(v)
|
|
46
|
+
case v
|
|
47
|
+
when Integer then v <= INT_MAX
|
|
48
|
+
when Array then v.all? { int64_only?(_1) }
|
|
49
|
+
when Hash then v.all? { |k, x| int64_only?(k) && int64_only?(x) }
|
|
50
|
+
else true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A text field of the protocol is a str, whatever the String's
|
|
55
|
+
# encoding was.
|
|
56
|
+
def text(s)
|
|
57
|
+
s.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
58
|
+
end
|
|
18
59
|
end
|
|
19
60
|
end
|
|
20
61
|
|
|
@@ -62,7 +103,10 @@ module Asterism
|
|
|
62
103
|
end
|
|
63
104
|
end
|
|
64
105
|
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
# The calls being answered, per thread: an exposed method runs on the
|
|
107
|
+
# thread that answers (the receiving thread, or whatever polls), and
|
|
108
|
+
# Asterism.current_call on another thread is not inside that call.
|
|
109
|
+
def self.call_stack
|
|
110
|
+
Thread.current[:asterism_call_stack] ||= []
|
|
67
111
|
end
|
|
68
112
|
end
|
data/lib/asterism/version.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Asterism
|
|
2
2
|
# The gem's version (the CRuby gem asterism). The Zenoh binding it needs
|
|
3
|
-
# is in asterism.gemspec (asterism-zenoh ~> 0.4.0 for 0.5.0
|
|
4
|
-
# the bindings did not change; see CHANGELOG.md).
|
|
5
|
-
|
|
3
|
+
# is in asterism.gemspec (asterism-zenoh ~> 0.4.0 for 0.5.0 to 0.7.0:
|
|
4
|
+
# the bindings did not change; see CHANGELOG.md). 0.7.0 speaks wire
|
|
5
|
+
# protocol 1 only (Asterism::PROTOCOL, docs/protocol.md).
|
|
6
|
+
VERSION = "0.7.0"
|
|
6
7
|
end
|
data/mrblib/asterism.rb
CHANGED
|
@@ -7,20 +7,25 @@
|
|
|
7
7
|
# # Asterism["...", timeout: 5.0])
|
|
8
8
|
# Asterism.poll # call regularly (answers incoming calls)
|
|
9
9
|
#
|
|
10
|
-
# Keys (
|
|
11
|
-
# asterism/<node>/<app>/<object>/call
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# asterism/<node
|
|
16
|
-
#
|
|
17
|
-
# asterism/<node
|
|
10
|
+
# Keys and payloads: protocol 1 (docs/protocol.md):
|
|
11
|
+
# asterism/<node>/<app>/<object>/call/<caller>
|
|
12
|
+
# get: {"method" => name, "args" => [...], "kwargs" => {...}}
|
|
13
|
+
# reply {"value" => v} or
|
|
14
|
+
# {"error" => {"kind" => k, "class" => c, "message" => m}}
|
|
15
|
+
# asterism/<node>/<app>/<object>/meta
|
|
16
|
+
# get: reply {"methods" => [{"name" => n, "arity" => a}, ...]}
|
|
17
|
+
# asterism/<node>/<app> liveliness: the application is up
|
|
18
|
+
# asterism/<node>/<app>/<object> liveliness: the object is exposed
|
|
19
|
+
# asterism/<node>/@profile get: reply the node's profile (profile.rb)
|
|
18
20
|
#
|
|
19
21
|
# Everything is polled: nothing runs behind the application's back. A call
|
|
20
22
|
# that waits for its answer keeps polling meanwhile and answers the calls
|
|
21
23
|
# that come in, so two machines calling each other do not lock up.
|
|
22
24
|
module Asterism
|
|
23
25
|
ROOT = "asterism"
|
|
26
|
+
# The version of the wire protocol this implementation speaks (and the
|
|
27
|
+
# only one: protocol 0, the 0.x releases before 0.7.0, is not spoken).
|
|
28
|
+
PROTOCOL = 1
|
|
24
29
|
# The time limit of a call (Asterism[path] without timeout:), in seconds
|
|
25
30
|
# and in milliseconds.
|
|
26
31
|
DEFAULT_TIMEOUT = 2.0
|
|
@@ -51,17 +56,49 @@ module Asterism
|
|
|
51
56
|
class Disconnected < Error; end
|
|
52
57
|
|
|
53
58
|
# The remote method raised, or could not be called (not exposed, wrong
|
|
54
|
-
# number of arguments). remote_class is the class name on the other side
|
|
59
|
+
# number of arguments). remote_class is the class name on the other side,
|
|
60
|
+
# remote_kind what happened, as a protocol word (a String): "raised" (the
|
|
61
|
+
# method raised; remote_class is then the callee language's own name),
|
|
62
|
+
# "not_exposed", "no_object", "arguments", "malformed", "unsendable",
|
|
63
|
+
# "nesting", "protocol" (docs/protocol.md 4.4); nil when the answer did
|
|
64
|
+
# not say.
|
|
55
65
|
class RemoteError < Error
|
|
56
|
-
attr_reader :remote_class, :remote_message
|
|
66
|
+
attr_reader :remote_class, :remote_message, :remote_kind
|
|
57
67
|
|
|
58
|
-
def initialize(remote_class, remote_message)
|
|
68
|
+
def initialize(remote_class, remote_message, remote_kind = nil)
|
|
59
69
|
@remote_class = remote_class.to_s
|
|
60
70
|
@remote_message = remote_message.to_s
|
|
71
|
+
@remote_kind = remote_kind.nil? ? nil : remote_kind.to_s
|
|
61
72
|
super("#{@remote_class}: #{@remote_message}")
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
|
|
76
|
+
# What Asterism.current_call returns while an exposed method runs for a
|
|
77
|
+
# call: who called and what. from is the calling node's ID (a String),
|
|
78
|
+
# verified how far that is checked: :claimed (the caller named itself in
|
|
79
|
+
# the call's key; a router's access control is what makes the name
|
|
80
|
+
# binding, docs/protocol.md 4.7) or :local (a call from this same
|
|
81
|
+
# application, which does not go on the network). object is the exposed
|
|
82
|
+
# object's name, method the method's name (Strings).
|
|
83
|
+
class CurrentCall
|
|
84
|
+
attr_reader :from, :verified, :object, :method
|
|
85
|
+
|
|
86
|
+
def initialize(from, verified, object, method)
|
|
87
|
+
@from = from
|
|
88
|
+
@verified = verified
|
|
89
|
+
@object = object
|
|
90
|
+
@method = method
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def inspect
|
|
94
|
+
"#<Asterism::CurrentCall #{@object}.#{@method} from #{@from} (#{@verified})>"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_s
|
|
98
|
+
inspect
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
65
102
|
# The old names of renamed constants: Asterism::Timeout (deprecated) is
|
|
66
103
|
# TimeoutError. A constant alias would shadow Ruby's Timeout module inside
|
|
67
104
|
# `module Asterism`, and cannot warn.
|
|
@@ -79,6 +116,9 @@ module Asterism
|
|
|
79
116
|
MAX_DEPTH = 16
|
|
80
117
|
|
|
81
118
|
# The value as it will be sent (Symbols become Strings), or EncodeError.
|
|
119
|
+
# A String goes as a MessagePack str, a binary String (String#b) as a
|
|
120
|
+
# bin: the MessagePack module decides by the String's encoding
|
|
121
|
+
# (ASCII-8BIT on CRuby, the binary flag on mruby).
|
|
82
122
|
def self.check(v, depth = 0)
|
|
83
123
|
raise ::Asterism::EncodeError, "value nested too deep (max #{MAX_DEPTH})" if depth > MAX_DEPTH
|
|
84
124
|
if v.nil? || v == true || v == false || v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(String)
|
|
@@ -101,7 +141,15 @@ module Asterism
|
|
|
101
141
|
::MessagePack.pack(check(v))
|
|
102
142
|
end
|
|
103
143
|
|
|
104
|
-
#
|
|
144
|
+
# A String for a text field of the protocol (an error's class and
|
|
145
|
+
# message): always a str, also when it came as a binary String.
|
|
146
|
+
def self.text(s)
|
|
147
|
+
"#{s}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# nil when the bytes are not exactly one MessagePack value of the types
|
|
151
|
+
# above (the MessagePack module refuses trailing bytes, ext types and
|
|
152
|
+
# integers outside int64).
|
|
105
153
|
def self.unpack(bytes)
|
|
106
154
|
return nil if bytes.nil? || bytes.empty?
|
|
107
155
|
::MessagePack.unpack(bytes)
|
|
@@ -219,7 +267,7 @@ module Asterism
|
|
|
219
267
|
s.close
|
|
220
268
|
raise Error, "#{node}/#{app} is already on the network"
|
|
221
269
|
end
|
|
222
|
-
@
|
|
270
|
+
@app_token = s.liveliness("#{ROOT}/#{node}/#{app}")
|
|
223
271
|
@queryable = s.queryable("#{ROOT}/#{node}/#{app}/**", depth: 32)
|
|
224
272
|
@watch = s.liveliness_watch("#{ROOT}/**", depth: watch_depth)
|
|
225
273
|
@profile_queryable = s.queryable("#{ROOT}/#{node}/#{Profile::KEY}", depth: 4)
|
|
@@ -231,9 +279,9 @@ module Asterism
|
|
|
231
279
|
@session = s
|
|
232
280
|
@node = node
|
|
233
281
|
@app = app
|
|
234
|
-
@objects = {} # name => [object, {method name => arity}, token]
|
|
282
|
+
@objects = {} # name => [object, {method name => arity}, token, caller keyword or nil]
|
|
235
283
|
@alive = {} # "node/app/object" => node (from liveliness)
|
|
236
|
-
@
|
|
284
|
+
@apps = {} # "node/app" => node (from liveliness)
|
|
237
285
|
@proxies = {}
|
|
238
286
|
@depth = 0
|
|
239
287
|
@lost = nil
|
|
@@ -245,8 +293,8 @@ module Asterism
|
|
|
245
293
|
self
|
|
246
294
|
end
|
|
247
295
|
|
|
248
|
-
# Whether
|
|
249
|
-
# same application). Waits for the answer (a router answers at once), at
|
|
296
|
+
# Whether node/app is alive already (its application token or an object:
|
|
297
|
+
# a second copy of the same application). Waits for the answer (a router answers at once), at
|
|
250
298
|
# most check_ms. @api private
|
|
251
299
|
def self.already_there?(s, node, app, check_ms = 1500)
|
|
252
300
|
get_ms = check_ms > 1000 ? check_ms - 500 : check_ms
|
|
@@ -301,7 +349,7 @@ module Asterism
|
|
|
301
349
|
@session = nil
|
|
302
350
|
@objects = {}
|
|
303
351
|
@alive = {}
|
|
304
|
-
@
|
|
352
|
+
@apps = {}
|
|
305
353
|
@node_list = nil
|
|
306
354
|
@nodes_changed = true
|
|
307
355
|
@proxies = {}
|
|
@@ -313,7 +361,7 @@ module Asterism
|
|
|
313
361
|
@releasing = nil
|
|
314
362
|
end
|
|
315
363
|
end
|
|
316
|
-
@
|
|
364
|
+
@app_token = nil
|
|
317
365
|
@queryable = nil
|
|
318
366
|
@watch = nil
|
|
319
367
|
@profile_queryable = nil
|
|
@@ -376,7 +424,11 @@ module Asterism
|
|
|
376
424
|
# Makes obj callable from other machines as <node>/<app>/<name>. Only the
|
|
377
425
|
# listed methods can be called. methods: is an Array of names, or a Hash
|
|
378
426
|
# name => number of arguments (checked before calling; -1 for any).
|
|
379
|
-
|
|
427
|
+
# caller: a keyword name (caller: :from): each call passes the calling
|
|
428
|
+
# node's ID (Asterism.current_call.from) to the method as that keyword,
|
|
429
|
+
# and a call that brings the keyword itself is refused (an "arguments"
|
|
430
|
+
# error), so the value is always the one Asterism took from the call.
|
|
431
|
+
def self.expose(name, obj, methods:, caller: nil)
|
|
380
432
|
s = session!
|
|
381
433
|
name = check_name("object", name)
|
|
382
434
|
table = {}
|
|
@@ -389,9 +441,17 @@ module Asterism
|
|
|
389
441
|
table.each_key do |m|
|
|
390
442
|
raise ArgumentError, "#{obj.class} has no public method #{m}" unless obj.respond_to?(m)
|
|
391
443
|
end
|
|
444
|
+
kw = nil
|
|
445
|
+
unless caller.nil?
|
|
446
|
+
unless caller.is_a?(Symbol) || caller.is_a?(String)
|
|
447
|
+
raise ArgumentError, "caller: expects a keyword name (caller: :from), got #{caller.inspect}"
|
|
448
|
+
end
|
|
449
|
+
kw = caller.to_s
|
|
450
|
+
raise ArgumentError, "caller: expects a keyword name (caller: :from), got #{caller.inspect}" if kw.empty?
|
|
451
|
+
end
|
|
392
452
|
unexpose(name)
|
|
393
453
|
token = s.liveliness("#{ROOT}/#{@node}/#{@app}/#{name}")
|
|
394
|
-
@objects[name] = [obj, table, token]
|
|
454
|
+
@objects[name] = [obj, table, token, kw]
|
|
395
455
|
"#{@node}/#{@app}/#{name}"
|
|
396
456
|
rescue Asterism::Zenoh::Error => e
|
|
397
457
|
raise Disconnected, e.message
|
|
@@ -424,7 +484,7 @@ module Asterism
|
|
|
424
484
|
|
|
425
485
|
# ------------------------------------------------------------ nodes coming and going
|
|
426
486
|
|
|
427
|
-
# on_join { |node| }: another node appeared (
|
|
487
|
+
# on_join { |node| }: another node appeared (one of its applications or
|
|
428
488
|
# objects). The nodes there already join on the first polls after
|
|
429
489
|
# connecting. on_leave { |node| }: a node is gone (no token and no object
|
|
430
490
|
# left); when the connection is lost, every node known until then leaves.
|
|
@@ -546,7 +606,11 @@ module Asterism
|
|
|
546
606
|
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
547
607
|
# @api private
|
|
548
608
|
def self.wait_until
|
|
549
|
-
|
|
609
|
+
if (@depth || 0) >= MAX_NESTING
|
|
610
|
+
# Kept, so that a method that lets it out is answered as "nesting".
|
|
611
|
+
@nesting_error = Error.new("calls nested too deep (max #{MAX_NESTING})")
|
|
612
|
+
raise @nesting_error
|
|
613
|
+
end
|
|
550
614
|
@depth = (@depth || 0) + 1
|
|
551
615
|
begin
|
|
552
616
|
until yield
|
|
@@ -592,13 +656,13 @@ module Asterism
|
|
|
592
656
|
i += 1
|
|
593
657
|
rest = key[ROOT.length + 1, key.length].to_s
|
|
594
658
|
parts = rest.split("/")
|
|
595
|
-
if parts.size ==
|
|
659
|
+
if parts.size == 2 # an application: asterism/<node>/<app>
|
|
596
660
|
if alive
|
|
597
|
-
@
|
|
661
|
+
@apps[rest] = parts[0]
|
|
598
662
|
else
|
|
599
|
-
@
|
|
663
|
+
@apps.delete(rest)
|
|
600
664
|
end
|
|
601
|
-
elsif parts.size == 3
|
|
665
|
+
elsif parts.size == 3 # an object: asterism/<node>/<app>/<object>
|
|
602
666
|
if alive
|
|
603
667
|
@alive[rest] = parts[0]
|
|
604
668
|
else
|
|
@@ -622,7 +686,7 @@ module Asterism
|
|
|
622
686
|
return list if list
|
|
623
687
|
list = [@node]
|
|
624
688
|
seen = { @node => true }
|
|
625
|
-
names = @
|
|
689
|
+
names = @apps.values
|
|
626
690
|
i = 0
|
|
627
691
|
while i < names.size
|
|
628
692
|
n = names[i]
|
|
@@ -689,14 +753,16 @@ module Asterism
|
|
|
689
753
|
# @api private
|
|
690
754
|
def self.call_async(path, method, args, kwargs, timeout_ms)
|
|
691
755
|
parts = split_path(path)
|
|
692
|
-
payload = Codec.pack(
|
|
756
|
+
payload = Codec.pack(request(method, args, kwargs))
|
|
693
757
|
if parts[0] == @node && parts[1] == @app && @session
|
|
694
758
|
# This application's own object: no network (a session does not see
|
|
695
759
|
# its own queryable), same encoding and checks as a remote call.
|
|
696
|
-
return Future.answered(path, method, dispatch(parts[2], payload))
|
|
760
|
+
return Future.answered(path, method, dispatch(parts[2], payload, @node, :local))
|
|
697
761
|
end
|
|
698
762
|
begin
|
|
699
|
-
|
|
763
|
+
s = session!
|
|
764
|
+
# The caller's node ID is the key's last chunk (docs/protocol.md 4.1).
|
|
765
|
+
g = s.get("#{ROOT}/#{path}/call/#{@node}", timeout_ms: timeout_ms, payload: payload)
|
|
700
766
|
rescue Asterism::Zenoh::Error => e
|
|
701
767
|
lost(e.message)
|
|
702
768
|
raise Disconnected, e.message
|
|
@@ -704,18 +770,28 @@ module Asterism
|
|
|
704
770
|
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
705
771
|
end
|
|
706
772
|
|
|
773
|
+
# The request of a call: a map with the method's name, and the arguments
|
|
774
|
+
# and keyword arguments when there are any (absent means none).
|
|
775
|
+
# @api private
|
|
776
|
+
def self.request(method, args, kwargs)
|
|
777
|
+
req = { "method" => method.to_s }
|
|
778
|
+
req["args"] = args unless args.nil? || args.empty?
|
|
779
|
+
req["kwargs"] = kwargs unless kwargs.nil? || kwargs.empty?
|
|
780
|
+
req
|
|
781
|
+
end
|
|
782
|
+
|
|
707
783
|
# @api private
|
|
708
784
|
def self.call(path, method, args, kwargs, timeout_ms)
|
|
709
785
|
call_async(path, method, args, kwargs, timeout_ms).value
|
|
710
786
|
end
|
|
711
787
|
|
|
712
|
-
# The meta reply of path: {"methods" => [
|
|
788
|
+
# The meta reply of path: {"methods" => [{"name" => n, "arity" => a}, ...]}.
|
|
713
789
|
# @api private
|
|
714
790
|
def self.meta(path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
715
791
|
parts = split_path(path)
|
|
716
792
|
if parts[0] == @node && parts[1] == @app && @session
|
|
717
793
|
m = meta_of(parts[2])
|
|
718
|
-
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}") unless m
|
|
794
|
+
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}", "no_object") unless m
|
|
719
795
|
return m
|
|
720
796
|
end
|
|
721
797
|
begin
|
|
@@ -730,6 +806,21 @@ module Asterism
|
|
|
730
806
|
|
|
731
807
|
# ------------------------------------------------------------ answering
|
|
732
808
|
|
|
809
|
+
# The call being answered now (an Asterism::CurrentCall: who called,
|
|
810
|
+
# which object and method), while an exposed method runs for a call; nil
|
|
811
|
+
# outside one. Calls answered while that method waits for its own call
|
|
812
|
+
# are on top of it until they end.
|
|
813
|
+
def self.current_call
|
|
814
|
+
stack = call_stack
|
|
815
|
+
stack.empty? ? nil : stack[stack.size - 1]
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# The calls being answered, innermost last (CRuby: per thread).
|
|
819
|
+
# @api private
|
|
820
|
+
def self.call_stack
|
|
821
|
+
@call_stack ||= []
|
|
822
|
+
end
|
|
823
|
+
|
|
733
824
|
# @api private
|
|
734
825
|
def self.answer_calls
|
|
735
826
|
qa = @queryable
|
|
@@ -753,62 +844,121 @@ module Asterism
|
|
|
753
844
|
end
|
|
754
845
|
end
|
|
755
846
|
|
|
756
|
-
# asterism/<node>/<app>/<object
|
|
757
|
-
#
|
|
847
|
+
# asterism/<node>/<app>/<object>/call/<caller> and
|
|
848
|
+
# asterism/<node>/<app>/<object>/meta; the node and app may come as
|
|
849
|
+
# wildcards (the query matched this application's queryable).
|
|
758
850
|
# @api private
|
|
759
851
|
def self.answer(q)
|
|
760
852
|
parts = q.key.split("/")
|
|
761
|
-
|
|
853
|
+
n = parts.size
|
|
854
|
+
return unless n == 5 || n == 6
|
|
762
855
|
obj = parts[3]
|
|
763
856
|
kind = parts[4]
|
|
764
857
|
prefix = "#{ROOT}/#{@node}/#{@app}/"
|
|
765
858
|
if kind == "meta"
|
|
859
|
+
return unless n == 5
|
|
766
860
|
names = obj == "*" ? @objects.keys : [obj]
|
|
767
|
-
names.each do |
|
|
768
|
-
m = meta_of(
|
|
769
|
-
q.reply("#{prefix}#{
|
|
861
|
+
names.each do |nm|
|
|
862
|
+
m = meta_of(nm)
|
|
863
|
+
q.reply("#{prefix}#{nm}/meta", Codec.pack(m)) if m
|
|
864
|
+
end
|
|
865
|
+
elsif kind == "call" && valid_name?(obj)
|
|
866
|
+
if n == 5
|
|
867
|
+
# A protocol 0 call (no caller chunk): refused, so that its sender
|
|
868
|
+
# learns why at once instead of waiting.
|
|
869
|
+
q.reply("#{prefix}#{obj}/call",
|
|
870
|
+
error_reply("protocol", "Asterism::Error",
|
|
871
|
+
"#{@node}/#{@app} speaks Asterism protocol #{PROTOCOL}: call " \
|
|
872
|
+
"asterism/<node>/<app>/<object>/call/<caller> with a map " \
|
|
873
|
+
"{\"method\", \"args\", \"kwargs\"} (asterism 0.7.0 and later)"))
|
|
874
|
+
elsif valid_name?(parts[5])
|
|
875
|
+
q.reply("#{prefix}#{obj}/call/#{parts[5]}", dispatch(obj, q.payload, parts[5], :claimed))
|
|
770
876
|
end
|
|
771
|
-
elsif kind == "call" && obj != "*"
|
|
772
|
-
q.reply("#{prefix}#{obj}/call", dispatch(obj, q.payload))
|
|
773
877
|
end
|
|
774
878
|
end
|
|
775
879
|
|
|
880
|
+
# Whether s can be a node, app or object name (check_name without raising).
|
|
881
|
+
# @api private
|
|
882
|
+
def self.valid_name?(s)
|
|
883
|
+
return false if s.nil? || s.empty? || s.start_with?("@")
|
|
884
|
+
!(s.include?("/") || s.include?("*") || s.include?("$") || s.include?("?") || s.include?("#"))
|
|
885
|
+
end
|
|
886
|
+
|
|
776
887
|
# @api private
|
|
777
888
|
def self.meta_of(name)
|
|
778
889
|
entry = @objects[name]
|
|
779
890
|
return nil unless entry
|
|
780
891
|
list = []
|
|
781
|
-
entry[1].each { |m, n| list <<
|
|
892
|
+
entry[1].each { |m, n| list << { "name" => m, "arity" => n } }
|
|
782
893
|
{ "methods" => list }
|
|
783
894
|
end
|
|
784
895
|
|
|
785
|
-
|
|
896
|
+
MALFORMED = "malformed call (expected a map {\"method\" => name, \"args\" => [...], \"kwargs\" => {...}})"
|
|
897
|
+
|
|
898
|
+
# Runs a call on an exposed object; returns the packed reply. from is
|
|
899
|
+
# the caller's node ID, verified how it is known (CurrentCall).
|
|
786
900
|
# @api private
|
|
787
|
-
def self.dispatch(obj_name, payload)
|
|
788
|
-
req = Codec.unpack(payload)
|
|
789
|
-
|
|
790
|
-
return error_reply("ArgumentError", "malformed call (expected [method, args, kwargs])")
|
|
791
|
-
end
|
|
901
|
+
def self.dispatch(obj_name, payload, from, verified)
|
|
902
|
+
req = parse_request(Codec.unpack(payload))
|
|
903
|
+
return error_reply("malformed", "ArgumentError", MALFORMED) unless req
|
|
792
904
|
entry = @objects[obj_name]
|
|
793
|
-
return error_reply("NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
905
|
+
return error_reply("no_object", "NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
794
906
|
name = req[0]
|
|
795
907
|
args = req[1]
|
|
796
908
|
kw = req[2]
|
|
797
909
|
arity = entry[1][name]
|
|
798
910
|
if arity.nil?
|
|
799
|
-
return error_reply("
|
|
911
|
+
return error_reply("not_exposed", "NoMethodError",
|
|
912
|
+
"undefined method '#{name}' for #{@node}/#{@app}/#{obj_name} (not exposed)")
|
|
800
913
|
end
|
|
801
914
|
if arity >= 0 && args.size != arity
|
|
802
|
-
return error_reply("ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
915
|
+
return error_reply("arguments", "ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
916
|
+
end
|
|
917
|
+
ckw = entry[3]
|
|
918
|
+
if ckw
|
|
919
|
+
if kw.key?(ckw)
|
|
920
|
+
return error_reply("arguments", "ArgumentError",
|
|
921
|
+
"#{ckw}: is given by Asterism, not by the caller " \
|
|
922
|
+
"(#{@node}/#{@app}/#{obj_name} is exposed with caller: :#{ckw})")
|
|
923
|
+
end
|
|
924
|
+
kw[ckw] = from
|
|
803
925
|
end
|
|
926
|
+
stack = call_stack
|
|
927
|
+
stack << CurrentCall.new(from, verified, obj_name, name)
|
|
804
928
|
begin
|
|
805
929
|
value = invoke(entry[0], name, args, kw)
|
|
806
|
-
Codec.pack(["ok", value])
|
|
807
|
-
rescue EncodeError => e
|
|
808
|
-
error_reply("Asterism::EncodeError", "the return value: #{e.message}")
|
|
809
930
|
rescue => e
|
|
810
|
-
error_reply(e.class.to_s, e.message)
|
|
931
|
+
return error_reply(e.equal?(@nesting_error) ? "nesting" : "raised", e.class.to_s, e.message)
|
|
932
|
+
ensure
|
|
933
|
+
stack.pop
|
|
934
|
+
end
|
|
935
|
+
begin
|
|
936
|
+
Codec.pack({ "value" => value })
|
|
937
|
+
rescue EncodeError => e
|
|
938
|
+
error_reply("unsendable", "Asterism::EncodeError", "the return value: #{e.message}")
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# [method, args, kwargs] of a decoded request, or nil when it is not one:
|
|
943
|
+
# a map with "method" (a non-empty str), "args" (an array, absent: none)
|
|
944
|
+
# and "kwargs" (a map with str keys, absent: none). Other keys are
|
|
945
|
+
# ignored (room for later additions).
|
|
946
|
+
# @api private
|
|
947
|
+
def self.parse_request(req)
|
|
948
|
+
return nil unless req.is_a?(Hash)
|
|
949
|
+
m = req["method"]
|
|
950
|
+
return nil unless m.is_a?(String) && !m.empty?
|
|
951
|
+
args = req.key?("args") ? req["args"] : []
|
|
952
|
+
return nil unless args.is_a?(Array)
|
|
953
|
+
kw = req.key?("kwargs") ? req["kwargs"] : {}
|
|
954
|
+
return nil unless kw.is_a?(Hash)
|
|
955
|
+
keys = kw.keys
|
|
956
|
+
i = 0
|
|
957
|
+
while i < keys.size
|
|
958
|
+
return nil unless keys[i].is_a?(String)
|
|
959
|
+
i += 1
|
|
811
960
|
end
|
|
961
|
+
[m, args, kw]
|
|
812
962
|
end
|
|
813
963
|
|
|
814
964
|
# @api private
|
|
@@ -817,13 +967,14 @@ module Asterism
|
|
|
817
967
|
obj.public_send(name, *args)
|
|
818
968
|
else
|
|
819
969
|
opts = {}
|
|
820
|
-
kw.each { |k, v| opts[k.
|
|
970
|
+
kw.each { |k, v| opts[k.to_sym] = v }
|
|
821
971
|
obj.public_send(name, *args, **opts)
|
|
822
972
|
end
|
|
823
973
|
end
|
|
824
974
|
|
|
975
|
+
# {"error" => {"kind", "class", "message"}}, packed.
|
|
825
976
|
# @api private
|
|
826
|
-
def self.error_reply(klass, message)
|
|
827
|
-
::MessagePack.pack(
|
|
977
|
+
def self.error_reply(kind, klass, message)
|
|
978
|
+
::MessagePack.pack({ "error" => { "kind" => kind, "class" => Codec.text(klass), "message" => Codec.text(message) } })
|
|
828
979
|
end
|
|
829
980
|
end
|
data/mrblib/future.rb
CHANGED
|
@@ -48,16 +48,22 @@ module Asterism
|
|
|
48
48
|
@finished
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# The answer: {"value" => v} gives v, {"error" => {"kind", "class",
|
|
52
|
+
# "message"}} raises RemoteError; anything else is a malformed answer.
|
|
51
53
|
def value
|
|
52
54
|
raw = raw_value
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
raise ::Asterism::Error, "malformed answer from #{@path}" unless raw.is_a?(Hash)
|
|
56
|
+
if raw.key?("error")
|
|
57
|
+
e = raw["error"]
|
|
58
|
+
raise ::Asterism::Error, "malformed answer from #{@path}" unless e.is_a?(Hash)
|
|
59
|
+
kind = e["kind"]
|
|
60
|
+
raise ::Asterism::RemoteError.new(e["class"], e["message"], kind.is_a?(String) ? kind : nil)
|
|
55
61
|
end
|
|
56
|
-
raise ::Asterism::
|
|
57
|
-
raw[
|
|
62
|
+
raise ::Asterism::Error, "malformed answer from #{@path}" unless raw.key?("value")
|
|
63
|
+
raw["value"]
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
# The decoded answer (meta replies are not
|
|
66
|
+
# The decoded answer (meta and profile replies are not value maps).
|
|
61
67
|
def raw_value
|
|
62
68
|
::Asterism.wait_until { done? } unless done?
|
|
63
69
|
if @reply.nil?
|
data/mrblib/profile.rb
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
#
|
|
8
8
|
# Key: asterism/<node>/@profile (a queryable of every application connected
|
|
9
9
|
# as that node). The reply is a MessagePack Hash:
|
|
10
|
+
# "protocol" the wire protocol the node speaks (Asterism::PROTOCOL, 1);
|
|
11
|
+
# a node without it speaks protocol 0
|
|
10
12
|
# "kind" what Ruby runs the node: "family_mruby", "picoruby",
|
|
11
13
|
# "cruby", "console" (others are allowed)
|
|
12
14
|
# "host" where it runs, optional ("atoms3r", "tab5", "sim", ...)
|
|
13
15
|
# "name" a display name, optional
|
|
14
|
-
# "icon" PNG bytes, optional (8 KB and
|
|
16
|
+
# "icon" PNG bytes as a MessagePack bin, optional (8 KB and
|
|
17
|
+
# 256x256 at most)
|
|
15
18
|
# "icon_hash" the icon's summary (Profile.png_info): a reader that has
|
|
16
19
|
# that icon already asks again with the parameter icon=0 and
|
|
17
20
|
# gets the rest without it
|
|
@@ -87,6 +90,13 @@ module Asterism
|
|
|
87
90
|
nil
|
|
88
91
|
end
|
|
89
92
|
|
|
93
|
+
# The bytes as a binary String (String#b), so that they are sent as a
|
|
94
|
+
# MessagePack bin; as they are where String#b is missing.
|
|
95
|
+
# @api private
|
|
96
|
+
def self.binary(s)
|
|
97
|
+
s.respond_to?(:b) ? s.b : s
|
|
98
|
+
end
|
|
99
|
+
|
|
90
100
|
# @api private
|
|
91
101
|
def self.type?(b, pos, c0, c1, c2, c3)
|
|
92
102
|
b.getbyte(pos + 4) == c0 && b.getbyte(pos + 5) == c1 && b.getbyte(pos + 6) == c2 && b.getbyte(pos + 7) == c3
|
|
@@ -107,6 +117,8 @@ module Asterism
|
|
|
107
117
|
kind = h["kind"]
|
|
108
118
|
return nil unless kind.is_a?(String) && kind?(kind)
|
|
109
119
|
out = { "kind" => kind }
|
|
120
|
+
pr = h["protocol"]
|
|
121
|
+
out["protocol"] = pr if pr.is_a?(Integer) && pr >= 0
|
|
110
122
|
host = h["host"]
|
|
111
123
|
out["host"] = host if host.is_a?(String) && !host.empty? && host.bytesize <= HOST_MAX
|
|
112
124
|
name = h["name"]
|
|
@@ -301,7 +313,7 @@ module Asterism
|
|
|
301
313
|
# @api private
|
|
302
314
|
def self.profile_hash(with_icon)
|
|
303
315
|
set = @profile_set || {}
|
|
304
|
-
h = { "kind" => set["kind"] || default_profile_kind }
|
|
316
|
+
h = { "protocol" => PROTOCOL, "kind" => set["kind"] || default_profile_kind }
|
|
305
317
|
h["host"] = set["host"] if set["host"]
|
|
306
318
|
h["name"] = set["name"] if set["name"]
|
|
307
319
|
icon = profile_icon
|
|
@@ -324,7 +336,8 @@ module Asterism
|
|
|
324
336
|
bytes = path ? read_icon_file(path) : nil
|
|
325
337
|
end
|
|
326
338
|
info = bytes ? Profile.png_info(bytes) : nil
|
|
327
|
-
|
|
339
|
+
# A binary String: it goes as a MessagePack bin.
|
|
340
|
+
@profile_icon = info ? [Profile.binary(bytes), info[2]] : false
|
|
328
341
|
end
|
|
329
342
|
@profile_icon ? @profile_icon : nil
|
|
330
343
|
end
|
data/mrblib/proxy.rb
CHANGED
|
@@ -3,9 +3,9 @@ module Asterism
|
|
|
3
3
|
# Any method it does not have itself becomes a call that waits for the
|
|
4
4
|
# answer. respond_to? and remote_methods come from the object's meta.
|
|
5
5
|
#
|
|
6
|
-
# Its own methods (not forwarded): async,
|
|
7
|
-
# asterism_refresh, remote_methods, respond_to?, methods,
|
|
8
|
-
# and those of Object.
|
|
6
|
+
# Its own methods (not forwarded): async, asterism_call, asterism_path,
|
|
7
|
+
# asterism_meta, asterism_refresh, remote_methods, respond_to?, methods,
|
|
8
|
+
# inspect, to_s and those of Object.
|
|
9
9
|
class Proxy
|
|
10
10
|
# Conversions Ruby asks for implicitly (puts, Array#flatten, ...): never
|
|
11
11
|
# forwarded.
|
|
@@ -27,6 +27,15 @@ module Asterism
|
|
|
27
27
|
::Asterism.call(@path, name, args, kw, @timeout_ms)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Calls the remote method by its name (a String or Symbol): for the
|
|
31
|
+
# names the proxy cannot forward (those Object and Kernel define:
|
|
32
|
+
# display, hash, class, fail, ...) and for names that are not Ruby
|
|
33
|
+
# identifiers (from objects written in other languages).
|
|
34
|
+
def asterism_call(name, *args, **kw, &blk)
|
|
35
|
+
raise ::ArgumentError, "a block cannot be sent to #{@path}" if blk
|
|
36
|
+
::Asterism.call(@path, name.to_s, args, kw, @timeout_ms)
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
# In Ruby rather than through respond_to_missing?: Object#respond_to? is
|
|
31
40
|
# C, and calling back into Ruby from it (which then waits for the meta)
|
|
32
41
|
# costs another interpreter entry on the C stack.
|
|
@@ -52,7 +61,8 @@ module Asterism
|
|
|
52
61
|
remote_methods
|
|
53
62
|
end
|
|
54
63
|
|
|
55
|
-
# The meta reply, fetched once:
|
|
64
|
+
# The meta reply, fetched once:
|
|
65
|
+
# {"methods" => [{"name" => name, "arity" => n}, ...]}.
|
|
56
66
|
def asterism_meta
|
|
57
67
|
@meta ||= ::Asterism.meta(@path, @timeout_ms)
|
|
58
68
|
end
|
|
@@ -67,7 +77,9 @@ module Asterism
|
|
|
67
77
|
def remote_names
|
|
68
78
|
list = asterism_meta["methods"]
|
|
69
79
|
return [] unless list.is_a?(Array)
|
|
70
|
-
|
|
80
|
+
names = []
|
|
81
|
+
list.each { |m| names << m["name"].to_s if m.is_a?(Hash) && m["name"].is_a?(String) }
|
|
82
|
+
names
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
# The same object, calls that do not wait: each returns a Future.
|
|
@@ -100,6 +112,12 @@ module Asterism
|
|
|
100
112
|
!::Asterism::Proxy::LOCAL_ONLY.include?(name.to_sym)
|
|
101
113
|
end
|
|
102
114
|
|
|
115
|
+
# Proxy#asterism_call that does not wait: returns a Future.
|
|
116
|
+
def asterism_call(name, *args, **kw, &blk)
|
|
117
|
+
raise ::ArgumentError, "a block cannot be sent to #{@path}" if blk
|
|
118
|
+
::Asterism.call_async(@path, name.to_s, args, kw, @timeout_ms)
|
|
119
|
+
end
|
|
120
|
+
|
|
103
121
|
def inspect
|
|
104
122
|
"#<Asterism::AsyncProxy #{@path}>"
|
|
105
123
|
end
|