cool.io 1.10.1 → 1.11.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.
@@ -0,0 +1,339 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'rbconfig'
5
+ require 'timeout'
6
+
7
+ describe 'kqueue notification-backed StatWatcher', :if => RUBY_PLATFORM =~ /darwin/ do
8
+ before do
9
+ @directory = Dir.mktmpdir('coolio-kq-notify-')
10
+ @loop = Coolio::Loop.new
11
+ @watchers = []
12
+ @threads = []
13
+ end
14
+
15
+ after do
16
+ @threads.each(&:join)
17
+ @watchers.each { |watcher| watcher.detach if watcher.attached? }
18
+ FileUtils.remove_entry(@directory)
19
+ @loop = nil
20
+ @watchers = nil
21
+ GC.start # exercise cleanup of unreachable loops and watchers
22
+ end
23
+
24
+ # Most tests use a 60-second interval so native notifications must deliver
25
+ # changes before their deadlines. Recovery tests can override the interval.
26
+ def watch(path, interval = 60)
27
+ watcher = Coolio::StatWatcher.new(path, interval)
28
+ watcher.attach(@loop)
29
+ @watchers << watcher
30
+ watcher
31
+ end
32
+
33
+ def await_change(watcher, size: nil, exists: nil, timeout: 2, &mutation)
34
+ change = nil
35
+ first_previous = nil
36
+ reactor = @loop
37
+ stopped = false
38
+ stop = -> do
39
+ unless stopped
40
+ stopped = true
41
+ reactor.stop
42
+ end
43
+ end
44
+ watcher.define_singleton_method(:on_change) do |previous, current|
45
+ first_previous ||= previous
46
+ if (size.nil? || current.size == size) && (exists.nil? || (current.nlink != 0) == exists)
47
+ change = [first_previous, current]
48
+ stop.call
49
+ end
50
+ end
51
+ deadline = Coolio::TimerWatcher.new(timeout, false)
52
+ deadline.on_timer { stop.call }
53
+ deadline.attach(@loop)
54
+ @threads << Thread.new { sleep 0.1; mutation.call }
55
+ @loop.run
56
+ expect(change).not_to be_nil
57
+ change
58
+ ensure
59
+ deadline.detach if deadline && deadline.attached?
60
+ @threads.each(&:join)
61
+ end
62
+
63
+ def in_child
64
+ pid = fork do
65
+ begin
66
+ yield
67
+ exit! 0
68
+ rescue Exception => error
69
+ warn error.full_message
70
+ exit! 1
71
+ end
72
+ end
73
+ begin
74
+ _, status = Timeout.timeout(10) { Process.wait2(pid) }
75
+ rescue Timeout::Error
76
+ Process.kill('KILL', pid)
77
+ Process.wait(pid)
78
+ raise 'forked watcher test timed out'
79
+ end
80
+ expect(status.success?).to eq(true)
81
+ end
82
+
83
+ it 'wakes a blocked loop for a file size change without waiting for polling' do
84
+ path = File.join(@directory, 'file')
85
+ File.write(path, 'a')
86
+ previous, current = await_change(watch(path), size: 6) { File.write(path, 'longer') }
87
+ expect(previous.size).to eq(1)
88
+ expect(current.size).to eq(6)
89
+ end
90
+
91
+ it 'notifies for a truncation' do
92
+ path = File.join(@directory, 'file')
93
+ File.write(path, 'x' * 40)
94
+ await_change(watch(path), size: 0) { File.truncate(path, 0) }
95
+ end
96
+
97
+ it 'notifies for creation, deletion, and recreation of a watched file' do
98
+ directory = File.join(@directory, 'missing')
99
+ path = File.join(directory, 'file')
100
+ watcher = watch(path)
101
+ previous, current = await_change(watcher, size: 7, exists: true) do
102
+ Dir.mkdir(directory)
103
+ File.write(path, 'created')
104
+ end
105
+ expect(previous.nlink).to eq(0)
106
+ expect(current.nlink).not_to eq(0)
107
+ _, current = await_change(watcher, exists: false) { File.delete(path) }
108
+ expect(current.nlink).to eq(0)
109
+ _, current = await_change(watcher, size: 9, exists: true) { File.write(path, 'recreated') }
110
+ expect(current.size).to eq(9)
111
+ end
112
+
113
+ it 'notifies when a watched file is renamed away (log rotation)' do
114
+ path = File.join(@directory, 'file')
115
+ File.write(path, 'a')
116
+ _, current = await_change(watch(path)) { File.rename(path, "#{path}.old") }
117
+ expect(current.nlink).to eq(0)
118
+ end
119
+
120
+ it 'notifies when a watched file is replaced under the same name' do
121
+ path = File.join(@directory, 'file')
122
+ File.write(path, 'a')
123
+ watcher = watch(path)
124
+ _, current = await_change(watcher, size: 6, exists: true) do
125
+ File.rename(path, "#{path}.old")
126
+ File.write(path, 'longer')
127
+ end
128
+ expect(current.size).to eq(6)
129
+ end
130
+
131
+ it 'notifies for atomic replacement and subsequent changes' do
132
+ path = File.join(@directory, 'file')
133
+ replacement = File.join(@directory, 'replacement')
134
+ File.write(path, 'a')
135
+ File.write(replacement, 'longer')
136
+ watcher = watch(path)
137
+ await_change(watcher, size: 6) { File.rename(replacement, path) }
138
+ await_change(watcher, size: 7) { File.write(path, 'changed') }
139
+ end
140
+
141
+ it 'uses parent hints when a symlink is replaced' do
142
+ path = File.join(@directory, 'link')
143
+ File.symlink('missing', path)
144
+ watcher = watch(path)
145
+ _, current = await_change(watcher, size: 22, exists: true) do
146
+ File.unlink(path)
147
+ File.symlink('another-missing-target', path)
148
+ end
149
+ expect(current.mode & 0o170000).to eq(0o120000)
150
+ expect(current.size).to eq('another-missing-target'.bytesize)
151
+ end
152
+
153
+ it 'attaches without blocking for a FIFO or a path below it' do
154
+ in_child do
155
+ path = File.join(@directory, 'fifo')
156
+ File.mkfifo(path)
157
+ fifo = watch(path, 0.1)
158
+ watch(File.join(path, 'missing'), 0.1)
159
+ await_change(fifo, exists: false) { File.unlink(path) }
160
+ end
161
+ end
162
+
163
+ it 'does not wake a FIFO writer when attaching or retrying registration' do
164
+ in_child do
165
+ path = File.join(@directory, 'fifo')
166
+ File.mkfifo(path)
167
+ reader, ready = IO.pipe
168
+ writer = fork do
169
+ reader.close
170
+ ready.write('ready')
171
+ File.open(path, File::WRONLY) { ready.write('opened') }
172
+ exit! 0
173
+ end
174
+ ready.close
175
+ begin
176
+ expect(reader.read(5)).to eq('ready')
177
+ expect(IO.select([reader], nil, nil, 0.1)).to be_nil
178
+ watch(path, 0.1)
179
+ watch(File.join(path, 'missing'), 0.1)
180
+ deadline = Coolio::TimerWatcher.new(0.35, false)
181
+ deadline.on_timer { @loop.stop }
182
+ deadline.attach(@loop)
183
+ @loop.run
184
+ expect(IO.select([reader], nil, nil, 0)).to be_nil
185
+ ensure
186
+ deadline.detach if deadline && deadline.attached?
187
+ Process.kill('KILL', writer)
188
+ Process.wait(writer)
189
+ reader.close
190
+ end
191
+ end
192
+ end
193
+
194
+ it 'continues polling in a child without fork checking' do
195
+ path = File.join(@directory, 'file')
196
+ File.write(path, 'a')
197
+ watcher = watch(path, 0.1)
198
+ in_child do
199
+ await_change(watcher, size: 6) { File.write(path, 'longer') }
200
+ end
201
+ end
202
+
203
+ it 'notifies when a child is added to a watched directory' do
204
+ directory = File.join(@directory, 'sub')
205
+ Dir.mkdir(directory)
206
+ await_change(watch(directory)) { File.write(File.join(directory, 'child'), 'a') }
207
+ end
208
+
209
+ it 'notifies for a metadata-only change' do
210
+ path = File.join(@directory, 'file')
211
+ File.write(path, 'a')
212
+ mode = (File.stat(path).mode & 0o777) ^ 0o100
213
+ await_change(watch(path)) { File.chmod(mode, path) }
214
+ end
215
+
216
+ it 're-registers when the watched directory itself is recreated' do
217
+ directory = File.join(@directory, 'sub')
218
+ Dir.mkdir(directory)
219
+ watcher = watch(directory)
220
+ await_change(watcher, exists: false) { FileUtils.remove_entry(directory) }
221
+ await_change(watcher, exists: true) { Dir.mkdir(directory) }
222
+ await_change(watcher, timeout: 5) { File.write(File.join(directory, 'child'), 'a' * 6) }
223
+ end
224
+
225
+ it 're-registers when a parent of the watched file is recreated under the same name' do
226
+ sub = File.join(@directory, 'sub')
227
+ Dir.mkdir(sub)
228
+ path = File.join(sub, 'file')
229
+ File.write(path, 'a')
230
+ watcher = watch(path)
231
+ await_change(watcher, exists: false) { FileUtils.remove_entry(sub) }
232
+ await_change(watcher, size: 6, exists: true, timeout: 5) do
233
+ Dir.mkdir(sub)
234
+ File.write(path, 'longer')
235
+ end
236
+ end
237
+
238
+ it 'transfers notification monitoring to another loop' do
239
+ path = File.join(@directory, 'file')
240
+ File.write(path, 'a')
241
+ watcher = watch(path)
242
+ old_loop = @loop
243
+ @loop = Coolio::Loop.new
244
+ watcher.attach(@loop)
245
+ old_loop.run_nonblock
246
+ _, current = await_change(watcher, size: 6) { File.write(path, 'longer') }
247
+ expect(current.size).to eq(6)
248
+ end
249
+
250
+ it 'recovers when an ancestor is renamed and the original path is recreated' do
251
+ sub = File.join(@directory, 'sub')
252
+ Dir.mkdir(sub)
253
+ path = File.join(sub, 'file')
254
+ File.write(path, 'a')
255
+ watcher = watch(path, 0)
256
+
257
+ await_change(watcher, size: 6, exists: true, timeout: 8) do
258
+ File.rename(sub, "#{sub}.old")
259
+ Dir.mkdir(sub)
260
+ File.write(path, 'longer')
261
+ end
262
+ # The polling backstop must also transfer native monitoring to the new file.
263
+ await_change(watcher, size: 10) { File.write(path, 'evenlonger') }
264
+ end
265
+
266
+ [false, true].each do |missing|
267
+ it "does not inherit #{missing ? 'parent hint' : 'file'} descriptors across exec" do
268
+ path = File.join(@directory, 'file')
269
+ File.write(path, 'a') unless missing
270
+ target = missing ? @directory : path
271
+ watch(path)
272
+
273
+ script = <<~'RUBY'
274
+ target = File.stat(ARGV.fetch(0))
275
+ leaked = Dir.children('/dev/fd').any? do |fd|
276
+ begin
277
+ stat = IO.for_fd(Integer(fd), autoclose: false).stat
278
+ stat.dev == target.dev && stat.ino == target.ino
279
+ rescue Errno::EBADF, ArgumentError
280
+ false
281
+ end
282
+ end
283
+ puts leaked
284
+ RUBY
285
+ inherited = IO.popen([RbConfig.ruby, '-e', script, target], close_others: false, &:read)
286
+ expect(inherited.strip).to eq('false')
287
+ end
288
+ end
289
+
290
+ it 'keeps monitoring when another watcher on the same path detaches' do
291
+ path = File.join(@directory, 'file')
292
+ File.write(path, 'a')
293
+ first = watch(path)
294
+ second = watch(path)
295
+ await_change(first, size: 6) { File.write(path, 'b' * 6) }
296
+ first.detach
297
+ await_change(second, size: 7) { File.write(path, 'c' * 7) }
298
+ end
299
+
300
+ it 'notifies every watcher of many watched files within one burst' do
301
+ # Exceed the native event batch size without requiring hundreds of fds.
302
+ paths = (1..64).map do |i|
303
+ path = File.join(@directory, "file#{i}")
304
+ File.write(path, 'a')
305
+ watch(path)
306
+ end
307
+ notified = 0
308
+ reactor = @loop
309
+ paths.each do |watcher|
310
+ watcher.define_singleton_method(:on_change) do |_previous, current|
311
+ if current.size == 2
312
+ notified += 1
313
+ reactor.stop if notified == paths.size
314
+ end
315
+ end
316
+ end
317
+ deadline = Coolio::TimerWatcher.new(5, false)
318
+ deadline.on_timer { @loop.stop }
319
+ deadline.attach(@loop)
320
+ paths.each { |watcher| File.open(watcher.path, 'a') { |file| file.write 'b' } }
321
+ @loop.run
322
+ deadline.detach if deadline.attached?
323
+ expect(notified).to eq(paths.size)
324
+ end
325
+
326
+ it 're-registers inherited watchers after fork without changing the parent loop' do
327
+ @loop = Coolio::Loop.new(fork_check: true)
328
+ path = File.join(@directory, 'file')
329
+ File.write(path, 'a')
330
+ watcher = watch(path)
331
+
332
+ in_child do
333
+ await_change(watcher, size: 6) { File.write(path, 'longer') }
334
+ watcher.detach
335
+ end
336
+ await_change(watcher, size: 10) { File.write(path, 'evenlonger') }
337
+ end
338
+
339
+ end
@@ -19,10 +19,10 @@ class MyStatWatcher < Cool.io::StatWatcher
19
19
  end
