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.
@@ -1,27 +1,17 @@
1
- require 'pathname'
1
+ require 'listen/adapter'
2
+ require 'listen/change'
3
+ require 'listen/record'
2
4
 
3
5
  module Listen
4
6
  class Listener
5
- attr_reader :directories, :directories_records, :block, :adapter, :adapter_options, :use_relative_paths
6
-
7
- BLOCKING_PARAMETER_DEPRECATION_MESSAGE = <<-EOS.gsub(/^\s*/, '')
8
- The blocking parameter of Listen::Listener#start is deprecated.\n
9
- Please use Listen::Adapter#start for a non-blocking listener and Listen::Listener#start! for a blocking one.
10
- EOS
7
+ attr_accessor :options, :directories, :paused, :changes, :block
11
8
 
12
9
  RELATIVE_PATHS_WITH_MULTIPLE_DIRECTORIES_WARNING_MESSAGE = "The relative_paths option doesn't work when listening to multiple diretories."
13
10
 
14
11
  # Initializes the directories listener.
15
12
  #
16
13
  # @param [String] directory the directories to listen to
17
- # @param [Hash] options the listen options
18
- # @option options [Regexp] ignore a pattern for ignoring paths
19
- # @option options [Regexp] filter a pattern for filtering paths
20
- # @option options [Float] latency the delay between checking for changes in seconds
21
- # @option options [Boolean] relative_paths whether or not to use relative-paths in the callback
22
- # @option options [Boolean] force_polling whether to force the polling adapter or not
23
- # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
24
- # @option options [Class] force_adapter force the use of this adapter class, skipping usual adapter selection
14
+ # @param [Hash] options the listen options (see Listen::Listener::Options)
25
15
  #
26
16
  # @yield [modified, added, removed] the changed files
27
17
  # @yieldparam [Array<String>] modified the list of modified files
@@ -29,295 +19,114 @@ module Listen
29
19
  # @yieldparam [Array<String>] removed the list of removed files
30
20
  #
31
21
  def initialize(*args, &block)
32
- options = args.last.is_a?(Hash) ? args.pop : {}
33
- directories = args.flatten
34
- initialize_directories_and_directories_records(directories)
35
- initialize_relative_paths_usage(options)
36
- @block = block
37
-
38
- ignore(*options.delete(:ignore))
39
- filter(*options.delete(:filter))
40
-
41
- @adapter_options = options
22
+ @options = _init_options(args.last.is_a?(Hash) ? args.pop : {})
23
+ @directories = args.flatten.map { |path| Pathname.new(path) }
24
+ @changes = []
25
+ @block = block
26
+ _init_debug
42
27
  end
43
28
 
44
29
  # Starts the listener by initializing the adapter and building
45
30
  # the directory record concurrently, then it starts the adapter to watch
46
31
  # for changes. The current thread is not blocked after starting.
47
32
  #
48
- # @see Listen::Listener#start!
49
- #
50
- def start(deprecated_blocking = nil)
51
- Kernel.warn "[Listen warning]:\n#{BLOCKING_PARAMETER_DEPRECATION_MESSAGE}" unless deprecated_blocking.nil?
52
- setup
53
- adapter.start
54
- end
55
-
56
- # Starts the listener by initializing the adapter and building
57
- # the directory record concurrently, then it starts the adapter to watch
58
- # for changes. The current thread is blocked after starting.
59
- #
60
- # @see Listen::Listener#start
61
- #
62
- # @since 1.0.0
63
- #
64
- def start!
65
- setup
66
- adapter.start!
33
+ def start
34
+ _signals_trap
35
+ _init_actors
36
+ unpause
37
+ adapter.async.start
38
+ @thread = Thread.new { _wait_for_changes }
67
39
  end
68
40
 
69
- # Stops the listener.
70
- #
71
41
  def stop
72
- adapter && adapter.stop
42
+ Celluloid::Actor.kill(adapter)
43
+ Celluloid::Actor[:listen_change_pool].terminate
44
+ record && record.terminate
45
+ @thread && @thread.kill
73
46
  end
