listen 1.3.1 → 2.0.0.beta.1

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