20
20
  end
21
21
 
22
- def run_with_file_change(path, compact: false)
22
+ def run_with_file_change(path, watcher_class: MyStatWatcher, compact: false)
23
23
  reactor = Cool.io::Loop.new
24
24
 
25
- sw = MyStatWatcher.new(path)
25
+ sw = watcher_class.new(path)
26
26
  sw.attach(reactor)
27
27
 
28
28
  # libev retains the path pointer passed to ev_stat_init() for the lifetime of
@@ -155,12 +155,11 @@ describe Cool.io::StatWatcher do
155
155
  end
156
156
 
157
157
  it "should raise when the handler does not take 2 parameters" do
158
- class MyStatWatcher < Cool.io::StatWatcher
159
- remove_method :on_change
158
+ watcher_class = Class.new(MyStatWatcher) do
160
159
  def on_change
161
160
  end
162
161
  end
163
- expect { watcher.accessed }.to raise_error(ArgumentError)
162
+ expect { run_with_file_change(TEMP_FILE_PATH, watcher_class: watcher_class) }.to raise_error(ArgumentError)
164
163
  end
165
164
 
166
165
  end
@@ -53,7 +53,16 @@ describe Cool.io::TimerWatcher do
53
53
  expect(nr).to eq(:foo)
54
54
  end
