listen 0.5.3 → 3.7.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -186
  3. data/CONTRIBUTING.md +45 -0
  4. data/{LICENSE → LICENSE.txt} +3 -1
  5. data/README.md +332 -181
  6. data/bin/listen +11 -0
  7. data/lib/listen/adapter/base.rb +129 -0
  8. data/lib/listen/adapter/bsd.rb +107 -0
  9. data/lib/listen/adapter/config.rb +25 -0
  10. data/lib/listen/adapter/darwin.rb +77 -0
  11. data/lib/listen/adapter/linux.rb +108 -0
  12. data/lib/listen/adapter/polling.rb +40 -0
  13. data/lib/listen/adapter/windows.rb +96 -0
  14. data/lib/listen/adapter.rb +32 -201
  15. data/lib/listen/backend.rb +40 -0
  16. data/lib/listen/change.rb +69 -0
  17. data/lib/listen/cli.rb +65 -0
  18. data/lib/listen/directory.rb +93 -0
  19. data/lib/listen/error.rb +11 -0
  20. data/lib/listen/event/config.rb +40 -0
  21. data/lib/listen/event/loop.rb +94 -0
  22. data/lib/listen/event/processor.rb +126 -0
  23. data/lib/listen/event/queue.rb +54 -0
  24. data/lib/listen/file.rb +95 -0
  25. data/lib/listen/fsm.rb +133 -0
  26. data/lib/listen/listener/config.rb +41 -0
  27. data/lib/listen/listener.rb +93 -160
  28. data/lib/listen/logger.rb +36 -0
  29. data/lib/listen/monotonic_time.rb +27 -0
  30. data/lib/listen/options.rb +26 -0
  31. data/lib/listen/queue_optimizer.rb +129 -0
  32. data/lib/listen/record/entry.rb +66 -0
  33. data/lib/listen/record/symlink_detector.rb +41 -0
  34. data/lib/listen/record.rb +123 -0
  35. data/lib/listen/silencer/controller.rb +50 -0
  36. data/lib/listen/silencer.rb +106 -0
  37. data/lib/listen/thread.rb +54 -0
  38. data/lib/listen/version.rb +3 -1
  39. data/lib/listen.rb +40 -32
  40. metadata +87 -38
  41. data/lib/listen/adapters/darwin.rb +0 -85
  42. data/lib/listen/adapters/linux.rb +0 -113
  43. data/lib/listen/adapters/polling.rb +0 -67
  44. data/lib/listen/adapters/windows.rb +0 -87
  45. data/lib/listen/dependency_manager.rb +0 -126
  46. data/lib/listen/directory_record.rb +0 -344
  47. data/lib/listen/multi_listener.rb +0 -121
  48. data/lib/listen/turnstile.rb +0 -28
