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.
@@ -11,8 +11,10 @@ module Listen
11
11
  class DirectoryRecord
12
12
  attr_reader :directory, :paths, :sha1_checksums
13
13
 
14
+ # The default list of directories that get ignored by the listener.
14
15
  DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
15
16
 
17
+ # The default list of files that get ignored by the listener.
16
18
  DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
17
19
 
18
20
  # Defines the used precision based on the type of mtime returned by the
@@ -56,15 +58,14 @@ module Listen
56
58
  def initialize(directory)
57
59
  raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
58
60
 
59
- @directory = directory
60
- @ignoring_patterns = Set.new
61
- @filtering_patterns = Set.new
62
- @sha1_checksums = Hash.new
61
+ @directory, @sha1_checksums = directory, Hash.new
62
+ @ignoring_patterns, @filtering_patterns = Set.new, Set.new
63
63
 
64
64
  @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
65
65
  end
66
66
 
67
- # Returns the ignoring patterns in the record
67
+ # Returns the ignoring patterns in the record to know
68
+ # which paths should be ignored.
68
69
  #
69
70
  # @return [Array<Regexp>] the ignoring patterns
70
71
  #
@@ -72,7 +73,7 @@ module Listen
72
73
  @ignoring_patterns.to_a
73
74
  end
74
75
 
75
- # Returns the filtering patterns used in the record to know
76
+ # Returns the filtering patterns in the record to know
76
77
  # which paths should be stored.
77
78
  #
78
79
  # @return [Array<Regexp>] the filtering patterns
@@ -86,10 +87,10 @@ module Listen
86
87
  # @example Ignore some paths
87
88
  # ignore %r{^ignored/path/}, /man/
88
89
  #
89
- # @param [Regexp] regexp a pattern for ignoring paths
90
+ # @param [Regexp] regexps a list of patterns for ignoring paths
90
91
  #
91
92
  def ignore(*regexps)
92
- @ignoring_patterns.merge(regexps)
93
+ @ignoring_patterns.merge(regexps).reject! { |r| r.nil? }
93
94
  end
94
95
 
95
96
  # Replaces ignoring patterns in the record.
@@ -97,37 +98,37 @@ module Listen
97
98
  # @example Ignore only these paths
98
99
  # ignore! %r{^ignored/path/}, /man/
99
100
  #
100
- # @param [Regexp] regexp a pattern for ignoring paths
101
+ # @param [Regexp] regexps a list of patterns for ignoring paths
101
102
  #
102
103
  def ignore!(*regexps)
103
- @ignoring_patterns.replace(regexps)
104
+ @ignoring_patterns.replace(regexps).reject! { |r| r.nil? }
104
105
  end
105
106
 
106
- # Adds filtering patterns to the listener.
107
+ # Adds filtering patterns to the record.
107
108
  #
108
109
  # @example Filter some files
109
- # ignore /\.txt$/, /.*\.zip/
110
+ # filter /\.txt$/, /.*\.zip/
110
111
  #
111
- # @param [Regexp] regexp a pattern for filtering paths
112
+ # @param [Regexp] regexps a list of patterns for filtering files
112
113
  #
113
114
  def filter(*regexps)
114
- @filtering_patterns.merge(regexps)
115
+ @filtering_patterns.merge(regexps).reject! { |r| r.nil? }
115
116
  end
116
117
 
117
- # Replaces filtering patterns in the listener.
118
+ # Replaces filtering patterns in the record.
118
119
  #
119
120
  # @example Filter only these files
120
- # ignore /\.txt$/, /.*\.zip/
121
+ # filter! /\.txt$/, /.*\.zip/
121
122
  #
122
- # @param [Regexp] regexp a pattern for filtering paths
123
+ # @param [Regexp] regexps a list of patterns for filtering files
123
124
  #
124
125
  def filter!(*regexps)
125
- @filtering_patterns.replace(regexps)
126
+ @filtering_patterns.replace(regexps).reject! { |r| r.nil? }
126
127
  end
127
128
 
128
129
  # Returns whether a path should be ignored or not.
129
130
  #
130
- # @param [String] path the path to test.
131
+ # @param [String] path the path to test
131
132
  #
