listen 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,28 +5,49 @@ module Listen
5
5
  #
6
6
  class Darwin < Adapter
7
7
 
8
- # Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
8
+ LAST_SEPARATOR_REGEX = /\/$/
9
+
10
+ # Initializes the Adapter. See {Listen::Adapter#initialize} for more info.
9
11
  #
10
- def initialize(directory, options = {}, &callback)
12
+ def initialize(directories, options = {}, &callback)
11
13
  super
12
- init_worker
14
+ @worker = init_worker
13
15
  end
14
16
 
15
- # Start the adapter.
17
+ # Starts the adapter.
16
18
  #
17
- def start
18
- super
19
- @worker.run
19
+ # @param [Boolean] blocking whether or not to block the current thread after starting
20
+ #
21
+ def start(blocking = true)
22
+ @mutex.synchronize do
23
+ return if @stop == false
24
+ super
25
+ end
26
+
27
+ @worker_thread = Thread.new { @worker.run }
28
+ @poll_thread = Thread.new { poll_changed_dirs }
29
+
30
+ # The FSEvent worker needs sometime to startup. Turnstiles can't
31
+ # be used to wait for it as it runs in a loop.
32
+ # TODO: Find a better way to block until the worker starts.
33
+ sleep @latency
34
+ @poll_thread.join if blocking
20
35
  end
21
36
 
22
- # Stop the adapter.
37
+ # Stops the adapter.
23
38
  #
24
39
  def stop
25
- super
40
+ @mutex.synchronize do
41
+ return if @stop == true
42
+ super
43
+ end
44
+
26
45
  @worker.stop
46
+ Thread.kill(@worker_thread) if @worker_thread
47
+ @poll_thread.join
27
48
  end
28
49
 
29
- # Check if the adapter is usable on the current OS.
50
+ # Checks if the adapter is usable on the current OS.
30
51
  #
31
52
  # @return [Boolean] whether usable or not
32
53
  #
@@ -39,17 +60,21 @@ module Listen
39
60
  false
40
61
  end
41
62
 
42
- private
63
+ private
43
64
 
44
- # Initialiaze FSEvent worker and set watch callback block
65
+ # Initializes a FSEvent worker and adds a watcher for
66
+ # each directory passed to the adapter.
67
+ #
68
+ # @return [FSEvent] initialized worker
45
69
  #
46
70
  def init_worker
47
- @worker = FSEvent.new
48
- @worker.watch(@directory, :latency => @latency) do |changed_dirs|
49
- next if @paused
50
-
51
- changed_dirs.map! { |path| path.sub /\/$/, '' }
52
- @callback.call(changed_dirs, {})
71
+ FSEvent.new.tap do |worker|
72
+ worker.watch(@directories.dup, :latency => @latency) do |changes|
73
+ next if @paused
74
+ @mutex.synchronize do
75
+ changes.each { |path| @changed_dirs << path.sub(LAST_SEPARATOR_REGEX, '') }
76
+ end
77
+ end
53
78
  end
54
79
  end
55
80
 
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module Listen
4
2
  module Adapters
5
3
 
@@ -14,27 +12,39 @@ module Listen
14
12
  #
15
13
  class Linux < Adapter
16
14
 
17
- # Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
15
+ # Initializes the Adapter. See {Listen::Adapter#initialize} for more info.
18
16
  #
19
- def initialize(directory, options = {}, &callback)
17
+ def initialize(directories, options = {}, &callback)
20
18
  super
21
- @changed_dirs = Set.new
22
- init_worker
19
+ @worker = init_worker
23
20
  end
24
21
 
25
- # Start the adapter.
22
+ # Starts the adapter.
26
23
  #
27
- def start
28
- super
29
- Thread.new { @worker.run }
30
- poll_changed_dirs
24
+ # @param [Boolean] blocking whether or not to block the current thread after starting
25
+ #
26
+ def start(blocking = true)
27
+ @mutex.synchronize do
28
+ return if @stop == false
29
+ super
30
+ end
31
+
32
+ @worker_thread = Thread.new { @worker.run }
33
+ @poll_thread = Thread.new { poll_changed_dirs }
34
+ @poll_thread.join if blocking
31
35
  end
