omq 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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/LICENSE +15 -0
  4. data/README.md +145 -0
  5. data/lib/omq/pair.rb +13 -0
  6. data/lib/omq/pub_sub.rb +77 -0
  7. data/lib/omq/push_pull.rb +21 -0
  8. data/lib/omq/req_rep.rb +23 -0
  9. data/lib/omq/router_dealer.rb +36 -0
  10. data/lib/omq/socket.rb +178 -0
  11. data/lib/omq/version.rb +5 -0
  12. data/lib/omq/zmtp/codec/command.rb +207 -0
  13. data/lib/omq/zmtp/codec/frame.rb +104 -0
  14. data/lib/omq/zmtp/codec/greeting.rb +96 -0
  15. data/lib/omq/zmtp/codec.rb +18 -0
  16. data/lib/omq/zmtp/connection.rb +233 -0
  17. data/lib/omq/zmtp/engine.rb +339 -0
  18. data/lib/omq/zmtp/mechanism/null.rb +70 -0
  19. data/lib/omq/zmtp/options.rb +57 -0
  20. data/lib/omq/zmtp/reactor.rb +142 -0
  21. data/lib/omq/zmtp/readable.rb +29 -0
  22. data/lib/omq/zmtp/routing/dealer.rb +57 -0
  23. data/lib/omq/zmtp/routing/fan_out.rb +89 -0
  24. data/lib/omq/zmtp/routing/pair.rb +68 -0
  25. data/lib/omq/zmtp/routing/pub.rb +62 -0
  26. data/lib/omq/zmtp/routing/pull.rb +48 -0
  27. data/lib/omq/zmtp/routing/push.rb +57 -0
  28. data/lib/omq/zmtp/routing/rep.rb +83 -0
  29. data/lib/omq/zmtp/routing/req.rb +70 -0
  30. data/lib/omq/zmtp/routing/round_robin.rb +69 -0
  31. data/lib/omq/zmtp/routing/router.rb +88 -0
  32. data/lib/omq/zmtp/routing/sub.rb +80 -0
  33. data/lib/omq/zmtp/routing/xpub.rb +74 -0
  34. data/lib/omq/zmtp/routing/xsub.rb +80 -0
  35. data/lib/omq/zmtp/routing.rb +38 -0
  36. data/lib/omq/zmtp/transport/inproc.rb +299 -0
  37. data/lib/omq/zmtp/transport/ipc.rb +114 -0
  38. data/lib/omq/zmtp/transport/tcp.rb +98 -0
  39. data/lib/omq/zmtp/valid_peers.rb +21 -0
  40. data/lib/omq/zmtp/writable.rb +44 -0
  41. data/lib/omq/zmtp.rb +47 -0
  42. data/lib/omq.rb +19 -0
  43. metadata +110 -0
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # SUB socket routing: subscription-based receive from PUB peers.
7
+ #
8
+ # Sends SUBSCRIBE/CANCEL commands to connected PUB peers.
9
+ #
10
+ class Sub
11
+
12
+ # @param engine [Engine]
13
+ #
14
+ def initialize(engine)
15
+ @engine = engine
16
+ @connections = []
17
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
+ @subscriptions = Set.new
19
+ @tasks = []
20
+ end
21
+
22
+ # @return [Async::LimitedQueue]
23
+ #
24
+ attr_reader :recv_queue
25
+
26
+ # @param connection [Connection]
27
+ #
28
+ def connection_added(connection)
29
+ @connections << connection
30
+ # Send existing subscriptions to new peer
31
+ @subscriptions.each do |prefix|
32
+ connection.send_command(Codec::Command.subscribe(prefix))
33
+ end
34
+ task = @engine.start_recv_pump(connection, @recv_queue)
35
+ @tasks << task if task
36
+ end
37
+
38
+ # @param connection [Connection]
39
+ #
40
+ def connection_removed(connection)
41
+ @connections.delete(connection)
42
+ end
43
+
44
+ # SUB is read-only.
45
+ #
46
+ def enqueue(_parts)
47
+ raise "SUB sockets cannot send"
48
+ end
49
+
50
+ # Subscribes to a topic prefix.
51
+ #
52
+ # @param prefix [String]
53
+ #
54
+ def subscribe(prefix)
55
+ @subscriptions << prefix
56
+ @connections.each do |conn|
57
+ conn.send_command(Codec::Command.subscribe(prefix))
58
+ end
59
+ end
60
+
61
+ # Unsubscribes from a topic prefix.
62
+ #
63
+ # @param prefix [String]
64
+ #
65
+ def unsubscribe(prefix)
66
+ @subscriptions.delete(prefix)
67
+ @connections.each do |conn|
68
+ conn.send_command(Codec::Command.cancel(prefix))
69
+ end
70
+ end
71
+
72
+ def stop
73
+ @tasks.each(&:stop)
74
+ @tasks.clear
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # XPUB socket routing: like PUB but exposes subscription messages.
7
+ #
8
+ # Subscription/unsubscription messages from peers are delivered to
9
+ # the application as data frames: \x01 + prefix for subscribe,
10
+ # \x00 + prefix for unsubscribe.
11
+ #
12
+ class XPub
13
+ include FanOut
14
+
15
+ # @param engine [Engine]
16
+ #
17
+ def initialize(engine)
18
+ @engine = engine
19
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
20
+ @tasks = []
21
+ init_fan_out(engine)
22
+ end
23
+
24
+ # @return [Async::LimitedQueue]
25
+ #
26
+ attr_reader :recv_queue, :send_queue
27
+
28
+ # @param connection [Connection]
29
+ #
30
+ def connection_added(connection)
31
+ @connections << connection
32
+ @subscriptions[connection] = Set.new
33
+ start_subscription_listener(connection)
34
+ start_send_pump unless @send_pump_started
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ @subscriptions.delete(connection)
42
+ end
43
+
44
+ # @param parts [Array<String>]
45
+ #
46
+ def enqueue(parts)
47
+ @send_queue.enqueue(parts)
48
+ end
49
+
50
+ #
51
+ def stop
52
+ @tasks.each(&:stop)
53
+ @tasks.clear
54
+ end
55
+
56
+ private
57
+
58
+ # Expose subscription to application as data message.
59
+ #
60
+ def on_subscribe(conn, prefix)
61
+ super
62
+ @recv_queue.enqueue(["\x01#{prefix}".b])
63
+ end
64
+
65
+ # Expose unsubscription to application as data message.
66
+ #
67
+ def on_cancel(conn, prefix)
68
+ super
69
+ @recv_queue.enqueue(["\x00#{prefix}".b])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # XSUB socket routing: like SUB but subscriptions sent as data messages.
7
+ #
8
+ # Subscriptions are sent as data frames: \x01 + prefix for subscribe,
9
+ # \x00 + prefix for unsubscribe.
10
+ #
11
+ class XSub
12
+
13
+ # @param engine [Engine]
14
+ #
15
+ def initialize(engine)
16
+ @engine = engine
17
+ @connections = []
18
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
20
+ @tasks = []
21
+ @send_pump_started = false
22
+ end
23
+
24
+ # @return [Async::LimitedQueue]
25
+ #
26
+ attr_reader :recv_queue, :send_queue
27
+
28
+ # @param connection [Connection]
29
+ #
30
+ def connection_added(connection)
31
+ @connections << connection
32
+ task = @engine.start_recv_pump(connection, @recv_queue)
33
+ @tasks << task if task
34
+ start_send_pump unless @send_pump_started
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ end
42
+
43
+ # @param parts [Array<String>]
44
+ #
45
+ def enqueue(parts)
46
+ @send_queue.enqueue(parts)
47
+ end
48
+
49
+ #
50
+ def stop
51
+ @tasks.each(&:stop)
52
+ @tasks.clear
53
+ end
54
+
55
+ private
56
+
57
+ def start_send_pump
58
+ @send_pump_started = true
59
+ @tasks << Reactor.spawn_pump do
60
+ loop do
61
+ parts = @send_queue.dequeue
62
+ frame = parts.first&.b
63
+ next if frame.nil? || frame.empty?
64
+
65
+ flag = frame.getbyte(0)
66
+ prefix = frame.byteslice(1..) || "".b
67
+
68
+ case flag
69
+ when 0x01
70
+ @connections.each { |c| c.send_command(Codec::Command.subscribe(prefix)) }
71
+ when 0x00
72
+ @connections.each { |c| c.send_command(Codec::Command.cancel(prefix)) }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/queue"
5
+ require "async/limited_queue"
6
+
7
+ module OMQ
8
+ module ZMTP
9
+ # Routing strategies for each ZMQ socket type.
10
+ #
11
+ # Each strategy manages how messages flow between connections and
12
+ # the socket's send/recv queues.
13
+ #
14
+ module Routing
15
+ # Returns the routing strategy class for a socket type.
16
+ #
17
+ # @param socket_type [Symbol] e.g. :PAIR, :REQ
18
+ # @return [Class]
19
+ #
20
+ def self.for(socket_type)
21
+ case socket_type
22
+ when :PAIR then Pair
23
+ when :REQ then Req
24
+ when :REP then Rep
25
+ when :DEALER then Dealer
26
+ when :ROUTER then Router
27
+ when :PUB then Pub
28
+ when :SUB then Sub
29
+ when :XPUB then XPub
30
+ when :XSUB then XSub
31
+ when :PUSH then Push
32
+ when :PULL then Pull
33
+ else raise ArgumentError, "unknown socket type: #{socket_type}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/queue"
5
+
6
+ module OMQ
7
+ module ZMTP
8
+ module Transport
9
+ # In-process transport.
10
+ #
11
+ # Both peers are Ruby backend sockets in the same process (native
12
+ # ZMQ's inproc registry is separate and unreachable). Messages are
13
+ # transferred as Ruby arrays — no ZMTP framing, no byte
14
+ # serialization. String parts are frozen by Writable#send to
15
+ # prevent shared mutable state without copying.
16
+ #
17
+ module Inproc
18
+ # Socket types that exchange commands (SUBSCRIBE/CANCEL) over inproc.
19
+ #
20
+ COMMAND_TYPES = %i[PUB SUB XPUB XSUB].freeze
21
+
22
+ # Global registry of bound inproc endpoints.
23
+ #
24
+ @registry = {}
25
+ @mutex = Mutex.new
26
+ @waiters = Hash.new { |h, k| h[k] = [] }
27
+
28
+
29
+ class << self
30
+ # Binds an engine to an inproc endpoint.
31
+ #
32
+ # @param endpoint [String] e.g. "inproc://my-endpoint"
33
+ # @param engine [Engine] the owning engine
34
+ # @return [Listener]
35
+ # @raise [ArgumentError] if endpoint is already bound
36
+ #
37
+ def bind(endpoint, engine)
38
+ @mutex.synchronize do
39
+ raise ArgumentError, "endpoint already bound: #{endpoint}" if @registry.key?(endpoint)
40
+ @registry[endpoint] = engine
41
+
42
+ # Wake any pending connects
43
+ @waiters[endpoint].each { |p| p.resolve(true) }
44
+ @waiters.delete(endpoint)
45
+ end
46
+ Listener.new(endpoint)
47
+ end
48
+
49
+ # Connects to a bound inproc endpoint.
50
+ #
51
+ # @param endpoint [String] e.g. "inproc://my-endpoint"
52
+ # @param engine [Engine] the connecting engine
53
+ # @return [void]
54
+ #
55
+ def connect(endpoint, engine)
56
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
57
+
58
+ unless bound_engine
59
+ # Endpoint not bound yet. Wait with timeout derived from
60
+ # reconnect_interval. If it doesn't appear, silently return —
61
+ # matching ZMQ 4.x behavior where inproc connect to an
62
+ # unbound endpoint succeeds but messages go nowhere.
63
+ # A background task retries periodically.
64
+ ri = engine.options.reconnect_interval
65
+ timeout = ri.is_a?(Range) ? ri.begin : ri
66
+ promise = Async::Promise.new
67
+ @mutex.synchronize { @waiters[endpoint] << promise }
68
+ unless promise.wait?(timeout: timeout)
69
+ @mutex.synchronize { @waiters[endpoint].delete(promise) }
70
+ start_connect_retry(endpoint, engine)
71
+ return
72
+ end
73
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
74
+ end
75
+
76
+ establish_link(engine, bound_engine, endpoint)
77
+ end
78
+
79
+ # Removes a bound endpoint from the registry.
80
+ #
81
+ # @param endpoint [String]
82
+ # @return [void]
83
+ #
84
+ def unbind(endpoint)
85
+ @mutex.synchronize { @registry.delete(endpoint) }
86
+ end
87
+
88
+ # Resets the registry. Used in tests.
89
+ #
90
+ # @return [void]
91
+ #
92
+ def reset!
93
+ @mutex.synchronize do
94
+ @registry.clear
95
+ @waiters.clear
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def establish_link(client_engine, server_engine, endpoint)
102
+ client_type = client_engine.socket_type
103
+ server_type = server_engine.socket_type
104
+
105
+ unless ZMTP::VALID_PEERS[client_type]&.include?(server_type)
106
+ raise ProtocolError,
107
+ "incompatible socket types: #{client_type} cannot connect to #{server_type}"
108
+ end
109
+
110
+ # Only PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
111
+ # over inproc. All other types use only the direct recv queue
112
+ # bypass for data, so no internal queues are needed.
113
+ needs_commands = COMMAND_TYPES.include?(client_type) ||
114
+ COMMAND_TYPES.include?(server_type)
115
+
116
+ if needs_commands
117
+ a_to_b = Async::Queue.new
118
+ b_to_a = Async::Queue.new
119
+ end
120
+
121
+ client_pipe = DirectPipe.new(
122
+ send_queue: needs_commands ? a_to_b : nil,
123
+ receive_queue: needs_commands ? b_to_a : nil,
124
+ peer_identity: server_engine.options.identity,
125
+ peer_type: server_type.to_s,
126
+ )
127
+ server_pipe = DirectPipe.new(
128
+ send_queue: needs_commands ? b_to_a : nil,
129
+ receive_queue: needs_commands ? a_to_b : nil,
130
+ peer_identity: client_engine.options.identity,
131
+ peer_type: client_type.to_s,
132
+ )
133
+
134
+ client_pipe.peer = server_pipe
135
+ server_pipe.peer = client_pipe
136
+
137
+ client_engine.connection_ready(client_pipe, endpoint: endpoint)
138
+ server_engine.connection_ready(server_pipe, endpoint: endpoint)
139
+ end
140
+
141
+ def start_connect_retry(endpoint, engine)
142
+ Reactor.spawn_pump do
143
+ ri = engine.options.reconnect_interval
144
+ ivl = ri.is_a?(Range) ? ri.begin : ri
145
+ loop do
146
+ sleep ivl
147
+ bound_engine = @mutex.synchronize { @registry[endpoint] }
148
+ if bound_engine
149
+ establish_link(engine, bound_engine, endpoint)
150
+ break
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # A bound inproc endpoint handle.
158
+ #
159
+ class Listener
160
+ # @return [String] the bound endpoint
161
+ #
162
+ attr_reader :endpoint
163
+
164
+ # @param endpoint [String]
165
+ #
166
+ def initialize(endpoint)
167
+ @endpoint = endpoint
168
+ end
169
+
170
+ # Stops the listener by removing it from the registry.
171
+ #
172
+ # @return [void]
173
+ #
174
+ def stop
175
+ Inproc.unbind(@endpoint)
176
+ end
177
+ end
178
+
179
+ # A direct in-process pipe that transfers Ruby arrays through queues.
180
+ #
181
+ # Implements the same interface as Connection so routing strategies
182
+ # can use it transparently.
183
+ #
184
+ # When a routing strategy sets {#direct_recv_queue} on a pipe,
185
+ # {#send_message} enqueues directly into the peer's recv queue,
186
+ # bypassing the intermediate internal queues and the recv pump
187
+ # task entirely. This reduces inproc from 3 queue hops to 1.
188
+ #
189
+ class DirectPipe
190
+ # @return [String] peer's socket type
191
+ #
192
+ attr_reader :peer_socket_type
193
+
194
+ # @return [String] peer's identity
195
+ #
196
+ attr_reader :peer_identity
197
+
198
+ # @return [DirectPipe, nil] the other end of this pipe pair
199
+ #
200
+ attr_accessor :peer
201
+
202
+ # @return [Async::LimitedQueue, nil] when set, {#send_message}
203
+ # enqueues directly here instead of using the internal queue
204
+ #
205
+ attr_accessor :direct_recv_queue
206
+
207
+ # @return [Proc, nil] optional transform applied before
208
+ # enqueuing into {#direct_recv_queue}
209
+ #
210
+ attr_accessor :direct_recv_transform
211
+
212
+ # @param send_queue [Async::Queue, nil] outgoing command queue
213
+ # (nil for non-PUB/SUB types that don't exchange commands)
214
+ # @param receive_queue [Async::Queue, nil] incoming command queue
215
+ # @param peer_identity [String]
216
+ # @param peer_type [String]
217
+ #
218
+ def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
219
+ @send_queue = send_queue
220
+ @receive_queue = receive_queue
221
+ @peer_identity = peer_identity || "".b
222
+ @peer_socket_type = peer_type
223
+ @closed = false
224
+ @peer = nil
225
+ @direct_recv_queue = nil
226
+ @direct_recv_transform = nil
227
+ end
228
+
229
+ # Sends a multi-frame message.
230
+ #
231
+ # When {#direct_recv_queue} is set (inproc fast path), the
232
+ # message is delivered directly to the peer's recv queue,
233
+ # skipping the internal pipe queues and the recv pump.
234
+ #
235
+ # @param parts [Array<String>]
236
+ # @return [void]
237
+ #
238
+ def send_message(parts)
239
+ raise IOError, "closed" if @closed
240
+ if @direct_recv_queue
241
+ msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
242
+ @direct_recv_queue.enqueue(msg)
243
+ else
244
+ @send_queue.enqueue(parts)
245
+ end
246
+ end
247
+
248
+ # Receives a multi-frame message.
249
+ #
250
+ # @return [Array<String>]
251
+ # @raise [EOFError] if closed
252
+ #
253
+ def receive_message
254
+ msg = @receive_queue.dequeue
255
+ raise EOFError, "connection closed" if msg.nil?
256
+ msg
257
+ end
258
+
259
+ # Sends a command via the internal command queue.
260
+ # Only available for PUB/SUB-family pipes.
261
+ #
262
+ # @param command [Codec::Command]
263
+ # @return [void]
264
+ #
265
+ def send_command(command)
266
+ raise IOError, "closed" if @closed
267
+ @send_queue.enqueue([:command, command])
268
+ end
269
+
270
+ # Reads one command frame from the internal command queue.
271
+ # Used by PUB/XPUB subscription listeners.
272
+ #
273
+ # @return [Codec::Frame]
274
+ #
275
+ def read_frame
276
+ loop do
277
+ item = @receive_queue.dequeue
278
+ raise EOFError, "connection closed" if item.nil?
279
+ if item.is_a?(Array) && item.first == :command
280
+ cmd = item[1]
281
+ return Codec::Frame.new(cmd.to_body, command: true)
282
+ end
283
+ end
284
+ end
285
+
286
+ # Closes this pipe end.
287
+ #
288
+ # @return [void]
289
+ #
290
+ def close
291
+ return if @closed
292
+ @closed = true
293
+ @send_queue&.enqueue(nil) # close sentinel
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "io/stream"
5
+
6
+ module OMQ
7
+ module ZMTP
8
+ module Transport
9
+ # IPC transport using Unix domain sockets.
10
+ #
11
+ # Supports both file-based paths and Linux abstract namespace
12
+ # (paths starting with @).
13
+ #
14
+ module IPC
15
+ class << self
16
+ # Binds an IPC server.
17
+ #
18
+ # @param endpoint [String] e.g. "ipc:///tmp/my.sock" or "ipc://@abstract"
19
+ # @param engine [Engine]
20
+ # @return [Listener]
21
+ #
22
+ def bind(endpoint, engine)
23
+ path = parse_path(endpoint)
24
+ sock_path = to_socket_path(path)
25
+
26
+ # Remove stale socket file for file-based paths
27
+ File.delete(sock_path) if !abstract?(path) && File.exist?(sock_path)
28
+
29
+ server = UNIXServer.new(sock_path)
30
+
31
+ accept_task = Reactor.spawn_pump do
32
+ loop do
33
+ client = server.accept
34
+ Reactor.run do
35
+ engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: endpoint)
36
+ rescue => e
37
+ client.close rescue nil
38
+ raise if !e.is_a?(ProtocolError) && !e.is_a?(EOFError)
39
+ end
40
+ end
41
+ rescue IOError
42
+ # server closed
43
+ end
44
+
45
+ Listener.new(endpoint, server, accept_task, path)
46
+ end
47
+
48
+ # Connects to an IPC endpoint.
49
+ #
50
+ # @param endpoint [String]
51
+ # @param engine [Engine]
52
+ # @return [void]
53
+ #
54
+ def connect(endpoint, engine)
55
+ path = parse_path(endpoint)
56
+ sock_path = to_socket_path(path)
57
+ sock = UNIXSocket.new(sock_path)
58
+ engine.handle_connected(IO::Stream::Buffered.wrap(sock, minimum_write_size: 0), endpoint: endpoint)
59
+ end
60
+
61
+ private
62
+
63
+ # Extracts path from "ipc://path".
64
+ #
65
+ def parse_path(endpoint)
66
+ endpoint.sub(%r{\Aipc://}, "")
67
+ end
68
+
69
+ # Converts @ prefix to \0 for abstract namespace.
70
+ #
71
+ def to_socket_path(path)
72
+ if abstract?(path)
73
+ "\0#{path[1..]}"
74
+ else
75
+ path
76
+ end
77
+ end
78
+
79
+ # @return [Boolean] true if abstract namespace path
80
+ #
81
+ def abstract?(path)
82
+ path.start_with?("@")
83
+ end
84
+ end
85
+
86
+ # A bound IPC listener.
87
+ #
88
+ class Listener
89
+ # @return [String] the endpoint
90
+ #
91
+ attr_reader :endpoint
92
+
93
+ def initialize(endpoint, server, accept_task, path)
94
+ @endpoint = endpoint
95
+ @server = server
96
+ @accept_task = accept_task
97
+ @path = path
98
+ end
99
+
100
+ # Stops the listener.
101
+ #
102
+ def stop
103
+ @accept_task.stop
104
+ @server.close rescue nil
105
+ # Clean up socket file for file-based paths
106
+ unless @path.start_with?("@")
107
+ File.delete(@path) rescue nil
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end