asterism 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +140 -32
- data/lib/asterism/cruby/credentials.rb +152 -0
- data/lib/asterism/cruby/objects.rb +19 -6
- data/lib/asterism/cruby/ros.rb +1 -1
- data/lib/asterism/cruby/zenoh.rb +13 -2
- data/lib/asterism/cruby.rb +51 -7
- data/lib/asterism/version.rb +4 -3
- data/lib/asterism.rb +1 -1
- data/mrblib/asterism.rb +248 -62
- data/mrblib/future.rb +11 -5
- data/mrblib/profile.rb +17 -4
- data/mrblib/proxy.rb +23 -5
- metadata +11 -4
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/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.1"
|
|
6
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
|
@@ -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
|
|
@@ -241,12 +289,13 @@ module Asterism
|
|
|
241
289
|
@known_set = {} # the same, as node => true
|
|
242
290
|
@node_list = [node] # what nodes returns (this node first), rebuilt on a change
|
|
243
291
|
@nodes_changed = false # liveliness changed since tell_nodes last compared
|
|
292
|
+
@was_reconnecting = false
|
|
244
293
|
tell_profile # for those who follow the profiles (the reply is the queryable's)
|
|
245
294
|
self
|
|
246
295
|
end
|
|
247
296
|
|
|
248
|
-
# Whether
|
|
249
|
-
# same application). Waits for the answer (a router answers at once), at
|
|
297
|
+
# Whether node/app is alive already (its application token or an object:
|
|
298
|
+
# a second copy of the same application). Waits for the answer (a router answers at once), at
|
|
250
299
|
# most check_ms. @api private
|
|
251
300
|
def self.already_there?(s, node, app, check_ms = 1500)
|
|
252
301
|
get_ms = check_ms > 1000 ? check_ms - 500 : check_ms
|
|
@@ -272,6 +321,20 @@ module Asterism
|
|
|
272
321
|
true
|
|
273
322
|
end
|
|
274
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
|
+
|
|
275
338
|
def self.node_id
|
|
276
339
|
@node
|
|
277
340
|
end
|
|
@@ -301,7 +364,7 @@ module Asterism
|
|
|
301
364
|
@session = nil
|
|
302
365
|
@objects = {}
|
|
303
366
|
@alive = {}
|
|
304
|
-
@
|
|
367
|
+
@apps = {}
|
|
305
368
|
@node_list = nil
|
|
306
369
|
@nodes_changed = true
|
|
307
370
|
@proxies = {}
|
|
@@ -313,7 +376,7 @@ module Asterism
|
|
|
313
376
|
@releasing = nil
|
|
314
377
|
end
|
|
315
378
|
end
|
|
316
|
-
@
|
|
379
|
+
@app_token = nil
|
|
317
380
|
@queryable = nil
|
|
318
381
|
@watch = nil
|
|
319
382
|
@profile_queryable = nil
|
|
@@ -376,7 +439,11 @@ module Asterism
|
|
|
376
439
|
# Makes obj callable from other machines as <node>/<app>/<name>. Only the
|
|
377
440
|
# listed methods can be called. methods: is an Array of names, or a Hash
|
|
378
441
|
# name => number of arguments (checked before calling; -1 for any).
|
|
379
|
-
|
|
442
|
+
# caller: a keyword name (caller: :from): each call passes the calling
|
|
443
|
+
# node's ID (Asterism.current_call.from) to the method as that keyword,
|
|
444
|
+
# and a call that brings the keyword itself is refused (an "arguments"
|
|
445
|
+
# error), so the value is always the one Asterism took from the call.
|
|
446
|
+
def self.expose(name, obj, methods:, caller: nil)
|
|
380
447
|
s = session!
|
|
381
448
|
name = check_name("object", name)
|
|
382
449
|
table = {}
|
|
@@ -389,9 +456,17 @@ module Asterism
|
|
|
389
456
|
table.each_key do |m|
|
|
390
457
|
raise ArgumentError, "#{obj.class} has no public method #{m}" unless obj.respond_to?(m)
|
|
391
458
|
end
|
|
459
|
+
kw = nil
|
|
460
|
+
unless caller.nil?
|
|
461
|
+
unless caller.is_a?(Symbol) || caller.is_a?(String)
|
|
462
|
+
raise ArgumentError, "caller: expects a keyword name (caller: :from), got #{caller.inspect}"
|
|
463
|
+
end
|
|
464
|
+
kw = caller.to_s
|
|
465
|
+
raise ArgumentError, "caller: expects a keyword name (caller: :from), got #{caller.inspect}" if kw.empty?
|
|
466
|
+
end
|
|
392
467
|
unexpose(name)
|
|
393
468
|
token = s.liveliness("#{ROOT}/#{@node}/#{@app}/#{name}")
|
|
394
|
-
@objects[name] = [obj, table, token]
|
|
469
|
+
@objects[name] = [obj, table, token, kw]
|
|
395
470
|
"#{@node}/#{@app}/#{name}"
|
|
396
471
|
rescue Asterism::Zenoh::Error => e
|
|
397
472
|
raise Disconnected, e.message
|
|
@@ -424,7 +499,7 @@ module Asterism
|
|
|
424
499
|
|
|
425
500
|
# ------------------------------------------------------------ nodes coming and going
|
|
426
501
|
|
|
427
|
-
# on_join { |node| }: another node appeared (
|
|
502
|
+
# on_join { |node| }: another node appeared (one of its applications or
|
|
428
503
|
# objects). The nodes there already join on the first polls after
|
|
429
504
|
# connecting. on_leave { |node| }: a node is gone (no token and no object
|
|
430
505
|
# left); when the connection is lost, every node known until then leaves.
|
|
@@ -534,19 +609,43 @@ module Asterism
|
|
|
534
609
|
lost("the connection was lost")
|
|
535
610
|
return false
|
|
536
611
|
end
|
|
612
|
+
back_again(s)
|
|
537
613
|
answer_calls
|
|
538
614
|
answer_profile
|
|
539
615
|
follow_liveliness
|
|
540
616
|
!@session.nil?
|
|
541
617
|
rescue Asterism::Zenoh::Error => e
|
|
542
|
-
|
|
543
|
-
|
|
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?
|
|
544
639
|
end
|
|
545
640
|
|
|
546
641
|
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
547
642
|
# @api private
|
|
548
643
|
def self.wait_until
|
|
549
|
-
|
|
644
|
+
if (@depth || 0) >= MAX_NESTING
|
|
645
|
+
# Kept, so that a method that lets it out is answered as "nesting".
|
|
646
|
+
@nesting_error = Error.new("calls nested too deep (max #{MAX_NESTING})")
|
|
647
|
+
raise @nesting_error
|
|
648
|
+
end
|
|
550
649
|
@depth = (@depth || 0) + 1
|
|
551
650
|
begin
|
|
552
651
|
until yield
|
|
@@ -592,13 +691,13 @@ module Asterism
|
|
|
592
691
|
i += 1
|
|
593
692
|
rest = key[ROOT.length + 1, key.length].to_s
|
|
594
693
|
parts = rest.split("/")
|
|
595
|
-
if parts.size ==
|
|
694
|
+
if parts.size == 2 # an application: asterism/<node>/<app>
|
|
596
695
|
if alive
|
|
597
|
-
@
|
|
696
|
+
@apps[rest] = parts[0]
|
|
598
697
|
else
|
|
599
|
-
@
|
|
698
|
+
@apps.delete(rest)
|
|
600
699
|
end
|
|
601
|
-
elsif parts.size == 3
|
|
700
|
+
elsif parts.size == 3 # an object: asterism/<node>/<app>/<object>
|
|
602
701
|
if alive
|
|
603
702
|
@alive[rest] = parts[0]
|
|
604
703
|
else
|
|
@@ -622,7 +721,7 @@ module Asterism
|
|
|
622
721
|
return list if list
|
|
623
722
|
list = [@node]
|
|
624
723
|
seen = { @node => true }
|
|
625
|
-
names = @
|
|
724
|
+
names = @apps.values
|
|
626
725
|
i = 0
|
|
627
726
|
while i < names.size
|
|
628
727
|
n = names[i]
|
|
@@ -689,39 +788,51 @@ module Asterism
|
|
|
689
788
|
# @api private
|
|
690
789
|
def self.call_async(path, method, args, kwargs, timeout_ms)
|
|
691
790
|
parts = split_path(path)
|
|
692
|
-
payload = Codec.pack(
|
|
791
|
+
payload = Codec.pack(request(method, args, kwargs))
|
|
693
792
|
if parts[0] == @node && parts[1] == @app && @session
|
|
694
793
|
# This application's own object: no network (a session does not see
|
|
695
794
|
# its own queryable), same encoding and checks as a remote call.
|
|
696
|
-
return Future.answered(path, method, dispatch(parts[2], payload))
|
|
795
|
+
return Future.answered(path, method, dispatch(parts[2], payload, @node, :local))
|
|
697
796
|
end
|
|
698
797
|
begin
|
|
699
|
-
|
|
798
|
+
s = session!
|
|
799
|
+
# The caller's node ID is the key's last chunk (docs/protocol.md 4.1).
|
|
800
|
+
g = s.get("#{ROOT}/#{path}/call/#{@node}", timeout_ms: timeout_ms, payload: payload)
|
|
700
801
|
rescue Asterism::Zenoh::Error => e
|
|
701
|
-
|
|
802
|
+
lost_if_closed(e.message)
|
|
702
803
|
raise Disconnected, e.message
|
|
703
804
|
end
|
|
704
805
|
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
705
806
|
end
|
|
706
807
|
|
|
808
|
+
# The request of a call: a map with the method's name, and the arguments
|
|
809
|
+
# and keyword arguments when there are any (absent means none).
|
|
810
|
+
# @api private
|
|
811
|
+
def self.request(method, args, kwargs)
|
|
812
|
+
req = { "method" => method.to_s }
|
|
813
|
+
req["args"] = args unless args.nil? || args.empty?
|
|
814
|
+
req["kwargs"] = kwargs unless kwargs.nil? || kwargs.empty?
|
|
815
|
+
req
|
|
816
|
+
end
|
|
817
|
+
|
|
707
818
|
# @api private
|
|
708
819
|
def self.call(path, method, args, kwargs, timeout_ms)
|
|
709
820
|
call_async(path, method, args, kwargs, timeout_ms).value
|
|
710
821
|
end
|
|
711
822
|
|
|
712
|
-
# The meta reply of path: {"methods" => [
|
|
823
|
+
# The meta reply of path: {"methods" => [{"name" => n, "arity" => a}, ...]}.
|
|
713
824
|
# @api private
|
|
714
825
|
def self.meta(path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
715
826
|
parts = split_path(path)
|
|
716
827
|
if parts[0] == @node && parts[1] == @app && @session
|
|
717
828
|
m = meta_of(parts[2])
|
|
718
|
-
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}") unless m
|
|
829
|
+
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}", "no_object") unless m
|
|
719
830
|
return m
|
|
720
831
|
end
|
|
721
832
|
begin
|
|
722
833
|
g = session!.get("#{ROOT}/#{path}/meta", timeout_ms: timeout_ms)
|
|
723
834
|
rescue Asterism::Zenoh::Error => e
|
|
724
|
-
|
|
835
|
+
lost_if_closed(e.message)
|
|
725
836
|
raise Disconnected, e.message
|
|
726
837
|
end
|
|
727
838
|
f = Future.new(path, "meta", g, now_ms + timeout_ms, timeout_ms)
|
|
@@ -730,6 +841,21 @@ module Asterism
|
|
|
730
841
|
|
|
731
842
|
# ------------------------------------------------------------ answering
|
|
732
843
|
|
|
844
|
+
# The call being answered now (an Asterism::CurrentCall: who called,
|
|
845
|
+
# which object and method), while an exposed method runs for a call; nil
|
|
846
|
+
# outside one. Calls answered while that method waits for its own call
|
|
847
|
+
# are on top of it until they end.
|
|
848
|
+
def self.current_call
|
|
849
|
+
stack = call_stack
|
|
850
|
+
stack.empty? ? nil : stack[stack.size - 1]
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# The calls being answered, innermost last (CRuby: per thread).
|
|
854
|
+
# @api private
|
|
855
|
+
def self.call_stack
|
|
856
|
+
@call_stack ||= []
|
|
857
|
+
end
|
|
858
|
+
|
|
733
859
|
# @api private
|
|
734
860
|
def self.answer_calls
|
|
735
861
|
qa = @queryable
|
|
@@ -753,62 +879,121 @@ module Asterism
|
|
|
753
879
|
end
|
|
754
880
|
end
|
|
755
881
|
|
|
756
|
-
# asterism/<node>/<app>/<object
|
|
757
|
-
#
|
|
882
|
+
# asterism/<node>/<app>/<object>/call/<caller> and
|
|
883
|
+
# asterism/<node>/<app>/<object>/meta; the node and app may come as
|
|
884
|
+
# wildcards (the query matched this application's queryable).
|
|
758
885
|
# @api private
|
|
759
886
|
def self.answer(q)
|
|
760
887
|
parts = q.key.split("/")
|
|
761
|
-
|
|
888
|
+
n = parts.size
|
|
889
|
+
return unless n == 5 || n == 6
|
|
762
890
|
obj = parts[3]
|
|
763
891
|
kind = parts[4]
|
|
764
892
|
prefix = "#{ROOT}/#{@node}/#{@app}/"
|
|
765
893
|
if kind == "meta"
|
|
894
|
+
return unless n == 5
|
|
766
895
|
names = obj == "*" ? @objects.keys : [obj]
|
|
767
|
-
names.each do |
|
|
768
|
-
m = meta_of(
|
|
769
|
-
q.reply("#{prefix}#{
|
|
896
|
+
names.each do |nm|
|
|
897
|
+
m = meta_of(nm)
|
|
898
|
+
q.reply("#{prefix}#{nm}/meta", Codec.pack(m)) if m
|
|
899
|
+
end
|
|
900
|
+
elsif kind == "call" && valid_name?(obj)
|
|
901
|
+
if n == 5
|
|
902
|
+
# A protocol 0 call (no caller chunk): refused, so that its sender
|
|
903
|
+
# learns why at once instead of waiting.
|
|
904
|
+
q.reply("#{prefix}#{obj}/call",
|
|
905
|
+
error_reply("protocol", "Asterism::Error",
|
|
906
|
+
"#{@node}/#{@app} speaks Asterism protocol #{PROTOCOL}: call " \
|
|
907
|
+
"asterism/<node>/<app>/<object>/call/<caller> with a map " \
|
|
908
|
+
"{\"method\", \"args\", \"kwargs\"} (asterism 0.7.0 and later)"))
|
|
909
|
+
elsif valid_name?(parts[5])
|
|
910
|
+
q.reply("#{prefix}#{obj}/call/#{parts[5]}", dispatch(obj, q.payload, parts[5], :claimed))
|
|
770
911
|
end
|
|
771
|
-
elsif kind == "call" && obj != "*"
|
|
772
|
-
q.reply("#{prefix}#{obj}/call", dispatch(obj, q.payload))
|
|
773
912
|
end
|
|
774
913
|
end
|
|
775
914
|
|
|
915
|
+
# Whether s can be a node, app or object name (check_name without raising).
|
|
916
|
+
# @api private
|
|
917
|
+
def self.valid_name?(s)
|
|
918
|
+
return false if s.nil? || s.empty? || s.start_with?("@")
|
|
919
|
+
!(s.include?("/") || s.include?("*") || s.include?("$") || s.include?("?") || s.include?("#"))
|
|
920
|
+
end
|
|
921
|
+
|
|
776
922
|
# @api private
|
|
777
923
|
def self.meta_of(name)
|
|
778
924
|
entry = @objects[name]
|
|
779
925
|
return nil unless entry
|
|
780
926
|
list = []
|
|
781
|
-
entry[1].each { |m, n| list <<
|
|
927
|
+
entry[1].each { |m, n| list << { "name" => m, "arity" => n } }
|
|
782
928
|
{ "methods" => list }
|
|
783
929
|
end
|
|
784
930
|
|
|
785
|
-
|
|
931
|
+
MALFORMED = "malformed call (expected a map {\"method\" => name, \"args\" => [...], \"kwargs\" => {...}})"
|
|
932
|
+
|
|
933
|
+
# Runs a call on an exposed object; returns the packed reply. from is
|
|
934
|
+
# the caller's node ID, verified how it is known (CurrentCall).
|
|
786
935
|
# @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
|
|
936
|
+
def self.dispatch(obj_name, payload, from, verified)
|
|
937
|
+
req = parse_request(Codec.unpack(payload))
|
|
938
|
+
return error_reply("malformed", "ArgumentError", MALFORMED) unless req
|
|
792
939
|
entry = @objects[obj_name]
|
|
793
|
-
return error_reply("NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
940
|
+
return error_reply("no_object", "NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
794
941
|
name = req[0]
|
|
795
942
|
args = req[1]
|
|
796
943
|
kw = req[2]
|
|
797
944
|
arity = entry[1][name]
|
|
798
945
|
if arity.nil?
|
|
799
|
-
return error_reply("
|
|
946
|
+
return error_reply("not_exposed", "NoMethodError",
|
|
947
|
+
"undefined method '#{name}' for #{@node}/#{@app}/#{obj_name} (not exposed)")
|
|
800
948
|
end
|
|
801
949
|
if arity >= 0 && args.size != arity
|
|
802
|
-
return error_reply("ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
950
|
+
return error_reply("arguments", "ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
951
|
+
end
|
|
952
|
+
ckw = entry[3]
|
|
953
|
+
if ckw
|
|
954
|
+
if kw.key?(ckw)
|
|
955
|
+
return error_reply("arguments", "ArgumentError",
|
|
956
|
+
"#{ckw}: is given by Asterism, not by the caller " \
|
|
957
|
+
"(#{@node}/#{@app}/#{obj_name} is exposed with caller: :#{ckw})")
|
|
958
|
+
end
|
|
959
|
+
kw[ckw] = from
|
|
803
960
|
end
|
|
961
|
+
stack = call_stack
|
|
962
|
+
stack << CurrentCall.new(from, verified, obj_name, name)
|
|
804
963
|
begin
|
|
805
964
|
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
965
|
rescue => e
|
|
810
|
-
error_reply(e.class.to_s, e.message)
|
|
966
|
+
return error_reply(e.equal?(@nesting_error) ? "nesting" : "raised", e.class.to_s, e.message)
|
|
967
|
+
ensure
|
|
968
|
+
stack.pop
|
|
969
|
+
end
|
|
970
|
+
begin
|
|
971
|
+
Codec.pack({ "value" => value })
|
|
972
|
+
rescue EncodeError => e
|
|
973
|
+
error_reply("unsendable", "Asterism::EncodeError", "the return value: #{e.message}")
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
# [method, args, kwargs] of a decoded request, or nil when it is not one:
|
|
978
|
+
# a map with "method" (a non-empty str), "args" (an array, absent: none)
|
|
979
|
+
# and "kwargs" (a map with str keys, absent: none). Other keys are
|
|
980
|
+
# ignored (room for later additions).
|
|
981
|
+
# @api private
|
|
982
|
+
def self.parse_request(req)
|
|
983
|
+
return nil unless req.is_a?(Hash)
|
|
984
|
+
m = req["method"]
|
|
985
|
+
return nil unless m.is_a?(String) && !m.empty?
|
|
986
|
+
args = req.key?("args") ? req["args"] : []
|
|
987
|
+
return nil unless args.is_a?(Array)
|
|
988
|
+
kw = req.key?("kwargs") ? req["kwargs"] : {}
|
|
989
|
+
return nil unless kw.is_a?(Hash)
|
|
990
|
+
keys = kw.keys
|
|
991
|
+
i = 0
|
|
992
|
+
while i < keys.size
|
|
993
|
+
return nil unless keys[i].is_a?(String)
|
|
994
|
+
i += 1
|
|
811
995
|
end
|
|
996
|
+
[m, args, kw]
|
|
812
997
|
end
|
|
813
998
|
|
|
814
999
|
# @api private
|
|
@@ -817,13 +1002,14 @@ module Asterism
|
|
|
817
1002
|
obj.public_send(name, *args)
|
|
818
1003
|
else
|
|
819
1004
|
opts = {}
|
|
820
|
-
kw.each { |k, v| opts[k.
|
|
1005
|
+
kw.each { |k, v| opts[k.to_sym] = v }
|
|
821
1006
|
obj.public_send(name, *args, **opts)
|
|
822
1007
|
end
|
|
823
1008
|
end
|
|
824
1009
|
|
|
1010
|
+
# {"error" => {"kind", "class", "message"}}, packed.
|
|
825
1011
|
# @api private
|
|
826
|
-
def self.error_reply(klass, message)
|
|
827
|
-
::MessagePack.pack(
|
|
1012
|
+
def self.error_reply(kind, klass, message)
|
|
1013
|
+
::MessagePack.pack({ "error" => { "kind" => kind, "class" => Codec.text(klass), "message" => Codec.text(message) } })
|
|
828
1014
|
end
|
|
829
1015
|
end
|