32
36
 
33
- # Stop the adapter.
37
+ # Stops the adapter.
34
38
  #
35
39
  def stop
36
- super
40
+ @mutex.synchronize do
41
+ return if @stop == true
42
+ super
43
+ end
44
+
37
45
  @worker.stop
46
+ Thread.kill(@worker_thread) if @worker_thread
47
+ @poll_thread.join
38
48
  end
39
49
 
40
50
  # Check if the adapter is usable on the current OS.
@@ -52,30 +62,35 @@ module Listen
52
62
 
53
63
  private
54
64
 
55
- # Initialize INotify worker and set watch callback block.
65
+ # Initializes a INotify worker and adds a watcher for
66
+ # each directory passed to the adapter.
56
67
  #
57
- def init_worker
58
- @worker = INotify::Notifier.new
59
- @worker.watch(@directory, *EVENTS.map(&:to_sym)) do |event|
60
- next if @paused
61
-
62
- unless event.name == "" # Event on root directory
63
- @changed_dirs << File.dirname(event.absolute_name)
64
- end
65
- end
66
- end
67
-
68
- # Polling around @changed_dirs presence.
68
+ # @return [INotify::Notifier] initialized worker
69
69
  #
70
- def poll_changed_dirs
71
- until @stop
72
- sleep(@latency)
70
+ def init_worker
71
+ worker = INotify::Notifier.new
72
+ @directories.each do |directory|
73
+ worker.watch(directory, *EVENTS.map(&:to_sym)) do |event|
74
+ if @paused || (
75
+ # Event on root directory
76
+ event.name == ""
77
+ ) || (
78
+ # INotify reports changes to files inside directories as events
79
+ # on the directories themselves too.
80
+ #
81
+ # @see http://linux.die.net/man/7/inotify
82
+ event.flags.include?(:isdir) and event.flags & [:close, :modify] != []
83
+ )
84
+ # Skip all of these!
85
+ next
86
+ end
73
87
 
74
- next if @changed_dirs.empty?
75
- changed_dirs = @changed_dirs.to_a
76
- @changed_dirs.clear
77
- @callback.call(changed_dirs, {})
88
+ @mutex.synchronize do
89
+ @changed_dirs << File.dirname(event.absolute_name)
90
+ end
91
+ end
78
92
  end
93
+ worker
79
94
  end
80
95
 
81
96
  end
@@ -13,22 +13,34 @@ module Listen
13
13
 
14
14
  # Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
15
15
  #
16
- def initialize(directory, options = {}, &callback)
16
+ def initialize(directories, options = {}, &callback)
17
17
  @latency ||= DEFAULT_POLLING_LATENCY
18
18
  super
19
19
  end
20
20
 
21
21
  # Start the adapter.
22
22
  #
23
- def start
24
- super
25
- poll
23
+ # @param [Boolean] blocking whether or not to block the current thread after starting
24
+ #
25
+ def start(blocking = true)
26
+ @mutex.synchronize do
27
+ return if @stop == false
28
+ super
29
+ end
30
+
31
+ @poll_thread = Thread.new { poll }
32
+ @poll_thread.join if blocking
26
33
  end
27
34
 
28
35
  # Stop the adapter.
29
36
  #
30
37
  def stop
31
- super
38
+ @mutex.synchronize do
39
+ return if @stop == true
40
+ super
41
+ end
42
+
43
+ @poll_thread.join
32
44
  end
33
45
 
34
46
  private
@@ -37,10 +49,11 @@ module Listen
37
49
  #
38
50
  def poll
39
51
  until @stop
40
- sleep 0.1 && next if @paused
52
+ sleep(0.1) && next if @paused
41
53
 
