winipc 0.1.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 +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/ext/winipc/extconf.rb +21 -0
- data/ext/winipc/winipc.c +1246 -0
- data/lib/winipc/version.rb +5 -0
- data/lib/winipc.rb +338 -0
- metadata +121 -0
data/lib/winipc.rb
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "winipc/version"
|
|
4
|
+
require "winipc/winipc" # native extension: classes + errors + K_* constants
|
|
5
|
+
|
|
6
|
+
# winipc — Windows local IPC for Ruby: named pipes, shared memory, and named
|
|
7
|
+
# synchronization objects, with a safe-by-default API.
|
|
8
|
+
#
|
|
9
|
+
# # named pipe, server side
|
|
10
|
+
# Winipc::Pipe.listen("myapp/control") do |server|
|
|
11
|
+
# conn = server.accept
|
|
12
|
+
# puts conn.read(1024)
|
|
13
|
+
# conn.write("ack")
|
|
14
|
+
# conn.close
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # client side (another process)
|
|
18
|
+
# Winipc::Pipe.connect("myapp/control") do |c|
|
|
19
|
+
# c.write("hello")
|
|
20
|
+
# c.read(1024)
|
|
21
|
+
# end
|
|
22
|
+
module Winipc
|
|
23
|
+
# ---- Win32 flag values (verified) --------------------------------------
|
|
24
|
+
PIPE_ACCESS_INBOUND = 0x00000001
|
|
25
|
+
PIPE_ACCESS_OUTBOUND = 0x00000002
|
|
26
|
+
PIPE_ACCESS_DUPLEX = 0x00000003
|
|
27
|
+
FILE_FLAG_OVERLAPPED = 0x40000000
|
|
28
|
+
PIPE_TYPE_MESSAGE = 0x00000004
|
|
29
|
+
PIPE_READMODE_MESSAGE = 0x00000002
|
|
30
|
+
PIPE_WAIT = 0x00000000
|
|
31
|
+
PIPE_REJECT_REMOTE_CLIENTS = 0x00000008
|
|
32
|
+
PIPE_UNLIMITED_INSTANCES = 255
|
|
33
|
+
GENERIC_READ = 0x80000000
|
|
34
|
+
GENERIC_WRITE = 0x40000000
|
|
35
|
+
|
|
36
|
+
# A Windows API failure carries the originating error code (GetLastError /
|
|
37
|
+
# the failing return), set on the exception in C.
|
|
38
|
+
class OSError
|
|
39
|
+
def code
|
|
40
|
+
@code
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Run a blocking native call cooperatively. Under a Fiber scheduler (e.g.
|
|
47
|
+
# winloop) the call is offloaded to a worker Thread so the calling fiber
|
|
48
|
+
# parks (Thread#value routes through the scheduler) and the event loop keeps
|
|
49
|
+
# serving other fibers; with no scheduler it runs inline (the C call already
|
|
50
|
+
# releases the GVL). On fiber unwind the worker is killed+joined so it can't
|
|
51
|
+
# leak or consume data destined for a later op.
|
|
52
|
+
#
|
|
53
|
+
# Caveat: if the fiber is unwound (e.g. Timeout) in the instant after the
|
|
54
|
+
# worker acquired a resource but before the value was delivered, that
|
|
55
|
+
# acquisition is lost — there is no generic way to hand it back (the same
|
|
56
|
+
# inherent limitation as a cancelled read that already pulled bytes).
|
|
57
|
+
def run_blocking
|
|
58
|
+
sched = Fiber.scheduler
|
|
59
|
+
return yield unless sched
|
|
60
|
+
|
|
61
|
+
worker = Thread.new do
|
|
62
|
+
Thread.current.report_on_exception = false
|
|
63
|
+
yield
|
|
64
|
+
end
|
|
65
|
+
begin
|
|
66
|
+
worker.value
|
|
67
|
+
ensure
|
|
68
|
+
if worker.alive?
|
|
69
|
+
worker.kill
|
|
70
|
+
worker.join
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Normalize a bare pipe name to the \\.\pipe\ namespace (pass a full
|
|
76
|
+
# \\server\pipe\name through unchanged).
|
|
77
|
+
def pipe_path(name)
|
|
78
|
+
s = name.to_s
|
|
79
|
+
s.start_with?("\\\\") ? s : "\\\\.\\pipe\\#{s}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Map a bare object name into the Local\ (default) or Global\ namespace.
|
|
83
|
+
def obj_path(name, scope)
|
|
84
|
+
prefix = case scope
|
|
85
|
+
when :local then "Local\\"
|
|
86
|
+
when :global then "Global\\"
|
|
87
|
+
else raise ArgumentError, "scope must be :local or :global, got #{scope.inspect}"
|
|
88
|
+
end
|
|
89
|
+
"#{prefix}#{name}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def server_access(direction)
|
|
93
|
+
case direction
|
|
94
|
+
when :duplex then PIPE_ACCESS_DUPLEX
|
|
95
|
+
when :inbound then PIPE_ACCESS_INBOUND
|
|
96
|
+
when :outbound then PIPE_ACCESS_OUTBOUND
|
|
97
|
+
else raise ArgumentError, "direction must be :duplex/:inbound/:outbound"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :server_access
|
|
101
|
+
|
|
102
|
+
def client_access(direction)
|
|
103
|
+
case direction
|
|
104
|
+
when :duplex then GENERIC_READ | GENERIC_WRITE
|
|
105
|
+
when :read then GENERIC_READ
|
|
106
|
+
when :write then GENERIC_WRITE
|
|
107
|
+
else raise ArgumentError, "direction must be :duplex/:read/:write"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
private_class_method :client_access
|
|
111
|
+
|
|
112
|
+
def ms_for(timeout)
|
|
113
|
+
return -1 if timeout.nil? # INFINITE (block until signaled / connected)
|
|
114
|
+
|
|
115
|
+
t = Float(timeout)
|
|
116
|
+
raise ArgumentError, "timeout must be non-negative, got #{timeout.inspect}" if t.negative?
|
|
117
|
+
|
|
118
|
+
ms = (t * 1000).round
|
|
119
|
+
# Never collapse a tiny-but-positive wait into a non-blocking poll.
|
|
120
|
+
ms.zero? && t.positive? ? 1 : ms
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Pipe
|
|
124
|
+
# Create a named-pipe server. Yields a Listener (auto-closed) if a block is
|
|
125
|
+
# given, else returns it.
|
|
126
|
+
def self.listen(name, mode: :byte, direction: :duplex, max_instances: :unlimited,
|
|
127
|
+
in_buffer: 65_536, out_buffer: 65_536, reject_remote: true, access: :owner)
|
|
128
|
+
open_mode = Winipc.send(:server_access, direction) | FILE_FLAG_OVERLAPPED
|
|
129
|
+
pipe_mode = PIPE_WAIT
|
|
130
|
+
pipe_mode |= PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE if mode == :message
|
|
131
|
+
pipe_mode |= PIPE_REJECT_REMOTE_CLIENTS if reject_remote
|
|
132
|
+
max = max_instances == :unlimited ? PIPE_UNLIMITED_INSTANCES : Integer(max_instances)
|
|
133
|
+
raise ArgumentError, "max_instances must be 1..255" unless (1..255).cover?(max)
|
|
134
|
+
|
|
135
|
+
listener = _listen(Winipc.pipe_path(name), open_mode, pipe_mode, max,
|
|
136
|
+
Integer(in_buffer), Integer(out_buffer), access)
|
|
137
|
+
return listener unless block_given?
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
yield listener
|
|
141
|
+
ensure
|
|
142
|
+
listener.close
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Connect to a named-pipe server. Yields a Conn (auto-closed) if a block is
|
|
147
|
+
# given, else returns it.
|
|
148
|
+
# +timeout+: nil fails fast if the server isn't connectable now; a number of
|
|
149
|
+
# seconds retries (server busy or not yet up) until it elapses.
|
|
150
|
+
def self.connect(name, mode: :byte, direction: :duplex, timeout: nil)
|
|
151
|
+
access = Winipc.send(:client_access, direction)
|
|
152
|
+
ms =
|
|
153
|
+
if timeout.nil?
|
|
154
|
+
0 # fail fast if the server isn't connectable now
|
|
155
|
+
else
|
|
156
|
+
t = Float(timeout)
|
|
157
|
+
raise ArgumentError, "timeout must be non-negative, got #{timeout.inspect}" if t.negative?
|
|
158
|
+
|
|
159
|
+
msr = (t * 1000).round
|
|
160
|
+
msr.zero? && t.positive? ? 1 : msr # a tiny positive timeout still waits, not fail-fast
|
|
161
|
+
end
|
|
162
|
+
conn = Winipc.run_blocking do
|
|
163
|
+
_connect(Winipc.pipe_path(name), access, mode == :message, ms)
|
|
164
|
+
end
|
|
165
|
+
return conn unless block_given?
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
yield conn
|
|
169
|
+
ensure
|
|
170
|
+
conn.close
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
class Listener
|
|
175
|
+
# Accept the next client. Blocks (cooperatively under a scheduler). With a
|
|
176
|
+
# timeout, returns nil if none connects in time.
|
|
177
|
+
def accept(timeout: nil)
|
|
178
|
+
Winipc.run_blocking { _accept(Winipc.ms_for(timeout)) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Accept clients in a loop, yielding (and closing) each connection.
|
|
182
|
+
def serve
|
|
183
|
+
loop do
|
|
184
|
+
conn = accept
|
|
185
|
+
break if conn.nil?
|
|
186
|
+
|
|
187
|
+
begin
|
|
188
|
+
yield conn
|
|
189
|
+
ensure
|
|
190
|
+
conn.close
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
class Conn
|
|
197
|
+
# Read up to +maxlen+ bytes (byte mode). Returns a binary String, or nil
|
|
198
|
+
# at clean EOF (peer closed).
|
|
199
|
+
def read(maxlen)
|
|
200
|
+
Winipc.run_blocking { _read(maxlen) }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Read one whole message (message mode), reassembling across the buffer.
|
|
204
|
+
# Returns a binary String or nil at EOF.
|
|
205
|
+
def read_message(maxlen = 65_536)
|
|
206
|
+
raise ModeError, "winipc: pipe is not in message mode" unless message?
|
|
207
|
+
|
|
208
|
+
Winipc.run_blocking { _read_message(maxlen) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Write +bytes+; returns the number of bytes written.
|
|
212
|
+
def write(bytes)
|
|
213
|
+
Winipc.run_blocking { _write(bytes) }
|
|
214
|
+
end
|
|
215
|
+
alias write_message write # in message mode each write is one message
|
|
216
|
+
|
|
217
|
+
def <<(bytes)
|
|
218
|
+
write(bytes)
|
|
219
|
+
self
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class SharedMemory
|
|
225
|
+
# Create a named, pagefile-backed shared-memory region of +size+ bytes.
|
|
226
|
+
def self.create(name, size, access: :readwrite, scope: :local, security: :owner)
|
|
227
|
+
_create(Winipc.obj_path(name, scope), Integer(size), access != :readonly, security)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Open an existing named region.
|
|
231
|
+
def self.open(name, access: :readwrite, scope: :local)
|
|
232
|
+
_open(Winipc.obj_path(name, scope), access != :readonly)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Flush a range (default: whole region) to backing storage.
|
|
236
|
+
def flush(offset = 0, len = nil)
|
|
237
|
+
_flush(offset, len)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class Mutex
|
|
242
|
+
def self.create(name, scope: :local, security: :owner)
|
|
243
|
+
_create(K_MUTEX, Winipc.obj_path(name, scope), nil, nil, security)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def self.open(name, scope: :local)
|
|
247
|
+
_open(K_MUTEX, Winipc.obj_path(name, scope))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# NOTE: a Windows mutex is owned by the acquiring THREAD, so Mutex waits are
|
|
251
|
+
# NOT offloaded to a worker (that would acquire on the wrong thread). They
|
|
252
|
+
# release the GVL but, under a fiber scheduler, block the loop for the wait.
|
|
253
|
+
# Use Event/Semaphore for fiber-cooperative coordination.
|
|
254
|
+
def lock(timeout: nil)
|
|
255
|
+
_wait(Winipc.ms_for(timeout)) != :timeout # :ok or :abandoned both acquire
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def try_lock
|
|
259
|
+
lock(timeout: 0)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def unlock
|
|
263
|
+
_unlock
|
|
264
|
+
self
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def synchronize(timeout: nil)
|
|
268
|
+
result = _wait(Winipc.ms_for(timeout))
|
|
269
|
+
raise TimeoutError, "winipc: mutex not acquired within timeout" if result == :timeout
|
|
270
|
+
|
|
271
|
+
begin
|
|
272
|
+
if result == :abandoned
|
|
273
|
+
raise Abandoned, "winipc: mutex was abandoned by a dead owner; shared state may be inconsistent"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
yield
|
|
277
|
+
ensure
|
|
278
|
+
_unlock
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class Event
|
|
284
|
+
def self.create(name, manual_reset: false, initial: false, scope: :local, security: :owner)
|
|
285
|
+
_create(K_EVENT, Winipc.obj_path(name, scope), manual_reset, initial, security)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.open(name, scope: :local)
|
|
289
|
+
_open(K_EVENT, Winipc.obj_path(name, scope))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Wait for the event to be signaled. Returns true, or false on timeout.
|
|
293
|
+
def wait(timeout: nil)
|
|
294
|
+
Winipc.run_blocking { _wait(Winipc.ms_for(timeout)) } == :ok
|
|
295
|
+
end
|
|
296
|
+
# #signal and #reset are defined in C.
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class Semaphore
|
|
300
|
+
def self.create(name, initial:, maximum:, scope: :local, security: :owner)
|
|
301
|
+
unless maximum.is_a?(Integer) && maximum > 0 && initial.is_a?(Integer) && initial >= 0 && initial <= maximum
|
|
302
|
+
raise ArgumentError, "require 0 <= initial <= maximum and maximum > 0"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
_create(K_SEM, Winipc.obj_path(name, scope), initial, maximum, security)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.open(name, scope: :local)
|
|
309
|
+
_open(K_SEM, Winipc.obj_path(name, scope))
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Acquire one unit. Returns true, or false on timeout.
|
|
313
|
+
def acquire(timeout: nil)
|
|
314
|
+
Winipc.run_blocking { _wait(Winipc.ms_for(timeout)) } == :ok
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def try_acquire
|
|
318
|
+
acquire(timeout: 0)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Release +count+ units; returns the previous count.
|
|
322
|
+
def release(count = 1)
|
|
323
|
+
_release(count)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Acquire one unit for the duration of the block, then release it. (Use
|
|
327
|
+
# #acquire / #release directly for multi-unit patterns.)
|
|
328
|
+
def synchronize(timeout: nil)
|
|
329
|
+
raise TimeoutError, "winipc: semaphore not acquired within timeout" unless acquire(timeout: timeout)
|
|
330
|
+
|
|
331
|
+
begin
|
|
332
|
+
yield
|
|
333
|
+
ensure
|
|
334
|
+
_release(1)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: winipc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ned
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: vcvars
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.1.1
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0.1'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.1.1
|
|
74
|
+
description: |
|
|
75
|
+
winipc is a native extension that exposes Windows local inter-process
|
|
76
|
+
communication through an ergonomic, safe-by-default Ruby API: duplex named
|
|
77
|
+
pipes (byte and message mode, with a server and a connect-with-retry client),
|
|
78
|
+
pagefile-backed shared memory via named file mappings, and named
|
|
79
|
+
synchronization objects (mutex, event, semaphore). Pipe handles are opened
|
|
80
|
+
for overlapped I/O so they cooperate with a fiber scheduler, and objects are
|
|
81
|
+
created with a restrictive security descriptor by default. Windows MSVC
|
|
82
|
+
(mswin) Ruby only.
|
|
83
|
+
executables: []
|
|
84
|
+
extensions:
|
|
85
|
+
- ext/winipc/extconf.rb
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- CHANGELOG.md
|
|
89
|
+
- LICENSE.txt
|
|
90
|
+
- README.md
|
|
91
|
+
- ext/winipc/extconf.rb
|
|
92
|
+
- ext/winipc/winipc.c
|
|
93
|
+
- lib/winipc.rb
|
|
94
|
+
- lib/winipc/version.rb
|
|
95
|
+
homepage: https://github.com/main-path/winipc
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/main-path/winipc
|
|
100
|
+
changelog_uri: https://github.com/main-path/winipc/blob/main/CHANGELOG.md
|
|
101
|
+
bug_tracker_uri: https://github.com/main-path/winipc/issues
|
|
102
|
+
rubygems_mfa_required: 'true'
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.0'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 3.6.9
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: 'Windows local IPC for Ruby: named pipes, shared memory, and named synchronization
|
|
120
|
+
objects.'
|
|
121
|
+
test_files: []
|