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.
data/lib/listen.rb CHANGED
@@ -1,18 +1,9 @@
1
- require 'listen/turnstile'
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 file listener if no block given
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
@@ -1,325 +1,44 @@
1
- require 'rbconfig'
2
- require 'thread'
3
- require 'set'
4
- require 'fileutils'
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
- class Adapter
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
- # The list of existing fallback adapters.
16
- FALLBACK_ADAPTERS = %w[Polling]
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
- # The default delay between checking for changes.
259
- #
260
- # @note This method can be overriden on a per-adapter basis.
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
- # Should start the worker in a new thread.
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
- # This method starts a new thread which poll for changes detected by
294
- # the adapter and report them back to the user.
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
- # Warn of polling fallback unless the :polling_fallback_message
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[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE
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
- # Polls changed directories and reports them back when there are changes.
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