132
133
  # @return [Boolean]
133
134
  #
@@ -138,7 +139,7 @@ module Listen
138
139
 
139
140
  # Returns whether a path should be filtered or not.
140
141
  #
141
- # @param [String] path the path to test.
142
+ # @param [String] path the path to test
142
143
  #
143
144
  # @return [Boolean]
144
145
  #
@@ -159,9 +160,9 @@ module Listen
159
160
  end
160
161
 
161
162
  # Detects changes in the passed directories, updates
162
- # the record with the new changes and returns the changes
163
+ # the record with the new changes and returns the changes.
163
164
  #
164
- # @param [Array] directories the list of directories scan for changes
165
+ # @param [Array] directories the list of directories to scan for changes
165
166
  # @param [Hash] options
166
167
  # @option options [Boolean] recursive scan all sub-directories recursively
167
168
  # @option options [Boolean] relative_paths whether or not to use relative paths for changes
@@ -174,6 +175,7 @@ module Listen
174
175
 
175
176
  directories.each do |directory|
176
177
  next unless directory[@directory] # Path is or inside directory
178
+
177
179
  detect_modifications_and_removals(directory, options)
178
180
  detect_additions(directory, options)
179
181
  end
@@ -188,9 +190,10 @@ module Listen
188
190
  # @return [String] the relative path
189
191
  #
190
192
  def relative_to_base(path)
191
- return nil unless path[@directory]
193
+ return nil unless path[directory]
194
+
192
195
  path = path.force_encoding("BINARY") if path.respond_to?(:force_encoding)
