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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yaml +8 -19
- data/.github/workflows/valgrind.yaml +44 -0
- data/Rakefile +6 -19
- data/cool.io.gemspec +1 -3
- data/ext/cool.io/buffer.c +82 -64
- data/ext/cool.io/cool.io.h +9 -0
- data/ext/cool.io/extconf.rb +0 -1
- data/ext/cool.io/iowatcher.c +1 -1
- data/ext/cool.io/loop.c +49 -29
- data/ext/cool.io/stat_watcher.c +19 -6
- data/ext/cool.io/timer_watcher.c +5 -5
- data/ext/cool.io/watcher.c +38 -13
- data/ext/cool.io/watcher.h +14 -15
- data/ext/libev/ev.c +7 -1
- data/ext/libev/ev_select.c +10 -0
- data/gems.rb +4 -0
- data/lib/cool.io/dns_resolver.rb +53 -9
- data/lib/cool.io/version.rb +2 -2
- data/libev_win_select.diff +25 -9
- data/spec/async_watcher_spec.rb +3 -1
- data/spec/detach_race_condition_spec.rb +117 -0
- data/spec/dns_spec.rb +186 -0
- data/spec/iobuffer_spec.rb +31 -0
- data/spec/stat_watcher_spec.rb +31 -1
- data/spec/tcp_server_spec.rb +28 -4
- metadata +6 -6
data/libev_win_select.diff
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
diff --git a/ext/libev/ev.c b/ext/libev/ev.c
|
|
2
|
-
index
|
|
2
|
+
index a59efb2..5c18fd6 100644
|
|
3
3
|
--- a/ext/libev/ev.c
|
|
4
4
|
+++ b/ext/libev/ev.c
|
|
5
|
-
@@ -
|
|
5
|
+
@@ -210,6 +210,13 @@
|
|
6
6
|
#else
|
|
7
7
|
# include <io.h>
|
|
8
8
|
# define WIN32_LEAN_AND_MEAN
|
|
9
|
-
|
|
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
|
|
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,
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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
|
-
@@ -
|
|
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
|
data/spec/async_watcher_spec.rb
CHANGED
|
@@ -30,7 +30,9 @@ describe Cool.io::AsyncWatcher, :env => :exclude_win do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# ensure children are ready
|
|
33
|
-
|
|
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
|
data/spec/iobuffer_spec.rb
CHANGED
|
@@ -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
|
data/spec/stat_watcher_spec.rb
CHANGED
|
@@ -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
|
data/spec/tcp_server_spec.rb
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|