55
55
 
56
+ it "reports whether it is attached" do
57
+ @watcher = Cool.io::TimerWatcher.new(interval, true)
58
+ expect(@watcher.attached?).to eq(false)
59
+ @watcher.attach(loop)
60
+ expect(@watcher.attached?).to eq(true)
61
+ @watcher.detach
62
+ expect(@watcher.attached?).to eq(false)
63
+ end
64
+
56
65
  after :each do
57
- @watcher.detach if defined?(@watcher)
66
+ @watcher.detach if defined?(@watcher) && @watcher.attached?
58
67
  end
59
68
  end
@@ -7,7 +7,7 @@ describe 'Windows stat notification internals', :if => RUBY_PLATFORM =~ /mingw|m
7
7
  before(:context) do
8
8
  @build = Dir.mktmpdir('coolio-stat-native-')
9
9
  source = File.expand_path('fixtures/windows_stat_native.c', __dir__)
10
- libev = File.expand_path('../ext/libev', __dir__)
10
+ coolio = File.expand_path('../ext/cool.io', __dir__)
11
11
  FileUtils.cp(source, @build)
12
12
  File.write(File.join(@build, 'extconf.rb'), <<~RUBY)
13
13
  begin
@@ -16,7 +16,7 @@ describe 'Windows stat notification internals', :if => RUBY_PLATFORM =~ /mingw|m
16
16
  # Other Windows toolchains can provide the compiler through PATH.