74
47
 
75
- # Pauses the listener.
76
- #
77
- # @return [Listen::Listener] the listener
78
- #
79
48
  def pause
80
- adapter.pause
81
- self
49
+ @paused = true
82
50
  end
83
51
 
84
- # Unpauses the listener.
85
- #
86
- # @return [Listen::Listener] the listener
87
- #
88
52
  def unpause
89
- build_directories_records
90
- adapter.unpause
91
- self
53
+ _build_record_if_needed
54
+ @paused = false
92
55
  end
93
56
 
94
- # Returns whether the listener is paused or not.
95
- #
96
- # @return [Boolean] adapter paused status
97
- #
98
57
  def paused?
99
- !!adapter && adapter.paused?
58
+ @paused == true
100
59
  end
101
60
 
102
- # Adds ignoring patterns to the listener.
103
- #
104
- # @param (see Listen::DirectoryRecord#ignore)
105
- #
106
- # @return [Listen::Listener] the listener
107
- #
108
- # @see Listen::DirectoryRecord#ignore
109
- #
110
- def ignore(*regexps)
111
- directories_records.each { |r| r.ignore(*regexps) }
112
- self
61
+ def listen?
62
+ @paused == false
113
63
  end
114
64
 
115
- # Replaces ignoring patterns in the listener.
116
- #
117
- # @param (see Listen::DirectoryRecord#ignore!)
118
- #
119
- # @return [Listen::Listener] the listener
120
- #
121
- # @see Listen::DirectoryRecord#ignore!
122
- #
123
- def ignore!(*regexps)
124
- directories_records.each { |r| r.ignore!(*regexps) }
125
- self
126
- end
127
-
128
- # Adds filtering patterns to the listener.
129
- #
130
- # @param (see Listen::DirectoryRecord#filter)
131
- #
132
- # @return [Listen::Listener] the listener
133
- #
134
- # @see Listen::DirectoryRecord#filter
135
- #
136
- def filter(*regexps)
137
- directories_records.each { |r| r.filter(*regexps) }
138
- self
65
+ def adapter
66
+ Celluloid::Actor[:listen_adapter]
139
67
  end
140
68
 
141
- # Replaces filtering patterns in the listener.
142
- #
143
- # @param (see Listen::DirectoryRecord#filter!)
144
- #
145
- # @return [Listen::Listener] the listener
146
- #
147
- # @see Listen::DirectoryRecord#filter!
148
- #
149
- def filter!(*regexps)
150
- directories_records.each { |r| r.filter!(*regexps) }
151
- self
69
+ def record
70
+ Celluloid::Actor[:listen_record]
152
71
  end
153
72
 
154
- # Sets the latency for the adapter. This is a helper method
155
- # to simplify changing the latency directly from the listener.
156
- #
157
- # @example Wait 0.5 seconds each time before checking changes
158
- # latency 0.5
159
- #
160
- # @param [Float] seconds the amount of delay, in seconds
161
- #
162
- # @return [Listen::Listener] the listener
163
- #
164
- def latency(seconds)
165
- @adapter_options[:latency] = seconds
166
- self
167
- end
73
+ private
168
74
 
169
- # Sets whether the use of the polling adapter
170
- # should be forced or not.
171
- #
172
- # @example Forcing the use of the polling adapter
173
- # force_polling true
174
- #
175
- # @param [Boolean] value whether to force the polling adapter or not
176
- #
177
- # @return [Listen::Listener] the listener
178
- #
179
- def force_polling(value)
180
- @adapter_options[:force_polling] = value
181
- self
75
+ def _init_options(options = {})
76
+ { debug: false,
77
+ latency: nil,
78
+ force_polling: false,
79
+ polling_fallback_message: nil }.merge(options)
182
80
  end
183
81
 
