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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61ef777b8fbf1ff9dc08c3a446f761ed639d7bc2ac5e20e4c54264826be6c0bf
|
|
4
|
+
data.tar.gz: 919dfaaf8288d31e3e3b284c4b9d68d526a5530ee675ed4ca044807ca9f5d679
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0844ffb50787023e260ff9209d9362de0668fbebbc2d8538c3d3cbccbd0ac5db33ce4f88b62d0eaa8b11b7229d472d2f4b9cf2748516bc67a1583b103fdddb16'
|
|
7
|
+
data.tar.gz: c37ff15a332dd4f3e3a10b8e522d4e01a4a179838361c8c4eb2dcf0d39ada61cfdac0a2ee01d82e44af88508ec2e9af23d06fc1d19d6a606a44f0af11838b213
|
data/bin/wineole-vba
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
5
|
+
require 'wineole/msoffice/vba'
|
|
6
|
+
|
|
7
|
+
VBA = WineOLE::MSOffice::VBA
|
|
8
|
+
|
|
9
|
+
USAGE = <<~TEXT
|
|
10
|
+
usage: wineole-vba [status|enable|disable]
|
|
11
|
+
|
|
12
|
+
status show whether programmatic access to VBA projects is on
|
|
13
|
+
enable turn it on
|
|
14
|
+
disable turn it off
|
|
15
|
+
|
|
16
|
+
Excel reads this setting when it starts, so an Excel that is already
|
|
17
|
+
running keeps whatever it had. Restart it for a change to take effect.
|
|
18
|
+
|
|
19
|
+
This is a macro security setting. Turning it on lets any Office
|
|
20
|
+
automation on this machine reach VBA projects, not just this library.
|
|
21
|
+
TEXT
|
|
22
|
+
|
|
23
|
+
def report(state)
|
|
24
|
+
case state
|
|
25
|
+
when :enabled then puts 'VBA project access: enabled'
|
|
26
|
+
when :disabled then puts 'VBA project access: disabled'
|
|
27
|
+
when :unset then puts 'VBA project access: disabled (the registry value is not set)'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
case ARGV[0]
|
|
32
|
+
when nil, 'status'
|
|
33
|
+
report(VBA.state)
|
|
34
|
+
when 'enable', 'disable'
|
|
35
|
+
ok = ARGV[0] == 'enable' ? VBA.enable! : VBA.disable!
|
|
36
|
+
unless ok
|
|
37
|
+
warn "wineole-vba: could not write the registry (is wine on PATH?)"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
report(VBA.state)
|
|
41
|
+
puts 'Restart Excel for this to take effect -- it reads the setting at startup.'
|
|
42
|
+
else
|
|
43
|
+
warn USAGE
|
|
44
|
+
exit 2
|
|
45
|
+
end
|
data/lib/wineole/client.rb
CHANGED
|
@@ -2,8 +2,11 @@ require 'socket'
|
|
|
2
2
|
require 'json'
|
|
3
3
|
require 'rbconfig'
|
|
4
4
|
require 'tmpdir'
|
|
5
|
+
require 'ipaddr'
|
|
6
|
+
require 'weakref'
|
|
5
7
|
require_relative 'errors'
|
|
6
8
|
require_relative 'proxy'
|
|
9
|
+
require_relative 'dispatcher'
|
|
7
10
|
|
|
8
11
|
module WineOLE
|
|
9
12
|
class Client
|
|
@@ -91,7 +94,14 @@ module WineOLE
|
|
|
91
94
|
private_class_method :handshake
|
|
92
95
|
|
|
93
96
|
def self.try_connect(host, port)
|
|
94
|
-
TCPSocket.new(host, port)
|
|
97
|
+
socket = TCPSocket.new(host, port)
|
|
98
|
+
# Mirrors the bridge's own set_nodelay (main.rs). Insurance only: the
|
|
99
|
+
# ~40 ms per-RPC stall this project hit was on the response side, and
|
|
100
|
+
# requests already go out in a single write, so this changes nothing
|
|
101
|
+
# today -- it keeps a future multi-write request path from
|
|
102
|
+
# reintroducing it.
|
|
103
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
104
|
+
socket
|
|
95
105
|
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH
|
|
96
106
|
nil
|
|
97
107
|
end
|
|
@@ -114,40 +124,464 @@ module WineOLE
|
|
|
114
124
|
|
|
115
125
|
def initialize(socket)
|
|
116
126
|
@socket = socket
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
# The one strong reference to the event sinks; see `on_event`. Written
|
|
128
|
+
# there and never read -- the reader thread reaches the sinks through
|
|
129
|
+
# the Mailbox's weak references instead, so this list is what keeps them
|
|
130
|
+
# alive. Delete it as dead code and a plain `client.on_event { }` keeps
|
|
131
|
+
# working only until the next garbage collection, after which the
|
|
132
|
+
# connection silently stops delivering events. The guard is
|
|
133
|
+
# ClientEventsTest#test_a_registered_consumer_survives_a_garbage_collection.
|
|
134
|
+
@event_sinks = []
|
|
135
|
+
@event_sinks_mutex = Mutex.new
|
|
136
|
+
# Eagerly, and deliberately: a Dispatcher is a Hash, a Queue and two
|
|
137
|
+
# nil slots until something attaches to it -- no thread, no sink, no
|
|
138
|
+
# socket traffic -- so building it here costs less than the lock that
|
|
139
|
+
# building it lazily would need, on a connection that may have several
|
|
140
|
+
# objects registering callbacks at once.
|
|
141
|
+
@dispatcher = Dispatcher.new(self)
|
|
142
|
+
@mailbox = Mailbox.new(socket).start
|
|
143
|
+
# Backs await_cleanup/signal_cleanup_done -- see CleanupWaiters below.
|
|
144
|
+
@cleanup_waiters = CleanupWaiters.new
|
|
119
145
|
ObjectSpace.define_finalizer(self, self.class.finalizer(socket))
|
|
120
146
|
end
|
|
121
147
|
|
|
148
|
+
# This connection's ONE dispatcher: the thread every callback on it runs
|
|
149
|
+
# on, in arrival order, one at a time. Per connection rather than per
|
|
150
|
+
# object because that is the promise the README makes to a caller who
|
|
151
|
+
# shares state between an Application callback and a Workbook callback --
|
|
152
|
+
# they can never be inside their blocks at the same time, so no lock of
|
|
153
|
+
# their own is needed. See Dispatcher.
|
|
154
|
+
#
|
|
155
|
+
# This Client and its Dispatcher hold each other, and an attached Events
|
|
156
|
+
# holds this Client back through the Dispatcher's target table. That ring
|
|
157
|
+
# is collected as a ring: no thread ever holds a strong reference to any
|
|
158
|
+
# of it across a park, so the whole connection is still collectible with
|
|
159
|
+
# callbacks registered on it -- which is what the finalizer above needs
|
|
160
|
+
# to be true if it is ever to close the socket.
|
|
161
|
+
attr_reader :dispatcher
|
|
162
|
+
|
|
163
|
+
# Register a consumer of server-initiated frames (those with no `id`).
|
|
164
|
+
# `Events` is the consumer. The block is called INLINE on the reader
|
|
165
|
+
# thread, which is the only thread reading this socket: the reader runs
|
|
166
|
+
# nothing but the hand-off it is given here, and the hand-off must neither
|
|
167
|
+
# block nor raise. A block that waited would stall every response on the
|
|
168
|
+
# connection; one that made a COM call of its own would leave nobody to
|
|
169
|
+
# read the answer and deadlock against itself. Enqueue and return.
|
|
170
|
+
#
|
|
171
|
+
# The block is also called once with `nil` when the stream ends, so a
|
|
172
|
+
# consumer parked on a queue can finish instead of blocking on it forever
|
|
173
|
+
# -- including when it registers after the stream has already ended, in
|
|
174
|
+
# which case it is handed `nil` right here.
|
|
175
|
+
#
|
|
176
|
+
# APPENDS, never replaces. A replacing registration would silently switch
|
|
177
|
+
# an earlier consumer off. The events feature puts up exactly one sink
|
|
178
|
+
# here for the whole connection (Dispatcher#attach) and routes by handle
|
|
179
|
+
# on the dispatcher thread, but nothing about this method assumes that:
|
|
180
|
+
# anything else on the connection can register its own consumer and must
|
|
181
|
+
# not be switched off by the events feature arming itself, or the other
|
|
182
|
+
# way round.
|
|
183
|
+
#
|
|
184
|
+
# A sink lives exactly as long as this Client. The strong reference is
|
|
185
|
+
# held HERE rather than in the Mailbox, because the running reader thread
|
|
186
|
+
# pins the Mailbox: a sink almost always closes over the object that owns
|
|
187
|
+
# it, and that object almost always holds this Client, so a strong
|
|
188
|
+
# reference from the Mailbox would pin the Client too -- no Client could
|
|
189
|
+
# be collected, its finalizer would never run and its socket would stay
|
|
190
|
+
# open for the life of the process, which is the very leak the Mailbox was
|
|
191
|
+
# extracted to fix. The Mailbox keeps a weak reference and can therefore
|
|
192
|
+
# reach a sink without keeping it (or its Client) alive. Nothing is lost:
|
|
193
|
+
# a collected Client has already had its socket closed by its finalizer,
|
|
194
|
+
# so there are no further events to deliver.
|
|
195
|
+
def on_event(&block)
|
|
196
|
+
@event_sinks_mutex.synchronize { @event_sinks << block }
|
|
197
|
+
@mailbox.on_event(&block)
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The way back out, and the reason it exists: a sink registered for the
|
|
202
|
+
# life of the connection is a consumer that cannot be dismantled. A
|
|
203
|
+
# connection whose last callback has been removed would otherwise keep an
|
|
204
|
+
# entry here (holding the Dispatcher, its target table and its parked
|
|
205
|
+
# thread alive) and an entry in the Mailbox that the reader walks for
|
|
206
|
+
# every frame -- so a connection would go on paying for events after
|
|
207
|
+
# every object on it had stopped listening. Measured on the code before
|
|
208
|
+
# any of this came down: 50 proxies that registered one callback and
|
|
209
|
+
# removed it left 51 live threads and 50 sink entries.
|
|
210
|
+
#
|
|
211
|
+
# Identity, not equality: two consumers can be `==` without being the same
|
|
212
|
+
# registration, and removing the wrong one silently stops a live consumer.
|
|
213
|
+
def off_event(block)
|
|
214
|
+
@event_sinks_mutex.synchronize { @event_sinks.reject! { |sink| sink.equal?(block) } }
|
|
215
|
+
@mailbox.off_event(block)
|
|
216
|
+
self
|
|
217
|
+
end
|
|
218
|
+
|
|
122
219
|
def call(method, params = {})
|
|
123
|
-
@
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
220
|
+
response = @mailbox.request(method, params)
|
|
221
|
+
# A sentinel, not a frame: the end of the stream is something that
|
|
222
|
+
# happened here, and saying so in the shape of a wire error would make
|
|
223
|
+
# it indistinguishable from one the bridge really sent -- a bridge that
|
|
224
|
+
# ever reported a WineOLE::ProtocolError of its own would have had it
|
|
225
|
+
# rewritten into a local one.
|
|
226
|
+
raise ProtocolError, 'connection closed' if response.equal?(Mailbox::CLOSED)
|
|
227
|
+
|
|
228
|
+
if response['error']
|
|
229
|
+
klass = response['error']['class']
|
|
230
|
+
# WineOLE::InstanceClosingError is the one remote error class this
|
|
231
|
+
# client resolves to its own local class rather than wrapping in a
|
|
232
|
+
# generic RemoteError -- so a caller can rescue it directly instead
|
|
233
|
+
# of pattern-matching on RemoteError#remote_class.
|
|
234
|
+
if klass == 'WineOLE::InstanceClosingError'
|
|
235
|
+
raise InstanceClosingError, response['error']['message']
|
|
131
236
|
end
|
|
132
|
-
raise RemoteError.new(
|
|
133
|
-
response['result']
|
|
237
|
+
raise RemoteError.new(klass, response['error']['message'])
|
|
134
238
|
end
|
|
239
|
+
response['result']
|
|
135
240
|
end
|
|
136
241
|
|
|
137
242
|
def close
|
|
138
|
-
@
|
|
243
|
+
@mailbox.close
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Blocks until the dispatcher finishes the $cleanup for `seq` (the
|
|
247
|
+
# client closure, then the release_event that follows it). If the
|
|
248
|
+
# caller IS the dispatcher thread -- `ole_release` called from inside a
|
|
249
|
+
# callback -- do not wait: the $cleanup frame is queued behind the
|
|
250
|
+
# current callback and will only run after it returns, so waiting here
|
|
251
|
+
# would deadlock the dispatcher against itself.
|
|
252
|
+
def await_cleanup(seq)
|
|
253
|
+
return if on_dispatcher_thread?
|
|
254
|
+
|
|
255
|
+
@cleanup_waiters.await(seq)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Called by the dispatcher once it has finished delivering $cleanup
|
|
259
|
+
# `seq` (Task 8), to release whatever thread is parked in `await_cleanup`
|
|
260
|
+
# for it.
|
|
261
|
+
def signal_cleanup_done(seq)
|
|
262
|
+
@cleanup_waiters.signal(seq)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Is the calling thread this connection's own dispatcher thread?
|
|
266
|
+
def on_dispatcher_thread?
|
|
267
|
+
@dispatcher.on_thread?(Thread.current)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# The socket, the waiter table and the event sinks -- everything the
|
|
271
|
+
# reader thread shares with the calling threads.
|
|
272
|
+
#
|
|
273
|
+
# This is a separate object for a garbage-collection reason, not a
|
|
274
|
+
# tidiness one. A block captures its whole binding, `self` included, so a
|
|
275
|
+
# `Thread.new { read_loop }` written inside `Client#initialize` would make
|
|
276
|
+
# the running reader thread a permanent GC root for the Client: no Client
|
|
277
|
+
# could ever be collected, the ObjectSpace finalizer above would never
|
|
278
|
+
# run, and its socket would stay open for the life of the process. The
|
|
279
|
+
# thread is started from inside a Mailbox instead, so what it pins is this
|
|
280
|
+
# object -- which holds no reference back to the Client, not even through
|
|
281
|
+
# the event sinks: those it reaches only weakly (see Client#on_event).
|
|
282
|
+
class Mailbox
|
|
283
|
+
# Handed to a waiter when the stream ends. An object, not a frame:
|
|
284
|
+
# nothing the bridge can send is `equal?` to it, so `Client#call` can
|
|
285
|
+
# tell "this connection is over" from anything the bridge reported.
|
|
286
|
+
CLOSED = Object.new.freeze
|
|
287
|
+
|
|
288
|
+
def initialize(socket)
|
|
289
|
+
@socket = socket
|
|
290
|
+
@next_id = 0
|
|
291
|
+
# Guards the socket WRITE and the bookkeeping below -- never a wait
|
|
292
|
+
# for a response. Holding a lock across the round trip is what made a
|
|
293
|
+
# COM call from inside an event callback impossible to even send while
|
|
294
|
+
# the main thread was waiting.
|
|
295
|
+
@mutex = Mutex.new
|
|
296
|
+
@waiters = {}
|
|
297
|
+
# WeakRefs, and deliberately so: the Client owns the sinks (see
|
|
298
|
+
# Client#on_event). This list only lets the reader thread reach them.
|
|
299
|
+
@event_sinks = []
|
|
300
|
+
@closed = false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def start
|
|
304
|
+
@reader = Thread.new { read_loop }
|
|
305
|
+
@reader.abort_on_exception = false
|
|
306
|
+
self
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def on_event(&block)
|
|
310
|
+
closed = @mutex.synchronize do
|
|
311
|
+
# A sink registered on a dead connection is not registered at all:
|
|
312
|
+
# the reader is gone, no frame can ever reach it, and leaving it in
|
|
313
|
+
# the list would only let a `fail_all_waiters` still in flight hand
|
|
314
|
+
# it a second end-of-stream.
|
|
315
|
+
@event_sinks << WeakRef.new(block) unless @closed
|
|
316
|
+
@closed
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Outside the mutex, because this runs user code on the CALLER's
|
|
320
|
+
# thread: a consumer that closed the client from its end-of-stream
|
|
321
|
+
# branch would otherwise deadlock on a lock this method still held.
|
|
322
|
+
# Without this hand-off a consumer that attached after the bridge died
|
|
323
|
+
# -- user code putting a handler on an existing proxy -- would never be
|
|
324
|
+
# told the stream had ended, and its dispatcher thread would park on an
|
|
325
|
+
# empty queue for the life of the process.
|
|
326
|
+
deliver(block, nil) if closed
|
|
327
|
+
self
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Drops one sink, and every dead weak reference met on the way past --
|
|
331
|
+
# the reader only prunes those when it delivers, and a connection that
|
|
332
|
+
# is quiet between registrations would otherwise keep them.
|
|
333
|
+
def off_event(block)
|
|
334
|
+
@mutex.synchronize do
|
|
335
|
+
@event_sinks.reject! do |ref|
|
|
336
|
+
sink = begin
|
|
337
|
+
ref.__getobj__
|
|
338
|
+
rescue WeakRef::RefError
|
|
339
|
+
nil
|
|
340
|
+
end
|
|
341
|
+
sink.nil? || sink.equal?(block)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
self
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Sends one request and blocks this thread -- and only this thread --
|
|
348
|
+
# until the reader routes the matching response back.
|
|
349
|
+
def request(method, params)
|
|
350
|
+
slot = Waiter.new
|
|
351
|
+
id = nil
|
|
352
|
+
@mutex.synchronize do
|
|
353
|
+
raise ProtocolError, 'connection closed' if @closed
|
|
354
|
+
|
|
355
|
+
id = (@next_id += 1)
|
|
356
|
+
@waiters[id] = slot
|
|
357
|
+
@socket.write(JSON.generate({id: id, method: method, params: params}) + "\n")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
slot.take
|
|
361
|
+
ensure
|
|
362
|
+
@mutex.synchronize { @waiters.delete(id) } if id
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def close
|
|
366
|
+
@mutex.synchronize { @closed = true }
|
|
367
|
+
@socket.close
|
|
368
|
+
# A sink runs on the reader thread, so a `close` called from inside one
|
|
369
|
+
# would have that thread join itself -- ThreadError, raised out of the
|
|
370
|
+
# one method whose whole job is to shut the connection down cleanly.
|
|
371
|
+
# The reader is on its way out anyway: the socket above is closed, so
|
|
372
|
+
# its next read ends the loop.
|
|
373
|
+
@reader.join(2) if @reader && @reader != Thread.current
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
def read_loop
|
|
379
|
+
while (line = @socket.gets)
|
|
380
|
+
frame = begin
|
|
381
|
+
JSON.parse(line)
|
|
382
|
+
rescue JSON::ParserError
|
|
383
|
+
next
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# `null`, `123` and `[]` are all valid JSON and none of them is a
|
|
387
|
+
# frame. Skipped exactly like an unparseable line: asking a
|
|
388
|
+
# non-Hash for `key?` would raise NoMethodError, which is not in
|
|
389
|
+
# this loop's rescue list, so one such line would kill the reader
|
|
390
|
+
# and every later call on the connection with it.
|
|
391
|
+
next unless frame.is_a?(Hash)
|
|
392
|
+
|
|
393
|
+
if frame.key?('id')
|
|
394
|
+
slot = @mutex.synchronize { @waiters[frame['id']] }
|
|
395
|
+
slot&.fill(frame)
|
|
396
|
+
else
|
|
397
|
+
# Handed off, never run here.
|
|
398
|
+
dispatch_to_sinks(frame)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
rescue IOError, Errno::EBADF, Errno::ECONNRESET
|
|
402
|
+
nil
|
|
403
|
+
ensure
|
|
404
|
+
fail_all_waiters
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# A waiter that is never woken waits forever, so EOF has to reach every
|
|
408
|
+
# one of them.
|
|
409
|
+
def fail_all_waiters
|
|
410
|
+
pending = @mutex.synchronize do
|
|
411
|
+
@closed = true
|
|
412
|
+
@waiters.values.tap { @waiters.clear }
|
|
413
|
+
end
|
|
414
|
+
pending.each { |w| w.fill(CLOSED) }
|
|
415
|
+
|
|
416
|
+
# Tell every event consumer the stream is over, so its dispatcher
|
|
417
|
+
# thread can finish instead of blocking on an empty queue forever.
|
|
418
|
+
# Without this each Events leaks a thread for the life of the process.
|
|
419
|
+
dispatch_to_sinks(nil)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The sinks are copied out under the mutex and called WITHOUT it. Holding
|
|
423
|
+
# it across the dispatch would make a call issued from inside an event
|
|
424
|
+
# consumer unable to even reach the wire -- the reader would be holding
|
|
425
|
+
# the very lock that guards the socket write, on the very thread the
|
|
426
|
+
# consumer runs on.
|
|
427
|
+
def dispatch_to_sinks(frame)
|
|
428
|
+
live_sinks.each { |sink| deliver(sink, frame) }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# The sinks that are still alive, resolved from their weak references,
|
|
432
|
+
# dropping the ones whose Client has been collected.
|
|
433
|
+
def live_sinks
|
|
434
|
+
@mutex.synchronize do
|
|
435
|
+
live = []
|
|
436
|
+
@event_sinks.select! do |ref|
|
|
437
|
+
sink = begin
|
|
438
|
+
ref.__getobj__
|
|
439
|
+
rescue WeakRef::RefError
|
|
440
|
+
nil
|
|
441
|
+
end
|
|
442
|
+
live << sink if sink
|
|
443
|
+
!sink.nil?
|
|
444
|
+
end
|
|
445
|
+
live
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# One misbehaving consumer must not take the connection down with it.
|
|
450
|
+
# The sinks share a single reader thread, so an exception raised out of
|
|
451
|
+
# one of them would end the read loop -- every other consumer on this
|
|
452
|
+
# connection would stop seeing events, and every later call would fail
|
|
453
|
+
# with "connection closed". Reported rather than swallowed: a sink that
|
|
454
|
+
# raises is a bug in the sink.
|
|
455
|
+
def deliver(sink, frame)
|
|
456
|
+
sink.call(frame)
|
|
457
|
+
rescue StandardError => e
|
|
458
|
+
warn "wineole: event consumer raised #{e.class}: #{e.message}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# One response, handed from the reader thread to the caller.
|
|
462
|
+
class Waiter
|
|
463
|
+
def initialize
|
|
464
|
+
@mutex = Mutex.new
|
|
465
|
+
@cond = ConditionVariable.new
|
|
466
|
+
@value = nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# First write wins. The reader fills a waiter and wakes its caller, but
|
|
470
|
+
# the caller has not yet re-acquired the Mailbox mutex to delete itself
|
|
471
|
+
# from the table; if the read loop ends in that window, the sweep would
|
|
472
|
+
# otherwise overwrite the answer the bridge really sent with "connection
|
|
473
|
+
# closed" and the caller would raise for a request that succeeded. It
|
|
474
|
+
# also hardens the nil sentinel `take` waits on: nothing can take a
|
|
475
|
+
# value back once it is there.
|
|
476
|
+
def fill(value)
|
|
477
|
+
@mutex.synchronize do
|
|
478
|
+
return unless @value.nil?
|
|
479
|
+
|
|
480
|
+
@value = value
|
|
481
|
+
@cond.broadcast
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def take
|
|
486
|
+
@mutex.synchronize do
|
|
487
|
+
@cond.wait(@mutex) while @value.nil?
|
|
488
|
+
@value
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# The await/signal handshake behind Client#await_cleanup and
|
|
495
|
+
# #signal_cleanup_done, pulled out of Client so it can be unit-tested
|
|
496
|
+
# without a live connection -- building a real Client for this would
|
|
497
|
+
# open a socket, and the coordination itself has nothing to do with the
|
|
498
|
+
# wire. One mutex guards a ConditionVariable per in-flight `seq` and the
|
|
499
|
+
# set of `seq`s the dispatcher has already finished -- the same shape as
|
|
500
|
+
# Mailbox::Waiter above, generalized from one outstanding key to many.
|
|
501
|
+
class CleanupWaiters
|
|
502
|
+
# How long `await` will wait for a `seq` that never gets signalled
|
|
503
|
+
# before giving up and returning anyway. A caller stuck here forever
|
|
504
|
+
# because a bridge or a dispatcher died mid-cleanup would be a worse
|
|
505
|
+
# failure than one that eventually gets control back, even if the
|
|
506
|
+
# instance's fate at that point is unknown.
|
|
507
|
+
TIMEOUT = 30
|
|
508
|
+
|
|
509
|
+
def initialize
|
|
510
|
+
@mutex = Mutex.new
|
|
511
|
+
@conds = {}
|
|
512
|
+
@done = {}
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Blocks the calling thread until `signal(seq)` is called from another
|
|
516
|
+
# thread, or TIMEOUT seconds pass, whichever comes first. Returns
|
|
517
|
+
# immediately, without waiting at all, when `seq` was already
|
|
518
|
+
# signalled before this call started -- the same "first write wins,
|
|
519
|
+
# a late arrival still sees it" shape as Mailbox::Waiter.
|
|
520
|
+
def await(seq)
|
|
521
|
+
@mutex.synchronize do
|
|
522
|
+
cond = (@conds[seq] ||= ConditionVariable.new)
|
|
523
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TIMEOUT
|
|
524
|
+
until @done[seq]
|
|
525
|
+
left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
526
|
+
break if left <= 0
|
|
527
|
+
|
|
528
|
+
cond.wait(@mutex, left)
|
|
529
|
+
end
|
|
530
|
+
# Cleared on the way out so a `seq` (sequence numbers are not
|
|
531
|
+
# reused) never accumulates an entry once nobody can still be
|
|
532
|
+
# waiting on it.
|
|
533
|
+
@done.delete(seq)
|
|
534
|
+
@conds.delete(seq)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def signal(seq)
|
|
539
|
+
@mutex.synchronize do
|
|
540
|
+
@done[seq] = true
|
|
541
|
+
@conds[seq]&.broadcast
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Is the bridge on the other end of this connection reachable only through
|
|
547
|
+
# the loopback interface -- i.e. the same machine?
|
|
548
|
+
#
|
|
549
|
+
# Deliberately the same test the bridge itself uses to decide whether a
|
|
550
|
+
# token is required (`peer_addr().ip().is_loopback()` in main.rs). Anything
|
|
551
|
+
# that keys off "is this local" -- path conversion in the Office wrapper,
|
|
552
|
+
# for one -- must agree with the bridge, or a connection ends up remote for
|
|
553
|
+
# authentication and local for everything else.
|
|
554
|
+
#
|
|
555
|
+
# `IPAddr#loopback?` covers all of 127.0.0.0/8 and ::1, matching Rust's
|
|
556
|
+
# `IpAddr::is_loopback`. A host's own NIC address is NOT loopback, and that
|
|
557
|
+
# is intended: it is a different machine as far as this boundary cares.
|
|
558
|
+
def loopback?
|
|
559
|
+
# The `false` pins peeraddr's reverse-lookup behaviour, which otherwise
|
|
560
|
+
# follows `BasicSocket.do_not_reverse_lookup` -- a process-wide mutable
|
|
561
|
+
# default owned by whatever application embeds this library, not by
|
|
562
|
+
# this method. Index [3] is the numeric address regardless of the
|
|
563
|
+
# setting, but without `false` a host app that flips that global
|
|
564
|
+
# triggers an OS-level reverse DNS/PTR lookup as a side effect of
|
|
565
|
+
# computing a tuple whose other elements are discarded here. That
|
|
566
|
+
# lookup is slowest exactly where it matters: a loopback PTR resolves
|
|
567
|
+
# instantly from /etc/hosts, while a remote peer's can hang on an
|
|
568
|
+
# unreachable or misconfigured resolver -- blocking the very check
|
|
569
|
+
# meant to tell loopback from non-loopback peers.
|
|
570
|
+
IPAddr.new(@socket.peeraddr(false)[3]).loopback?
|
|
571
|
+
rescue StandardError
|
|
572
|
+
false
|
|
139
573
|
end
|
|
140
574
|
|
|
141
|
-
def create(class_name)
|
|
142
|
-
Proxy.create(class_name, self)
|
|
575
|
+
def create(class_name, cleanup: nil)
|
|
576
|
+
Proxy.create(class_name, self, cleanup: cleanup)
|
|
143
577
|
end
|
|
144
578
|
|
|
145
|
-
def connect(class_name)
|
|
146
|
-
Proxy.connect(class_name, self)
|
|
579
|
+
def connect(class_name, cleanup: nil)
|
|
580
|
+
Proxy.connect(class_name, self, cleanup: cleanup)
|
|
147
581
|
end
|
|
148
582
|
|
|
149
|
-
def connect_or_create(class_name)
|
|
150
|
-
Proxy.connect_or_create(class_name, self)
|
|
583
|
+
def connect_or_create(class_name, cleanup: nil)
|
|
584
|
+
Proxy.connect_or_create(class_name, self, cleanup: cleanup)
|
|
151
585
|
end
|
|
152
586
|
end
|
|
153
587
|
end
|