asterism 0.5.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 +112 -0
- data/README.md +163 -31
- data/lib/asterism/cruby/objects.rb +23 -4
- data/lib/asterism/cruby/ros.rb +16 -4
- data/lib/asterism/cruby/zenoh.rb +32 -6
- data/lib/asterism/cruby.rb +51 -7
- data/lib/asterism/version.rb +4 -3
- data/mrblib/asterism.rb +340 -75
- data/mrblib/future.rb +11 -5
- data/mrblib/profile.rb +17 -4
- data/mrblib/proxy.rb +23 -5
- metadata +1 -1
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)
|
|
@@ -227,22 +275,26 @@ module Asterism
|
|
|
227
275
|
s.close
|
|
228
276
|
raise Disconnected, e.message
|
|
229
277
|
end
|
|
278
|
+
s.extend(SharedSession) # Asterism.session: the application's close does not close it
|
|
230
279
|
@session = s
|
|
231
280
|
@node = node
|
|
232
281
|
@app = app
|
|
233
|
-
@objects = {} # name => [object, {method name => arity}, token]
|
|
234
|
-
@alive = {} # "node/app/object" =>
|
|
235
|
-
@
|
|
282
|
+
@objects = {} # name => [object, {method name => arity}, token, caller keyword or nil]
|
|
283
|
+
@alive = {} # "node/app/object" => node (from liveliness)
|
|
284
|
+
@apps = {} # "node/app" => node (from liveliness)
|
|
236
285
|
@proxies = {}
|
|
237
286
|
@depth = 0
|
|
238
287
|
@lost = nil
|
|
239
288
|
@known = [] # the other nodes told to on_join so far
|
|
289
|
+
@known_set = {} # the same, as node => true
|
|
290
|
+
@node_list = [node] # what nodes returns (this node first), rebuilt on a change
|
|
291
|
+
@nodes_changed = false # liveliness changed since tell_nodes last compared
|
|
240
292
|
tell_profile # for those who follow the profiles (the reply is the queryable's)
|
|
241
293
|
self
|
|
242
294
|
end
|
|
243
295
|
|
|
244
|
-
# Whether
|
|
245
|
-
# 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
|
|
246
298
|
# most check_ms. @api private
|
|
247
299
|
def self.already_there?(s, node, app, check_ms = 1500)
|
|
248
300
|
get_ms = check_ms > 1000 ? check_ms - 500 : check_ms
|
|
@@ -297,21 +349,68 @@ module Asterism
|
|
|
297
349
|
@session = nil
|
|
298
350
|
@objects = {}
|
|
299
351
|
@alive = {}
|
|
300
|
-
@
|
|
352
|
+
@apps = {}
|
|
353
|
+
@node_list = nil
|
|
354
|
+
@nodes_changed = true
|
|
301
355
|
@proxies = {}
|
|
302
|
-
|
|
303
|
-
|
|
356
|
+
if s
|
|
357
|
+
@releasing = s
|
|
358
|
+
begin
|
|
359
|
+
s.close
|
|
360
|
+
ensure
|
|
361
|
+
@releasing = nil
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
@app_token = nil
|
|
304
365
|
@queryable = nil
|
|
305
366
|
@watch = nil
|
|
306
367
|
@profile_queryable = nil
|
|
307
368
|
end
|
|
308
369
|
|
|
309
|
-
#
|
|
370
|
+
# The Zenoh session of the connection (an Asterism::Zenoh::Session), for
|
|
371
|
+
# what the object layer does not do: put and subscribe, a ROS 2 node
|
|
372
|
+
# (Asterism::ROS::Node.new(Asterism.session, ...)), anything of the
|
|
373
|
+
# Zenoh API. One connection for everything: on a router that lets each
|
|
374
|
+
# pass in once, a second session would not get in, and a ROS 2 node on
|
|
375
|
+
# this session is part of this node (the same Zenoh ID).
|
|
376
|
+
#
|
|
377
|
+
# The session stays Asterism's: its close does nothing (it warns once);
|
|
378
|
+
# Asterism.close closes it, and with it everything declared on it (the
|
|
379
|
+
# application's subscribers, publishers, ROS 2 nodes). After a lost
|
|
380
|
+
# connection and a new Asterism.connect, take Asterism.session again and
|
|
381
|
+
# declare those again. Polling it (session.poll, node.poll) is fine:
|
|
382
|
+
# Asterism.poll polls it too, each entity keeps what came for it.
|
|
383
|
+
# Raises Disconnected when not connected (as the calls do).
|
|
384
|
+
def self.session
|
|
385
|
+
raise Disconnected, (@lost || "not connected") unless connected?
|
|
386
|
+
@session
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# @api private (the name before 0.6.0; Asterism.session is the same)
|
|
310
390
|
def self.session!
|
|
311
391
|
raise Disconnected, (@lost || "not connected") unless @session
|
|
312
392
|
@session
|
|
313
393
|
end
|
|
314
394
|
|
|
395
|
+
# Whether Asterism itself is closing s (SharedSession#close).
|
|
396
|
+
# @api private
|
|
397
|
+
def self.releasing?(s)
|
|
398
|
+
!@releasing.nil? && @releasing.equal?(s)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Extended onto the session of Asterism.connect: the session belongs to
|
|
402
|
+
# Asterism, so a close from anyone else (the application, a library that
|
|
403
|
+
# was handed Asterism.session) does nothing. Asterism.close and a lost
|
|
404
|
+
# connection close it.
|
|
405
|
+
module SharedSession
|
|
406
|
+
def close
|
|
407
|
+
return super if ::Asterism.releasing?(self)
|
|
408
|
+
::Asterism.warn_once(self, "asterism: Asterism.session belongs to Asterism; its close is ignored " \
|
|
409
|
+
"(Asterism.close closes it)")
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
315
414
|
# The connection went away: closed, but the next Asterism.poll still tells
|
|
316
415
|
# on_leave about every node known until then.
|
|
317
416
|
# @api private
|
|
@@ -325,7 +424,11 @@ module Asterism
|
|
|
325
424
|
# Makes obj callable from other machines as <node>/<app>/<name>. Only the
|
|
326
425
|
# listed methods can be called. methods: is an Array of names, or a Hash
|
|
327
426
|
# name => number of arguments (checked before calling; -1 for any).
|
|
328
|
-
|
|
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)
|
|
329
432
|
s = session!
|
|
330
433
|
name = check_name("object", name)
|
|
331
434
|
table = {}
|
|
@@ -338,9 +441,17 @@ module Asterism
|
|
|
338
441
|
table.each_key do |m|
|
|
339
442
|
raise ArgumentError, "#{obj.class} has no public method #{m}" unless obj.respond_to?(m)
|
|
340
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
|
|
341
452
|
unexpose(name)
|
|
342
453
|
token = s.liveliness("#{ROOT}/#{@node}/#{@app}/#{name}")
|
|
343
|
-
@objects[name] = [obj, table, token]
|
|
454
|
+
@objects[name] = [obj, table, token, kw]
|
|
344
455
|
"#{@node}/#{@app}/#{name}"
|
|
345
456
|
rescue Asterism::Zenoh::Error => e
|
|
346
457
|
raise Disconnected, e.message
|
|
@@ -361,6 +472,10 @@ module Asterism
|
|
|
361
472
|
# Call from the application's update loop. Answers the calls that came
|
|
362
473
|
# in, follows who is alive and calls the on_join / on_leave blocks. false
|
|
363
474
|
# once the connection is closed.
|
|
475
|
+
#
|
|
476
|
+
# Cheap when nothing came: each queue is looked at by its count first,
|
|
477
|
+
# and the list of nodes (and the comparison behind on_join / on_leave)
|
|
478
|
+
# is made again only after a liveliness change.
|
|
364
479
|
def self.poll
|
|
365
480
|
ok = @session ? pump : false
|
|
366
481
|
tell_nodes
|
|
@@ -369,7 +484,7 @@ module Asterism
|
|
|
369
484
|
|
|
370
485
|
# ------------------------------------------------------------ nodes coming and going
|
|
371
486
|
|
|
372
|
-
# on_join { |node| }: another node appeared (
|
|
487
|
+
# on_join { |node| }: another node appeared (one of its applications or
|
|
373
488
|
# objects). The nodes there already join on the first polls after
|
|
374
489
|
# connecting. on_leave { |node| }: a node is gone (no token and no object
|
|
375
490
|
# left); when the connection is lost, every node known until then leaves.
|
|
@@ -408,14 +523,43 @@ module Asterism
|
|
|
408
523
|
def self.tell_nodes
|
|
409
524
|
known = @known
|
|
410
525
|
return if known.nil?
|
|
411
|
-
|
|
412
|
-
|
|
526
|
+
# Nothing came on the liveliness watch since the last comparison (and
|
|
527
|
+
# the connection is neither new nor lost): nothing can have changed.
|
|
528
|
+
return unless @nodes_changed
|
|
529
|
+
@nodes_changed = false
|
|
530
|
+
now = []
|
|
531
|
+
now_set = {}
|
|
532
|
+
if @session
|
|
533
|
+
list = node_list
|
|
534
|
+
i = 1 # list[0] is this node
|
|
535
|
+
while i < list.size
|
|
536
|
+
n = list[i]
|
|
537
|
+
now << n
|
|
538
|
+
now_set[n] = true
|
|
539
|
+
i += 1
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
known_set = @known_set || {}
|
|
413
543
|
joined = []
|
|
414
544
|
left = []
|
|
415
|
-
|
|
416
|
-
|
|
545
|
+
i = 0
|
|
546
|
+
while i < now.size
|
|
547
|
+
joined << now[i] unless known_set[now[i]]
|
|
548
|
+
i += 1
|
|
549
|
+
end
|
|
550
|
+
i = 0
|
|
551
|
+
while i < known.size
|
|
552
|
+
left << known[i] unless now_set[known[i]]
|
|
553
|
+
i += 1
|
|
554
|
+
end
|
|
417
555
|
# After a loss, everything has been told: nothing more to compare.
|
|
418
|
-
|
|
556
|
+
if @session
|
|
557
|
+
@known = now
|
|
558
|
+
@known_set = now_set
|
|
559
|
+
else
|
|
560
|
+
@known = nil
|
|
561
|
+
@known_set = nil
|
|
562
|
+
end
|
|
419
563
|
tell_each("on_join", @on_join, joined)
|
|
420
564
|
tell_each("on_leave", @on_leave, left)
|
|
421
565
|
end
|
|
@@ -462,7 +606,11 @@ module Asterism
|
|
|
462
606
|
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
463
607
|
# @api private
|
|
464
608
|
def self.wait_until
|
|
465
|
-
|
|
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
|
|
466
614
|
@depth = (@depth || 0) + 1
|
|
467
615
|
begin
|
|
468
616
|
until yield
|
|
@@ -493,9 +641,10 @@ module Asterism
|
|
|
493
641
|
# @api private
|
|
494
642
|
def self.follow_liveliness
|
|
495
643
|
w = @watch
|
|
496
|
-
return unless w
|
|
644
|
+
return unless w && w.pending > 0
|
|
497
645
|
# Array forms instead of blocks called from C (see Future#collect).
|
|
498
646
|
events = w.each_pending
|
|
647
|
+
@nodes_changed = true if events.size > 0
|
|
499
648
|
if events.size > 0 && w.dropped > 0
|
|
500
649
|
warn_once(w, "asterism: #{w.dropped} liveliness changes dropped (more than #{watch_depth} at once); " \
|
|
501
650
|
"Asterism.each and Asterism.nodes may miss some objects")
|
|
@@ -507,32 +656,57 @@ module Asterism
|
|
|
507
656
|
i += 1
|
|
508
657
|
rest = key[ROOT.length + 1, key.length].to_s
|
|
509
658
|
parts = rest.split("/")
|
|
510
|
-
if parts.size ==
|
|
659
|
+
if parts.size == 2 # an application: asterism/<node>/<app>
|
|
511
660
|
if alive
|
|
512
|
-
@
|
|
661
|
+
@apps[rest] = parts[0]
|
|
513
662
|
else
|
|
514
|
-
@
|
|
663
|
+
@apps.delete(rest)
|
|
515
664
|
end
|
|
516
|
-
elsif parts.size == 3
|
|
665
|
+
elsif parts.size == 3 # an object: asterism/<node>/<app>/<object>
|
|
517
666
|
if alive
|
|
518
|
-
@alive[rest] =
|
|
667
|
+
@alive[rest] = parts[0]
|
|
519
668
|
else
|
|
520
669
|
@alive.delete(rest)
|
|
521
670
|
end
|
|
522
671
|
end
|
|
523
672
|
end
|
|
673
|
+
@node_list = nil
|
|
524
674
|
end
|
|
525
675
|
|
|
526
676
|
# The node IDs alive now, this one first.
|
|
527
677
|
def self.nodes
|
|
528
678
|
return [] unless @session
|
|
679
|
+
node_list.dup
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# The list behind nodes, made again only after a liveliness change (the
|
|
683
|
+
# caller must not change it). @api private
|
|
684
|
+
def self.node_list
|
|
685
|
+
list = @node_list
|
|
686
|
+
return list if list
|
|
529
687
|
list = [@node]
|
|
530
|
-
|
|
531
|
-
@
|
|
532
|
-
|
|
533
|
-
|
|
688
|
+
seen = { @node => true }
|
|
689
|
+
names = @apps.values
|
|
690
|
+
i = 0
|
|
691
|
+
while i < names.size
|
|
692
|
+
n = names[i]
|
|
693
|
+
unless seen[n]
|
|
694
|
+
seen[n] = true
|
|
695
|
+
list << n
|
|
696
|
+
end
|
|
697
|
+
i += 1
|
|
698
|
+
end
|
|
699
|
+
owners = @alive.values
|
|
700
|
+
i = 0
|
|
701
|
+
while i < owners.size
|
|
702
|
+
n = owners[i]
|
|
703
|
+
unless seen[n]
|
|
704
|
+
seen[n] = true
|
|
705
|
+
list << n
|
|
706
|
+
end
|
|
707
|
+
i += 1
|
|
534
708
|
end
|
|
535
|
-
list
|
|
709
|
+
@node_list = list
|
|
536
710
|
end
|
|
537
711
|
|
|
538
712
|
# The exposed objects alive now whose <node>/<app>/<object> matches the
|
|
@@ -543,11 +717,15 @@ module Asterism
|
|
|
543
717
|
def self.each(pattern = "**")
|
|
544
718
|
paths = []
|
|
545
719
|
if @session
|
|
720
|
+
seen = {}
|
|
546
721
|
@objects.each_key do |name|
|
|
547
722
|
path = "#{@node}/#{@app}/#{name}"
|
|
548
|
-
|
|
723
|
+
if glob?(pattern, path)
|
|
724
|
+
paths << path
|
|
725
|
+
seen[path] = true
|
|
726
|
+
end
|
|
549
727
|
end
|
|
550
|
-
@alive.each_key { |path| paths << path if glob?(pattern, path)
|
|
728
|
+
@alive.each_key { |path| paths << path if !seen[path] && glob?(pattern, path) }
|
|
551
729
|
end
|
|
552
730
|
proxies = paths.map { |path| self[path] }
|
|
553
731
|
return proxies unless block_given?
|
|
@@ -575,14 +753,16 @@ module Asterism
|
|
|
575
753
|
# @api private
|
|
576
754
|
def self.call_async(path, method, args, kwargs, timeout_ms)
|
|
577
755
|
parts = split_path(path)
|
|
578
|
-
payload = Codec.pack(
|
|
756
|
+
payload = Codec.pack(request(method, args, kwargs))
|
|
579
757
|
if parts[0] == @node && parts[1] == @app && @session
|
|
580
758
|
# This application's own object: no network (a session does not see
|
|
581
759
|
# its own queryable), same encoding and checks as a remote call.
|
|
582
|
-
return Future.answered(path, method, dispatch(parts[2], payload))
|
|
760
|
+
return Future.answered(path, method, dispatch(parts[2], payload, @node, :local))
|
|
583
761
|
end
|
|
584
762
|
begin
|
|
585
|
-
|
|
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)
|
|
586
766
|
rescue Asterism::Zenoh::Error => e
|
|
587
767
|
lost(e.message)
|
|
588
768
|
raise Disconnected, e.message
|
|
@@ -590,18 +770,28 @@ module Asterism
|
|
|
590
770
|
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
591
771
|
end
|
|
592
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
|
+
|
|
593
783
|
# @api private
|
|
594
784
|
def self.call(path, method, args, kwargs, timeout_ms)
|
|
595
785
|
call_async(path, method, args, kwargs, timeout_ms).value
|
|
596
786
|
end
|
|
597
787
|
|
|
598
|
-
# The meta reply of path: {"methods" => [
|
|
788
|
+
# The meta reply of path: {"methods" => [{"name" => n, "arity" => a}, ...]}.
|
|
599
789
|
# @api private
|
|
600
790
|
def self.meta(path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
601
791
|
parts = split_path(path)
|
|
602
792
|
if parts[0] == @node && parts[1] == @app && @session
|
|
603
793
|
m = meta_of(parts[2])
|
|
604
|
-
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
|
|
605
795
|
return m
|
|
606
796
|
end
|
|
607
797
|
begin
|
|
@@ -616,10 +806,25 @@ module Asterism
|
|
|
616
806
|
|
|
617
807
|
# ------------------------------------------------------------ answering
|
|
618
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
|
+
|
|
619
824
|
# @api private
|
|
620
825
|
def self.answer_calls
|
|
621
826
|
qa = @queryable
|
|
622
|
-
return unless qa
|
|
827
|
+
return unless qa && qa.pending > 0
|
|
623
828
|
# Without a block, so that a handler that waits (and so polls) does not
|
|
624
829
|
# re-enter the queryable's iteration.
|
|
625
830
|
queries = qa.each_pending
|
|
@@ -639,62 +844,121 @@ module Asterism
|
|
|
639
844
|
end
|
|
640
845
|
end
|
|
641
846
|
|
|
642
|
-
# asterism/<node>/<app>/<object
|
|
643
|
-
#
|
|
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).
|
|
644
850
|
# @api private
|
|
645
851
|
def self.answer(q)
|
|
646
852
|
parts = q.key.split("/")
|
|
647
|
-
|
|
853
|
+
n = parts.size
|
|
854
|
+
return unless n == 5 || n == 6
|
|
648
855
|
obj = parts[3]
|
|
649
856
|
kind = parts[4]
|
|
650
857
|
prefix = "#{ROOT}/#{@node}/#{@app}/"
|
|
651
858
|
if kind == "meta"
|
|
859
|
+
return unless n == 5
|
|
652
860
|
names = obj == "*" ? @objects.keys : [obj]
|
|
653
|
-
names.each do |
|
|
654
|
-
m = meta_of(
|
|
655
|
-
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))
|
|
656
876
|
end
|
|
657
|
-
elsif kind == "call" && obj != "*"
|
|
658
|
-
q.reply("#{prefix}#{obj}/call", dispatch(obj, q.payload))
|
|
659
877
|
end
|
|
660
878
|
end
|
|
661
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
|
+
|
|
662
887
|
# @api private
|
|
663
888
|
def self.meta_of(name)
|
|
664
889
|
entry = @objects[name]
|
|
665
890
|
return nil unless entry
|
|
666
891
|
list = []
|
|
667
|
-
entry[1].each { |m, n| list <<
|
|
892
|
+
entry[1].each { |m, n| list << { "name" => m, "arity" => n } }
|
|
668
893
|
{ "methods" => list }
|
|
669
894
|
end
|
|
670
895
|
|
|
671
|
-
|
|
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).
|
|
672
900
|
# @api private
|
|
673
|
-
def self.dispatch(obj_name, payload)
|
|
674
|
-
req = Codec.unpack(payload)
|
|
675
|
-
|
|
676
|
-
return error_reply("ArgumentError", "malformed call (expected [method, args, kwargs])")
|
|
677
|
-
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
|
|
678
904
|
entry = @objects[obj_name]
|
|
679
|
-
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
|
|
680
906
|
name = req[0]
|
|
681
907
|
args = req[1]
|
|
682
908
|
kw = req[2]
|
|
683
909
|
arity = entry[1][name]
|
|
684
910
|
if arity.nil?
|
|
685
|
-
return error_reply("
|
|
911
|
+
return error_reply("not_exposed", "NoMethodError",
|
|
912
|
+
"undefined method '#{name}' for #{@node}/#{@app}/#{obj_name} (not exposed)")
|
|
686
913
|
end
|
|
687
914
|
if arity >= 0 && args.size != arity
|
|
688
|
-
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
|
|
689
925
|
end
|
|
926
|
+
stack = call_stack
|
|
927
|
+
stack << CurrentCall.new(from, verified, obj_name, name)
|
|
690
928
|
begin
|
|
691
929
|
value = invoke(entry[0], name, args, kw)
|
|
692
|
-
Codec.pack(["ok", value])
|
|
693
|
-
rescue EncodeError => e
|
|
694
|
-
error_reply("Asterism::EncodeError", "the return value: #{e.message}")
|
|
695
930
|
rescue => e
|
|
696
|
-
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
|
|
697
960
|
end
|
|
961
|
+
[m, args, kw]
|
|
698
962
|
end
|
|
699
963
|
|
|
700
964
|
# @api private
|
|
@@ -703,13 +967,14 @@ module Asterism
|
|
|
703
967
|
obj.public_send(name, *args)
|
|
704
968
|
else
|
|
705
969
|
opts = {}
|
|
706
|
-
kw.each { |k, v| opts[k.
|
|
970
|
+
kw.each { |k, v| opts[k.to_sym] = v }
|
|
707
971
|
obj.public_send(name, *args, **opts)
|
|
708
972
|
end
|
|
709
973
|
end
|
|
710
974
|
|
|
975
|
+
# {"error" => {"kind", "class", "message"}}, packed.
|
|
711
976
|
# @api private
|
|
712
|
-
def self.error_reply(klass, message)
|
|
713
|
-
::MessagePack.pack(
|
|
977
|
+
def self.error_reply(kind, klass, message)
|
|
978
|
+
::MessagePack.pack({ "error" => { "kind" => kind, "class" => Codec.text(klass), "message" => Codec.text(message) } })
|
|
714
979
|
end
|
|
715
980
|
end
|