wineole 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/bin/wineole-vba +45 -0
- data/lib/wineole/client.rb +454 -20
- data/lib/wineole/dispatcher.rb +534 -0
- data/lib/wineole/errors.rb +6 -0
- data/lib/wineole/events.rb +353 -0
- data/lib/wineole/msoffice/address.rb +67 -0
- data/lib/wineole/msoffice/book.rb +114 -0
- data/lib/wineole/msoffice/color.rb +87 -0
- data/lib/wineole/msoffice/controls.rb +450 -0
- data/lib/wineole/msoffice/excel.rb +311 -0
- data/lib/wineole/msoffice/format.rb +362 -0
- data/lib/wineole/msoffice/forms.rb +163 -0
- data/lib/wineole/msoffice/passthrough.rb +46 -0
- data/lib/wineole/msoffice/paths.rb +62 -0
- data/lib/wineole/msoffice/range.rb +149 -0
- data/lib/wineole/msoffice/sheet.rb +101 -0
- data/lib/wineole/msoffice/vba.rb +199 -0
- data/lib/wineole/msoffice/vba_api.rb +366 -0
- data/lib/wineole/msoffice/vba_block.rb +144 -0
- data/lib/wineole/msoffice.rb +28 -0
- data/lib/wineole/proxy.rb +126 -15
- data/lib/wineole.rb +10 -1
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +21 -2
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
require 'weakref'
|
|
2
|
+
require_relative 'errors'
|
|
3
|
+
|
|
4
|
+
module WineOLE
|
|
5
|
+
# The one dispatcher thread of a CONNECTION, and the queue it parks on.
|
|
6
|
+
#
|
|
7
|
+
# One thread per connection is a promise the README makes to the caller
|
|
8
|
+
# ("Callbacks run on one dispatcher thread per connection, in arrival
|
|
9
|
+
# order, one at a time"), and the whole value of it is that a caller never
|
|
10
|
+
# needs a lock BETWEEN callbacks: a Hash shared by an Application callback
|
|
11
|
+
# and a Workbook callback is safe because the two can never be inside their
|
|
12
|
+
# blocks at the same time. A thread per `Events` object breaks exactly that
|
|
13
|
+
# and nothing else -- measured, with two `Events` on one connection: both
|
|
14
|
+
# callbacks were inside their blocks simultaneously, on 2 distinct threads,
|
|
15
|
+
# while every other event assertion in the suite still passed.
|
|
16
|
+
#
|
|
17
|
+
# Everything here is DERIVED from there being a registered callback
|
|
18
|
+
# somewhere on the connection, the same way `Events` derives its
|
|
19
|
+
# subscription and its Advise from one: the thread and the sink go up at
|
|
20
|
+
# the first `attach` and come back down after the last `detach`. An
|
|
21
|
+
# `ole_events` nobody registered on costs no thread, and a connection whose
|
|
22
|
+
# callbacks have all been removed goes back to costing none.
|
|
23
|
+
#
|
|
24
|
+
# THE MUTEX IS NEVER HELD WHILE USER CODE RUNS. A callback that registers a
|
|
25
|
+
# callback on ANOTHER object -- `sheet.ole_events.on(...)` from inside an
|
|
26
|
+
# Application callback -- reaches `attach` on this very thread, and would
|
|
27
|
+
# deadlock against a lock its own delivery still held.
|
|
28
|
+
class Dispatcher
|
|
29
|
+
def initialize(client)
|
|
30
|
+
@client = client
|
|
31
|
+
# handle -> Events, at most ONE Events per handle per connection. That
|
|
32
|
+
# invariant is what makes a handle a sufficient routing key, and
|
|
33
|
+
# `Proxy#ole_events` is what guarantees it in shipped code: it memoizes,
|
|
34
|
+
# and a bridge id is unique per session, so two Proxies never share a
|
|
35
|
+
# handle. `Events.new(client, handle)` is public, though, so `attach`
|
|
36
|
+
# enforces it rather than trusting it -- a second Events silently
|
|
37
|
+
# unseating the first would leave a registered callback that never
|
|
38
|
+
# fires, and either object's last `off` would then stop the other.
|
|
39
|
+
#
|
|
40
|
+
# Held STRONGLY and on purpose. This is what keeps an
|
|
41
|
+
# `Events` alive exactly as long as it has registrations, even when the
|
|
42
|
+
# caller kept no reference to the Proxy it came from
|
|
43
|
+
# (`xl.ole_events.on('Click') { }` leaves the caller holding nothing).
|
|
44
|
+
# The chain is Client -> Dispatcher -> @targets -> Events. Hold them
|
|
45
|
+
# weakly and the Events is collected out from under a live connection:
|
|
46
|
+
# callbacks stop firing, the bridge stays advised, and every event it
|
|
47
|
+
# goes on sending leaks its argument handles because nobody is left to
|
|
48
|
+
# release them. Nothing here pins the Client from outside, because the
|
|
49
|
+
# Client holds this Dispatcher and the Events holds the Client -- the
|
|
50
|
+
# whole ring is collected together or not at all.
|
|
51
|
+
@targets = {}
|
|
52
|
+
# handle -> on_cleanup closure. Registered by a Proxy on the on_cleanup
|
|
53
|
+
# path; a $cleanup frame for that handle runs the closure here, on the
|
|
54
|
+
# dispatcher thread, the same COM-safe context every other callback runs
|
|
55
|
+
# on. Kept beside @targets because a client that uses on_cleanup but
|
|
56
|
+
# subscribes to no events still needs the sink and thread up, so this
|
|
57
|
+
# counts toward "is there a registered callback on the connection" the
|
|
58
|
+
# same way @targets does.
|
|
59
|
+
@cleanups = {}
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@queue = Queue.new
|
|
62
|
+
@sink = nil
|
|
63
|
+
@thread = nil
|
|
64
|
+
# A collected Dispatcher must not leave its thread parked on a queue
|
|
65
|
+
# nothing can ever push to again. The finalizer pushes the same :stop
|
|
66
|
+
# the end of the stream does, and captures the QUEUE ONLY: capturing
|
|
67
|
+
# `self` would keep this object reachable from ObjectSpace's finalizer
|
|
68
|
+
# table forever, so it could never be collected and the finalizer could
|
|
69
|
+
# never run -- the same rule as Client.finalizer.
|
|
70
|
+
ObjectSpace.define_finalizer(self, self.class.stopper(@queue))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.stopper(queue)
|
|
74
|
+
proc { queue << :stop }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The connection's one sink, built on the class so it captures the queue
|
|
78
|
+
# and nothing else. It is registered once, at the first `attach`, and
|
|
79
|
+
# does no filtering: every frame on the connection goes on the one queue
|
|
80
|
+
# and is routed by handle on the dispatcher thread, which is what makes
|
|
81
|
+
# "in arrival order, one at a time" true across objects rather than
|
|
82
|
+
# within one.
|
|
83
|
+
#
|
|
84
|
+
# This runs on the reader thread -- or, for a client whose stream has
|
|
85
|
+
# already ended, on the thread that is registering right now. It must
|
|
86
|
+
# enqueue and return either way: it may not block and it may not raise.
|
|
87
|
+
def self.sink(queue)
|
|
88
|
+
proc do |frame|
|
|
89
|
+
# nil means the connection is gone.
|
|
90
|
+
queue << (frame.nil? ? :stop : frame)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Registers `events` as the target for its handle, and puts up whatever
|
|
95
|
+
# the connection does not have yet. Called from `Events#arm` under that
|
|
96
|
+
# object's @wire_mutex; two `Events` on one connection have two of those,
|
|
97
|
+
# so @mutex is what makes this safe between them.
|
|
98
|
+
#
|
|
99
|
+
# The calls out of this class made under @mutex are `Client#on_event`
|
|
100
|
+
# here and `Client#off_event` in `detach`, and both are safe by
|
|
101
|
+
# inspection: neither runs user code (the sink above is ours, and only
|
|
102
|
+
# enqueues), and the locks they take -- the Client's sink list and the
|
|
103
|
+
# Mailbox's -- are ordered against @mutex, not disjoint from it. This
|
|
104
|
+
# class does wait for the Mailbox's lock, in `release`, but only with
|
|
105
|
+
# @mutex NOT held; and nothing holding either of those locks ever waits
|
|
106
|
+
# for @mutex: `Client#on_event` lets its sink list go before it calls
|
|
107
|
+
# the Mailbox, and the Mailbox drops its lock before it calls a sink.
|
|
108
|
+
# The one thing either can do on this thread -- `on_event` handing an
|
|
109
|
+
# end-of-stream nil straight back when the stream has already ended --
|
|
110
|
+
# only pushes :stop on the queue.
|
|
111
|
+
#
|
|
112
|
+
# That hand-off reaches the FIRST object to arm on a dead connection and
|
|
113
|
+
# not a second one, because by then the sink is already registered: the
|
|
114
|
+
# second object's thread parks on a queue nothing will push to until the
|
|
115
|
+
# Dispatcher's finalizer pushes :stop as the ring is collected. Bounded,
|
|
116
|
+
# and no ordinary path reaches it -- `client.close` still ends the
|
|
117
|
+
# stream through the one sink that is up.
|
|
118
|
+
def attach(handle, events)
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
current = @targets[handle]
|
|
121
|
+
if current && !current.equal?(events)
|
|
122
|
+
# Only a DIFFERENT object is refused: the caller holding two
|
|
123
|
+
# `Events` for one object. The same one is let through so that a
|
|
124
|
+
# re-attach can never be mistaken for that (an ordinary `on` after
|
|
125
|
+
# `close` finds the slot empty, since `detach` cleared it). See
|
|
126
|
+
# @targets.
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"handle #{handle} already has an Events on this connection; one object's " \
|
|
129
|
+
'events belong to one Events (Proxy#ole_events memoizes for this reason)'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@targets[handle] = events
|
|
133
|
+
if @sink.nil?
|
|
134
|
+
@sink = Dispatcher.sink(@queue)
|
|
135
|
+
@client.on_event(&@sink)
|
|
136
|
+
end
|
|
137
|
+
next if @thread&.alive?
|
|
138
|
+
|
|
139
|
+
# `&Dispatcher.method(:run)`, not `{ run_loop }`. A block captures its
|
|
140
|
+
# whole binding, `self` included, so a thread started from a block
|
|
141
|
+
# written in an instance method is a permanent GC root for this
|
|
142
|
+
# Dispatcher -- and through @client, for the Client. Measured on
|
|
143
|
+
# exactly that code, one layer down: the Client was never collected,
|
|
144
|
+
# its finalizer never ran, and its socket stayed open for the life of
|
|
145
|
+
# the process. That is the same leak the Mailbox was extracted to fix
|
|
146
|
+
# (see Client::Mailbox), walked back in through this thread. A Method
|
|
147
|
+
# object on the class captures the class and nothing else, so what
|
|
148
|
+
# the thread holds is a WeakRef and a queue.
|
|
149
|
+
@thread = Thread.new(WeakRef.new(self), @queue, &Dispatcher.method(:run))
|
|
150
|
+
@thread.abort_on_exception = false
|
|
151
|
+
end
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The other half of `attach`, for one object. The connection's thread and
|
|
156
|
+
# sink only come down with the LAST one: an Application whose callbacks
|
|
157
|
+
# are all removed must not stop the Workbook's events on the same
|
|
158
|
+
# connection.
|
|
159
|
+
def detach(handle, events)
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
# By identity, and for the reason `Client#off_event` uses identity:
|
|
162
|
+
# removing a routing entry that belongs to somebody else silently
|
|
163
|
+
# stops a live consumer. With `attach` refusing a second Events on
|
|
164
|
+
# one handle this can only be a stale detach -- `disarm` on an object
|
|
165
|
+
# that has already left -- but "it cannot happen" is not a reason to
|
|
166
|
+
# write the unguarded delete.
|
|
167
|
+
@targets.delete(handle) if @targets[handle].equal?(events)
|
|
168
|
+
sink = @sink
|
|
169
|
+
# A registered cleanup keeps the sink and thread up even with no
|
|
170
|
+
# events left: the $cleanup frame it is waiting for still has to be
|
|
171
|
+
# delivered on this connection's dispatcher.
|
|
172
|
+
next unless @targets.empty? && @cleanups.empty? && sink
|
|
173
|
+
|
|
174
|
+
@sink = nil
|
|
175
|
+
@client.off_event(sink)
|
|
176
|
+
# After the sink comes off, never before: the marker means "nothing
|
|
177
|
+
# more arrives behind this through the sink", and a frame the reader
|
|
178
|
+
# pushed between the two is behind the marker, drained by
|
|
179
|
+
# `confirm_idle` and released. Not quite airtight: a reader ALREADY
|
|
180
|
+
# inside `dispatch_to_sinks` when `off_event` ran still holds the old
|
|
181
|
+
# sink, and can push after the drain too. Such a frame stays on the
|
|
182
|
+
# queue with no thread, until the next `attach` starts one -- which
|
|
183
|
+
# delivers it if that object is back for the event, and releases it
|
|
184
|
+
# otherwise. Bounded by that one dispatch, and unseen in a 400-cycle
|
|
185
|
+
# detach/attach stress (2572 frames), but it is there. Never a join,
|
|
186
|
+
# either -- the thread reaching here is very often the dispatcher
|
|
187
|
+
# itself, in a callback that called `off`.
|
|
188
|
+
@queue << :idle
|
|
189
|
+
end
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Register a client closure to run when the bridge asks (a $cleanup frame
|
|
194
|
+
# for `handle`). Arms the connection's sink and thread the same way
|
|
195
|
+
# `attach` does, so a client that uses on_cleanup but subscribes to no
|
|
196
|
+
# events still has a dispatcher to deliver the frame on. Runs no user code
|
|
197
|
+
# under @mutex -- storing a closure and installing our own sink are the
|
|
198
|
+
# only things done here -- so holding it across the whole body is safe,
|
|
199
|
+
# exactly as it is in `attach`.
|
|
200
|
+
def register_cleanup(handle, block)
|
|
201
|
+
@mutex.synchronize do
|
|
202
|
+
@cleanups[handle] = block
|
|
203
|
+
if @sink.nil?
|
|
204
|
+
@sink = Dispatcher.sink(@queue)
|
|
205
|
+
@client.on_event(&@sink)
|
|
206
|
+
end
|
|
207
|
+
next if @thread&.alive?
|
|
208
|
+
|
|
209
|
+
# A Method object on the class, never a block: see `attach` for why a
|
|
210
|
+
# block started as a thread here is a permanent GC root for the Client.
|
|
211
|
+
@thread = Thread.new(WeakRef.new(self), @queue, &Dispatcher.method(:run))
|
|
212
|
+
@thread.abort_on_exception = false
|
|
213
|
+
end
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The other half of `register_cleanup`. Brings the sink and thread down
|
|
218
|
+
# with the `@queue << :idle` hand-off, exactly as `detach`'s last-target
|
|
219
|
+
# case does -- but only when nothing is left to deliver to: a cleanup
|
|
220
|
+
# removed while events are still registered must not stop them, and vice
|
|
221
|
+
# versa, which is why the guard is the same `@targets.empty? &&
|
|
222
|
+
# @cleanups.empty?` teardown condition.
|
|
223
|
+
def unregister_cleanup(handle)
|
|
224
|
+
@mutex.synchronize do
|
|
225
|
+
@cleanups.delete(handle)
|
|
226
|
+
sink = @sink
|
|
227
|
+
next unless @targets.empty? && @cleanups.empty? && sink
|
|
228
|
+
|
|
229
|
+
@sink = nil
|
|
230
|
+
@client.off_event(sink)
|
|
231
|
+
@queue << :idle
|
|
232
|
+
end
|
|
233
|
+
self
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The dispatcher thread's body, on the class so it captures no instance.
|
|
237
|
+
# The Dispatcher is reached weakly and never held across a `pop`, which
|
|
238
|
+
# is what lets a Client with events on it still be collected.
|
|
239
|
+
#
|
|
240
|
+
# THIS THREAD SURVIVES EVERYTHING. A dead dispatcher is permanent and
|
|
241
|
+
# silent: the callbacks stay registered, the bridge stays advised, every
|
|
242
|
+
# later event's argument handles leak on the bridge for the life of the
|
|
243
|
+
# connection, the queue grows without bound, and `on_error` is never
|
|
244
|
+
# told. Measured on the code before this rescue existed -- a callback
|
|
245
|
+
# raising outside StandardError, or a frame whose `args` was not an
|
|
246
|
+
# array -- 10 later events were queued and never delivered, and 1 of 11
|
|
247
|
+
# release_events was sent. So the rescue is `Exception`, not
|
|
248
|
+
# StandardError: the narrow rescue exists to let a fatal exception reach
|
|
249
|
+
# a thread that can act on it, and there is no such thread here -- this
|
|
250
|
+
# one dying takes the whole feature down without a word. Reported through
|
|
251
|
+
# `report`, which cannot raise, and the loop goes on to the next frame.
|
|
252
|
+
def self.run(ref, queue)
|
|
253
|
+
while (item = queue.pop)
|
|
254
|
+
break if item == :stop
|
|
255
|
+
|
|
256
|
+
dispatcher = begin
|
|
257
|
+
ref.__getobj__
|
|
258
|
+
rescue WeakRef::RefError
|
|
259
|
+
break # there is nobody left to deliver to
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
begin
|
|
263
|
+
break if step(dispatcher, item)
|
|
264
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
265
|
+
dispatcher.__send__(:report, e, item)
|
|
266
|
+
end
|
|
267
|
+
# Dropped before parking again: a local still pointing at the
|
|
268
|
+
# Dispatcher would pin it for as long as this thread waits, which is
|
|
269
|
+
# precisely what the WeakRef is here to avoid.
|
|
270
|
+
dispatcher = nil
|
|
271
|
+
end
|
|
272
|
+
ensure
|
|
273
|
+
# However this thread ended, the Dispatcher must not go on believing it
|
|
274
|
+
# has one: `attach` decides whether to start a thread from exactly that.
|
|
275
|
+
begin
|
|
276
|
+
ref.__getobj__.__send__(:thread_finished, Thread.current)
|
|
277
|
+
rescue WeakRef::RefError
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# One queued item. Answers whether the thread is to end. On the class,
|
|
283
|
+
# like `run`, so it captures no instance -- and it no longer needs the
|
|
284
|
+
# queue: what is left on it when this thread goes is taken off under
|
|
285
|
+
# @mutex by `confirm_idle`, which is the whole of the fix that made the
|
|
286
|
+
# hand-off safe.
|
|
287
|
+
def self.step(dispatcher, item)
|
|
288
|
+
case item
|
|
289
|
+
when :idle
|
|
290
|
+
# `detach` left this behind when the last target went away. It only
|
|
291
|
+
# ends the thread if that is still true: an `attach` that got in
|
|
292
|
+
# first answers no and the SAME thread carries on, which is what
|
|
293
|
+
# keeps "one dispatcher, arrival order" true across a detach/attach
|
|
294
|
+
# cycle instead of briefly running two.
|
|
295
|
+
#
|
|
296
|
+
# When the answer is yes, `confirm_idle` hands back everything that
|
|
297
|
+
# was still queued -- taken off the queue under the same lock, at the
|
|
298
|
+
# same instant, as the thread slot was cleared. Releasing it is a
|
|
299
|
+
# round trip per frame, so it is done HERE, out of that lock: holding
|
|
300
|
+
# @mutex across a round trip would stall every attach and detach on
|
|
301
|
+
# the connection behind it. A thread that has decided to go can
|
|
302
|
+
# therefore still be RELEASING while its successor runs, but it can
|
|
303
|
+
# no longer take anything off the queue, which is what it must not
|
|
304
|
+
# do: a frame pushed after that instant came from a later `attach`'s
|
|
305
|
+
# sink, and that sink was installed under the same lock.
|
|
306
|
+
leftover = dispatcher.__send__(:confirm_idle)
|
|
307
|
+
return false if leftover.nil?
|
|
308
|
+
|
|
309
|
+
# Whatever happens in here, the answer is "end": the slot is already
|
|
310
|
+
# clear, so a successor may be running, and a raise escaping to `run`'s
|
|
311
|
+
# rescue would keep THIS thread on the queue beside it. `release`
|
|
312
|
+
# rescues StandardError itself; this is for the rest.
|
|
313
|
+
begin
|
|
314
|
+
leftover.each { |left| dispatcher.__send__(:release, left) }
|
|
315
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
316
|
+
dispatcher.__send__(:report, e, item)
|
|
317
|
+
end
|
|
318
|
+
true
|
|
319
|
+
when Array
|
|
320
|
+
item.last.call if item.first == :barrier
|
|
321
|
+
false
|
|
322
|
+
when Hash
|
|
323
|
+
# Real frames are always Hashes here (:idle/:stop/[:barrier,..] are the
|
|
324
|
+
# non-Hash items). A $cleanup frame goes to the client's on_cleanup
|
|
325
|
+
# closure and is acked; every other frame is routed by handle to its
|
|
326
|
+
# Events, exactly as before.
|
|
327
|
+
if item['event'] == '$cleanup'
|
|
328
|
+
dispatcher.__send__(:run_cleanup, item)
|
|
329
|
+
else
|
|
330
|
+
dispatcher.__send__(:route, item)
|
|
331
|
+
end
|
|
332
|
+
false
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Is `thread` this dispatcher's own callback thread? Used by
|
|
337
|
+
# Client#await_cleanup to avoid a self-wait when `ole_release` is called
|
|
338
|
+
# from inside a callback -- the $cleanup frame that release triggers is
|
|
339
|
+
# queued behind the very callback asking the question, so waiting for it
|
|
340
|
+
# here would deadlock the dispatcher against itself.
|
|
341
|
+
def on_thread?(thread)
|
|
342
|
+
@mutex.synchronize { @thread }&.equal?(thread) || false
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Tests only. Production code never needs any of these, because callbacks
|
|
346
|
+
# are the delivery mechanism.
|
|
347
|
+
def stopped_for_test?(seconds)
|
|
348
|
+
thread = @mutex.synchronize { @thread }
|
|
349
|
+
return true if thread.nil?
|
|
350
|
+
|
|
351
|
+
!thread.join(seconds).nil?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The Thread itself, for the collectability tests: a Thread holds only
|
|
355
|
+
# what was handed to it -- a WeakRef and the queue -- so carrying one out
|
|
356
|
+
# of a `weak_ref_to` block pins neither this Dispatcher nor its Client.
|
|
357
|
+
def thread_for_test
|
|
358
|
+
@mutex.synchronize { @thread }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Blocks until the dispatcher has finished everything queued so far.
|
|
362
|
+
# Bounded, because the dispatcher not finishing is exactly what a test
|
|
363
|
+
# using this is hunting: an unbounded wait would hang the suite instead
|
|
364
|
+
# of reporting it.
|
|
365
|
+
#
|
|
366
|
+
# A Mutex/ConditionVariable barrier rather than the shorter
|
|
367
|
+
# `Queue#pop(timeout:)`: that keyword arrived in Ruby 3.2 and this gem
|
|
368
|
+
# declares `required_ruby_version >= 3.0` (wineole.gemspec), so on the
|
|
369
|
+
# oldest Ruby it supports the shorter form is an ArgumentError raised
|
|
370
|
+
# from shipped code.
|
|
371
|
+
def drain_for_test(seconds = 5)
|
|
372
|
+
done = false
|
|
373
|
+
lock = Mutex.new
|
|
374
|
+
cond = ConditionVariable.new
|
|
375
|
+
@queue << [:barrier, -> { lock.synchronize { done = true; cond.broadcast } }]
|
|
376
|
+
|
|
377
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
378
|
+
lock.synchronize do
|
|
379
|
+
until done
|
|
380
|
+
left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
381
|
+
raise Error, "the dispatcher did not drain within #{seconds}s" if left <= 0
|
|
382
|
+
|
|
383
|
+
cond.wait(lock, left)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
self
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private
|
|
390
|
+
|
|
391
|
+
# A $cleanup frame: run the client closure for its handle on THIS thread
|
|
392
|
+
# (COM-safe, like every other callback), then tell the bridge the closure
|
|
393
|
+
# is done and wake whoever is blocked in await_cleanup. The closure's own
|
|
394
|
+
# exception must not stop any of that -- the bridge runs the steps
|
|
395
|
+
# regardless (choice B), so a raising closure still ends with release_event
|
|
396
|
+
# and the waiter signalled.
|
|
397
|
+
#
|
|
398
|
+
# The closure is looked up UNDER @mutex and called WITHOUT it: like a
|
|
399
|
+
# target's callback, it is free to `on`/`off`/`leave_open` anything on this
|
|
400
|
+
# connection, each of which re-enters this class and would deadlock against
|
|
401
|
+
# a lock its own delivery still held.
|
|
402
|
+
def run_cleanup(frame)
|
|
403
|
+
handle = frame['handle']
|
|
404
|
+
seq = frame['seq']
|
|
405
|
+
block = @mutex.synchronize { @cleanups[handle] }
|
|
406
|
+
begin
|
|
407
|
+
block&.call
|
|
408
|
+
rescue StandardError => e
|
|
409
|
+
warn "wineole: on_cleanup closure raised #{e.class}: #{e.message}"
|
|
410
|
+
ensure
|
|
411
|
+
begin
|
|
412
|
+
@client.call('release_event', {seq: seq})
|
|
413
|
+
rescue StandardError
|
|
414
|
+
nil
|
|
415
|
+
end
|
|
416
|
+
@client.signal_cleanup_done(seq)
|
|
417
|
+
@mutex.synchronize { @cleanups.delete(handle) }
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# One frame, to the object it names. The target is looked up under
|
|
422
|
+
# @mutex and called WITHOUT it: `deliver` runs user code, and the
|
|
423
|
+
# callback is free to `on` or `off` anything on this connection, which
|
|
424
|
+
# comes straight back here as an `attach` or a `detach`.
|
|
425
|
+
#
|
|
426
|
+
# `__send__` rather than making `Events#deliver` public: delivering is
|
|
427
|
+
# not something a caller may do -- that a registered callback is the only
|
|
428
|
+
# way in is the whole claim of that class -- and a public `deliver` would
|
|
429
|
+
# say otherwise in the one place a user reads. The Dispatcher is the
|
|
430
|
+
# other half of the same feature, and this is the only place it reaches
|
|
431
|
+
# across.
|
|
432
|
+
def route(frame)
|
|
433
|
+
target = target_for(frame)
|
|
434
|
+
# A frame for a handle with no target was minted before the
|
|
435
|
+
# unsubscribe reached the bridge. Not delivered -- `off` means off --
|
|
436
|
+
# but still released by the ensure below, because the COM objects
|
|
437
|
+
# behind those handles would otherwise sit on the bridge until the
|
|
438
|
+
# connection closed.
|
|
439
|
+
target.__send__(:deliver, frame) if target
|
|
440
|
+
ensure
|
|
441
|
+
# The arguments are valid for the callback and no longer. Releasing in
|
|
442
|
+
# an ensure is what makes that true even when the frame never reached a
|
|
443
|
+
# callback at all -- a malformed `args` raises inside `deliver` before
|
|
444
|
+
# any callback is reached, and its handles would otherwise leak for the
|
|
445
|
+
# life of the connection.
|
|
446
|
+
release(frame)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# The frames of one event are released together, whether they reached a
|
|
450
|
+
# callback, reached one that raised, reached no target at all, or were
|
|
451
|
+
# still queued when the last target left. One statement of the rule --
|
|
452
|
+
# `route`'s ensure and the `:idle` arm both come here -- because "when is
|
|
453
|
+
# there something to give back" answered in two places is how the two
|
|
454
|
+
# answers drift apart.
|
|
455
|
+
def release(frame)
|
|
456
|
+
# `args: null` says the bridge minted NOTHING for this event: it is
|
|
457
|
+
# serialized as null rather than left out precisely so the client can
|
|
458
|
+
# tell that from "this event had zero arguments" (protocol.rs), and
|
|
459
|
+
# nothing is inserted in the bridge's event table for it. Sending a
|
|
460
|
+
# release anyway is a synchronous round trip from the dispatcher, which
|
|
461
|
+
# caps the event rate at one RTT -- in exactly the `args: false` case a
|
|
462
|
+
# caller reaches for to keep up with a high-frequency event.
|
|
463
|
+
return if !frame.is_a?(Hash) || frame['args'].nil?
|
|
464
|
+
|
|
465
|
+
@client.call('release_event', {seq: frame['seq']})
|
|
466
|
+
rescue StandardError
|
|
467
|
+
# The connection is going away; there is nothing left to release.
|
|
468
|
+
nil
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def target_for(item)
|
|
472
|
+
return nil unless item.is_a?(Hash)
|
|
473
|
+
|
|
474
|
+
@mutex.synchronize { @targets[item['handle']] }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Never raises. It is called from the dispatcher's own rescue, so an
|
|
478
|
+
# exception out of here is the one thing that could still end the thread.
|
|
479
|
+
# Routed to the object the frame names, so that an `on_error` registered
|
|
480
|
+
# on it is told; a frame that names nobody has no handler to reach.
|
|
481
|
+
def report(err, item)
|
|
482
|
+
target = target_for(item)
|
|
483
|
+
if target
|
|
484
|
+
target.__send__(:report, err, item)
|
|
485
|
+
else
|
|
486
|
+
warn "wineole: dispatcher raised #{err.class}: #{err.message}"
|
|
487
|
+
end
|
|
488
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
489
|
+
nil # even $stderr being gone must not end the dispatcher
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# The dispatcher's question when it reaches an :idle marker: is it still
|
|
493
|
+
# true that there is nothing to deliver to? Answering it under the same
|
|
494
|
+
# lock `attach` decides in is what makes "start a thread only if there is
|
|
495
|
+
# none" safe -- either this clears @thread first and `attach` starts a
|
|
496
|
+
# fresh one, or `attach` registers its target first and this thread keeps
|
|
497
|
+
# running.
|
|
498
|
+
#
|
|
499
|
+
# nil when the answer is no; otherwise everything still on the queue,
|
|
500
|
+
# drained HERE rather than by the caller. That is the whole point of it
|
|
501
|
+
# being here: clearing the thread slot lets an `attach` start a
|
|
502
|
+
# successor, and the successor's sink is installed under this same lock,
|
|
503
|
+
# so a queue emptied in the same critical section as the slot is cleared
|
|
504
|
+
# provably holds nothing of the successor's. Draining afterwards instead,
|
|
505
|
+
# outside the lock, is what let a departing thread pop a frame belonging
|
|
506
|
+
# to an object that had just attached and give it back rather than
|
|
507
|
+
# deliver it -- measured: two dispatcher threads alive at once, and a
|
|
508
|
+
# live subscription's event released, never delivered.
|
|
509
|
+
def confirm_idle
|
|
510
|
+
@mutex.synchronize do
|
|
511
|
+
# A registered cleanup counts the same as a target: the thread must
|
|
512
|
+
# stay to deliver the $cleanup frame it is waiting for.
|
|
513
|
+
next nil unless @targets.empty? && @cleanups.empty?
|
|
514
|
+
|
|
515
|
+
@thread = nil
|
|
516
|
+
drain_queue
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Everything on the queue, without blocking on it -- which is what makes
|
|
521
|
+
# it safe to call under @mutex. Whatever comes back was minted before the
|
|
522
|
+
# unsubscribe reached the bridge, with no callback left to hand it to.
|
|
523
|
+
def drain_queue
|
|
524
|
+
leftover = []
|
|
525
|
+
loop { leftover << @queue.pop(true) }
|
|
526
|
+
rescue ThreadError
|
|
527
|
+
leftover # the queue is empty, which is the only way out of that loop
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def thread_finished(thread)
|
|
531
|
+
@mutex.synchronize { @thread = nil if @thread.equal?(thread) }
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
data/lib/wineole/errors.rb
CHANGED
|
@@ -3,6 +3,12 @@ module WineOLE
|
|
|
3
3
|
class NotSerializableError < Error; end
|
|
4
4
|
class StaleReferenceError < Error; end
|
|
5
5
|
class ProtocolError < Error; end
|
|
6
|
+
# Raised by a call that arrived after the bridge decided this instance's
|
|
7
|
+
# root proxy is on its way out (a client `$cleanup` closure is running, or
|
|
8
|
+
# has already run). Distinguished from a generic RemoteError so a caller
|
|
9
|
+
# can rescue "this instance is closing" specifically rather than pattern-
|
|
10
|
+
# matching on RemoteError#remote_class.
|
|
11
|
+
class InstanceClosingError < Error; end
|
|
6
12
|
|
|
7
13
|
class RemoteError < Error
|
|
8
14
|
attr_reader :remote_class
|