cool.io 1.9.4 → 1.10.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 +4 -4
- data/.github/workflows/test.yaml +1 -0
- data/.github/workflows/valgrind.yaml +41 -0
- data/Rakefile +7 -19
- data/cool.io.gemspec +1 -3
- data/ext/cool.io/buffer.c +77 -52
- data/ext/cool.io/cool.io.h +6 -0
- data/ext/cool.io/ev_wrap.h +1 -0
- data/ext/cool.io/extconf.rb +0 -1
- data/ext/cool.io/iowatcher.c +18 -1
- data/ext/cool.io/loop.c +21 -19
- data/ext/cool.io/stat_watcher.c +44 -19
- data/ext/cool.io/timer_watcher.c +1 -1
- data/ext/cool.io/watcher.c +13 -1
- data/ext/cool.io/watcher.h +0 -17
- data/ext/libev/ev.c +80 -12
- data/ext/libev/ev_select.c +10 -0
- data/ext/libev/ev_vars.h +4 -0
- data/ext/libev/ev_win32_stat.c +851 -0
- data/ext/libev/ev_wrap.h +4 -0
- data/gems.rb +4 -0
- data/lib/cool.io/dns_resolver.rb +53 -8
- data/lib/cool.io/loop.rb +5 -1
- 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 +59 -25
- data/spec/dns_spec.rb +197 -0
- data/spec/fixtures/windows_stat_native.c +1305 -0
- data/spec/iobuffer_spec.rb +88 -1
- data/spec/loop_spec.rb +31 -0
- data/spec/stat_watcher_spec.rb +86 -1
- data/spec/tcp_server_spec.rb +34 -10
- data/spec/watcher_reattach_spec.rb +54 -0
- data/spec/windows_stat_native_spec.rb +193 -0
- data/spec/windows_stat_watcher_spec.rb +363 -0
- metadata +14 -2
data/spec/iobuffer_spec.rb
CHANGED
|
@@ -31,8 +31,51 @@ 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
|
+
expect { buffer.read(-(2**100)) }.to raise_error ArgumentError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "clamps a length which does not fit in a C int to the buffer size" do
|
|
43
|
+
buffer << "foobar"
|
|
44
|
+
expect(buffer.read 2**31).to eq "foobar"
|
|
45
|
+
expect(buffer.size).to eq 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "clamps lengths larger than an unsigned long or size_t to the buffer size" do
|
|
49
|
+
[2**32, 2**64, 2**100].each do |length|
|
|
50
|
+
buffer << "foobar"
|
|
51
|
+
expect(buffer.read(length)).to eq "foobar"
|
|
52
|
+
expect(buffer.size).to eq 0
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "keeps integer coercion for numeric and to_int lengths" do
|
|
57
|
+
length = Object.new
|
|
58
|
+
def length.to_int
|
|
59
|
+
2
|
|
60
|
+
end
|
|
61
|
+
buffer << "foobar"
|
|
62
|
+
expect(buffer.read(length)).to eq "fo"
|
|
63
|
+
expect(buffer.read(2.9)).to eq "ob"
|
|
64
|
+
expect(buffer.to_str).to eq "ar"
|
|
65
|
+
end
|
|
34
66
|
end
|
|
35
|
-
|
|
67
|
+
|
|
68
|
+
it "raises RangeError when to_str is called on a buffer past LONG_MAX",
|
|
69
|
+
:if => [0].pack("l!").bytesize == 4 do
|
|
70
|
+
long_max = 2**31 - 1
|
|
71
|
+
node_size = 16 * 1024 * 1024
|
|
72
|
+
big = Cool.io::Buffer.new(node_size)
|
|
73
|
+
chunk = "x" * node_size
|
|
74
|
+
((long_max / node_size) + 1).times { big << chunk }
|
|
75
|
+
expect(big.size).to be > long_max
|
|
76
|
+
expect { big.to_str }.to raise_error(RangeError)
|
|
77
|
+
end
|
|
78
|
+
|
|
36
79
|
describe "provides methods for performing non-blocking I/O" do
|
|
37
80
|
require "tempfile"
|
|
38
81
|
|
|
@@ -126,6 +169,31 @@ describe Cool.io::Buffer do
|
|
|
126
169
|
expect(buffer.size).to eq 0
|
|
127
170
|
expect(buffer.empty?).to eq true
|
|
128
171
|
end
|
|
172
|
+
|
|
173
|
+
it "reuses pooled nodes instead of leaking them" do
|
|
174
|
+
skip "GC.stat(:malloc_increase_bytes) is not available" unless GC.stat.key?(:malloc_increase_bytes)
|
|
175
|
+
node_size = Cool.io::Buffer.default_node_size
|
|
176
|
+
data = "a" * (node_size * 2 + 1)
|
|
177
|
+
cycle = proc do
|
|
178
|
+
buffer << data
|
|
179
|
+
buffer.clear
|
|
180
|
+
buffer << "x"
|
|
181
|
+
buffer.clear
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Marking a Buffer frees its pool, so keep GC off from the warm-up on.
|
|
185
|
+
GC.start
|
|
186
|
+
gc_was_disabled = GC.disable
|
|
187
|
+
begin
|
|
188
|
+
3.times { cycle.call }
|
|
189
|
+
before = GC.stat(:malloc_increase_bytes)
|
|
190
|
+
200.times { cycle.call }
|
|
191
|
+
grown = GC.stat(:malloc_increase_bytes) - before
|
|
192
|
+
ensure
|
|
193
|
+
GC.enable unless gc_was_disabled
|
|
194
|
+
end
|
|
195
|
+
expect(grown).to be < node_size * 4
|
|
196
|
+
end
|
|
129
197
|
end
|
|
130
198
|
|
|
131
199
|
context "#read_frame" do
|
|
@@ -142,6 +210,25 @@ describe Cool.io::Buffer do
|
|
|
142
210
|
expect(data).to eq "foo\nbarbaz"
|
|
143
211
|
expect(buffer.to_str).to eq ""
|
|
144
212
|
end
|
|
213
|
+
|
|
214
|
+
it "raises TypeError instead of crashing when data is not a String" do
|
|
215
|
+
buffer << "hello world"
|
|
216
|
+
expect { buffer.read_frame 12345, " ".ord }.to raise_error(TypeError)
|
|
217
|
+
expect { buffer.read_frame nil, " ".ord }.to raise_error(TypeError)
|
|
218
|
+
expect { buffer.read_frame [], " ".ord }.to raise_error(TypeError)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "raises FrozenError when data is a frozen String" do
|
|
222
|
+
buffer << "hello world"
|
|
223
|
+
expect { buffer.read_frame "frozen".freeze, " ".ord }.to raise_error(FrozenError)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "coerces objects responding to #to_str" do
|
|
227
|
+
buffer << "foo\nbar"
|
|
228
|
+
convertible = Object.new
|
|
229
|
+
def convertible.to_str; +""; end
|
|
230
|
+
expect(buffer.read_frame convertible, "\n".ord).to eq true
|
|
231
|
+
end
|
|
145
232
|
end
|
|
146
233
|
|
|
147
234
|
end
|
data/spec/loop_spec.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'rbconfig'
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
describe Cool.io::Loop do
|
|
6
|
+
it "ignores $LIBEV_FLAGS when skip_environment is true" do
|
|
7
|
+
script = <<~'RUBY'
|
|
8
|
+
require 'cool.io'
|
|
9
|
+
loop = Cool.io::Loop.new(skip_environment: true)
|
|
10
|
+
fired = false
|
|
11
|
+
timer = Cool.io::TimerWatcher.new(0.01)
|
|
12
|
+
timer.on_timer { fired = true; timer.detach }
|
|
13
|
+
timer.attach(loop)
|
|
14
|
+
loop.run
|
|
15
|
+
print fired
|
|
16
|
+
RUBY
|
|
17
|
+
lib = File.expand_path('../lib', __dir__)
|
|
18
|
+
# No CI platform has the Solaris event port backend, so honoring this would leave no backend.
|
|
19
|
+
env = { 'LIBEV_FLAGS' => Cool.io::Loop::EVBACKEND_PORT.to_s }
|
|
20
|
+
output, error, status = Open3.capture3(env, RbConfig.ruby, '-I', lib, '-e', script)
|
|
21
|
+
expect(status.success?).to eq(true), error
|
|
22
|
+
expect(output).to eq("true")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "raises an error when no requested backend is usable" do
|
|
26
|
+
backend = RUBY_PLATFORM.include?("solaris") ? :kqueue : :port
|
|
27
|
+
expect {
|
|
28
|
+
Cool.io::Loop.new(skip_environment: true, backend: backend)
|
|
29
|
+
}.to raise_error(RuntimeError, /no usable backend/)
|
|
30
|
+
end
|
|
31
|
+
end
|
data/spec/stat_watcher_spec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'tmpdir'
|
|
2
3
|
|
|
3
4
|
TEMP_FILE_PATH = "./test.txt"
|
|
4
5
|
|
|
@@ -18,12 +19,31 @@ class MyStatWatcher < Cool.io::StatWatcher
|
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def run_with_file_change(path)
|
|
22
|
+
def run_with_file_change(path, compact: false)
|
|
22
23
|
reactor = Cool.io::Loop.new
|
|
23
24
|
|
|
24
25
|
sw = MyStatWatcher.new(path)
|
|
25
26
|
sw.attach(reactor)
|
|
26
27
|
|
|
28
|
+
# libev retains the path pointer passed to ev_stat_init() for the lifetime of
|
|
29
|
+
# the watcher. If the watcher held RSTRING_PTR(@path) directly, a compaction
|
|
30
|
+
# that relocates the @path String would leave libev dereferencing a stale
|
|
31
|
+
# address on every stat. Force a maximal relocation here so the watcher keeps
|
|
32
|
+
# operating against a path buffer it owns rather than Ruby-managed memory.
|
|
33
|
+
if compact
|
|
34
|
+
if GC.respond_to?(:verify_compaction_references)
|
|
35
|
+
# verify_compaction_references moves every movable object to a fresh slot.
|
|
36
|
+
# Its keyword arguments have varied across Ruby versions, so fall back.
|
|
37
|
+
begin
|
|
38
|
+
GC.verify_compaction_references(expand_heap: true, toplevel: true)
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
GC.verify_compaction_references(expand_heap: true)
|
|
41
|
+
end
|
|
42
|
+
elsif GC.respond_to?(:compact)
|
|
43
|
+
GC.compact
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
27
47
|
tw = Cool.io::TimerWatcher.new(INTERVAL, true)
|
|
28
48
|
tw.on_timer do
|
|
29
49
|
reactor.stop if sw.accessed
|
|
@@ -69,6 +89,71 @@ describe Cool.io::StatWatcher do
|
|
|
69
89
|
expect(watcher.previous.ino).to eq(watcher.current.ino)
|
|
70
90
|
end
|
|
71
91
|
|
|
92
|
+
it "keeps firing on_change after GC compaction relocates objects" do
|
|
93
|
+
skip "GC compaction not available" unless GC.respond_to?(:compact)
|
|
94
|
+
|
|
95
|
+
watcher = run_with_file_change(TEMP_FILE_PATH, compact: true)
|
|
96
|
+
expect(watcher.accessed).to eq(true)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "reports sizes and timestamps beyond the 32-bit range" do
|
|
100
|
+
Dir.mktmpdir do |dir|
|
|
101
|
+
path = File.join(dir, "large")
|
|
102
|
+
File.write(path, "a")
|
|
103
|
+
size = 2**32 + 1
|
|
104
|
+
time = Time.utc(2040, 1, 1)
|
|
105
|
+
current = nil
|
|
106
|
+
reactor = Cool.io::Loop.new
|
|
107
|
+
watcher = Cool.io::StatWatcher.new(path, INTERVAL)
|
|
108
|
+
watcher.define_singleton_method(:on_change) { |_, cur| current = cur }
|
|
109
|
+
watcher.attach(reactor)
|
|
110
|
+
|
|
111
|
+
File.truncate(path, size)
|
|
112
|
+
File.utime(time, time, path)
|
|
113
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
114
|
+
until (current && current.size == size && current.mtime == time) ||
|
|
115
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
116
|
+
reactor.run_once(0.1)
|
|
117
|
+
end
|
|
118
|
+
watcher.detach
|
|
119
|
+
|
|
120
|
+
expect(current.size).to eq(size)
|
|
121
|
+
expect(current.mtime).to eq(time)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "reports blksize and blocks as File.lstat does" do
|
|
126
|
+
Dir.mktmpdir do |dir|
|
|
127
|
+
path = File.join(dir, "file")
|
|
128
|
+
File.write(path, "a")
|
|
129
|
+
size = 8192
|
|
130
|
+
current = nil
|
|
131
|
+
reactor = Cool.io::Loop.new
|
|
132
|
+
watcher = Cool.io::StatWatcher.new(path, INTERVAL)
|
|
133
|
+
watcher.define_singleton_method(:on_change) { |_, cur| current = cur }
|
|
134
|
+
watcher.attach(reactor)
|
|
135
|
+
begin
|
|
136
|
+
File.write(path, "a" * size)
|
|
137
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
138
|
+
until (current && current.size == size) ||
|
|
139
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
140
|
+
reactor.run_once(0.1)
|
|
141
|
+
end
|
|
142
|
+
ensure
|
|
143
|
+
watcher.detach
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
stat = File.lstat(path)
|
|
147
|
+
expect(current).not_to be_nil
|
|
148
|
+
expect(current.blksize).to eq(stat.blksize)
|
|
149
|
+
expect(current.blocks).to eq(stat.blocks)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "raises ArgumentError when the path contains a null byte" do
|
|
154
|
+
expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
|
|
155
|
+
end
|
|
156
|
+
|
|
72
157
|
it "should raise when the handler does not take 2 parameters" do
|
|
73
158
|
class MyStatWatcher < Cool.io::StatWatcher
|
|
74
159
|
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
|
|
@@ -151,9 +175,9 @@ describe Coolio::TCPServer do
|
|
|
151
175
|
it "connected socket called on_connect" do
|
|
152
176
|
begin
|
|
153
177
|
connected = false
|
|
154
|
-
server = Cool.io::TCPServer.new(
|
|
178
|
+
server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { connected = true })
|
|
155
179
|
loop.attach server
|
|
156
|
-
s = TCPSocket.open(
|
|
180
|
+
s = TCPSocket.open(HOST, port)
|
|
157
181
|
loop.run_once
|
|
158
182
|
s.close
|
|
159
183
|
expect(connected).to eq true
|
|
@@ -177,9 +201,9 @@ describe Coolio::TCPServer do
|
|
|
177
201
|
it "closed socket called on_close" do
|
|
178
202
|
begin
|
|
179
203
|
closed = false
|
|
180
|
-
server = Cool.io::TCPServer.new(
|
|
204
|
+
server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { closed = true })
|
|
181
205
|
loop.attach server
|
|
182
|
-
s = TCPSocket.open(
|
|
206
|
+
s = TCPSocket.open(HOST, port)
|
|
183
207
|
loop.run_once
|
|
184
208
|
s.close
|
|
185
209
|
loop.run_once
|
|
@@ -205,10 +229,10 @@ describe Coolio::TCPServer do
|
|
|
205
229
|
it "server socket received data" do
|
|
206
230
|
begin
|
|
207
231
|
data = "aaa"
|
|
208
|
-
server = Cool.io::TCPServer.new(
|
|
232
|
+
server = Cool.io::TCPServer.new(HOST, port, Echo, proc { |d| data = d })
|
|
209
233
|
loop.attach server
|
|
210
234
|
thread = Thread.new { loop.run }
|
|
211
|
-
s = TCPSocket.open(
|
|
235
|
+
s = TCPSocket.open(HOST, port)
|
|
212
236
|
s.write "zzz"
|
|
213
237
|
sleep 0.1
|
|
214
238
|
expect(data).to eq "zzz"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
describe 'Watcher reattachment' do
|
|
4
|
+
[:timer, :io].each do |type|
|
|
5
|
+
[false, true].each do |disabled|
|
|
6
|
+
[false, true].each do |same_loop|
|
|
7
|
+
it "reattaches a #{disabled ? 'disabled' : 'enabled'} #{type} watcher to #{same_loop ? 'the same' : 'another'} loop" do
|
|
8
|
+
original = Coolio::Loop.new
|
|
9
|
+
destination = same_loop ? original : Coolio::Loop.new
|
|
10
|
+
if type == :io
|
|
11
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
12
|
+
writer = TCPSocket.new('127.0.0.1', server.addr[1])
|
|
13
|
+
reader = server.accept
|
|
14
|
+
server.close
|
|
15
|
+
watcher = Coolio::IOWatcher.new(reader)
|
|
16
|
+
else
|
|
17
|
+
watcher = Coolio::TimerWatcher.new(0.01, true)
|
|
18
|
+
end
|
|
19
|
+
fired = false
|
|
20
|
+
callback = proc { fired = true }
|
|
21
|
+
if type == :io
|
|
22
|
+
watcher.on_readable(&callback)
|
|
23
|
+
else
|
|
24
|
+
watcher.on_timer(&callback)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
watcher.attach(original)
|
|
28
|
+
watcher.disable if disabled
|
|
29
|
+
expect(watcher.attach(destination)).to equal(watcher)
|
|
30
|
+
expect(watcher.evloop).to equal(destination)
|
|
31
|
+
expect(watcher).to be_enabled
|
|
32
|
+
expect(destination.instance_variable_get(:@active_watchers)).to eq(1)
|
|
33
|
+
unless same_loop
|
|
34
|
+
expect(original.watchers).to be_empty
|
|
35
|
+
expect(original.instance_variable_get(:@active_watchers)).to eq(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
writer.write('a') if writer
|
|
39
|
+
sleep 0.02 if type == :timer
|
|
40
|
+
destination.run_once(0.1)
|
|
41
|
+
expect(fired).to eq(true)
|
|
42
|
+
watcher.detach
|
|
43
|
+
expect(destination.watchers).to be_empty
|
|
44
|
+
expect(destination.instance_variable_get(:@active_watchers)).to eq(0)
|
|
45
|
+
ensure
|
|
46
|
+
watcher.detach if watcher && watcher.attached?
|
|
47
|
+
reader.close if reader
|
|
48
|
+
writer.close if writer
|
|
49
|
+
server.close if server && !server.closed?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
|
|
6
|
+
describe 'Windows stat notification internals', :if => RUBY_PLATFORM =~ /mingw|mswin/ do
|
|
7
|
+
before(:context) do
|
|
8
|
+
@build = Dir.mktmpdir('coolio-stat-native-')
|
|
9
|
+
source = File.expand_path('fixtures/windows_stat_native.c', __dir__)
|
|
10
|
+
libev = File.expand_path('../ext/libev', __dir__)
|
|
11
|
+
FileUtils.cp(source, @build)
|
|
12
|
+
File.write(File.join(@build, 'extconf.rb'), <<~RUBY)
|
|
13
|
+
begin
|
|
14
|
+
require 'devkit' if RUBY_PLATFORM =~ /mingw/
|
|
15
|
+
rescue LoadError
|
|
16
|
+
# Other Windows toolchains can provide the compiler through PATH.
|
|
17
|
+
end
|
|
18
|
+
require 'mkmf'
|
|
19
|
+
$INCFLAGS << ' -I"#{libev}"'
|
|
20
|
+
abort 'ws2_32 is required' unless have_library('ws2_32')
|
|
21
|
+
create_makefile('coolio_stat_native')
|
|
22
|
+
RUBY
|
|
23
|
+
File.open(File.join(@build, 'extconf.rb'), 'a') do |file|
|
|
24
|
+
file.write <<~'RUBY'
|
|
25
|
+
# Match cool.io's Windows link order so Winsock calls resolve to ws2_32.
|
|
26
|
+
contents = File.read('Makefile')
|
|
27
|
+
contents.gsub!(/LIBS = (.*) (\S*ws2_32\S*)/i) do
|
|
28
|
+
"LIBS = #{Regexp.last_match(2)} #{Regexp.last_match(1)}"
|
|
29
|
+
end
|
|
30
|
+
File.write('Makefile', contents)
|
|
31
|
+
make = ENV['MAKE'] || RbConfig::CONFIG['MAKE'] || (RUBY_PLATFORM =~ /mswin/ ? 'nmake' : 'make')
|
|
32
|
+
abort 'native stat test build failed' unless system(make)
|
|
33
|
+
RUBY
|
|
34
|
+
end
|
|
35
|
+
output = IO.popen([RbConfig.ruby, 'extconf.rb'], chdir: @build, err: [:child, :out], &:read)
|
|
36
|
+
raise output unless $?.success?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
after(:context) do
|
|
40
|
+
FileUtils.remove_entry(@build) if @build && File.directory?(@build)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def native_result(script, *args)
|
|
44
|
+
error_log = File.join(@build, 'native-error.log')
|
|
45
|
+
command = [RbConfig.ruby, '-I', @build, '-rcoolio_stat_native', '-e',
|
|
46
|
+
"$stdout.binmode; #{script}", *args]
|
|
47
|
+
output = IO.popen(command, 'rb', err: error_log, &:read)
|
|
48
|
+
expect($?.success?).to eq(true), File.read(error_log)
|
|
49
|
+
Marshal.load(output)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def native_paths(paths)
|
|
53
|
+
args = paths.flat_map { |path, parent, _| [path, parent ? '1' : '0'] }
|
|
54
|
+
native_result(<<~'RUBY', *args)
|
|
55
|
+
paths = ARGV.each_slice(2).map { |path, parent| CoolioStatNativeSpec.path(path, parent == '1') }
|
|
56
|
+
$stdout.write(Marshal.dump(paths))
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'preserves ordinary and extended-length drive roots' do
|
|
61
|
+
paths = [
|
|
62
|
+
['C:\\file.txt', true, 'C:\\'],
|
|
63
|
+
['C:\\', false, 'C:\\'],
|
|
64
|
+
['C:\\', true, 'C:\\'],
|
|
65
|
+
['\\\\?\\C:\\file.txt', true, '\\\\?\\C:\\'],
|
|
66
|
+
['\\\\?\\C:\\', false, '\\\\?\\C:\\'],
|
|
67
|
+
['\\\\?\\C:\\', true, '\\\\?\\C:\\'],
|
|
68
|
+
['\\\\?\\C:\\dir\\file', true, '\\\\?\\C:\\dir']
|
|
69
|
+
]
|
|
70
|
+
expect(native_paths(paths)).to eq(paths.map(&:last))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'preserves ordinary and extended-length UNC share roots' do
|
|
74
|
+
paths = [
|
|
75
|
+
['\\\\server\\share\\', false, '\\\\server\\share\\'],
|
|
76
|
+
['\\\\server\\share\\', true, '\\\\server\\share\\'],
|
|
77
|
+
['\\\\server\\share\\file', true, '\\\\server\\share\\'],
|
|
78
|
+
['\\\\?\\UNC\\server\\share\\', false, '\\\\?\\UNC\\server\\share\\'],
|
|
79
|
+
['\\\\?\\UNC\\server\\share\\file', true, '\\\\?\\UNC\\server\\share\\']
|
|
80
|
+
]
|
|
81
|
+
expect(native_paths(paths)).to eq(paths.map(&:last))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'falls back to polling on pipe failure and recovers without creating a second pipe' do
|
|
85
|
+
path = File.join(@build, 'watched.txt')
|
|
86
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.check_pipe(ARGV[0])))', path)
|
|
87
|
+
expect(result).to eq(true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'destroys the loop without waiting for delayed worker completions' do
|
|
91
|
+
path = File.join(@build, 'shutdown.txt')
|
|
92
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.check_shutdown(ARGV[0])))', path)
|
|
93
|
+
expect(result).to eq(true)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[[0, 'polling'], [1, 'notifications'], [2, 'polling after an unrelated name change']].each do |mode, description|
|
|
97
|
+
it "checks a shared directory once without alias lookups during #{description}" do
|
|
98
|
+
result = native_result(<<~'RUBY', @build, mode.to_s)
|
|
99
|
+
result = CoolioStatNativeSpec.path_work(ARGV[0], ARGV[1].to_i)
|
|
100
|
+
$stdout.write(Marshal.dump(result))
|
|
101
|
+
RUBY
|
|
102
|
+
# One open and two identity queries for 32 changed files; no alias work.
|
|
103
|
+
expect(result).to eq([1, 2, 0, true])
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'keeps finding each watch as watchers stop, restart and re-register' do
|
|
108
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.watch_index(ARGV[0])))', @build)
|
|
109
|
+
expect(result).to eq([1, 2, 0, true, true])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'shares directories and routes name records through the directory index' do
|
|
113
|
+
result = native_result(<<~'RUBY', @build)
|
|
114
|
+
dir = File.join(ARGV[0], 'a')
|
|
115
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
|
116
|
+
File.write(File.join(dir, "\u00e9.txt"), 'a')
|
|
117
|
+
$stdout.write(Marshal.dump(CoolioStatNativeSpec.name_index(ARGV[0])))
|
|
118
|
+
RUBY
|
|
119
|
+
expect(result).to eq([true,
|
|
120
|
+
[false, true, false, true, false, true, true],
|
|
121
|
+
[true, true, false, true, false, true, true]])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'relinks changed alias names and unlinks them when the watcher stops' do
|
|
125
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.alias_index(ARGV[0])))', @build)
|
|
126
|
+
expect(result).to eq([true, false, true, true, true])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'skips name lookups for roots and wildcard names' do
|
|
130
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.name_guards(ARGV[0])))', @build)
|
|
131
|
+
expect(result).to eq(root: true, wildcard: true)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'routes records for the short and the long name of a watched file' do
|
|
135
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.short_alias(ARGV[0])))', @build)
|
|
136
|
+
skip 'the volume does not create 8.3 names' if result.nil?
|
|
137
|
+
expect(result).to eq(short: true, long: true)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'keeps sharing directories, queues and routing names after the tables grow' do
|
|
141
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.index_growth(ARGV[0])))', @build)
|
|
142
|
+
expect(result).to eq([true, true, true, true])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
[[1, 'polling'], [0, 'an explicit registration']].each do |polling, description|
|
|
146
|
+
it "keeps a shared child directory when a relative directory moves with the current directory during #{description}" do
|
|
147
|
+
first, second = %w[cwd1 cwd2].map { |name| File.join(@build, "#{name}-#{polling}") }
|
|
148
|
+
FileUtils.mkdir_p(File.join(first, 'rel'))
|
|
149
|
+
File.write(File.join(first, 'rel', 'f.txt'), 'a')
|
|
150
|
+
FileUtils.mkdir_p(File.join(second, 'rel'))
|
|
151
|
+
script = '$stdout.write(Marshal.dump(CoolioStatNativeSpec.children_cwd(ARGV[0], ARGV[1], ARGV[2].to_i)))'
|
|
152
|
+
result = native_result(script, first, second, polling.to_s)
|
|
153
|
+
expect(result).to eq(shared: true, failed: false, moved: false, followed: true)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def replaced_dir(mode)
|
|
158
|
+
script = '$stdout.write(Marshal.dump(CoolioStatNativeSpec.replaced_dir(ARGV[0], ARGV[1].to_i)))'
|
|
159
|
+
native_result(script, @build, mode.to_s)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'moves directory and file watchers to a directory recreated under the same name' do
|
|
163
|
+
expect(replaced_dir(0)).to eq(directory: true, file: true)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'moves the watchers when a rename record arrives before the creation record' do
|
|
167
|
+
expect(replaced_dir(1)).to eq(directory: true, file: true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'registers the contents of a directory that was missing while the watcher registered again' do
|
|
171
|
+
expect(replaced_dir(2)).to eq(directory: true, file: nil)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'refreshes aliases after replacement records and retries failed lookups' do
|
|
175
|
+
path = File.join(@build, 'aliases.txt')
|
|
176
|
+
result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.alias_recovery(ARGV[0])))', path)
|
|
177
|
+
expect(result).to eq([true, true, true, true])
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'uses effective notification flags and respects EVFLAG_NOENV' do
|
|
181
|
+
path = File.join(@build, 'flags.txt')
|
|
182
|
+
# EVFLAG_NOINOTIFY = 0x00100000; EVFLAG_NOENV = 0x01000000.
|
|
183
|
+
[[0, 1048576, false], [16777216, 1048576, true],
|
|
184
|
+
[1048576, 0, true], [17825792, 0, false]].each do |flags, environment, expected|
|
|
185
|
+
result = native_result(<<~'RUBY', path, flags.to_s, environment.to_s)
|
|
186
|
+
ENV['LIBEV_FLAGS'] = ARGV[2]
|
|
187
|
+
enabled = CoolioStatNativeSpec.notification_enabled(ARGV[0], ARGV[1].to_i)
|
|
188
|
+
$stdout.write(Marshal.dump(enabled))
|
|
189
|
+
RUBY
|
|
190
|
+
expect(result).to eq(expected), "flags=#{flags}, LIBEV_FLAGS=#{environment}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|