42
54
  start = Time.now.to_f
43
- @callback.call([@directory], :recursive => true)
55
+ @callback.call(@directories.dup, :recursive => true)
56
+ @turnstile.signal
44
57
  nap_time = @latency - (Time.now.to_f - start)
45
58
  sleep(nap_time) if nap_time > 0
46
59
  end
@@ -7,30 +7,42 @@ module Listen
7
7
  #
8
8
  class Windows < Adapter
9
9
 
10
- # Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
10
+ # Initializes the Adapter. See {Listen::Adapter#initialize} for more info.
11
11
  #
12
- def initialize(directory, options = {}, &callback)
12
+ def initialize(directories, options = {}, &callback)
13
13
  super
14
- @changed_dirs = Set.new
15
- init_worker
14
+ @worker = init_worker
16
15
  end
17
16
 
18
- # Start the adapter.
17
+ # Starts the adapter.
19
18
  #
20
- def start
21
- super
22
- Thread.new { @worker.run }
23
- poll_changed_dirs
19
+ # @param [Boolean] blocking whether or not to block the current thread after starting
20
+ #
21
+ def start(blocking = true)
22
+ @mutex.synchronize do
23
+ return if @stop == false
24
+ super
25
+ end
26
+
27
+ @worker_thread = Thread.new { @worker.run }
28
+ @poll_thread = Thread.new { poll_changed_dirs(true) }
29
+ @poll_thread.join if blocking
24
30
  end
25
31
 
26
- # Stop the adapter.
32
+ # Stops the adapter.
27
33
  #
28
34
  def stop
29
- super
35
+ @mutex.synchronize do
36
+ return if @stop == true
37
+ super
38
+ end
39
+
30
40
  @worker.stop
41
+ Thread.kill(@worker_thread) if @worker_thread
42
+ @poll_thread.join
31
43
  end
32
44
 
33
- # Check if the adapter is usable on the current OS.
45
+ # Checks if the adapter is usable on the current OS.
34
46
  #
35
47
  # @return [Boolean] whether usable or not
36
48
  #
@@ -45,28 +57,23 @@ module Listen
45
57
 
46
58
  private
47
59
 
48
- # Initialiaze FSEvent worker and set watch callback block
60
+ # Initializes a FChange worker and adds a watcher for
61
+ # each directory passed to the adapter.
49
62
  #
50
- def init_worker
51
- @worker = FChange::Notifier.new
52
- @worker.watch(@directory, :all_events, :recursive) do |event|
53
- next if @paused
54
-
55
- @changed_dirs << File.expand_path(event.watcher.path)
56
- end
57
- end
58
-
59
- # Polling around @changed_dirs presence.
63
+ # @return [FChange::Notifier] initialized worker
60
64
  #
61
- def poll_changed_dirs
62
- until @stop
63
- sleep(@latency)
64
-
65
- next if @changed_dirs.empty?
66
- changed_dirs = @changed_dirs.to_a
67
- @changed_dirs.clear
68
- @callback.call(changed_dirs, :recursive => true)
65
+ def init_worker
66
+ worker = FChange::Notifier.new
67
+ @directories.each do |directory|
68
+ watcher = worker.watch(directory, :all_events, :recursive) do |event|
69
+ next if @paused
70
+ @mutex.synchronize do
71
+ @changed_dirs << File.expand_path(event.watcher.path)
72
+ end
73
+ end
74
+ worker.add_watcher(watcher)
69
75
  end
76
+ worker
70
77
  end
71
78
 
72
79
  end
