listen 0.3.3 → 0.4.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.
- data/CHANGELOG.md +22 -0
- data/README.md +146 -29
- data/lib/listen.rb +22 -10
- data/lib/listen/adapter.rb +89 -50
- data/lib/listen/adapters/darwin.rb +43 -18
- data/lib/listen/adapters/linux.rb +48 -33
- data/lib/listen/adapters/polling.rb +20 -7
- data/lib/listen/adapters/windows.rb +38 -31
- data/lib/listen/directory_record.rb +254 -0
- data/lib/listen/listener.rb +59 -228
- data/lib/listen/multi_listener.rb +121 -0
- data/lib/listen/turnstile.rb +28 -0
- data/lib/listen/version.rb +1 -1
- metadata +16 -11
data/lib/listen/listener.rb
CHANGED
@@ -1,26 +1,18 @@
|
|
1
|
-
require 'find'
|
2
|
-
require 'digest/sha1'
|
3
|
-
|
4
|
-
require 'listen/adapter'
|
5
|
-
require 'listen/adapters/darwin'
|
6
|
-
require 'listen/adapters/linux'
|
7
|
-
require 'listen/adapters/polling'
|
8
|
-
require 'listen/adapters/windows'
|
9
|
-
|
10
1
|
module Listen
|
11
2
|
class Listener
|
12
|
-
|
3
|
+
attr_reader :directory, :directory_record, :adapter
|
13
4
|
|
14
|
-
#
|
15
|
-
|
5
|
+
# The default value for using relative paths in the callback.
|
6
|
+
DEFAULT_TO_RELATIVE_PATHS = false
|
16
7
|
|
17
|
-
#
|
8
|
+
# Initializes the directory listener.
|
18
9
|
#
|
19
|
-
# @param [String
|
10
|
+
# @param [String] directory the directory to listen to
|
20
11
|
# @param [Hash] options the listen options
|
21
12
|
# @option options [String] ignore a list of paths to ignore
|
22
13
|
# @option options [Regexp] filter a list of regexps file filters
|
23
14
|
# @option options [Float] latency the delay between checking for changes in seconds
|
15
|
+
# @option options [Boolean] relative_paths whether or not to use relative-paths in the callback
|
24
16
|
# @option options [Boolean] force_polling whether to force the polling adapter or not
|
25
17
|
# @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
|
26
18
|
#
|
@@ -29,81 +21,84 @@ module Listen
|
|
29
21
|
# @yieldparam [Array<String>] added the list of added files
|
30
22
|
# @yieldparam [Array<String>] removed the list of removed files
|
31
23
|
#
|
32
|
-
# @return [Listen::Listener] the file listener
|
33
|
-
#
|
34
24
|
def initialize(directory, options = {}, &block)
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@
|
38
|
-
@
|
39
|
-
|
40
|
-
@
|
41
|
-
@
|
25
|
+
@block = block
|
26
|
+
@directory = directory
|
27
|
+
@directory_record = DirectoryRecord.new(directory)
|
28
|
+
@use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
|
29
|
+
|
30
|
+
@use_relative_paths = options.delete(:relative_paths) if options[:relative_paths]
|
31
|
+
@directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
|
32
|
+
@directory_record.filter(*options.delete(:filter)) if options[:filter]
|
42
33
|
|
43
34
|
@adapter_options = options
|
44
35
|
end
|
45
36
|
|
46
|
-
#
|
37
|
+
# Starts the listener by initializing the adapter and building
|
38
|
+
# the directory record concurrently, then it starts the adapter to watch
|
39
|
+
# for changes.
|
47
40
|
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@
|
41
|
+
# @param [Boolean] blocking whether or not to block the current thread after starting
|
42
|
+
#
|
43
|
+
def start(blocking = true)
|
44
|
+
t = Thread.new { @adapter = initialize_adapter }
|
45
|
+
@directory_record.build
|
46
|
+
t.join
|
47
|
+
@adapter.start(blocking)
|
53
48
|
end
|
54
49
|
|
55
|
-
#
|
50
|
+
# Stops the listener.
|
56
51
|
#
|
57
52
|
def stop
|
58
53
|
@adapter.stop
|
59
54
|
end
|
60
55
|
|
61
|
-
#
|
56
|
+
# Pauses the listener.
|
57
|
+
#
|
58
|
+
# @return [Listen::Listener] the listener
|
62
59
|
#
|
63
60
|
def pause
|
64
61
|
@adapter.paused = true
|
62
|
+
self
|
65
63
|
end
|
66
64
|
|
67
|
-
#
|
65
|
+
# Unpauses the listener.
|
66
|
+
#
|
67
|
+
# @return [Listen::Listener] the listener
|
68
68
|
#
|
69
69
|
def unpause
|
70
|
-
|
70
|
+
@directory_record.build
|
71
71
|
@adapter.paused = false
|
72
|
+
self
|
72
73
|
end
|
73
74
|
|
74
|
-
#
|
75
|
+
# Returns whether the listener is paused or not.
|
75
76
|
#
|
76
77
|
# @return [Boolean] adapter paused status
|
77
78
|
#
|
78
79
|
def paused?
|
79
|
-
|
80
|
+
!!@adapter && @adapter.paused == true
|
80
81
|
end
|
81
82
|
|
82
|
-
#
|
83
|
-
#
|
84
|
-
# @example Ignore some paths
|
85
|
-
# ignore ".git", ".svn"
|
83
|
+
# Adds ignored paths to the listener.
|
86
84
|
#
|
87
|
-
# @param
|
85
|
+
# @param (see Listen::DirectoryRecord#ignore)
|
88
86
|
#
|
89
|
-
# @return [Listen::Listener] the listener
|
87
|
+
# @return [Listen::Listener] the listener
|
90
88
|
#
|
91
89
|
def ignore(*paths)
|
92
|
-
@
|
90
|
+
@directory_record.ignore(*paths)
|
93
91
|
self
|
94
92
|
end
|
95
93
|
|
96
|
-
#
|
94
|
+
# Adds file filters to the listener.
|
97
95
|
#
|
98
|
-
# @
|
99
|
-
# ignore /\.txt$/, /.*\.zip/
|
96
|
+
# @param (see Listen::DirectoryRecord#filter)
|
100
97
|
#
|
101
|
-
# @
|
102
|
-
#
|
103
|
-
# @return [Listen::Listener] the listener itself
|
98
|
+
# @return [Listen::Listener] the listener
|
104
99
|
#
|
105
100
|
def filter(*regexps)
|
106
|
-
@
|
101
|
+
@directory_record.filter(*regexps)
|
107
102
|
self
|
108
103
|
end
|
109
104
|
|
@@ -115,7 +110,7 @@ module Listen
|
|
115
110
|
#
|
116
111
|
# @param [Float] seconds the amount of delay, in seconds
|
117
112
|
#
|
118
|
-
# @return [Listen::Listener] the listener
|
113
|
+
# @return [Listen::Listener] the listener
|
119
114
|
#
|
120
115
|
def latency(seconds)
|
121
116
|
@adapter_options[:latency] = seconds
|
@@ -128,9 +123,9 @@ module Listen
|
|
128
123
|
# @example Forcing the use of the polling adapter
|
129
124
|
# force_polling true
|
130
125
|
#
|
131
|
-
# @param [Boolean] value
|
126
|
+
# @param [Boolean] value whether to force the polling adapter or not
|
132
127
|
#
|
133
|
-
# @return [Listen::Listener] the listener
|
128
|
+
# @return [Listen::Listener] the listener
|
134
129
|
#
|
135
130
|
def force_polling(value)
|
136
131
|
@adapter_options[:force_polling] = value
|
@@ -144,212 +139,48 @@ module Listen
|
|
144
139
|
#
|
145
140
|
# @param [String, Boolean] value to change polling fallback message or remove it
|
146
141
|
#
|
147
|
-
# @return [Listen::Listener] the listener
|
142
|
+
# @return [Listen::Listener] the listener
|
148
143
|
#
|
149
144
|
def polling_fallback_message(value)
|
150
145
|
@adapter_options[:polling_fallback_message] = value
|
151
146
|
self
|
152
147
|
end
|
153
148
|
|
154
|
-
#
|
149
|
+
# Sets the callback that gets called on changes.
|
155
150
|
#
|
156
151
|
# @example Assign a callback to be called on changes
|
157
152
|
# callback = lambda { |modified, added, removed| ... }
|
158
153
|
# change &callback
|
159
154
|
#
|
160
|
-
# @param [
|
155
|
+
# @param [Proc] block the callback proc
|
161
156
|
#
|
162
|
-
# @return [Listen::Listener] the listener
|
157
|
+
# @return [Listen::Listener] the listener
|
163
158
|
#
|
164
159
|
def change(&block) # modified, added, removed
|
165
160
|
@block = block
|
166
161
|
self
|
167
162
|
end
|
168
163
|
|
169
|
-
#
|
164
|
+
# Runs the callback passing it the changes if there are any.
|
170
165
|
#
|
171
|
-
# @param
|
166
|
+
# @param (see Listen::DirectoryRecord#fetch_changes)
|
172
167
|
#
|
173
|
-
def on_change(directories,
|
174
|
-
changes =
|
168
|
+
def on_change(directories, options = {})
|
169
|
+
changes = @directory_record.fetch_changes(directories, options.merge(
|
170
|
+
:relative_paths => @use_relative_paths
|
171
|
+
))
|
175
172
|
unless changes.values.all? { |paths| paths.empty? }
|
176
173
|
@block.call(changes[:modified],changes[:added],changes[:removed])
|
177
174
|
end
|
178
175
|
end
|
179
176
|
|
180
|
-
|
181
|
-
#
|
182
|
-
def init_paths
|
183
|
-
@paths = Hash.new { |h,k| h[k] = {} }
|
184
|
-
all_existing_paths { |path| insert_path(path) }
|
185
|
-
@diffed_at = Time.now.to_i
|
186
|
-
end
|
187
|
-
|
188
|
-
# Detect changes diff in a directory.
|
189
|
-
#
|
190
|
-
# @param [Array] directories the list of directories to diff
|
191
|
-
# @param [Hash] options
|
192
|
-
# @option options [Boolean] recursive scan all sub-direcoties recursively (true when polling)
|
193
|
-
# @return [Hash<Array>] the file changes
|
194
|
-
#
|
195
|
-
def diff(directories, options = {})
|
196
|
-
@changes = { :modified => [], :added => [], :removed => [] }
|
197
|
-
directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
|
198
|
-
directories.each do |directory|
|
199
|
-
detect_modifications_and_removals(directory, options)
|
200
|
-
detect_additions(directory, options)
|
201
|
-
end
|
202
|
-
@diffed_at = Time.now.to_i
|
203
|
-
@changes
|
204
|
-
end
|
205
|
-
|
206
|
-
private
|
177
|
+
private
|
207
178
|
|
208
|
-
#
|
179
|
+
# Initializes an adapter passing it the callback and adapters' options.
|
209
180
|
#
|
210
181
|
def initialize_adapter
|
211
182
|
callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
|
212
183
|
Adapter.select_and_initialize(@directory, @adapter_options, &callback)
|
213
184
|
end
|
214
|
-
|
215
|
-
# Research all existing paths (directories & files) filtered and without ignored directories paths.
|
216
|
-
#
|
217
|
-
# @yield [path] the existing path
|
218
|
-
#
|
219
|
-
def all_existing_paths
|
220
|
-
Find.find(@directory) do |path|
|
221
|
-
next if @directory == path
|
222
|
-
|
223
|
-
if File.directory?(path)
|
224
|
-
if ignored_path?(path)
|
225
|
-
Find.prune # Don't look any further into this directory.
|
226
|
-
else
|
227
|
-
yield(path)
|
228
|
-
end
|
229
|
-
elsif !ignored_path?(path) && filtered_file?(path)
|
230
|
-
yield(path)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
# Insert a path with its type (Dir or File) in @paths.
|
236
|
-
#
|
237
|
-
# @param [String] path the path to insert in @paths.
|
238
|
-
#
|
239
|
-
def insert_path(path)
|
240
|
-
@paths[File.dirname(path)][File.basename(path)] = File.directory?(path) ? 'Dir' : 'File'
|
241
|
-
end
|
242
|
-
|
243
|
-
# Find is a path exists in @paths.
|
244
|
-
#
|
245
|
-
# @param [String] path the path to find in @paths.
|
246
|
-
# @return [Boolean]
|
247
|
-
#
|
248
|
-
def existing_path?(path)
|
249
|
-
@paths[File.dirname(path)][File.basename(path)] != nil
|
250
|
-
end
|
251
|
-
|
252
|
-
# Detect modifications and removals recursivly in a directory.
|
253
|
-
# Modifications detection are based on mtime first and on checksum when mtime == last diffed_at
|
254
|
-
#
|
255
|
-
# @param [String] directory the path to analyze
|
256
|
-
# @param [Hash] options
|
257
|
-
# @option options [Boolean] recursive scan all sub-direcoties recursively (when polling)
|
258
|
-
#
|
259
|
-
def detect_modifications_and_removals(directory, options = {})
|
260
|
-
@paths[directory].each do |basename, type|
|
261
|
-
path = File.join(directory, basename)
|
262
|
-
|
263
|
-
case type
|
264
|
-
when 'Dir'
|
265
|
-
if File.directory?(path)
|
266
|
-
detect_modifications_and_removals(path, options) if options[:recursive]
|
267
|
-
else
|
268
|
-
detect_modifications_and_removals(path, :recursive => true)
|
269
|
-
@paths[directory].delete(basename)
|
270
|
-
@paths.delete("#{directory}/#{basename}")
|
271
|
-
end
|
272
|
-
when 'File'
|
273
|
-
if File.exist?(path)
|
274
|
-
new_mtime = File.mtime(path).to_i
|
275
|
-
if @diffed_at < new_mtime || (@diffed_at == new_mtime && content_modified?(path))
|
276
|
-
@changes[:modified] << relative_path(path)
|
277
|
-
end
|
278
|
-
else
|
279
|
-
@paths[directory].delete(basename)
|
280
|
-
@sha1_checksums.delete(path)
|
281
|
-
@changes[:removed] << relative_path(path)
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
# Tests if the file content has been modified by
|
288
|
-
# comparing the SHA1 checksum.
|
289
|
-
#
|
290
|
-
# @param [String] path the file path
|
291
|
-
#
|
292
|
-
def content_modified?(path)
|
293
|
-
sha1_checksum = Digest::SHA1.file(path).to_s
|
294
|
-
if @sha1_checksums[path] != sha1_checksum
|
295
|
-
@sha1_checksums[path] = sha1_checksum
|
296
|
-
true
|
297
|
-
else
|
298
|
-
false
|
299
|
-
end
|
300
|
-
end
|
301
|
-
|
302
|
-
# Detect additions in a directory.
|
303
|
-
#
|
304
|
-
# @param [String] directory the path to analyze
|
305
|
-
# @param [Hash] options
|
306
|
-
# @option options [Boolean] recursive scan all sub-direcoties recursively (when polling)
|
307
|
-
#
|
308
|
-
def detect_additions(directory, options = {})
|
309
|
-
Find.find(directory) do |path|
|
310
|
-
next if @directory == path
|
311
|
-
|
312
|
-
if File.directory?(path)
|
313
|
-
if ignored_path?(path) || (directory != path && (!options[:recursive] && existing_path?(path)))
|
314
|
-
Find.prune # Don't look any further into this directory.
|
315
|
-
else
|
316
|
-
insert_path(path)
|
317
|
-
end
|
318
|
-
elsif !ignored_path?(path) && filtered_file?(path) && !existing_path?(path)
|
319
|
-
@changes[:added] << relative_path(path) if File.file?(path)
|
320
|
-
insert_path(path)
|
321
|
-
end
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
# Convert absolute path to a path relative to the listener directory (by default).
|
326
|
-
#
|
327
|
-
# @param [String] path the path to convert
|
328
|
-
# @param [String] directory the directoy path to relative from
|
329
|
-
# @return [String] the relative converted path
|
330
|
-
#
|
331
|
-
def relative_path(path, directory = @directory)
|
332
|
-
base_dir = directory.sub(/\/$/, '')
|
333
|
-
path.sub(%r(^#{base_dir}/), '')
|
334
|
-
end
|
335
|
-
|
336
|
-
# Test if a path should be ignored or not.
|
337
|
-
#
|
338
|
-
# @param [String] path the path to test.
|
339
|
-
# @return [Boolean]
|
340
|
-
#
|
341
|
-
def ignored_path?(path)
|
342
|
-
@ignored_paths.any? { |ignored_path| path =~ /#{ignored_path}$/ }
|
343
|
-
end
|
344
|
-
|
345
|
-
# Test if a file path should be filtered or not.
|
346
|
-
#
|
347
|
-
# @param [String] path the file path to test.
|
348
|
-
# @return [Boolean]
|
349
|
-
#
|
350
|
-
def filtered_file?(path)
|
351
|
-
@file_filters.empty? || @file_filters.any? { |file_filter| path =~ file_filter }
|
352
|
-
end
|
353
|
-
|
354
185
|
end
|
355
186
|
end
|
@@ -0,0 +1,121 @@
|
|
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 [String] ignore a list of paths to ignore
|
10
|
+
# @option options [Regexp] filter a list of regexps file filters
|
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
|
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 { @adapter = initialize_adapter }
|
42
|
+
@directories_records.each { |r| r.build }
|
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
|