184
- # Sets whether to force the use of a particular adapter, rather than
185
- # going through usual adapter selection process on start.
186
- #
187
- # @example Force use of Linux polling
188
- # force_adapter Listen::Adapters::Linux
189
- #
190
- # @param [Class] adapter class to use for file system event notification.
191
- #
192
- # @return [Listen::Listener] the listener
193
- #
194
- def force_adapter(adapter_class)
195
- @adapter_options[:force_adapter] = adapter_class
196
- self
82
+ def _init_debug
83
+ if options[:debug]
84
+ Celluloid.logger.level = Logger::INFO
85
+ else
86
+ Celluloid.logger = nil
87
+ end
197
88
  end
198
89
 
199
- # Sets whether the paths in the callback should be
200
- # relative or absolute.
201
- #
202
- # @example Enabling relative paths in the callback
203
- # relative_paths true
204
- #
205
- # @param [Boolean] value whether to enable relative paths in the callback or not
206
- #
207
- # @return [Listen::Listener] the listener
208
- #
209
- def relative_paths(value)
210
- @use_relative_paths = value
211
- self
90
+ def _init_actors
91
+ cores = Celluloid.cores || 2
92
+ Celluloid::Actor[:listen_change_pool] = Change.pool(size: cores, args: self)
93
+ Celluloid::Actor[:listen_adapter] = Adapter.new(self)
94
+ Celluloid::Actor[:listen_record] = Record.new(self)
212
95
  end
213
96
 
214
- # Defines a custom polling fallback message or disable it.
215
- #
216
- # @example Disabling the polling fallback message
217
- # polling_fallback_message false
218
- #
219
- # @param [String, Boolean] value to change polling fallback message or remove it
220
- #
221
- # @return [Listen::Listener] the listener
222
- #
223
- def polling_fallback_message(value)
224
- @adapter_options[:polling_fallback_message] = value
225
- self
97
+ def _signals_trap
98
+ if Signal.list.keys.include?('INT')
99
+ Signal.trap('INT') { exit }
100
+ end
226
101
  end
227
102
 
228
- # Sets the callback that gets called on changes.
229
- #
230
- # @example Assign a callback to be called on changes
231
- # callback = lambda { |modified, added, removed| ... }
232
- # change &callback
233
- #
234
- # @param [Proc] block the callback proc
235
- #
236
- # @return [Listen::Listener] the listener
237
- #
238
- def change(&block) # modified, added, removed
239
- @block = block
240
- self
103
+ def _build_record_if_needed
104
+ record && record.build
241
105
  end
242
106
 
243
- # Runs the callback passing it the changes if there are any.
244
- #
245
- # @param (see Listen::DirectoryRecord#fetch_changes)
246
- #
247
- # @see Listen::DirectoryRecord#fetch_changes
248
- #
249
- def on_change(directories, options = {})
250
- changes = fetch_records_changes(directories, options)
251
- unless changes.values.all? { |paths| paths.empty? }
252
- block.call(changes[:modified], changes[:added], changes[:removed])
107
+ def _wait_for_changes
108
+ loop do
109
+ changes = _pop_changes
110
+ unless changes.values.all?(&:empty?)
111
+ block.call(
112
+ changes[:modified].uniq,
113
+ changes[:added].uniq,
114
+ changes[:removed].uniq)
115
+ end
116
+ sleep 0.1
253
117
  end
254
118
  rescue => ex
255
119
  Kernel.warn "[Listen warning]: Change block raise an execption: #{$!}"
256
120
  Kernel.warn "Backtrace:\n\t#{ex.backtrace.join("\n\t")}"
257
121
  end
258
122
 
259
- private
260
-
261
- # Initializes the directories to watch as well as the directories records.
262
- #
263
- # @see Listen::DirectoryRecord
264
- #
265
- def initialize_directories_and_directories_records(directories)
266
- @directories = directories.map { |d| Pathname.new(d).realpath.to_s }
267
- @directories_records = directories.map { |d| DirectoryRecord.new(d) }
268
- end
269
-
270
- # Initializes whether or not using relative paths.
271
- #
272
- def initialize_relative_paths_usage(options)
273
- if directories.size > 1 && options[:relative_paths]
274
- Kernel.warn "[Listen warning]: #{RELATIVE_PATHS_WITH_MULTIPLE_DIRECTORIES_WARNING_MESSAGE}"
123
+ def _pop_changes
124
+ changes = { modified: [], added: [], removed: [] }
125
+ until @changes.empty?
126
+ change = @changes.pop
127
+ change.each { |k, v| changes[k] << v.to_s }
275
128
  end
