listen 0.7.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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