193
- path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '')
196
+ path.sub(%r{^#{Regexp.quote(directory)}#{File::SEPARATOR}?}, '')
194
197
  end
195
198
 
196
199
  private
@@ -207,43 +210,69 @@ module Listen
207
210
  # @option options [Boolean] relative_paths whether or not to use relative paths for changes
208
211
  #
209
212
  def detect_modifications_and_removals(directory, options = {})
210
- @paths[directory].each do |basename, meta_data|
213
+ paths[directory].each do |basename, meta_data|
211
214
  path = File.join(directory, basename)
212
-
213
215
  case meta_data.type
214
216
  when 'Dir'
215
- if File.directory?(path)
216
- detect_modifications_and_removals(path, options) if options[:recursive]
217
- else
218
- detect_modifications_and_removals(path, { :recursive => true }.merge(options))
219
- @paths[directory].delete(basename)
220
- @paths.delete("#{directory}/#{basename}")
221
- end
217
+ detect_modification_or_removal_for_dir(path, options)
222
218
  when 'File'
223
- if File.exist?(path)
224
- new_mtime = mtime_of(path)
219
+ detect_modification_or_removal_for_file(path, meta_data, options)
220
+ end
221
+ end
222
+ end
225
223
 
226
- # First check if we are in the same second (to update checksums)
227
- # before checking the time difference
228
- if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
229
- # Update the sha1 checksum of the file
230
- insert_sha1_checksum(path)
224
+ def detect_modification_or_removal_for_dir(path, options)
231
225
 
232
- # Update the meta data of the file
233
- meta_data.mtime = new_mtime
234
- @paths[directory][basename] = meta_data
226
+ # Directory still exists
227
+ if File.directory?(path)
228
+ detect_modifications_and_removals(path, options) if options[:recursive]
235
229
 
236
- @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
237
- end
238
- else
239
- @paths[directory].delete(basename)
240
- @sha1_checksums.delete(path)
241
- @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
242
- end
243
- end
230
+ # Directory has been removed
231
+ else
232
+ detect_modifications_and_removals(path, options)
233
+ @paths[File.dirname(path)].delete(File.basename(path))
234
+ @paths.delete("#{File.dirname(path)}/#{File.basename(path)}")
235
+ end
236
+ end
237
+
238
+ def detect_modification_or_removal_for_file(path, meta_data, options)
239
+ # File still exists
240
+ if File.exist?(path)
241
+ detect_modification(path, meta_data, options)
242
+
243
+ # File has been removed
244
+ else
245
+ removal_detected(path, meta_data, options)
246
+ end
247
+ end
248
+
249
+ def detect_modification(path, meta_data, options)
250
+ new_mtime = mtime_of(path)
251
+
252
+ # First check if we are in the same second (to update checksums)
253
+ # before checking the time difference
254
+ if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
255
+ modification_detected(path, meta_data, new_mtime, options)
244
256
  end
245
257
  end
246
258
 
259
+ def modification_detected(path, meta_data, new_mtime, options)
260
+ # Update the sha1 checksum of the file
261
+ update_sha1_checksum(path)
262
+
263
+ # Update the meta data of the file
264
+ meta_data.mtime = new_mtime
265
+ @paths[File.dirname(path)][File.basename(path)] = meta_data
266
+
267
+ @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
268
+ end
269
+
270
+ def removal_detected(path, meta_data, options)
271
+ @paths[File.dirname(path)].delete(File.basename(path))
272
+ @sha1_checksums.delete(path)
273
+ @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
274
+ end
275
+
247
276
  # Detects additions in a directory.
248
277
  #
249
278
  # @param [String] directory the path to analyze
@@ -283,9 +312,10 @@ module Listen
283
312
  # @param [String] path the file path
284
313
  #
285
314
  def content_modified?(path)
315
+ return false unless File.ftype(path) == 'file'
286
316
  @sha1_checksum = sha1_checksum(path)
287
- if @sha1_checksums[path] == @sha1_checksum || !@sha1_checksums.key?(path)
288
- insert_sha1_checksum(path)
317
+ if sha1_checksums[path] == @sha1_checksum || !sha1_checksums.key?(path)
318
+ update_sha1_checksum(path)
289
319
  false
290
320
  else
291
321
  true
@@ -296,7 +326,7 @@ module Listen
296
326
  #
297
327
  # @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
298
328
  #
299
- def insert_sha1_checksum(path)
329
+ def update_sha1_checksum(path)
300
330
  if @sha1_checksum ||= sha1_checksum(path)
301
331
  @sha1_checksums[path] = @sha1_checksum
302
332
  @sha1_checksum = nil
@@ -314,13 +344,13 @@ module Listen
314
344
  end
315
345
 
316
346
  # Traverses the base directory looking for paths that should
317
- # be stored; thus paths that are filters or not ignored.
347
+ # be stored; thus paths that are filtered or not ignored.
318
348
  #
319
349
  # @yield [path] an important path
320
350
  #
321
351
  def important_paths
322
- Find.find(@directory) do |path|
323
- next if path == @directory
352
+ Find.find(directory) do |path|
353
+ next if path == directory
324
354
 
325
355
  if File.directory?(path)
326
356
  # Add a trailing slash to directories when checking if a directory is
@@ -355,7 +385,7 @@ module Listen
355
385
  # @return [Boolean]
356
386
  #
357
387
  def existing_path?(path)
358
- @paths[File.dirname(path)][File.basename(path)] != nil
388
+ paths[File.dirname(path)][File.basename(path)] != nil
359
389
  end
360
390
 
361
391
  # Returns the modification time of a file based on the precision defined by the system
@@ -2,14 +2,16 @@ require 'pathname'
2
2
 
3
3
  module Listen
4
4
  class Listener
5
- attr_reader :directory, :directory_record, :adapter
5
+ attr_reader :directories, :directories_records, :block, :adapter, :adapter_options, :use_relative_paths
6
6
 
7
- # The default value for using relative paths in the callback.
8
- DEFAULT_TO_RELATIVE_PATHS = false
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
9
11
 
10
- # Initializes the directory listener.
12
+ # Initializes the directories listener.
11
13
  #
12
- # @param [String] directory the directory to listen to
14
+ # @param [String] directory the directories to listen to
13
15
  # @param [Hash] options the listen options
14
16
  # @option options [Regexp] ignore a pattern for ignoring paths
15
17
  # @option options [Regexp] filter a pattern for filtering paths
@@ -23,36 +25,48 @@ module Listen
23
25
  # @yieldparam [Array<String>] added the list of added files
24
26
  # @yieldparam [Array<String>] removed the list of removed files
25
27
  #
26
- def initialize(directory, options = {}, &block)
27
- @block = block
28
- @directory = Pathname.new(directory).realpath.to_s
29
- @directory_record = DirectoryRecord.new(@directory)
30
- @use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
28
+ def initialize(*args, &block)
29
+ options = args.last.is_a?(Hash) ? args.pop : {}
30
+ directories = args.flatten
31
+ initialize_directories_and_directories_records(directories)
32
+ initialize_relative_paths_usage(options)
33
+ @block = block
31
34
 
32
- @use_relative_paths = options.delete(:relative_paths) if options[:relative_paths]
33
- @directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
34
- @directory_record.filter(*options.delete(:filter)) if options[:filter]
35
+ ignore(*options.delete(:ignore))
36
+ filter(*options.delete(:filter))
35
37
 
36
38
  @adapter_options = options
37
39
  end
38
40
 
39
41
  # Starts the listener by initializing the adapter and building
40
42
  # the directory record concurrently, then it starts the adapter to watch
41
- # for changes.
43
+ # for changes. The current thread is not blocked after starting.
42
44
  #
43
- # @param [Boolean] blocking whether or not to block the current thread after starting
45
+ # @see Listen::Listener#start!
44
46
  #
45
- def start(blocking = true)
46
- t = Thread.new { @directory_record.build }
47
- @adapter = initialize_adapter
48
- t.join
49
- @adapter.start(blocking)
47
+ def start(deprecated_blocking = nil)
48
+ Kernel.warn "[Listen warning]:\n#{BLOCKING_PARAMETER_DEPRECATION_MESSAGE}" unless deprecated_blocking.nil?
49
+ setup
50
+ adapter.start
51
+ end
52
+
53
+ # Starts the listener by initializing the adapter and building
54
+ # the directory record concurrently, then it starts the adapter to watch
55
+ # for changes. The current thread is blocked after starting.
56
+ #
57
+ # @see Listen::Listener#start
58
+ #
59
+ # @since 1.0.0
60
+ #
61
+ def start!
62
+ setup
63
+ adapter.start!
50
64
  end
51
65
 
52
66
  # Stops the listener.
53
67
  #
54
68
  def stop
55
- @adapter.stop
69
+ adapter.stop
56
70
  end
57
71
 
58
72
  # Pauses the listener.
@@ -60,7 +74,7 @@ module Listen
60
74
  # @return [Listen::Listener] the listener
61
75
  #
62
76
  def pause
63
- @adapter.paused = true
77
+ adapter.pause
64
78
  self
65
79
  end
66
80
 
@@ -69,8 +83,8 @@ module Listen
69
83
  # @return [Listen::Listener] the listener
70
84
  #
71
85
  def unpause
72
- @directory_record.build
73
- @adapter.paused = false
86
+ build_directories_records
87
+ adapter.unpause
74
88
  self
75
89
  end
76
90
 
@@ -79,7 +93,7 @@ module Listen
79
93
  # @return [Boolean] adapter paused status
80
94
  #
81
95
  def paused?
82
- !!@adapter && @adapter.paused == true
96
+ !!adapter && adapter.paused?
83
97
  end
84
98
 
85
99
  # Adds ignoring patterns to the listener.
@@ -88,8 +102,10 @@ module Listen
88
102
  #
89
103
  # @return [Listen::Listener] the listener
90
104
  #
105
+ # @see Listen::DirectoryRecord#ignore
106
+ #
91
107
  def ignore(*regexps)
92
- @directory_record.ignore(*regexps)
108
+ directories_records.each { |r| r.ignore(*regexps) }
93
109
  self
94
110
  end
95
111
 
@@ -99,8 +115,10 @@ module Listen
99
115
  #
100
116
  # @return [Listen::Listener] the listener
101
117
  #
118
+ # @see Listen::DirectoryRecord#ignore!
119
+ #
102
120
  def ignore!(*regexps)
103
- @directory_record.ignore!(*regexps)
121
+ directories_records.each { |r| r.ignore!(*regexps) }
104
122
  self
105
123
  end
106
124
 
@@ -110,19 +128,23 @@ module Listen
110
128
  #
111
129
  # @return [Listen::Listener] the listener
112
130
  #
131
+ # @see Listen::DirectoryRecord#filter
132
+ #
113
133
  def filter(*regexps)
114
- @directory_record.filter(*regexps)
134
+ directories_records.each { |r| r.filter(*regexps) }
115
135
  self
116
136
  end
117
137
 
118
- # Replacing filtering patterns in the listener.
138
+ # Replaces filtering patterns in the listener.
119
139
  #
120
140
  # @param (see Listen::DirectoryRecord#filter!)
121
141
  #
122
142
  # @return [Listen::Listener] the listener
123
143
  #
144
+ # @see Listen::DirectoryRecord#filter!
145
+ #
124
146
  def filter!(*regexps)
125
- @directory_record.filter!(*regexps)
147
+ directories_records.each { |r| r.filter!(*regexps) }
126
148
  self
127
149
  end
128
150
 
@@ -171,7 +193,7 @@ module Listen
171
193
  self
172
194
  end
173
195
 
174
- # Defines a custom polling fallback message of disable it.
196
+ # Defines a custom polling fallback message or disable it.
175
197
  #
176
198
  # @example Disabling the polling fallback message
177
199
  # polling_fallback_message false
@@ -204,22 +226,74 @@ module Listen
204
226
  #
205
227
  # @param (see Listen::DirectoryRecord#fetch_changes)
206
228
  #
229
+ # @see Listen::DirectoryRecord#fetch_changes
230
+ #
207
231
  def on_change(directories, options = {})
208
- changes = @directory_record.fetch_changes(directories, options.merge(
209
- :relative_paths => @use_relative_paths
210
- ))
232
+ changes = fetch_records_changes(directories, options)
211
233
  unless changes.values.all? { |paths| paths.empty? }
212
- @block.call(changes[:modified],changes[:added],changes[:removed])
234
+ block.call(changes[:modified], changes[:added], changes[:removed])
213
235
  end
214
236
  end
215
237
 
216
238
  private
217
239
 
240
+ # Initializes the directories to watch as well as the directories records.
241
+ #
242
+ # @see Listen::DirectoryRecord
243
+ #
244
+ def initialize_directories_and_directories_records(directories)
245
+ @directories = directories.map { |d| Pathname.new(d).realpath.to_s }
246
+ @directories_records = directories.map { |d| DirectoryRecord.new(d) }
247
+ end
248
+
249
+ # Initializes whether or not using relative paths.
250
+ #
251
+ def initialize_relative_paths_usage(options)
252
+ @use_relative_paths = directories.one? && options.delete(:relative_paths) { true }
253
+ end
254
+
255
+ # Build the directory record concurrently and initialize the adapter.
256
+ #
257
+ def setup
258
+ t = Thread.new { build_directories_records }
259
+ @adapter = initialize_adapter
260
+ t.join
261
+ end
262
+
218
263
  # Initializes an adapter passing it the callback and adapters' options.
219
264
  #
220
265
  def initialize_adapter
221
- callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
222
- Adapter.select_and_initialize(@directory, @adapter_options, &callback)
266
+ callback = lambda { |changed_directories, options| self.on_change(changed_directories, options) }
267
+ Adapter.select_and_initialize(directories, adapter_options, &callback)
268
+ end
269
+
270
+ # Build the watched directories' records.
271
+ #
272
+ def build_directories_records
273
+ directories_records.each { |r| r.build }
223
274
  end
275
+
276
+ # Returns the sum of all the changes to the directories records
277
+ #
278
+ # @param (see Listen::DirectoryRecord#fetch_changes)
279
+ #
280
+ # @return [Hash] the changes
281
+ #
282
+ def fetch_records_changes(directories_to_search, options)
283
+ directories_records.inject({}) do |h, r|
284
+ # directory records skips paths outside their range, so passing the
285
+ # whole `directories` array is not a problem.
286
+ record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => use_relative_paths))
287
+
288
+ if h.empty?
289
+ h.merge!(record_changes)
290
+ else
291
+ h.each { |k, v| h[k] += record_changes[k] }
292
+ end
293
+
294
+ h
295
+ end
296
+ end
297
+
224
298
  end
225
299
  end