17
17
  end
18
18
  require 'mkmf'
19
- $INCFLAGS << ' -I"#{libev}"'
19
+ $INCFLAGS << ' -I"#{coolio}"'
20
20
  abort 'ws2_32 is required' unless have_library('ws2_32')
21
21
  create_makefile('coolio_stat_native')
22
22
  RUBY
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cool.io
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.1
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake-compiler-dock
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -84,14 +70,12 @@ files:
84
70
  - LICENSE
85
71
  - README.md
86
72
  - Rakefile
87
- - appveyor.yml
88
73
  - cool.io.gemspec
89
74
  - examples/callbacked_echo_server.rb
90
75
  - examples/dslified_echo_client.rb
91
76
  - examples/dslified_echo_server.rb
92
77
  - examples/echo_client.rb
93
78
  - examples/echo_server.rb
94
- - examples/google.rb
95
79
  - ext/cool.io/.gitignore
96
80
  - ext/cool.io/buffer.c
97
81
  - ext/cool.io/cool.io.h
@@ -114,6 +98,7 @@ files:
114
98
  - ext/libev/ev.h
115
99
  - ext/libev/ev_epoll.c
116
100
  - ext/libev/ev_kqueue.c
101
+ - ext/libev/ev_kqueue_stat.c
117
102
  - ext/libev/ev_poll.c
