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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bdd93426e11cfe60fca09d51d9f12e32c5ea984767e18ec271a2ad6a471aba9
4
- data.tar.gz: a04c2be3c95c7283e06327bea2b4e7725f4794351ab21439a8593fc0c4798693
3
+ metadata.gz: 61ef777b8fbf1ff9dc08c3a446f761ed639d7bc2ac5e20e4c54264826be6c0bf
4
+ data.tar.gz: 919dfaaf8288d31e3e3b284c4b9d68d526a5530ee675ed4ca044807ca9f5d679
5
5
  SHA512:
6
- metadata.gz: 3cdd18036f72e3bd4d149c0fa71e9aab446cafa325936711bc2e5049584000431b9a224cd3c12cf508b6ac6d8c6f536039d4d86ea0adb94711dfc1f6f3e51dd2
7
- data.tar.gz: 149e5ffe74bab247cf3849137956b425424a75598e2c6d24c54b36ecdabb6a2b8462eb58e12a73e75c7f7d3ae9da1ba170b1192c6249be00829bf4fbd29806d0
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
@@ -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
- @next_id = 0
118
- @mutex = Mutex.new
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
- @mutex.synchronize do
124
- id = (@next_id += 1)
125
- @socket.write(JSON.generate({id: id, method: method, params: params}) + "\n")
126
- line = @socket.gets
127
- raise ProtocolError, 'connection closed' if line.nil?
128
- response = JSON.parse(line)
129
- unless response['id'] == id
130
- raise ProtocolError, "id mismatch: expected #{id}, got #{response['id']}"
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(response['error']['class'], response['error']['message']) if response['error']
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
- @socket.close
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