listen 1.3.1 → 2.0.0.beta.1
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/CHANGELOG.md +1 -368
- data/README.md +82 -215
- data/lib/listen.rb +2 -36
- data/lib/listen/adapter.rb +23 -304
- data/lib/listen/adapter/base.rb +40 -0
- data/lib/listen/adapter/bsd.rb +93 -0
- data/lib/listen/adapter/darwin.rb +44 -0
- data/lib/listen/adapter/linux.rb +92 -0
- data/lib/listen/adapter/polling.rb +49 -0
- data/lib/listen/adapter/windows.rb +63 -0
- data/lib/listen/change.rb +42 -0
- data/lib/listen/directory.rb +73 -0
- data/lib/listen/file.rb +108 -0
- data/lib/listen/listener.rb +69 -260
- data/lib/listen/record.rb +41 -0
- data/lib/listen/silencer.rb +44 -0
- data/lib/listen/version.rb +1 -1
- metadata +35 -17
- data/lib/listen/adapters/bsd.rb +0 -75
- data/lib/listen/adapters/darwin.rb +0 -48
- data/lib/listen/adapters/linux.rb +0 -81
- data/lib/listen/adapters/polling.rb +0 -58
- data/lib/listen/adapters/windows.rb +0 -91
- data/lib/listen/directory_record.rb +0 -406
- data/lib/listen/turnstile.rb +0 -32
data/lib/listen.rb
CHANGED
@@ -1,18 +1,9 @@
|
|
1
|
-
require '
|
1
|
+
require 'celluloid'
|
2
2
|
require 'listen/listener'
|
3
|
-
require 'listen/directory_record'
|
4
|
-
require 'listen/adapter'
|
5
3
|
|
6
4
|
module Listen
|
7
5
|
|
8
|
-
module Adapters
|
9
|
-
Adapter::ADAPTERS.each do |adapter|
|
10
|
-
require "listen/adapters/#{adapter.downcase}"
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
6
|
# Listens to file system modifications on a either single directory or multiple directories.
|
15
|
-
# When calling this method, the current thread is not blocked.
|
16
7
|
#
|
17
8
|
# @param (see Listen::Listener#new)
|
18
9
|
#
|
@@ -21,34 +12,9 @@ module Listen
|
|
21
12
|
# @yieldparam [Array<String>] added the list of added files
|
22
13
|
# @yieldparam [Array<String>] removed the list of removed files
|
23
14
|
#
|
24
|
-
# @return [Listen::Listener] the
|
15
|
+
# @return [Listen::Listener] the listener
|
25
16
|
#
|
26
17
|
def self.to(*args, &block)
|
27
|
-
listener = _init_listener(*args, &block)
|
28
|
-
|
29
|
-
block ? listener.start : listener
|
30
|
-
end
|
31
|
-
|
32
|
-
# Listens to file system modifications on a either single directory or multiple directories.
|
33
|
-
# When calling this method, the current thread is blocked.
|
34
|
-
#
|
35
|
-
# @param (see Listen::Listener#new)
|
36
|
-
#
|
37
|
-
# @yield [modified, added, removed] the changed files
|
38
|
-
# @yieldparam [Array<String>] modified the list of modified files
|
39
|
-
# @yieldparam [Array<String>] added the list of added files
|
40
|
-
# @yieldparam [Array<String>] removed the list of removed files
|
41
|
-
#
|
42
|
-
# @since 1.0.0
|
43
|
-
#
|
44
|
-
def self.to!(*args, &block)
|
45
|
-
_init_listener(*args, &block).start!
|
46
|
-
end
|
47
|
-
|
48
|
-
# @private
|
49
|
-
#
|
50
|
-
def self._init_listener(*args, &block)
|
51
18
|
Listener.new(*args, &block)
|
52
19
|
end
|
53
|
-
|
54
20
|
end
|
data/lib/listen/adapter.rb
CHANGED
@@ -1,325 +1,44 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
1
|
+
require 'listen/adapter/base'
|
2
|
+
require 'listen/adapter/bsd'
|
3
|
+
require 'listen/adapter/darwin'
|
4
|
+
require 'listen/adapter/linux'
|
5
|
+
require 'listen/adapter/polling'
|
6
|
+
require 'listen/adapter/windows'
|
5
7
|
|
6
8
|
module Listen
|
7
|
-
|
8
|
-
attr_accessor :directories, :callback, :stopped, :paused,
|
9
|
-
:mutex, :changed_directories, :turnstile, :latency,
|
10
|
-
:worker, :worker_thread, :poller_thread
|
11
|
-
|
12
|
-
# The list of existing optimized adapters.
|
9
|
+
module Adapter
|
13
10
|
OPTIMIZED_ADAPTERS = %w[Darwin Linux BSD Windows]
|
11
|
+
POLLING_FALLBACK_MESSAGE = "Listen will be polling for changes. Learn more at https://github.com/guard/listen#polling-fallback."
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# The list of all existing adapters.
|
19
|
-
ADAPTERS = OPTIMIZED_ADAPTERS + FALLBACK_ADAPTERS
|
20
|
-
|
21
|
-
# The default delay between checking for changes.
|
22
|
-
DEFAULT_LATENCY = 0.25
|
23
|
-
|
24
|
-
# The default warning message when falling back to polling adapter.
|
25
|
-
POLLING_FALLBACK_MESSAGE = <<-EOS.gsub(/^\s*/, '')
|
26
|
-
Listen will be polling for changes. Learn more at https://github.com/guard/listen#polling-fallback.
|
27
|
-
EOS
|
28
|
-
|
29
|
-
# Selects the appropriate adapter implementation for the
|
30
|
-
# current OS and initializes it.
|
31
|
-
#
|
32
|
-
# @param [String, Array<String>] directories the directories to watch
|
33
|
-
# @param [Hash] options the adapter options
|
34
|
-
# @option options [Boolean] force_polling to force polling or not
|
35
|
-
# @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
|
36
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
37
|
-
#
|
38
|
-
# @yield [changed_directories, options] callback the callback called when a change happens
|
39
|
-
# @yieldparam [Array<String>] changed_directories the changed directories
|
40
|
-
# @yieldparam [Hash] options callback options (like recursive: true)
|
41
|
-
#
|
42
|
-
# @return [Listen::Adapter] the chosen adapter
|
43
|
-
#
|
44
|
-
def self.select_and_initialize(directories, options = {}, &callback)
|
45
|
-
forced_adapter_class = options.delete(:force_adapter)
|
46
|
-
force_polling = options.delete(:force_polling)
|
47
|
-
|
48
|
-
if forced_adapter_class
|
49
|
-
forced_adapter_class.load_dependent_adapter
|
50
|
-
return forced_adapter_class.new(directories, options, &callback)
|
51
|
-
end
|
52
|
-
|
53
|
-
return Adapters::Polling.new(directories, options, &callback) if force_polling
|
54
|
-
|
55
|
-
OPTIMIZED_ADAPTERS.each do |adapter|
|
56
|
-
namespaced_adapter = Adapters.const_get(adapter)
|
57
|
-
if namespaced_adapter.send(:usable_and_works?, directories, options)
|
58
|
-
return namespaced_adapter.new(directories, options, &callback)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
self.warn_polling_fallback(options)
|
63
|
-
Adapters::Polling.new(directories, options, &callback)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Initializes the adapter.
|
67
|
-
#
|
68
|
-
# @param [String, Array<String>] directories the directories to watch
|
69
|
-
# @param [Hash] options the adapter options
|
70
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
71
|
-
#
|
72
|
-
# @yield [changed_directories, options] callback Callback called when a change happens
|
73
|
-
# @yieldparam [Array<String>] changed_directories the changed directories
|
74
|
-
# @yieldparam [Hash] options callback options (like recursive: true)
|
75
|
-
#
|
76
|
-
# @return [Listen::Adapter] the adapter
|
77
|
-
#
|
78
|
-
def initialize(directories, options = {}, &callback)
|
79
|
-
@directories = Array(directories)
|
80
|
-
@callback = callback
|
81
|
-
@stopped = true
|
82
|
-
@paused = false
|
83
|
-
@mutex = Mutex.new
|
84
|
-
@changed_directories = Set.new
|
85
|
-
@turnstile = Turnstile.new
|
86
|
-
@latency = options.fetch(:latency, default_latency)
|
87
|
-
@worker = initialize_worker
|
88
|
-
end
|
89
|
-
|
90
|
-
# Starts the adapter and don't block the current thread.
|
91
|
-
#
|
92
|
-
# @param [Boolean] blocking whether or not to block the current thread after starting
|
93
|
-
#
|
94
|
-
def start
|
95
|
-
mutex.synchronize do
|
96
|
-
return unless stopped
|
97
|
-
@stopped = false
|
98
|
-
end
|
99
|
-
|
100
|
-
start_worker
|
101
|
-
start_poller
|
102
|
-
end
|
103
|
-
|
104
|
-
# Starts the adapter and block the current thread.
|
105
|
-
#
|
106
|
-
# @since 1.0.0
|
107
|
-
#
|
108
|
-
def start!
|
109
|
-
start
|
110
|
-
blocking_thread.join
|
111
|
-
end
|
112
|
-
|
113
|
-
# Stops the adapter.
|
114
|
-
#
|
115
|
-
def stop
|
116
|
-
mutex.synchronize do
|
117
|
-
return if stopped
|
118
|
-
@stopped = true
|
119
|
-
turnstile.signal # ensure no thread is blocked
|
120
|
-
end
|
121
|
-
|
122
|
-
worker.stop if worker
|
123
|
-
Thread.kill(worker_thread) if worker_thread
|
124
|
-
if poller_thread
|
125
|
-
poller_thread.kill
|
126
|
-
poller_thread.join
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
# Pauses the adapter.
|
131
|
-
#
|
132
|
-
def pause
|
133
|
-
@paused = true
|
134
|
-
end
|
135
|
-
|
136
|
-
# Unpauses the adapter.
|
137
|
-
#
|
138
|
-
def unpause
|
139
|
-
@paused = false
|
140
|
-
end
|
141
|
-
|
142
|
-
# Returns whether the adapter is started or not.
|
143
|
-
#
|
144
|
-
# @return [Boolean] whether the adapter is started or not
|
145
|
-
#
|
146
|
-
def started?
|
147
|
-
!stopped
|
148
|
-
end
|
149
|
-
|
150
|
-
# Returns whether the adapter is paused or not.
|
151
|
-
#
|
152
|
-
# @return [Boolean] whether the adapter is paused or not
|
153
|
-
#
|
154
|
-
def paused?
|
155
|
-
paused
|
156
|
-
end
|
157
|
-
|
158
|
-
# Blocks the main thread until the poll thread
|
159
|
-
# runs the callback.
|
160
|
-
#
|
161
|
-
def wait_for_callback
|
162
|
-
turnstile.wait unless paused
|
163
|
-
end
|
164
|
-
|
165
|
-
# Blocks the main thread until N changes are
|
166
|
-
# detected.
|
167
|
-
#
|
168
|
-
def wait_for_changes(threshold = 0)
|
169
|
-
changes = 0
|
170
|
-
|
171
|
-
loop do
|
172
|
-
mutex.synchronize { changes = changed_directories.size }
|
173
|
-
|
174
|
-
return if paused || stopped
|
175
|
-
return if changes >= threshold
|
176
|
-
|
177
|
-
sleep(latency)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
# Checks if the adapter is usable and works on the current OS.
|
182
|
-
#
|
183
|
-
# @param [String, Array<String>] directories the directories to watch
|
184
|
-
# @param [Hash] options the adapter options
|
185
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
186
|
-
#
|
187
|
-
# @return [Boolean] whether the adapter is usable and work or not
|
188
|
-
#
|
189
|
-
def self.usable_and_works?(directories, options = {})
|
190
|
-
usable? && Array(directories).all? { |d| works?(d, options) }
|
191
|
-
end
|
192
|
-
|
193
|
-
# Checks if the adapter is usable on target OS.
|
194
|
-
#
|
195
|
-
# @return [Boolean] whether usable or not
|
196
|
-
#
|
197
|
-
def self.usable?
|
198
|
-
load_dependent_adapter if RbConfig::CONFIG['target_os'] =~ target_os_regex
|
199
|
-
end
|
200
|
-
|
201
|
-
# Load the adapter gem
|
202
|
-
#
|
203
|
-
# @return [Boolean] whether loaded or not
|
204
|
-
#
|
205
|
-
def self.load_dependent_adapter
|
206
|
-
return true if @loaded
|
207
|
-
require adapter_gem
|
208
|
-
return @loaded = true
|
209
|
-
end
|
210
|
-
|
211
|
-
# Runs a tests to determine if the adapter can actually pick up
|
212
|
-
# changes in a given directory and returns the result.
|
213
|
-
#
|
214
|
-
# @note This test takes some time depending on the adapter latency.
|
215
|
-
#
|
216
|
-
# @param [String, Pathname] directory the directory to watch
|
217
|
-
# @param [Hash] options the adapter options
|
218
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
219
|
-
#
|
220
|
-
# @return [Boolean] whether the adapter works or not
|
221
|
-
#
|
222
|
-
def self.works?(directory, options = {})
|
223
|
-
work = false
|
224
|
-
test_file = "#{directory}/.listen_test"
|
225
|
-
callback = lambda { |*| work = true }
|
226
|
-
adapter = self.new(directory, options, &callback)
|
227
|
-
adapter.start
|
228
|
-
|
229
|
-
FileUtils.touch(test_file)
|
230
|
-
|
231
|
-
t = Thread.new { sleep(adapter.latency * 5); adapter.stop }
|
232
|
-
|
233
|
-
adapter.wait_for_callback
|
234
|
-
work
|
235
|
-
ensure
|
236
|
-
Thread.kill(t) if t
|
237
|
-
FileUtils.rm(test_file, :force => true)
|
238
|
-
adapter.stop if adapter && adapter.started?
|
239
|
-
end
|
240
|
-
|
241
|
-
# Runs the callback and passes it the changes if there are any.
|
242
|
-
#
|
243
|
-
def report_changes
|
244
|
-
changed_dirs = nil
|
245
|
-
|
246
|
-
mutex.synchronize do
|
247
|
-
return if @changed_directories.empty?
|
248
|
-
changed_dirs = @changed_directories.to_a
|
249
|
-
@changed_directories.clear
|
250
|
-
end
|
251
|
-
|
252
|
-
callback.call(changed_dirs, {})
|
253
|
-
turnstile.signal
|
13
|
+
def self.new(listener)
|
14
|
+
adapter_class = _select(listener.options)
|
15
|
+
adapter_class.new(listener)
|
254
16
|
end
|
255
17
|
|
256
18
|
private
|
257
19
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
#
|
262
|
-
def default_latency
|
263
|
-
DEFAULT_LATENCY
|
264
|
-
end
|
265
|
-
|
266
|
-
# The thread on which the main thread should wait
|
267
|
-
# when the adapter has been started in blocking mode.
|
268
|
-
#
|
269
|
-
# @note This method can be overriden on a per-adapter basis.
|
270
|
-
#
|
271
|
-
def blocking_thread
|
272
|
-
worker_thread
|
273
|
-
end
|
274
|
-
|
275
|
-
# Initialize the adpater' specific worker.
|
276
|
-
#
|
277
|
-
# @note Each adapter must override this method
|
278
|
-
# to initialize its own @worker.
|
279
|
-
#
|
280
|
-
def initialize_worker
|
281
|
-
nil
|
282
|
-
end
|
20
|
+
def self._select(options)
|
21
|
+
return Polling if options[:force_polling] || not_mri?
|
22
|
+
return _usable_adapter_class if _usable_adapter_class
|
283
23
|
|
284
|
-
|
285
|
-
|
286
|
-
# @note Each adapter must override this method
|
287
|
-
# to start its worker on a new @worker_thread thread.
|
288
|
-
#
|
289
|
-
def start_worker
|
290
|
-
raise NotImplementedError, "#{self.class} cannot respond to: #{__method__}"
|
24
|
+
_warn_polling_fallback(options)
|
25
|
+
Polling
|
291
26
|
end
|
292
27
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
def start_poller
|
297
|
-
@poller_thread = Thread.new { poll_changed_directories }
|
28
|
+
def self._usable_adapter_class
|
29
|
+
adapters = OPTIMIZED_ADAPTERS.map { |adapter| Adapter.const_get(adapter) }
|
30
|
+
adapters.detect { |adapter| adapter.send(:usable?) }
|
298
31
|
end
|
299
32
|
|
300
|
-
|
301
|
-
# has been set to false.
|
302
|
-
#
|
303
|
-
# @param [String] warning an existing warning message
|
304
|
-
# @param [Hash] options the adapter options
|
305
|
-
# @option options [Boolean] polling_fallback_message to change polling fallback message or remove it
|
306
|
-
#
|
307
|
-
def self.warn_polling_fallback(options)
|
33
|
+
def self._warn_polling_fallback(options)
|
308
34
|
return if options[:polling_fallback_message] == false
|
309
35
|
|
310
|
-
warning = options
|
36
|
+
warning = options.fetch(:polling_fallback_message, POLLING_FALLBACK_MESSAGE)
|
311
37
|
Kernel.warn "[Listen warning]:\n#{warning.gsub(/^(.*)/, ' \1')}"
|
312
38
|
end
|
313
39
|
|
314
|
-
|
315
|
-
|
316
|
-
# @note This method can be overriden on a per-adapter basis.
|
317
|
-
#
|
318
|
-
def poll_changed_directories
|
319
|
-
until stopped
|
320
|
-
sleep(latency)
|
321
|
-
report_changes
|
322
|
-
end
|
40
|
+
def self.not_mri?
|
41
|
+
RUBY_ENGINE != 'ruby'
|
323
42
|
end
|
324
43
|
end
|
325
44
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Listen
|
2
|
+
module Adapter
|
3
|
+
|
4
|
+
class Base
|
5
|
+
include Celluloid
|
6
|
+
|
7
|
+
# The default delay between checking for changes.
|
8
|
+
DEFAULT_LATENCY = 0.1
|
9
|
+
|
10
|
+
attr_accessor :listener
|
11
|
+
|
12
|
+
def initialize(listener)
|
13
|
+
@listener = listener
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.usable?
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def _latency
|
27
|
+
listener.options[:latency] || DEFAULT_LATENCY
|
28
|
+
end
|
29
|
+
|
30
|
+
def _directories_path
|
31
|
+
listener.directories.map(&:to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def _notify_change(path, options)
|
35
|
+
Actor[:listen_change_pool].async.change(path, options) if listener.listen?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Listen
|
2
|
+
module Adapter
|
3
|
+
|
4
|
+
# Listener implementation for BSD's `kqueue`.
|
5
|
+
#
|
6
|
+
class BSD < Base
|
7
|
+
# Watched kqueue events
|
8
|
+
#
|
9
|
+
# @see http://www.freebsd.org/cgi/man.cgi?query=kqueue
|
10
|
+
# @see https://github.com/mat813/rb-kqueue/blob/master/lib/rb-kqueue/queue.rb
|
11
|
+
#
|
12
|
+
EVENTS = [:delete, :write, :extend, :attrib, :rename] # :link, :revoke
|
13
|
+
|
14
|
+
# The message to show when wdm gem isn't available
|
15
|
+
#
|
16
|
+
BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
|
17
|
+
Please add the following to your Gemfile to avoid polling for changes:
|
18
|
+
require 'rbconfig'
|
19
|
+
gem 'rb-kqueue', '>= 0.2' if RbConfig::CONFIG['target_os'] =~ /freebsd/i
|
20
|
+
EOS
|
21
|
+
|
22
|
+
def self.usable?
|
23
|
+
if RbConfig::CONFIG['target_os'] =~ /freebsd/i
|
24
|
+
require 'rb-kqueue'
|
25
|
+
require 'find'
|
26
|
+
true
|
27
|
+
end
|
28
|
+
rescue Gem::LoadError
|
29
|
+
Kernel.warn BUNDLER_DECLARE_GEM
|
30
|
+
end
|
31
|
+
|
32
|
+
def start
|
33
|
+
worker = _init_worker
|
34
|
+
worker.poll
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Initializes a kqueue Queue and adds a watcher for each files in
|
40
|
+
# the directories passed to the adapter.
|
41
|
+
#
|
42
|
+
# @return [INotify::Notifier] initialized kqueue
|
43
|
+
def _init_worker
|
44
|
+
KQueue::Queue.new.tap do |queue|
|
45
|
+
_directories_path.each do |path|
|
46
|
+
Find.find(path) { |file_path| _watch_file(file_path, queue) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def _worker_callback
|
52
|
+
lambda do |event|
|
53
|
+
_notify_change(_event_path(event), type: 'file', change: _change(event.flags))
|
54
|
+
|
55
|
+
# If it is a directory, and it has a write flag, it means a
|
56
|
+
# file has been added so find out which and deal with it.
|
57
|
+
# No need to check for removed files, kqueue will forget them
|
58
|
+
# when the vfs does.
|
59
|
+
_watch_for_new_file(event) if _new_file_added?(event)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def _change(event_flags)
|
64
|
+
{ modified: [:attrib, :extend],
|
65
|
+
added: [:write],
|
66
|
+
removed: [:rename, :delete] }.each do |change, flags|
|
67
|
+
return change unless (flags & event_flags).empty?
|
68
|
+
end
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def _event_path(event)
|
73
|
+
Pathname.new(event.watcher.path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def _new_file_added?(event)
|
77
|
+
File.directory?(event.watcher.path) && event.flags.include?(:write)
|
78
|
+
end
|
79
|
+
|
80
|
+
def _watch_for_new_file(event)
|
81
|
+
queue = event.watcher.queue
|
82
|
+
Find.find(path) do |file_path|
|
83
|
+
_watch_file(file_path, queue) unless queue.watchers.detect { |k,v| v.path == file.to_s }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def _watch_file(path, queue)
|
88
|
+
queue.watch_file(path, *EVENTS, &_worker_callback)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|