276
- @use_relative_paths = directories.one? && options.delete(:relative_paths) { false }
277
- end
278
-
279
- # Build the directory record concurrently and initialize the adapter.
280
- #
281
- def setup
282
- t = Thread.new { build_directories_records }
283
- @adapter = initialize_adapter
284
- t.join
285
- end
286
-
287
- # Initializes an adapter passing it the callback and adapters' options.
288
- #
289
- def initialize_adapter
290
- callback = lambda { |changed_directories, options| self.on_change(changed_directories, options) }
291
- Adapter.select_and_initialize(directories, adapter_options, &callback)
129
+ changes
292
130
  end
293
-
294
- # Build the watched directories' records.
295
- #
296
- def build_directories_records
297
- directories_records.each { |r| r.build }
298
- end
299
-
300
- # Returns the sum of all the changes to the directories records
301
- #
302
- # @param (see Listen::DirectoryRecord#fetch_changes)
303
- #
304
- # @return [Hash] the changes
305
- #
306
- def fetch_records_changes(directories_to_search, options)
307
- directories_records.inject({}) do |h, r|
308
- # directory records skips paths outside their range, so passing the
309
- # whole `directories` array is not a problem.
310
- record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => use_relative_paths))
311
-
312
- if h.empty?
313
- h.merge!(record_changes)
314
- else
315
- h.each { |k, v| h[k] += record_changes[k] }
316
- end
317
-
318
- h
319
- end
320
- end
321
-
322
131
  end
323
132
  end
@@ -0,0 +1,41 @@
1
+ module Listen
2
+ class Record
3
+ include Celluloid
4
+
5
+ attr_accessor :paths, :listener
6
+
7
+ def initialize(listener)
8
+ @listener = listener
9
+ @paths = _init_paths
10
+ end
11
+
12
+ def set_path(path, data)
13
+ @paths[::File.dirname(path)][::File.basename(path)] = file_data(path).merge(data)
14
+ end
15
+
16
+ def unset_path(path)
17
+ @paths[::File.dirname(path)].delete(::File.basename(path))
18
+ end
19
+
20
+ def file_data(path)
21
+ @paths[::File.dirname(path)][::File.basename(path)] || {}
22
+ end
23
+
24
+ def dir_entries(path)
25
+ @paths[path.to_s]
26
+ end
27
+
28
+ def build
29
+ @paths = _init_paths
30
+ listener.directories.each do |path|
31
+ Actor[:listen_change_pool].change(path, type: 'Dir', recursive: true, silence: true)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def _init_paths
38
+ Hash.new { |h, k| h[k] = Hash.new }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ module Listen
2
+ class Silencer
3
+ # The default list of directories that get ignored.
4
+ DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn .hg bundle log tmp vendor]
5
+
6
+ # The default list of files that get ignored.
7
+ DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
8
+
9
+ attr_accessor :options, :patterns
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ _init_patterns
14
+ end
15
+
16
+ def silenced?(path)
17
+ patterns.any? { |pattern| pattern =~ path.to_s }
18
+ end
19
+
20
+ private
21
+
22
+ def _init_patterns
23
+ @patterns = []
24
+ @patterns << _default_patterns unless options[:ignore!]
25
+ @patterns << options[:ignore] << options[:ignore!]
26
+ @patterns.compact
27
+ @patterns.flatten!
28
+ end
29
+
30
+ def _default_patterns
31
+ [_default_ignored_directories_patterns, _default_ignored_extensions_patterns]
32
+ end
33
+
34
+ def _default_ignored_directories_patterns
35
+ ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
36
+ %r{(?:#{ignored_directories.join('|')})}
37
+ end
38
+
39
+ def _default_ignored_extensions_patterns
40
+ ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
41
+ %r{(?:#{ignored_extensions.join('|')})$}
42
+ end
43
+ end
44
+ end