cool.io 1.9.0 → 1.9.5

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.
@@ -1,20 +1,26 @@
1
1
  diff --git a/ext/libev/ev.c b/ext/libev/ev.c
2
- index dae87f1..d15f6bd 100644
2
+ index a59efb2..5c18fd6 100644
3
3
  --- a/ext/libev/ev.c
4
4
  +++ b/ext/libev/ev.c
5
- @@ -207,6 +207,7 @@
5
+ @@ -210,6 +210,13 @@
6
6
  #else
7
7
  # include <io.h>
8
8
  # define WIN32_LEAN_AND_MEAN
9
- +# define FD_SETSIZE 1024
9
+ +/* ruby.h above already pulled in winsock2.h, so fd_set may be dimensioned
10
+ + * already. Defining FD_SETSIZE unconditionally here would only move the bound
11
+ + * used by EV_WIN_FD_SET, not the array it indexes. Take whatever is in effect
12
+ + * and only supply a default when nothing has been decided yet. */
13
+ +# ifndef FD_SETSIZE
14
+ +# define FD_SETSIZE 1024
15
+ +# endif
10
16
  # include <winsock2.h>
11
17
  # include <windows.h>
12
18
  # ifndef EV_SELECT_IS_WINSOCKET
13
19
  diff --git a/ext/libev/ev_select.c b/ext/libev/ev_select.c
14
- index f38d6ca..7050778 100644
20
+ index ed1fc7a..eccff2b 100644
15
21
  --- a/ext/libev/ev_select.c
16
22
  +++ b/ext/libev/ev_select.c
17
- @@ -67,6 +67,54 @@
23
+ @@ -67,6 +67,64 @@
18
24
 
19
25
  #include <string.h>
20
26
 
@@ -58,6 +64,16 @@ index f38d6ca..7050778 100644
58
64
  +#define EV_WIN_FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
59
65
  +#define EV_WIN_FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set *)(set))
60
66
  +#define EV_WIN_FD_COUNT(set) (((fd_set *)(set))->fd_count)
67
+ +
68
+ +/*
69
+ +fd_set is dimensioned by whatever FD_SETSIZE was in effect when winsock2.h was
70
+ +first pulled in, but EV_WIN_FD_SET and select_modify bound-check against the
71
+ +FD_SETSIZE visible here. The two are decided by separate paths, so if the bound
72
+ +ever exceeds the declared array we would write past the end of the allocation in
73
+ +select_init. Catch that at build time instead.
74
+ +*/
75
+ +typedef char coolio_fd_setsize_matches_fd_set[
76
+ + (sizeof (((fd_set *)0)->fd_array) / sizeof (SOCKET) >= (size_t)FD_SETSIZE) ? 1 : -1];
61
77
  +/* ######################################## */
62
78
  +#else
63
79
  +#define EV_WIN_FD_CLR FD_CLR
@@ -69,7 +85,7 @@ index f38d6ca..7050778 100644
69
85
  static void
70
86
  select_modify (EV_P_ int fd, int oev, int nev)