@@ -0,0 +1,254 @@
1
+ require 'set'
2
+ require 'find'
3
+ require 'pathname'
4
+ require 'digest/sha1'
5
+
6
+ module Listen
7
+
8
+ # The directory record stores information about
9
+ # a directory and keeps track of changes to
10
+ # the structure of its childs.
11
+ #
12
+ class DirectoryRecord
13
+ attr_reader :directory, :paths, :sha1_checksums
14
+
15
+ # Default paths' beginnings that doesn't get stored in the record
16
+ DEFAULT_IGNORED_PATHS = %w[.bundle .git .DS_Store log tmp vendor]
17
+
18
+ # Initializes a directory record.
19
+ #
20
+ # @option [String] directory the directory to keep track of
21
+ #
22
+ def initialize(directory)
23
+ @directory = directory
24
+ raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(@directory)
25
+
26
+ @ignored_paths = Set.new(DEFAULT_IGNORED_PATHS)
27
+ @filters = Set.new
28
+ @sha1_checksums = Hash.new
29
+ end
30
+
31
+ # Returns the ignored paths in the record
32
+ #
33
+ # @return [Array<String>] the ignored paths
34
+ #
35
+ def ignored_paths
36
+ @ignored_paths.to_a
37
+ end
38
+
39
+ # Returns the filters used in the record to know
40
+ # which paths should be stored.
41
+ #
42
+ # @return [Array<String>] the used filters
43
+ #
44
+ def filters
45
+ @filters.to_a
46
+ end
47
+
48
+ # Adds ignored path to the record.
49
+ #
50
+ # @example Ignore some paths
51
+ # ignore ".git", ".svn"
52
+ #
53
+ # @param [String, Array<String>] paths a path or a list of paths to ignore
54
+ #
55
+ def ignore(*paths)
56
+ @ignored_paths.merge(paths)
57
+ end
58
+
59
+ # Adds file filters to the listener.
60
+ #
61
+ # @example Filter some files
62
+ # ignore /\.txt$/, /.*\.zip/
63
+ #
64
+ # @param [Array<Regexp>] regexps a list of regexps file filters
65
+ #
66
+ # @return [Listen::Listener] the listener itself
67
+ #
68
+ def filter(*regexps)
69
+ @filters.merge(regexps)
70
+ end
71
+
72
+ # Returns whether a path should be ignored or not.
73
+ #
74
+ # @param [String] path the path to test.
75
+ #
76
+ # @return [Boolean]
77
+ #
78
+ def ignored?(path)
79
+ @ignored_paths.any? { |ignored_path| path =~ /#{ignored_path}$/ }
80
+ end
81
+
82
+ # Returns whether a path should be filtered or not.
83
+ #
84
+ # @param [String] path the path to test.
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ def filtered?(path)
89
+ @filters.empty? || @filters.any? { |filter| path =~ filter }
90
+ end
91
+
92
+ # Finds the paths that should be stored and adds them
93
+ # to the paths' hash.
94
+ #
95
+ def build
96
+ @paths = Hash.new { |h, k| h[k] = Hash.new }
97
+ important_paths { |path| insert_path(path) }
98
+ @updated_at = Time.now.to_i
99
+ end
100
+
101
+ # Detects changes in the passed directories, updates
102
+ # the record with the new changes and returns the changes
103
+ #
104
+ # @param [Array] directories the list of directories scan for changes
105
+ # @param [Hash] options
106
+ # @option options [Boolean] recursive scan all sub-directories recursively
107
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
108
+ #
109
+ # @return [Hash<Array>] the changes
110
+ #
111
+ def fetch_changes(directories, options = {})
112
+ @changes = { :modified => [], :added => [], :removed => [] }
113
+ directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
114
+ directories.each do |directory|
115
+ next unless directory[@directory] # Path is or inside directory
116
+ detect_modifications_and_removals(directory, options)
117
+ detect_additions(directory, options)
118
+ end
119
+ @updated_at = Time.now.to_i
120
+ @changes
121
+ end
122
+
123
+ private
124
+
125
+ # Detects modifications and removals recursively in a directory.
126
+ #
127
+ # @note Modifications detection begins by checking the modification time (mtime)
128
+ # of files and then by checking content changes (using SHA1-checksum)
129
+ # when the mtime of files is not changed.
130
+ #
131
+ # @param [String] directory the path to analyze
132
+ # @param [Hash] options
133
+ # @option options [Boolean] recursive scan all sub-directories recursively
134
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
135
+ #
136
+ def detect_modifications_and_removals(directory, options = {})
137
+ @paths[directory].each do |basename, type|
138
+ path = File.join(directory, basename)
139
+
140
+ case type
141
+ when 'Dir'
142
+ if File.directory?(path)
143
+ detect_modifications_and_removals(path, options) if options[:recursive]
144
+ else
145
+ detect_modifications_and_removals(path, { :recursive => true }.merge(options))
146
+ @paths[directory].delete(basename)
147
+ @paths.delete("#{directory}/#{basename}")
148
+ end
149
+ when 'File'
150
+ if File.exist?(path)
151
+ new_mtime = File.mtime(path).to_i
152
+ if @updated_at < new_mtime || (@updated_at == new_mtime && content_modified?(path))
153
+ @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
154
+ end
155
+ else
156
+ @paths[directory].delete(basename)
157
+ @sha1_checksums.delete(path)
158
+ @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Detects additions in a directory.
165
+ #
166
+ # @param [String] directory the path to analyze
167
+ # @param [Hash] options
168
+ # @option options [Boolean] recursive scan all sub-directories recursively
169
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
170
+ #
171
+ def detect_additions(directory, options = {})
172
+ Find.find(directory) do |path|
173
+ next if path == @directory
174
+
175
+ if File.directory?(path)
176
+ if ignored?(path) || (directory != path && (!options[:recursive] && existing_path?(path)))
177
+ Find.prune # Don't look any further into this directory.
178
+ else
179
+ insert_path(path)
180
+ end
181
+ elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
182
+ if File.file?(path)
183
+ @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
184
+ end
185
+ insert_path(path)
186
+ end
187
+ end
188
+ end
189
+
190
+ # Returns whether or not a file's content has been modified by
191
+ # comparing the SHA1-checksum to a stored one.
192
+ #
193
+ # @param [String] path the file path
194
+ #
195
+ def content_modified?(path)
196
+ sha1_checksum = Digest::SHA1.file(path).to_s
197
+ if @sha1_checksums[path] != sha1_checksum
198
+ @sha1_checksums[path] = sha1_checksum
199
+ true
200
+ else
201
+ false
202
+ end
203
+ end
204
+
205
+ # Traverses the base directory looking for paths that should
206
+ # be stored; thus paths that are filters or not ignored.
207
+ #
208
+ # @yield [path] an important path
209
+ #
210
+ def important_paths
211
+ Find.find(@directory) do |path|
212
+ next if path == @directory
213
+
214
+ if File.directory?(path)
215
+ if ignored?(path)
216
+ Find.prune # Don't look any further into this directory.
217
+ else
218
+ yield(path)
219
+ end
220
+ elsif !ignored?(path) && filtered?(path)
221
+ yield(path)
222
+ end
223
+ end
224
+ end
225
+
226
+ # Inserts a path with its type (Dir or File) in paths hash.
227
+ #
228
+ # @param [String] path the path to insert in @paths.
229
+ #
230
+ def insert_path(path)
231
+ @paths[File.dirname(path)][File.basename(path)] = File.directory?(path) ? 'Dir' : 'File'
232
+ end
233
+
234
+ # Returns whether or not a path exists in the paths hash.
235
+ #
236
+ # @param [String] path the path to check
237
+ #
238
+ # @return [Boolean]
239
+ #
240
+ def existing_path?(path)
241
+ @paths[File.dirname(path)][File.basename(path)] != nil
242
+ end
243
+
244
+ # Converts an absolute path to a path that's relative to the base directory.
245
+ #
246
+ # @param [String] path the path to convert
247
+ #
248
+ # @return [String] the relative path
249
+ #
250
+ def relative_to_base(path)
251
+ path.sub(%r(^#{@directory}/?/), '')
252
+ end
253
+ end
254
+ end