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