@@ -1,344 +0,0 @@
1
- require 'set'
2
- require 'find'
3
- require 'digest/sha1'
4
-
5
- module Listen
6
-
7
- # The directory record stores information about
8
- # a directory and keeps track of changes to
9
- # the structure of its childs.
10
- #
11
- class DirectoryRecord
12
- attr_reader :directory, :paths, :sha1_checksums
13
-
14
- DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
15
-
16
- DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
17
-
18
- # Defines the used precision based on the type of mtime returned by the
19
- # system (whether its in milliseconds or just seconds)
20
- #
21
- HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
22
-
23
- # Data structure used to save meta data about a path
24
- #
25
- MetaData = Struct.new(:type, :mtime)
26
-
27
- # Class methods
28
- #
29
- class << self
30
-
31
- # Creates the ignoring patterns from the default ignored
32
- # directories and extensions. It memoizes the generated patterns
33
- # to avoid unnecessary computation.
34
- #
35
- def generate_default_ignoring_patterns
36
- @@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
37
- # Add directories
38
- ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
39
- default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
40
-
41
- # Add extensions
42
- ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
43
- default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
44
- end
45
- end
46
- end
47
-
48
- # Initializes a directory record.
49
- #
50
- # @option [String] directory the directory to keep track of
51
- #
52
- def initialize(directory)
53
- raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
54
-
55
- @directory = directory
56
- @ignoring_patterns = Set.new
57
- @filtering_patterns = Set.new
58
- @sha1_checksums = Hash.new
59
-
60
- @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
61
- end
62
-
63
- # Returns the ignoring patterns in the record
64
- #
65
- # @return [Array<Regexp>] the ignoring patterns
66
- #
67
- def ignoring_patterns
68
- @ignoring_patterns.to_a
69
- end
70
-
71
- # Returns the filtering patterns used in the record to know
72
- # which paths should be stored.
73
- #
74
- # @return [Array<Regexp>] the filtering patterns
75
- #
76
- def filtering_patterns
77
- @filtering_patterns.to_a
78
- end
79
-
80
- # Adds ignoring patterns to the record.
81
- #
82
- # @example Ignore some paths
83
- # ignore %r{^ignored/path/}, /man/
84
- #
85
- # @param [Regexp] regexp a pattern for ignoring paths
86
- #
87
- def ignore(*regexps)
88
- @ignoring_patterns.merge(regexps)
89
- end
90
-
91
- # Adds filtering patterns to the listener.
92
- #
93
- # @example Filter some files
94
- # ignore /\.txt$/, /.*\.zip/
95
- #
96
- # @param [Regexp] regexp a pattern for filtering paths
97
- #
98
- def filter(*regexps)
99
- @filtering_patterns.merge(regexps)
100
- end
101
-
102
- # Returns whether a path should be ignored or not.
103
- #
104
- # @param [String] path the path to test.
105
- #
106
- # @return [Boolean]
107
- #
108
- def ignored?(path)
109
- path = relative_to_base(path)
110
- @ignoring_patterns.any? { |pattern| pattern =~ path }
111
- end
112
-
113
- # Returns whether a path should be filtered or not.
114
- #
115
- # @param [String] path the path to test.
116
- #
117
- # @return [Boolean]
118
- #
119
- def filtered?(path)
120
- # When no filtering patterns are set, ALL files are stored.
121
- return true if @filtering_patterns.empty?
122
-
123
- path = relative_to_base(path)
124
- @filtering_patterns.any? { |pattern| pattern =~ path }
125
- end
126
-
127
- # Finds the paths that should be stored and adds them
128
- # to the paths' hash.
129
- #
130
- def build
131
- @paths = Hash.new { |h, k| h[k] = Hash.new }
132
- important_paths { |path| insert_path(path) }
133
- end
134
-
135
- # Detects changes in the passed directories, updates
136
- # the record with the new changes and returns the changes
137
- #
138
- # @param [Array] directories the list of directories scan for changes
139
- # @param [Hash] options
140
- # @option options [Boolean] recursive scan all sub-directories recursively
141
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
142
- #
143
- # @return [Hash<Array>] the changes
144
- #
145
- def fetch_changes(directories, options = {})
146
- @changes = { :modified => [], :added => [], :removed => [] }
147
- directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
148
-
149
- directories.each do |directory|
150
- next unless directory[@directory] # Path is or inside directory
151
- detect_modifications_and_removals(directory, options)
152
- detect_additions(directory, options)
153
- end
154
-
155
- @changes
156
- end
157
-
158
- # Converts an absolute path to a path that's relative to the base directory.
159
- #
160
- # @param [String] path the path to convert
161
- #
162
- # @return [String] the relative path
163
- #
164
- def relative_to_base(path)
165
- return nil unless path[@directory]
166
- path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '')
167
- end
168
-
169
- private
170
-
171
- # Detects modifications and removals recursively in a directory.
172
- #
173
- # @note Modifications detection begins by checking the modification time (mtime)
174
- # of files and then by checking content changes (using SHA1-checksum)
175
- # when the mtime of files is not changed.
176
- #
177
- # @param [String] directory the path to analyze
178
- # @param [Hash] options
179
- # @option options [Boolean] recursive scan all sub-directories recursively
180
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
181
- #
182
- def detect_modifications_and_removals(directory, options = {})
183
- @paths[directory].each do |basename, meta_data|
184
- path = File.join(directory, basename)
185
-
186
- case meta_data.type
187
- when 'Dir'
188
- if File.directory?(path)
189
- detect_modifications_and_removals(path, options) if options[:recursive]
190
- else
191
- detect_modifications_and_removals(path, { :recursive => true }.merge(options))
192
- @paths[directory].delete(basename)
193
- @paths.delete("#{directory}/#{basename}")
194
- end
195
- when 'File'
196
- if File.exist?(path)
197
- new_mtime = mtime_of(path)
198
-
199
- # First check if we are in the same second (to update checksums)
200
- # before checking the time difference
201
- if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
202
- # Update the sha1 checksum of the file
203
- insert_sha1_checksum(path)
204
-
205
- # Update the meta data of the file
206
- meta_data.mtime = new_mtime
207
- @paths[directory][basename] = meta_data
208
-
209
- @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
210
- end
211
- else
212
- @paths[directory].delete(basename)
213
- @sha1_checksums.delete(path)
214
- @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
215
- end
216
- end
217
- end
218
- end
219
-
220
- # Detects additions in a directory.
221
- #
222
- # @param [String] directory the path to analyze
223
- # @param [Hash] options
224
- # @option options [Boolean] recursive scan all sub-directories recursively
225
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
226
- #
227
- def detect_additions(directory, options = {})
228
- # Don't process removed directories
229
- return unless File.exist?(directory)
230
-
231
- Find.find(directory) do |path|
232
- next if path == @directory
233
-
234
- if File.directory?(path)
235
- # Add a trailing slash to directories when checking if a directory is
236
- # ignored to optimize finding them as Find.find doesn't.
237
- if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path)))
238
- Find.prune # Don't look any further into this directory.
239
- else
240
- insert_path(path)
241
- end
242
- elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
243
- if File.file?(path)
244
- @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
245
- insert_path(path)
246
- end
247
- end
248
- end
249
- end
250
-
251
- # Returns whether or not a file's content has been modified by
252
- # comparing the SHA1-checksum to a stored one.
253
- # Ensure that the SHA1-checksum is inserted to the sha1_checksums
254
- # array for later comparaison if false.
255
- #
256
- # @param [String] path the file path
257
- #
258
- def content_modified?(path)
259
- @sha1_checksum = sha1_checksum(path)
260
- if @sha1_checksums[path] == @sha1_checksum || !@sha1_checksums.key?(path)
261
- insert_sha1_checksum(path)
262
- false
263
- else
264
- true
265
- end
266
- end
267
-
268
- # Inserts a SHA1-checksum path in @SHA1-checksums hash.
269
- #
270
- # @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
271
- #
272
- def insert_sha1_checksum(path)
273
- if @sha1_checksum ||= sha1_checksum(path)
274
- @sha1_checksums[path] = @sha1_checksum
275
- @sha1_checksum = nil
276
- end
277
- end
278
-
279
- # Returns the SHA1-checksum for the file path.
280
- #
281
- # @param [String] path the file path
282
- #
283
- def sha1_checksum(path)
284
- Digest::SHA1.file(path).to_s
285
- rescue Errno::EACCES, Errno::ENOENT, Errno::ENXIO
286
- nil
287
- end
288
-
289
- # Traverses the base directory looking for paths that should
290
- # be stored; thus paths that are filters or not ignored.
291
- #
292
- # @yield [path] an important path
293
- #
294
- def important_paths
295
- Find.find(@directory) do |path|
296
- next if path == @directory
297
-
298
- if File.directory?(path)
299
- # Add a trailing slash to directories when checking if a directory is
300
- # ignored to optimize finding them as Find.find doesn't.
301
- if ignored?(path + File::SEPARATOR)
302
- Find.prune # Don't look any further into this directory.
303
- else
304
- yield(path)
305
- end
306
- elsif !ignored?(path) && filtered?(path)
307
- yield(path)
308
- end
309
- end
310
- end
311
-
312
- # Inserts a path with its type (Dir or File) in paths hash.
313
- #
314
- # @param [String] path the path to insert in @paths.
315
- #
316
- def insert_path(path)
317
- meta_data = MetaData.new
318
- meta_data.type = File.directory?(path) ? 'Dir' : 'File'
319
- meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet
320
- @paths[File.dirname(path)][File.basename(path)] = meta_data
321
- rescue Errno::ENOENT
322
- end
323
-
324
- # Returns whether or not a path exists in the paths hash.
325
- #
326
- # @param [String] path the path to check
327
- #
328
- # @return [Boolean]
329
- #
330
- def existing_path?(path)
331
- @paths[File.dirname(path)][File.basename(path)] != nil
332
- end
333
-
334
- # Returns the modification time of a file based on the precision defined by the system
335
- #
336
- # @param [String] file the file for which the mtime must be returned
337
- #
338
- # @return [Fixnum, Float] the mtime of the file
339
- #
340
- def mtime_of(file)
341
- File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i)
342
- end
343
- end
344
- end
@@ -1,121 +0,0 @@
1
- module Listen
2
- class MultiListener < Listener
3
- attr_reader :directories, :directories_records, :adapter
4
-
5
- # Initializes the multiple directories listener.
6
- #
7
- # @param [String] directories the directories to listen to
8
- # @param [Hash] options the listen options
9
- # @option options [Regexp] ignore a pattern for ignoring paths
10
- # @option options [Regexp] filter a pattern for filtering paths
11
- # @option options [Float] latency the delay between checking for changes in seconds
12
- # @option options [Boolean] force_polling whether to force the polling adapter or not
13
- # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
14
- #
15
- # @yield [modified, added, removed] the changed files
16
- # @yieldparam [Array<String>] modified the list of modified files
17
- # @yieldparam [Array<String>] added the list of added files
18
- # @yieldparam [Array<String>] removed the list of removed files
19
- #
20
- def initialize(*args, &block)
21
- options = args.last.is_a?(Hash) ? args.pop : {}
22
- directories = args
23
-
24
- @block = block
25
- @directories = directories.map { |d| Pathname.new(d).realpath.to_s }
26
- @directories_records = @directories.map { |d| DirectoryRecord.new(d) }
27
-
28
- ignore(*options.delete(:ignore)) if options[:ignore]
29
- filter(*options.delete(:filter)) if options[:filter]
30
-
31
- @adapter_options = options
32
- end
33
-
34
- # Starts the listener by initializing the adapter and building
35
- # the directory record concurrently, then it starts the adapter to watch
36
- # for changes.
37
- #
38
- # @param [Boolean] blocking whether or not to block the current thread after starting
39
- #
40
- def start(blocking = true)
41
- t = Thread.new { @directories_records.each { |r| r.build } }
42
- @adapter = initialize_adapter
43
- t.join
44
- @adapter.start(blocking)
45
- end
46
-
47
- # Unpauses the listener.
48
- #
49
- # @return [Listen::Listener] the listener
50
- #
51
- def unpause
52
- @directories_records.each { |r| r.build }
53
- @adapter.paused = false
54
- self
55
- end
56
-
57
- # Adds ignored paths to the listener.
58
- #
59
- # @param (see Listen::DirectoryRecord#ignore)
60
- #
61
- # @return [Listen::Listener] the listener
62
- #
63
- def ignore(*paths)
64
- @directories_records.each { |r| r.ignore(*paths) }
65
- self
66
- end
67
-
68
- # Adds file filters to the listener.
69
- #
70
- # @param (see Listen::DirectoryRecord#filter)
71
- #
72
- # @return [Listen::Listener] the listener
73
- #
74
- def filter(*regexps)
75
- @directories_records.each { |r| r.filter(*regexps) }
76
- self
77
- end
78
-
79
- # Runs the callback passing it the changes if there are any.
80
- #
81
- # @param (see Listen::DirectoryRecord#fetch_changes)
82
- #
83
- def on_change(directories_to_search, options = {})
84
- changes = fetch_records_changes(directories_to_search, options)
85
- unless changes.values.all? { |paths| paths.empty? }
86
- @block.call(changes[:modified],changes[:added],changes[:removed])
87
- end
88
- end
89
-
90
- private
91
-
92
- # Initializes an adapter passing it the callback and adapters' options.
93
- #
94
- def initialize_adapter
95
- callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
96
- Adapter.select_and_initialize(@directories, @adapter_options, &callback)
97
- end
98
-
99
- # Returns the sum of all the changes to the directories records
100
- #
101
- # @param (see Listen::DirectoryRecord#fetch_changes)
102
- #
103
- # @return [Hash] the changes
104
- #
105
- def fetch_records_changes(directories_to_search, options)
106
- @directories_records.inject({}) do |h, r|
107
- # directory records skips paths outside their range, so passing the
108
- # whole `directories` array is not a problem.
109
- record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => DEFAULT_TO_RELATIVE_PATHS))
110
-
111
- if h.empty?
112
- h.merge!(record_changes)
113
- else
114
- h.each { |k, v| h[k] += record_changes[k] }
115
- end
116
-
117
- h
118
- end
119
- end
120
- end
121
- end
@@ -1,28 +0,0 @@
1
- module Listen
2
- # Allows two threads to wait on eachother.
3
- #
4
- # @note Only two threads can be used with this Turnstile
5
- # because of the current implementation.
6
- class Turnstile
7
-
8
- # Initialize the turnstile.
9
- #
10
- def initialize
11
- # Until ruby offers semahpores, only queues can be used
12
- # to implement a turnstile.
13
- @q = Queue.new
14
- end
15
-
16
- # Blocks the current thread until a signal is received.
17
- #
18
- def wait
19
- @q.pop if @q.num_waiting == 0
20
- end
21
-
22
- # Unblocks the waiting thread if there is one.
23
- #
24
- def signal
25
- @q.push :dummy if @q.num_waiting == 1
26
- end
27
- end
28
- end