71
87
  {
72
- @@ -91,17 +139,17 @@ select_modify (EV_P_ int fd, int oev, int nev)
88
+ @@ -91,17 +149,17 @@ select_modify (EV_P_ int fd, int oev, int nev)
73
89
  if ((oev ^ nev) & EV_READ)
74
90
  #endif
75
91
  if (nev & EV_READ)
@@ -91,7 +107,7 @@ index f38d6ca..7050778 100644
91
107
 
92
108
  #else
93
109
 
94
- @@ -197,8 +245,8 @@ select_poll (EV_P_ ev_tstamp timeout)
110
+ @@ -197,8 +255,8 @@ select_poll (EV_P_ ev_tstamp timeout)
95
111
  {
96
112
  if (timeout)
97
113
  {
@@ -102,7 +118,7 @@ index f38d6ca..7050778 100644
102
118
  }
103
119
 
104
120
  return;
105
- @@ -230,10 +278,10 @@ select_poll (EV_P_ ev_tstamp timeout)
121
+ @@ -230,10 +288,10 @@ select_poll (EV_P_ ev_tstamp timeout)
106
122
  int handle = fd;
107
123
  #endif
108
124
 
@@ -116,7 +132,7 @@ index f38d6ca..7050778 100644
116
132
  #endif
117
133
 
118
134
  if (expect_true (events))
119
- @@ -279,9 +327,9 @@ select_init (EV_P_ int flags)
135
+ @@ -280,9 +338,9 @@ select_init (EV_P_ int flags)
120
136
  backend_poll = select_poll;
121
137
 
122
138
  #if EV_SELECT_USE_FD_SET
@@ -30,7 +30,9 @@ describe Cool.io::AsyncWatcher, :env => :exclude_win do
30
30
  end
31
31
 
32
32
  # ensure children are ready
33
- nr_fork.times { expect(rd.sysread(1)).to eq('.') }
33
+ # rd may be O_NONBLOCK on macOS Ruby 3.2+ (IO.pipe sets O_NONBLOCK); use read
34
+ # instead of sysread so EAGAIN on an empty pipe is retried transparently.
35
+ nr_fork.times { expect(rd.read(1)).to eq('.') }
34
36
 
35
37
  # send our signals
36
38
  nr_signal.times { aw.signal }
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cool.io::Loop do
4
+ # An IOWatcher that drains its pipe and then runs a user-supplied block,
5
+ # receiving itself as the argument.
6
+ class Victim < Cool.io::IOWatcher
7
+ def initialize(io, &on_readable)
8
+ super(io)
9
+ @io = io
10
+ @on_readable = on_readable
11
+ end
12
+
13
+ def on_readable
14
+ begin
15
+ @io.read_nonblock(1024)
16
+ rescue IO::WaitReadable, EOFError
17
+ end
18
+ @on_readable.call(self) if @on_readable
19
+ end
20
+ end
21
+
22
+ # https://github.com/socketry/cool.io/issues/87
23
+ #
24
+ # Several watchers have an event pending in the same loop iteration. When the
25
+ # first one dispatched detaches the others, the loop must skip their now-stale
26
+ # pending events instead of dispatching them to a detached watcher. Before the
27
+ # fix that raised "TypeError: wrong argument type nil (expected Coolio::Loop)"
28
+ # and could crash the VM.
29
+ #
30
+ # This is exercised deterministically within a single thread (a preceding
31
+ # callback detaching another watcher in the same loop cycle). The original
32
+ # reproduction detached from a separate thread while the loop was polling,
33
+ # which is an unsupported concurrent mutation of libev (not thread-safe) and
34
+ # crashed intermittently on macOS.
35
+ it "does not raise when a watcher with a pending event is detached during dispatch" do
36
+ iterations = 200
37
+
38
+ expect {
39
+ iterations.times do
40
+ coolio_loop = Cool.io::Loop.new
41
+ pipes = []
42
+ watchers = []
43
+
44
+ 5.times do
45
+ r, w = IO.pipe
46
+ pipes << [r, w]
47
+
48
+ watcher = Victim.new(r) do |fired|
49
+ # Detach every other watcher whose event is already queued for this
50
+ # same loop iteration.
51
+ watchers.each do |other|
52
+ other.detach if !other.equal?(fired) && other.attached?
53
+ end
54
+ end
55
+ watcher.attach(coolio_loop)
56
+ watchers << watcher
57
+
58
+ w.write("dummy\n") # make the read end readable so an event is pending
59
+ end
60
+
61
+ coolio_loop.run_once
62
+
63
+ # Only the first dispatched watcher runs; it detaches the other four,
64
+ # whose pending events are then skipped.
65
+ expect(watchers.count(&:attached?)).to eq(1)
66
+
67
+ watchers.each { |watcher| watcher.detach if watcher.attached? }
68
+ pipes.each { |r, w| r.close; w.close }
69
+ end
70
+ }.not_to raise_error
71
+ end
72
+
73
+ class HttpHandler < Coolio::IO
74
+ RESPONSE = "HTTP/1.1 200 OK\r\nContent-Length: 1024\r\nConnection: close\r\n\r\n" + ("X" * 1024)
75
+
76
+ def on_connect
77
+ end
78
+
79
+ def on_read(data)
80
+ write(RESPONSE)
81
+ end
82
+
83
+ def on_write_complete
84
+ close
85
+ end
86
+ end
87
+
88
+ # https://github.com/socketry/cool.io/issues/89
89
+ it "does not cause memory leaks" do
90
+ port = 18989
91
+ loop = Coolio::Loop.default
92
+
93
+ server = Coolio::TCPServer.new('127.0.0.1', port, HttpHandler)
94
+ server.attach(loop)
95
+
96
+ event_thread = Thread.new { loop.run }
97
+
98
+ request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
99
+
100
+ 10.times do |iteration|
101
+ begin
102
+ sock = TCPSocket.new('127.0.0.1', port)
103
+ sock.write(request)
104
+ sock.read
105
+ sock.close
106
+ rescue => e
107
+ sleep 0.01
108
+ retry
109
+ end
110
+ end
111
+
112
+ server.close
113
+ event_thread.join
114
+
115
+ expect(loop.watchers).to be_empty
116
+ end
117
+ end
data/spec/dns_spec.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tempfile'
2
3
 
3
4
  VALID_DOMAIN = "google.com"
4
5
  INVALID_DOMAIN = "gibidigibigididibitidibigitibidigitidididi.com"
@@ -55,4 +56,189 @@ describe "DNS" do
55
56
  expect( Coolio::DNSResolver.hosts("localhost", file.path)).to eq @preferred_localhost_address
56
57
  end
57
58
  end
59
+
60
+ describe "IPv6 nameserver filtering" do
61
+ it "ignores IPv6 nameservers provided in arguments" do
62
+ resolver = Coolio::DNSResolver.new("example.com", "8.8.8.8", "2001:4860:4860::8888", "1.1.1.1")
63
+
64
+ nameservers = resolver.instance_variable_get(:@nameservers)
65
+ expect(nameservers).to eq(["8.8.8.8", "1.1.1.1"])
66
+ end
67
+
68
+ it "falls back to default IPv4 config if only IPv6 addresses are provided" do
69
+ allow(Resolv::DNS::Config).to receive(:default_config_hash).and_return({
70
+ nameserver: ["8.8.4.4", "2001:4860:4860::8844"]
71
+ })
72
+
73
+ resolver = Coolio::DNSResolver.new("example.com", "2001:4860:4860::8888")
74
+
75
+ nameservers = resolver.instance_variable_get(:@nameservers)
76
+ expect(nameservers).to eq(["8.8.4.4"])
77
+ end
78
+ end
79
+
80
+ describe "nameserver normalization" do
81
+ let(:localhost_address) do
82
+ Addrinfo.getaddrinfo("localhost", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM).first.ip_address
83
+ end
84
+
85
+ it "keeps the nameserver list as given" do
86
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
87
+
88
+ expect(resolver.instance_variable_get(:@nameservers)).to eq(["localhost"])
89
+ end
90
+
91
+ it "queries the numeric address of a nameserver given as a hostname" do
92
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
93
+ resolver.__send__(:send_request)
94
+
95
+ expect(resolver.instance_variable_get(:@queried_addresses)).to eq([localhost_address])
96
+ end
97
+
98
+ it "accepts responses from a nameserver which was given as a hostname" do
99
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
100
+ resolver.__send__(:send_request)
101
+ response = dns_response_for(resolver)
102
+
103
+ expect(
104
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 53, localhost_address, localhost_address])
105
+ ).to be true
106
+ end
107
+
108
+ it "looks a nameserver up once and reuses the result on retries" do
109
+ resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
110
+ resolver = Coolio::DNSResolver.new("example.com", "127.0.0.1")
111
+
112
+ expect(Addrinfo).to receive(:getaddrinfo).once.and_return(resolved)
113
+
114
+ 3.times { resolver.__send__(:send_request) }
115
+ end
116
+
117
+ it "does not reject an unresolvable nameserver at construction" do
118
+ allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
119
+
120
+ expect do
121
+ Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
122
+ end.to_not raise_error
123
+ end
124
+
125
+ it "surfaces an unresolvable nameserver as a SocketError from the request" do
126
+ allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
127
+ resolver = Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
128
+
129
+ expect { resolver.attach(@loop) }.to raise_error(SocketError)
130
+ expect(@loop.watchers).to be_empty
131
+ end
132
+
133
+ it "recovers when a nameserver is only transiently unresolvable" do
134
+ resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
135
+ resolver = Coolio::DNSResolver.new("example.com", "ns.example.test")
136
+
137
+ attempts = 0
138
+ allow(Addrinfo).to receive(:getaddrinfo) do
139
+ attempts += 1
140
+ raise SocketError, "getaddrinfo: Name or service not known" if attempts == 1
141
+
142
+ resolved
143
+ end
144
+
145
+ expect { resolver.__send__(:send_request) }.to raise_error(SocketError)
146
+ expect { resolver.__send__(:send_request) }.to_not raise_error
147
+ expect(resolver.instance_variable_get(:@queried_addresses)).to eq(["127.0.0.1"])
148
+ end
149
+ end
150
+
151
+ describe "response validation" do
152
+ let(:nameserver) { "127.0.0.1" }
153
+ let(:sender) { ["AF_INET", 53, nameserver, nameserver] }
154
+ let(:resolver) do
155
+ Coolio::DNSResolver.new("example.com", nameserver).tap { |r| r.__send__(:send_request) }
156
+ end
157
+
158
+ it "uses an unpredictable transaction ID for each query" do
159
+ ids = 10.times.map do
160
+ request_id_of(Coolio::DNSResolver.new("example.com", nameserver))
161
+ end
162
+
163
+ expect(ids.uniq.size).to be > 1
164
+ end
165
+
166
+ it "accepts a response carrying our transaction ID from the queried nameserver" do
167
+ expect(
168
+ resolver.__send__(:solicited_response?, dns_response_for(resolver), sender)
169
+ ).to be true
170
+ end
171
+
172
+ it "rejects a response carrying a different transaction ID" do
173
+ forged = dns_response_for(resolver, id: (request_id_of(resolver) + 1) % 65536)
174
+
175
+ expect(resolver.__send__(:solicited_response?, forged, sender)).to be false
176
+ end
177
+
178
+ it "rejects a response from a source address we did not query" do
179
+ response = dns_response_for(resolver)
180
+
181
+ expect(
182
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"])
183
+ ).to be false
184
+ end
185
+
186
+ it "rejects a response arriving before the request was sent" do
187
+ unsent = Coolio::DNSResolver.new("example.com", nameserver)
188
+
189
+ expect(unsent.__send__(:solicited_response?, dns_response_for(unsent), sender)).to be false
190
+ end
191
+
192
+ it "rejects a response from a source port other than the DNS port" do
193
+ response = dns_response_for(resolver)
194
+
195
+ expect(
196
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 4444, nameserver, nameserver])
197
+ ).to be false
198
+ end
199
+
200
+ it "rejects a truncated datagram" do
201
+ expect(resolver.__send__(:solicited_response?, "\0\0", sender)).to be false
202
+ end
203
+
204
+ it "resolves from a response sent by the queried nameserver" do
205
+ response = dns_response_for(resolver, address: "1.2.3.4")
206
+ allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock).and_return([response, sender])
207
+
208
+ expect(resolver).to receive(:on_success).with("1.2.3.4")
209
+ expect(resolver).to receive(:detach)
210
+
211
+ resolver.__send__(:on_readable)
212
+ end
213
+
214
+ it "ignores a spoofed response instead of resolving or failing it" do
215
+ forged = dns_response_for(resolver, address: "6.6.6.6")
216
+ allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock)
217
+ .and_return([forged, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"]])
218
+
219
+ expect(resolver).to_not receive(:on_success)
220
+ expect(resolver).to_not receive(:on_failure)
221
+ expect(resolver).to_not receive(:detach)
222
+
223
+ resolver.__send__(:on_readable)
224
+ end
225
+ end
226
+
227
+ def request_id_of(resolver)
228
+ resolver.__send__(:request_message)[0..1].unpack('n').first
229
+ end
230
+
231
+ # A response to the resolver's own query: header plus the echoed question,
232
+ # and an A record when an address is given.
233
+ def dns_response_for(resolver, id: request_id_of(resolver), address: nil)
234
+ question = resolver.instance_variable_get(:@question)
235
+ answer = if address
236
+ # Compressed name pointer, type A, class IN, TTL, RDLENGTH, RDATA
237
+ [0xc00c, 1, 1, 60, 4].pack('nnnNn') + address.split('.').map(&:to_i).pack('CCCC')
238
+ else
239
+ ""
240
+ end
241
+
242
+ [id, 0x81, 0x80, 1, answer.empty? ? 0 : 1, 0, 0].pack('nCCnnnn') + question + answer
243
+ end
58
244
  end
@@ -31,6 +31,18 @@ describe Cool.io::Buffer do
31
31
  expect(buffer << "baz").to eq "baz"
32
32
  expect(buffer.read 3).to eq "arb"
33
33
  end
34
+
35
+ it "raises ArgumentError for a length below one" do
36
+ buffer << "foo"
37
+ expect { buffer.read 0 }.to raise_error ArgumentError
38
+ expect { buffer.read(-1) }.to raise_error ArgumentError
39
+ end
40
+
41
+ it "clamps a length which does not fit in a C int to the buffer size" do
42
+ buffer << "foobar"
43
+ expect(buffer.read 2**31).to eq "foobar"
44
+ expect(buffer.size).to eq 0
45
+ end
34
46
  end
35
47
 
36
48
  describe "provides methods for performing non-blocking I/O" do
@@ -142,6 +154,25 @@ describe Cool.io::Buffer do
142
154
  expect(data).to eq "foo\nbarbaz"
143
155
  expect(buffer.to_str).to eq ""
144
156
  end
157
+
158
+ it "raises TypeError instead of crashing when data is not a String" do
159
+ buffer << "hello world"
160
+ expect { buffer.read_frame 12345, " ".ord }.to raise_error(TypeError)
161
+ expect { buffer.read_frame nil, " ".ord }.to raise_error(TypeError)
162
+ expect { buffer.read_frame [], " ".ord }.to raise_error(TypeError)
163
+ end
164
+
165
+ it "raises FrozenError when data is a frozen String" do
166
+ buffer << "hello world"
167
+ expect { buffer.read_frame "frozen".freeze, " ".ord }.to raise_error(FrozenError)
168
+ end
169
+
170
+ it "coerces objects responding to #to_str" do
171
+ buffer << "foo\nbar"
172
+ convertible = Object.new
173
+ def convertible.to_str; +""; end
174
+ expect(buffer.read_frame convertible, "\n".ord).to eq true
175
+ end
145
176
  end
146
177
 
147
178
  end
@@ -18,12 +18,31 @@ class MyStatWatcher < Cool.io::StatWatcher
18
18
  end
19
19
  end
20
20
 
21
- def run_with_file_change(path)
21
+ def run_with_file_change(path, compact: false)
22
22
  reactor = Cool.io::Loop.new
23
23
 
24
24
  sw = MyStatWatcher.new(path)
25
25
  sw.attach(reactor)
26
26
 
27
+ # libev retains the path pointer passed to ev_stat_init() for the lifetime of
28
+ # the watcher. If the watcher held RSTRING_PTR(@path) directly, a compaction
29
+ # that relocates the @path String would leave libev dereferencing a stale
30
+ # address on every stat. Force a maximal relocation here so the watcher keeps
31
+ # operating against a path buffer it owns rather than Ruby-managed memory.
32
+ if compact
33
+ if GC.respond_to?(:verify_compaction_references)
34
+ # verify_compaction_references moves every movable object to a fresh slot.
35
+ # Its keyword arguments have varied across Ruby versions, so fall back.
36
+ begin
37
+ GC.verify_compaction_references(expand_heap: true, toplevel: true)
38
+ rescue ArgumentError
39
+ GC.verify_compaction_references(expand_heap: true)
40
+ end
41
+ elsif GC.respond_to?(:compact)
42
+ GC.compact
43
+ end
44
+ end
45
+
27
46
  tw = Cool.io::TimerWatcher.new(INTERVAL, true)
28
47
  tw.on_timer do
29
48
  reactor.stop if sw.accessed
@@ -69,6 +88,17 @@ describe Cool.io::StatWatcher do
69
88
  expect(watcher.previous.ino).to eq(watcher.current.ino)
70
89
  end
71
90
 
91
+ it "keeps firing on_change after GC compaction relocates objects" do
92
+ skip "GC compaction not available" unless GC.respond_to?(:compact)
93
+
94
+ watcher = run_with_file_change(TEMP_FILE_PATH, compact: true)
95
+ expect(watcher.accessed).to eq(true)
96
+ end
97
+
98
+ it "raises ArgumentError when the path contains a null byte" do
99
+ expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
100
+ end
101
+
72
102
  it "should raise when the handler does not take 2 parameters" do
73
103
  class MyStatWatcher < Cool.io::StatWatcher
74
104
  remove_method :on_change
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'timeout'
2
3
 
3
4
  TIMEOUT = 0.010
4
5
  HOST = '127.0.0.1'
@@ -39,13 +40,31 @@ def on_message(data)
39
40
  @data = data
40
41
  end
41
42
 
43
+ # The reactor can be an order of magnitude slower under valgrind, so sleeping a
44
+ # fixed interval and assuming the event was processed by then makes these specs
45
+ # fail intermittently. Wait for the result instead. The timeout is generous
46
+ # enough that a slow machine still passes, and a genuine hang raises
47
+ # Timeout::Error here instead of blocking the suite forever.
48
+ WAIT_TIMEOUT = 5.0
49
+
50
+ def wait_until(timeout = WAIT_TIMEOUT)
51
+ Timeout.timeout(timeout) do
52
+ sleep 0.001 until yield
53
+ end
54
+ end
55
+
42
56
  def test_run(data = nil)
57
+ @data = ""
43
58
  reactor = Coolio::Loop.new
44
59
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
45
60
  reactor.attach(server)
46
61
  thread = Thread.new { reactor.run }
47
- send_data(data) if data
48
- sleep TIMEOUT
62
+ if data
63
+ send_data(data)
64
+ wait_until { @data == data }
65
+ else
66
+ sleep TIMEOUT
67
+ end
49
68
  reactor.stop
50
69
  server.detach
51
70
  send_data('') # to leave from blocking loop
@@ -86,6 +105,7 @@ ensure
86
105
  end
87
106
 
88
107
  def test_run_timeout(data = nil, timeout = TIMEOUT)
108
+ @data = ""
89
109
  reactor = Coolio::Loop.new
90
110
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
91
111
  reactor.attach(server)
@@ -95,8 +115,12 @@ def test_run_timeout(data = nil, timeout = TIMEOUT)
95
115
  reactor.run_once(timeout)
96
116
  end
97
117
  end
98
- send_data(data) if data
99
- sleep timeout
118
+ if data
119
+ send_data(data)
120
+ wait_until { @data == data }
121
+ else
122
+ sleep timeout
123
+ end
100
124
  server.detach
101
125
  running = false # another send is not required
102
126
  thread.join
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cool.io
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
8
8
  - Masahiro Nakagawa
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-10-02 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rake-compiler
@@ -78,6 +77,7 @@ extensions:
78
77
  extra_rdoc_files: []
79
78
  files:
80
79
  - ".github/workflows/test.yaml"
80
+ - ".github/workflows/valgrind.yaml"
81
81
  - ".gitignore"
82
82
  - ".rspec"
83
83
  - CHANGES.md
@@ -141,6 +141,7 @@ files:
141
141
  - libev_ruby_gil.diff
142
142
  - libev_win_select.diff
143
143
  - spec/async_watcher_spec.rb
144
+ - spec/detach_race_condition_spec.rb
144
145
  - spec/dns_spec.rb
145
146
  - spec/iobuffer_spec.rb
146
147
  - spec/spec_helper.rb
@@ -155,7 +156,6 @@ homepage: https://github.com/socketry/cool.io
155
156
  licenses:
156
157
  - MIT
157
158
  metadata: {}
158
- post_install_message:
159
159
  rdoc_options: []
160
160
  require_paths:
161
161
  - lib
@@ -170,12 +170,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
170
  - !ruby/object:Gem::Version
171
171
  version: '0'
172
172
  requirements: []
173
- rubygems_version: 3.5.11
174
- signing_key:
173
+ rubygems_version: 4.0.16
175
174
  specification_version: 4
176
175
  summary: A cool framework for doing high performance I/O in Ruby
177
176
  test_files:
178
177
  - spec/async_watcher_spec.rb
178
+ - spec/detach_race_condition_spec.rb
179
179
  - spec/dns_spec.rb
180
180
  - spec/iobuffer_spec.rb
181
181
  - spec/spec_helper.rb