asterism 0.1.0 → 0.2.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/README.md +189 -13
- data/lib/asterism/cruby/objects.rb +224 -0
- data/lib/asterism/cruby/ros.rb +305 -0
- data/lib/asterism/cruby/runner.rb +253 -0
- data/lib/asterism/cruby/zenoh.rb +422 -0
- data/lib/asterism/cruby.rb +6 -0
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +5 -0
- data/mrblib/asterism.rb +81 -9
- data/mrblib/future.rb +6 -1
- data/mrblib/ros.rb +182 -12
- metadata +7 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Asterism::ROS with a receiving thread and Enumerators (CRuby only).
|
|
2
|
+
# Asterism::ROS::Node, node.poll, the subscribe / every blocks and pattern
|
|
3
|
+
# matching on messages are the shared ones (mrblib/ros.rb), as on the
|
|
4
|
+
# boards; this layer runs node.poll on a receiving thread, routes what the
|
|
5
|
+
# blocks raise to on_error, and adds the Enumerators.
|
|
6
|
+
#
|
|
7
|
+
# Asterism::ROS.connect("tcp/192.0.2.2:7447", domain: 0) do |ros|
|
|
8
|
+
# node = ros.node("ruby_talker")
|
|
9
|
+
# chatter = node.publisher("/chatter", "std_msgs/msg/String")
|
|
10
|
+
# chatter << { data: "hello from Ruby" }
|
|
11
|
+
# node.subscribe("/odom", "nav_msgs/msg/Odometry") { |odom| p odom.pose.pose.position.x }
|
|
12
|
+
# node.every(0.1) { chatter << { data: Time.now.to_s } }
|
|
13
|
+
# node.service("/add_ruby", "example_interfaces/srv/AddTwoInts") { |req| { sum: req.a + req.b } }
|
|
14
|
+
# p node.call("/add_two_ints", "example_interfaces/srv/AddTwoInts", a: 1, b: 2).sum
|
|
15
|
+
# node.topic("/scan", "sensor_msgs/msg/LaserScan").each.lazy.first(1)
|
|
16
|
+
# ros.spin
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Locking: each connection has one lock (its Runner's). A tick (node.poll:
|
|
20
|
+
# services answered, subscription blocks, timers) holds it; so do the calls
|
|
21
|
+
# that change a node (making publishers, subscriptions, services, clients,
|
|
22
|
+
# timers) and publishing. A service call made on another thread while the connection
|
|
23
|
+
# spins waits for the ticks (Asterism::Runner#wait_until) instead of
|
|
24
|
+
# polling by itself.
|
|
25
|
+
module Asterism
|
|
26
|
+
module ROS
|
|
27
|
+
# Opens a Zenoh session and makes a Connection for nodes on it. domain:
|
|
28
|
+
# the ROS domain of the nodes made by ros.node. type_path: directories
|
|
29
|
+
# with generated types to add to Asterism::ROS::TYPE_PATH. The other
|
|
30
|
+
# options go to Asterism::Zenoh::Session.open. With a block: yields the
|
|
31
|
+
# Connection, closes it when the block ends and returns its value.
|
|
32
|
+
def self.connect(locator = nil, domain: 0, type_path: nil, interval: Runner::DEFAULT_INTERVAL, **opts)
|
|
33
|
+
Array(type_path).each { |d| TYPE_PATH << File.expand_path(d) unless TYPE_PATH.include?(File.expand_path(d)) }
|
|
34
|
+
zconn = ::Asterism::Zenoh.open(locator, interval: interval, **opts)
|
|
35
|
+
conn = Connection.new(zconn, domain: domain)
|
|
36
|
+
return conn unless block_given?
|
|
37
|
+
begin
|
|
38
|
+
yield conn
|
|
39
|
+
ensure
|
|
40
|
+
conn.close
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The ROS side of a Zenoh connection: makes nodes and spins them.
|
|
45
|
+
class Connection
|
|
46
|
+
attr_reader :zenoh, :domain
|
|
47
|
+
|
|
48
|
+
def initialize(zconn, domain: 0)
|
|
49
|
+
@zenoh = zconn
|
|
50
|
+
@domain = domain.to_i
|
|
51
|
+
@nodes = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def session = @zenoh.session
|
|
55
|
+
def runner = @zenoh.runner
|
|
56
|
+
|
|
57
|
+
# An Asterism::ROS::Node on this connection, with the block API
|
|
58
|
+
# (subscribe, every, topic; services answered while it spins).
|
|
59
|
+
def node(name, namespace: "/", enclave: "/")
|
|
60
|
+
runner.lock.synchronize do
|
|
61
|
+
n = Node.new(session, name, namespace: namespace, domain: @domain, enclave: enclave)
|
|
62
|
+
n.extend(Spinning)
|
|
63
|
+
n.asterism_spin_on(runner)
|
|
64
|
+
@nodes << n
|
|
65
|
+
n
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def nodes = @nodes.dup
|
|
70
|
+
|
|
71
|
+
# Spins on this thread (services, subscription blocks, timers) until
|
|
72
|
+
# stop, the connection closing, or Ctrl-C. Returns nil.
|
|
73
|
+
def spin
|
|
74
|
+
runner.run
|
|
75
|
+
end
|
|
76
|
+
alias run spin
|
|
77
|
+
|
|
78
|
+
# Spins on a thread of its own. Returns self.
|
|
79
|
+
def start
|
|
80
|
+
runner.start
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stop
|
|
85
|
+
runner.stop
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def running? = runner.running?
|
|
90
|
+
|
|
91
|
+
# on_error { |error, where| }: what subscription, timer and service
|
|
92
|
+
# blocks raise (the service request then gets no answer).
|
|
93
|
+
def on_error(&blk)
|
|
94
|
+
runner.on_error(&blk)
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Stops spinning, withdraws the nodes and closes the session.
|
|
99
|
+
def close
|
|
100
|
+
runner.stop
|
|
101
|
+
@nodes.each { |n| n.close rescue ::Asterism::Zenoh::Error }
|
|
102
|
+
@nodes = []
|
|
103
|
+
@zenoh.close
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def inspect
|
|
108
|
+
"#<Asterism::ROS::Connection domain #{@domain}#{running? ? ' spinning' : ''}>"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A service block raised; it was reported through on_error already.
|
|
113
|
+
class ServiceFailed < ::StandardError; end
|
|
114
|
+
|
|
115
|
+
# Added to the nodes a Connection makes (Node#extend). The methods of
|
|
116
|
+
# Node keep their meaning; these come on top.
|
|
117
|
+
module Spinning
|
|
118
|
+
def asterism_spin_on(runner)
|
|
119
|
+
@asterism_runner = runner
|
|
120
|
+
node = self
|
|
121
|
+
@asterism_task = runner.add { node.poll ? 0 : false }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# As Node#pump (node.poll and waiting service calls go through it). A
|
|
125
|
+
# service block that raised was reported (on_error) and its request
|
|
126
|
+
# left unanswered; polling goes on.
|
|
127
|
+
def pump(steps = 8)
|
|
128
|
+
super
|
|
129
|
+
rescue ::Asterism::ROS::ServiceFailed
|
|
130
|
+
!@session.closed?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The subscribe and every blocks (called from node.poll): what they
|
|
134
|
+
# raise goes to on_error, and the others still run.
|
|
135
|
+
def handle(sub, msg, info)
|
|
136
|
+
@asterism_runner.guard("subscribe #{sub.topic_key}") { super }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def fire(timer, now)
|
|
140
|
+
@asterism_runner.guard("every #{timer.period}") { super }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def asterism_runner = @asterism_runner
|
|
144
|
+
|
|
145
|
+
# The calls that change the node take the connection's lock.
|
|
146
|
+
%i[subscription client close every cancel_timer].each do |m|
|
|
147
|
+
define_method(m) do |*args, **kw, &blk|
|
|
148
|
+
@asterism_runner.lock.synchronize { super(*args, **kw, &blk) }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# As Node#publisher; publishing takes the lock too (a timer on the
|
|
153
|
+
# receiving thread and the application may publish at once).
|
|
154
|
+
def publisher(topic, type, qos: DEFAULT_QOS)
|
|
155
|
+
r = @asterism_runner
|
|
156
|
+
r.lock.synchronize do
|
|
157
|
+
pub = super
|
|
158
|
+
pub.extend(LockedPublisher)
|
|
159
|
+
pub.instance_variable_set(:@asterism_lock, r.lock)
|
|
160
|
+
pub
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# As Node#service: the block answers while the connection spins (or
|
|
165
|
+
# from node.poll). What it raises goes to on_error and that request
|
|
166
|
+
# gets no answer.
|
|
167
|
+
def service(name, type, qos: DEFAULT_QOS, depth: 8, &handler)
|
|
168
|
+
raise ArgumentError, "service needs a block" unless handler
|
|
169
|
+
r = @asterism_runner
|
|
170
|
+
where = "service #{name}"
|
|
171
|
+
wrapped = proc do |req|
|
|
172
|
+
handler.call(req)
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
r.report(e, where)
|
|
175
|
+
raise ::Asterism::ROS::ServiceFailed, e.message
|
|
176
|
+
end
|
|
177
|
+
r.lock.synchronize { super(name, type, qos: qos, depth: depth, &wrapped) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# With a block: as on the boards (Node#subscribe), the block gets each
|
|
181
|
+
# message (and its Attachment, or nil) from node.poll, here while the
|
|
182
|
+
# connection spins; returns the Subscription. Without: a Subscription
|
|
183
|
+
# whose each waits for messages (an Enumerator).
|
|
184
|
+
def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &blk)
|
|
185
|
+
r = @asterism_runner
|
|
186
|
+
r.lock.synchronize do
|
|
187
|
+
sub = blk ? super : subscription(topic, type, qos: qos, depth: depth)
|
|
188
|
+
sub.extend(Stream)
|
|
189
|
+
sub.asterism_stream_on(session)
|
|
190
|
+
sub
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A topic to iterate: each subscribes for the iteration and withdraws
|
|
195
|
+
# after it (node.topic("/scan", "sensor_msgs/msg/LaserScan").each.lazy.first(1)).
|
|
196
|
+
def topic(topic, type, qos: DEFAULT_QOS, depth: 16)
|
|
197
|
+
Topic.new(self, topic, type, qos, depth)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Node#call with the time limit in seconds as well: timeout: 2.0.
|
|
201
|
+
def call(service, type, request = nil, timeout: nil, timeout_ms: nil, **fields)
|
|
202
|
+
ms = timeout_ms || ::Asterism::Zenoh.ms(timeout, Client::DEFAULT_TIMEOUT_MS)
|
|
203
|
+
c = @asterism_runner.lock.synchronize do
|
|
204
|
+
@clients ||= {}
|
|
205
|
+
cl = @clients[service]
|
|
206
|
+
if cl.nil? || cl.closed?
|
|
207
|
+
cl = client(service, type)
|
|
208
|
+
@clients[service] = cl
|
|
209
|
+
end
|
|
210
|
+
cl
|
|
211
|
+
end
|
|
212
|
+
c.call(request, timeout_ms: ms, **fields)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
module LockedPublisher
|
|
217
|
+
def publish(msg)
|
|
218
|
+
@asterism_lock.synchronize { super }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Added to the subscriptions node.subscribe makes.
|
|
223
|
+
module Stream
|
|
224
|
+
include Enumerable
|
|
225
|
+
|
|
226
|
+
def asterism_stream_on(session)
|
|
227
|
+
@asterism_session = session
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Yields each message (and its Attachment) as it comes, waiting for
|
|
231
|
+
# the next; ends when the subscription or the session closes, or after
|
|
232
|
+
# timeout seconds. Without a block, an Enumerator.
|
|
233
|
+
def each(timeout: nil, &blk)
|
|
234
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
235
|
+
raise ::Asterism::Error, "this subscription has a block; its messages go there" if handler
|
|
236
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
237
|
+
loop do
|
|
238
|
+
got = each_pending
|
|
239
|
+
got.each { |m| blk.call(m[0], m[1]) }
|
|
240
|
+
break if closed? || @asterism_session.closed?
|
|
241
|
+
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
242
|
+
sleep(::Asterism::Zenoh::ENUM_STEP) if got.empty?
|
|
243
|
+
end
|
|
244
|
+
self
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# node.topic: subscribes while it is iterated.
|
|
250
|
+
class Topic
|
|
251
|
+
include Enumerable
|
|
252
|
+
|
|
253
|
+
attr_reader :name, :type
|
|
254
|
+
|
|
255
|
+
def initialize(node, name, type, qos, depth)
|
|
256
|
+
@node = node
|
|
257
|
+
@name = name
|
|
258
|
+
@type = type
|
|
259
|
+
@qos = qos
|
|
260
|
+
@depth = depth
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Yields each message and its Attachment, from a subscription made
|
|
264
|
+
# for this iteration (withdrawn when it ends: break, first(n), the
|
|
265
|
+
# time running out). Without a block, an Enumerator.
|
|
266
|
+
def each(timeout: nil, &blk)
|
|
267
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
268
|
+
sub = @node.subscribe(@name, @type, qos: @qos, depth: @depth)
|
|
269
|
+
begin
|
|
270
|
+
sub.each(timeout: timeout, &blk)
|
|
271
|
+
ensure
|
|
272
|
+
sub.close
|
|
273
|
+
end
|
|
274
|
+
self
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def inspect
|
|
278
|
+
"#<Asterism::ROS::Topic #{@name}>"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class Client
|
|
283
|
+
# Prepended: a call made on another thread while the connection
|
|
284
|
+
# spins waits for the ticks instead of polling the node by itself.
|
|
285
|
+
# (Calls from a board's update loop are on one thread; here the
|
|
286
|
+
# application may call from several.)
|
|
287
|
+
# (The sequence number is read once in the shared call_async, so
|
|
288
|
+
# several threads may call through one client without a lock.)
|
|
289
|
+
module Waiting
|
|
290
|
+
def wait_for(c)
|
|
291
|
+
loop do
|
|
292
|
+
r = ::Asterism::Runner.for(@session)
|
|
293
|
+
if r && r.running? && !r.current?
|
|
294
|
+
r.wait_until { c.done? }
|
|
295
|
+
return if c.done?
|
|
296
|
+
else
|
|
297
|
+
return r ? r.lock.synchronize { super } : super
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
prepend Waiting
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# The receiving loop behind the block API of the CRuby layer (CRuby only;
|
|
2
|
+
# the boards poll from their update loop instead).
|
|
3
|
+
#
|
|
4
|
+
# A Runner belongs to one Zenoh session. It holds tasks: small procs that
|
|
5
|
+
# take what has arrived (each_pending of the shared API) and hand it to the
|
|
6
|
+
# application's blocks. Every round of tasks (a tick) runs with the runner's
|
|
7
|
+
# lock held, so the shared, single-threaded layers are never entered by two
|
|
8
|
+
# threads at once. zenoh-c's own threads never call Ruby: they only fill the
|
|
9
|
+
# channels the tasks empty.
|
|
10
|
+
#
|
|
11
|
+
# runner.start ticks on a thread of its own (until stop or close)
|
|
12
|
+
# runner.run ticks on the calling thread (until stop, close or Ctrl-C)
|
|
13
|
+
# runner.stop ends either; joins the thread unless called from it
|
|
14
|
+
#
|
|
15
|
+
# A thread that waits for an answer while the runner ticks (proxy calls,
|
|
16
|
+
# ROS service calls) does not poll by itself: it waits on the runner's
|
|
17
|
+
# condition variable, and checks after each tick whether its answer came
|
|
18
|
+
# (wait_until). The runner's own thread, and any thread while the runner
|
|
19
|
+
# does not tick, poll as the shared layers always do.
|
|
20
|
+
require "monitor"
|
|
21
|
+
|
|
22
|
+
module Asterism
|
|
23
|
+
class Runner
|
|
24
|
+
# Pause between ticks when nothing arrived (seconds).
|
|
25
|
+
DEFAULT_INTERVAL = 0.002
|
|
26
|
+
# Longest a waiting thread sleeps before it checks again on its own.
|
|
27
|
+
WAIT_SLICE = 0.05
|
|
28
|
+
|
|
29
|
+
@registry = {}.compare_by_identity
|
|
30
|
+
@registry_lock = Mutex.new
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# The runner of a Zenoh session (nil when it has none).
|
|
34
|
+
def for(session)
|
|
35
|
+
@registry_lock.synchronize { @registry[session] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register(session, runner)
|
|
39
|
+
@registry_lock.synchronize { @registry[session] = runner }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def unregister(session, runner)
|
|
43
|
+
@registry_lock.synchronize { @registry.delete(session) if @registry[session].equal?(runner) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :lock, :session
|
|
48
|
+
|
|
49
|
+
# session: the Asterism::Zenoh::Session the tasks read from. lock: the
|
|
50
|
+
# Monitor guarding the layer the tasks call into (the object layer has a
|
|
51
|
+
# process-wide one). on_closed is called once, on the ticking thread,
|
|
52
|
+
# when the session is found closed.
|
|
53
|
+
def initialize(session, lock: Monitor.new, interval: DEFAULT_INTERVAL, name: "asterism")
|
|
54
|
+
@session = session
|
|
55
|
+
@lock = lock
|
|
56
|
+
@cond = @lock.new_cond
|
|
57
|
+
@interval = interval
|
|
58
|
+
@name = name
|
|
59
|
+
@tasks = []
|
|
60
|
+
@error_handler = nil
|
|
61
|
+
@thread = nil
|
|
62
|
+
@ticking = nil # the thread inside start / run
|
|
63
|
+
@stop = false
|
|
64
|
+
@closed = false
|
|
65
|
+
self.class.register(session, self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Adds a task: a proc called every tick with the lock held. It returns
|
|
69
|
+
# the number of things it handled (0 or nil when nothing came), and
|
|
70
|
+
# false once the session is found closed. Returns the task, for remove.
|
|
71
|
+
def add(task = nil, &blk)
|
|
72
|
+
t = task || blk
|
|
73
|
+
@lock.synchronize { @tasks << t }
|
|
74
|
+
t
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def remove(task)
|
|
78
|
+
@lock.synchronize { @tasks.delete(task) }
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# on_error { |error, where| }: errors raised by the application's blocks
|
|
83
|
+
# (StandardError) go here instead of stopping the runner. Without a
|
|
84
|
+
# handler they are printed with warn. where: what the block was for
|
|
85
|
+
# (e.g. "subscribe demo/**").
|
|
86
|
+
def on_error(&blk)
|
|
87
|
+
@error_handler = blk
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Runs the application's block; reports what it raises instead of
|
|
92
|
+
# letting it out. Returns the block's value, or nil when it raised.
|
|
93
|
+
def guard(where)
|
|
94
|
+
yield
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
report(e, where)
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def report(error, where)
|
|
101
|
+
if @error_handler
|
|
102
|
+
begin
|
|
103
|
+
@error_handler.call(error, where)
|
|
104
|
+
rescue StandardError => e2
|
|
105
|
+
warn "asterism: on_error raised #{e2.class}: #{e2.message}"
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
warn "asterism: #{where}: #{error.class}: #{error.message}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# One round of the tasks. Returns the number of things handled, or nil
|
|
113
|
+
# once the session is closed.
|
|
114
|
+
def tick
|
|
115
|
+
n = 0
|
|
116
|
+
closed = false
|
|
117
|
+
@lock.synchronize do
|
|
118
|
+
begin
|
|
119
|
+
@tasks.dup.each do |t|
|
|
120
|
+
r = begin
|
|
121
|
+
t.call
|
|
122
|
+
rescue ::Asterism::Zenoh::Error
|
|
123
|
+
false # the session went away under the task
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
report(e, "receiver")
|
|
126
|
+
0
|
|
127
|
+
end
|
|
128
|
+
if r == false
|
|
129
|
+
closed = true
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
n += r if r.is_a?(Integer)
|
|
133
|
+
end
|
|
134
|
+
ensure
|
|
135
|
+
@cond.broadcast
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
if closed
|
|
139
|
+
@closed = true
|
|
140
|
+
@stop = true
|
|
141
|
+
return nil
|
|
142
|
+
end
|
|
143
|
+
n
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Ticks on a thread of its own. Returns self. Does nothing when already
|
|
147
|
+
# ticking.
|
|
148
|
+
def start
|
|
149
|
+
@lock.synchronize do
|
|
150
|
+
return self if @ticking
|
|
151
|
+
@stop = false
|
|
152
|
+
@thread = Thread.new { loop_ticks }
|
|
153
|
+
@thread.name = "#{@name} receiver" if @thread.respond_to?(:name=)
|
|
154
|
+
@ticking = @thread
|
|
155
|
+
end
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Ticks on the calling thread until stop, the session closing, or
|
|
160
|
+
# Ctrl-C (which returns nil instead of raising). Raises Error when the
|
|
161
|
+
# runner already ticks on another thread.
|
|
162
|
+
def run
|
|
163
|
+
@lock.synchronize do
|
|
164
|
+
raise ::Asterism::Error, "already running (stop it first)" if @ticking
|
|
165
|
+
@stop = false
|
|
166
|
+
@ticking = Thread.current
|
|
167
|
+
end
|
|
168
|
+
begin
|
|
169
|
+
loop_ticks
|
|
170
|
+
rescue Interrupt
|
|
171
|
+
nil
|
|
172
|
+
ensure
|
|
173
|
+
@lock.synchronize do
|
|
174
|
+
@ticking = nil if @ticking.equal?(Thread.current)
|
|
175
|
+
@cond.broadcast
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Ends start / run after the current tick. From another thread it waits
|
|
182
|
+
# for the receiving thread to end (unless that thread is inside a tick
|
|
183
|
+
# this thread is waiting for). Idempotent.
|
|
184
|
+
def stop
|
|
185
|
+
@stop = true
|
|
186
|
+
th = @thread
|
|
187
|
+
if th && !th.equal?(Thread.current) && !@lock.mon_owned?
|
|
188
|
+
th.join
|
|
189
|
+
end
|
|
190
|
+
@lock.synchronize do
|
|
191
|
+
@thread = nil if @thread.equal?(th) && !(th && th.alive?)
|
|
192
|
+
@cond.broadcast
|
|
193
|
+
end
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Stops and forgets the session. The caller closes the session after.
|
|
198
|
+
def close
|
|
199
|
+
stop
|
|
200
|
+
self.class.unregister(@session, self)
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# True while start or run ticks.
|
|
205
|
+
def running?
|
|
206
|
+
t = @ticking
|
|
207
|
+
!t.nil? && t.alive? && !@stop
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# True on the thread that ticks.
|
|
211
|
+
def current?
|
|
212
|
+
@ticking.equal?(Thread.current)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def closed?
|
|
216
|
+
@closed
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# For a thread other than the ticking one: waits until the block is
|
|
220
|
+
# true, checking it after each tick with the lock held. Returns when the
|
|
221
|
+
# block is true, or when the runner stops (the caller then looks at its
|
|
222
|
+
# own state, e.g. a time limit or a closed session). The block also sees
|
|
223
|
+
# its own deadline: it is checked at least every WAIT_SLICE seconds.
|
|
224
|
+
def wait_until
|
|
225
|
+
@lock.synchronize do
|
|
226
|
+
until yield
|
|
227
|
+
break unless running?
|
|
228
|
+
@cond.wait(WAIT_SLICE)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def inspect
|
|
234
|
+
"#<Asterism::Runner #{@name} #{running? ? 'running' : 'stopped'}>"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def loop_ticks
|
|
240
|
+
until @stop
|
|
241
|
+
n = tick
|
|
242
|
+
break if n.nil?
|
|
243
|
+
sleep(@interval) if n == 0 && !@stop
|
|
244
|
+
end
|
|
245
|
+
ensure
|
|
246
|
+
@lock.synchronize do
|
|
247
|
+
@ticking = nil if @ticking.equal?(Thread.current)
|
|
248
|
+
@thread = nil if @thread.equal?(Thread.current)
|
|
249
|
+
@cond.broadcast
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|