118
103
  - ext/libev/ev_port.c
119
104
  - ext/libev/ev_select.c
@@ -144,8 +129,11 @@ files:
144
129
  - spec/async_watcher_spec.rb
145
130
  - spec/detach_race_condition_spec.rb
146
131
  - spec/dns_spec.rb
132
+ - spec/fixtures/kqueue_stat_native.c
147
133
  - spec/fixtures/windows_stat_native.c
148
134
  - spec/iobuffer_spec.rb
135
+ - spec/kqueue_stat_native_spec.rb
136
+ - spec/kqueue_stat_watcher_spec.rb
149
137
  - spec/loop_spec.rb
150
138
  - spec/spec_helper.rb
151
139
  - spec/stat_watcher_spec.rb
@@ -161,7 +149,11 @@ files:
161
149
  homepage: https://github.com/socketry/cool.io
162
150
  licenses:
163
151
  - MIT
164
- metadata: {}
152
+ metadata:
153
+ bug_tracker_uri: https://github.com/socketry/cool.io/issues
154
+ changelog_uri: https://github.com/socketry/cool.io/blob/main/CHANGES.md
155
+ source_code_uri: https://github.com/socketry/cool.io.git
156
+ rubygems_mfa_required: 'true'
165
157
  rdoc_options: []
166
158
  require_paths:
167
159
  - lib
@@ -169,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
169
161
  requirements:
170
162
  - - ">="
171
163
  - !ruby/object:Gem::Version
172
- version: '0'
164
+ version: '3.2'
173
165
  required_rubygems_version: !ruby/object:Gem::Requirement
174
166
  requirements:
175
167
  - - ">="
@@ -183,8 +175,11 @@ test_files:
183
175
  - spec/async_watcher_spec.rb
184
176
  - spec/detach_race_condition_spec.rb
185
177
  - spec/dns_spec.rb
178
+ - spec/fixtures/kqueue_stat_native.c
186
179
  - spec/fixtures/windows_stat_native.c
187
180
  - spec/iobuffer_spec.rb
181
+ - spec/kqueue_stat_native_spec.rb
182
+ - spec/kqueue_stat_watcher_spec.rb
188
183
  - spec/loop_spec.rb
189
184
  - spec/spec_helper.rb
190
185
  - spec/stat_watcher_spec.rb
data/appveyor.yml DELETED
@@ -1,18 +0,0 @@
1
- ---
2
- install:
3
- - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
4
- - ruby --version
5
- - gem --version
6
- - bundle install
7
- build: off
8
- test_script:
9
- - bundle exec rake -rdevkit
10
-
11
- environment:
12
- matrix:
13
- - ruby_version: "200"
14
- - ruby_version: "200-x64"
15
- - ruby_version: "21"
16
- - ruby_version: "21-x64"
17
- - ruby_version: "22"
18
- - ruby_version: "22-x64"
data/examples/google.rb DELETED
@@ -1,9 +0,0 @@
1
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
-
3
- require 'rubygems'
4
- require 'cool.io'
5
-
6
- l = Coolio::Loop.default
7
- c = Coolio::HttpClient.connect("www.google.com", 80).attach(l)
8
- c.request('GET', '/search', :query => { :q => 'feces'})
9
- l.run