directory_watcher 1.4.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,283 @@
1
+ # Collector reads items from a collection Queue and processes them to see if
2
+ # FileEvents should be put onto the notification Queue.
3
+ #
4
+ class DirectoryWatcher::Collector
5
+ include DirectoryWatcher::Threaded
6
+ include DirectoryWatcher::Logable
7
+
8
+ # Create a new StatCollector from the given Configuration, and an optional
9
+ # Scan.
10
+ #
11
+ # configuration - The Collector uses from Configuration:
12
+ # collection_queue - The Queue to read items from the Scanner on
13
+ # notification_queue - The Queue to submit the Events to the Notifier on
14
+ # stable - The number of times we see a file hasn't changed before
15
+ # emitting a stable event
16
+ # sort_by - the method used to sort events during on_scan results
17
+ # order_by - The method used to order events from call to on_scan
18
+ #
19
+ # pre_load_scan - A Scan to use to load our internal state from before. No
20
+ # events will be emitted for the FileStat's in this scan.
21
+ #
22
+ #def initialize( notification_queue, collection_queue, options = {} )
23
+ def initialize( config )
24
+ @stats = Hash.new
25
+ @stable_counts = Hash.new
26
+ @config = config
27
+ on_scan( DirectoryWatcher::Scan.new( config.glob ), false ) if config.pre_load?
28
+ self.interval = 0.01 # yes this is a fast loop
29
+ end
30
+
31
+ # The number of times we see a file hasn't changed before emitting a stable
32
+ # count. See Configuration#stable
33
+ def stable_threshold
34
+ @config.stable
35
+ end
36
+
37
+ # How to sort Scan results. See Configuration.
38
+ #
39
+ def sort_by
40
+ @config.sort_by
41
+ end
42
+
43
+ # How to order Scan results. See Configuration.
44
+ #
45
+ def order_by
46
+ @config.order_by
47
+ end
48
+
49
+ # The queue from which to read items from the scanners. See Configuration.
50
+ #
51
+ def collection_queue
52
+ @config.collection_queue
53
+ end
54
+
55
+ # The queue to write Events for the Notifier. See Configuration.
56
+ #
57
+ def notification_queue
58
+ @config.notification_queue
59
+ end
60
+
61
+ # Given the scan, update the set of stats with the results from the Scan and
62
+ # emit events to the notification queue as appropriate.
63
+ #
64
+ # scan - The Scan containing all the new FileStat items
65
+ # emit_events - Should events be emitted for the events in the scan
66
+ # (default: true)
67
+ #
68
+ # There is one odd thing that happens here. Scanners that are EventableScanners
69
+ # use on_stat to emit removed events, and the standard threaded Scanner only
70
+ # uses Scans. So we make sure and only emit removed events in this method if
71
+ # the scanner that gave us the scan was the basic threaded Scanner.
72
+ #
73
+ # TODO: Possibly fix this through another abstraction in the Scanners.
74
+ # No idea about what that would be yet.
75
+ #
76
+ # Returns nothing.
77
+ #
78
+ def on_scan( scan, emit_events = true )
79
+ seen_paths = Set.new
80
+ logger.debug "Sorting by #{sort_by} #{order_by}"
81
+ sorted_stats( scan.run ).each do |stat|
82
+ on_stat(stat, emit_events)
83
+ seen_paths << stat.path
84
+ end
85
+ emit_removed_events(seen_paths) if @config.scanner.nil?
86
+ end
87
+
88
+ # Process a single stat and emit an event if necessary.
89
+ #
90
+ # stat - The new FileStat to process and see if an event should
91
+ # be emitted
92
+ # emit_event - Whether or not an event should be emitted.
93
+ #
94
+ # Returns nothing
95
+ def on_stat( stat, emit_event = true )
96
+ orig_stat = update_stat( stat )
97
+ logger.debug "Emitting event for on_stat #{stat}"
98
+ emit_event_for( orig_stat, stat ) if emit_event
99
+ end
100
+
101
+ # Remove one item from the collection queue and process it.
102
+ #
103
+ # This method is required by the Threaded API
104
+ #
105
+ # Returns nothing
106
+ def run
107
+ case thing = collection_queue.deq
108
+ when ::DirectoryWatcher::Scan
109
+ on_scan(thing)
110
+ when ::DirectoryWatcher::FileStat
111
+ on_stat(thing)
112
+ else
113
+ raise "Unknown item in the queue: #{thing}"
114
+ end
115
+ end
116
+
117
+ # Write the current stats to the given IO object as a YAML document.
118
+ #
119
+ # io - The IO object to write the document to.
120
+ #
121
+ # Returns nothing.
122
+ def dump_stats( io )
123
+ YAML.dump(@stats, io)
124
+ end
125
+
126
+ # Read the current stats from the given IO object. Any existing stats in the
127
+ # Collector will be overwritten
128
+ #
129
+ # io - The IO object from which to read the document.
130
+ #
131
+ # Returns nothing.
132
+ def load_stats( io )
133
+ @stats = YAML.load(io)
134
+ end
135
+
136
+ #######
137
+ private
138
+ #######
139
+
140
+ # Sort the stats by +sort_by+ and +order_by+ returning the results
141
+ #
142
+ def sorted_stats( stats )
143
+ sorted = stats.sort_by{ |stat| stat.send(sort_by) }
144
+ sorted = sorted.reverse if order_by == :descending
145
+ return sorted
146
+ end
147
+
148
+ # Update the stats Hash with the new_stat information, return the old data
149
+ # that is being replaced.
150
+ #
151
+ def update_stat( new_stat )
152
+ old_stat = @stats.delete(new_stat.path)
153
+ @stats.store(new_stat.path, new_stat) unless new_stat.removed?
154
+ return old_stat
155
+ end
156
+
157
+ # Look for removed files and emit removed events for all of them.
158
+ #
159
+ # seen_paths - the list of files that we know currently exist
160
+ #
161
+ # Return nothing
162
+ def emit_removed_events( seen_paths )
163
+ @stats.keys.each do |existing_path|
164
+ next if seen_paths.include?(existing_path)
165
+ old_stat = @stats.delete(existing_path)
166
+ emit_event_for(old_stat, ::DirectoryWatcher::FileStat.for_removed_path(existing_path))
167
+ end
168
+ end
169
+
170
+ # Determine what type of event to emit, and put that event onto the
171
+ # notification queue.
172
+ #
173
+ # old_stat - The old FileStat
174
+ # new_stat - The new FileStat
175
+ #
176
+ # Returns nothing
177
+ def emit_event_for( old_stat, new_stat )
178
+ event = DirectoryWatcher::Event.from_stats( old_stat, new_stat )
179
+ if should_emit?(event) then
180
+ logger.debug "Sending event #{event.object_id} to notifcation queue"
181
+ notification_queue.enq( event )
182
+ else
183
+ logger.debug "Emitting of event #{event.object_id} cancelled"
184
+ end
185
+ end
186
+
187
+ # Should the event given actually be emitted.
188
+ #
189
+ # If the event passed in is NOT a stable event, return true
190
+ # If there is a stable_threshold, then check to see if the stable count for
191
+ # this event's path has crossed the stable threshold.
192
+ #
193
+ # This method has the side effect of updating the stable count of the path of
194
+ # the event. If we are going to return true for the stable event, then we
195
+ # reset the stable count of that event to 0.
196
+ #
197
+ # event - any event
198
+ #
199
+ # Returns whether or not to emit the event based upon its stability
200
+ def should_emit?( event )
201
+ if event.stable? then
202
+ if emitting_stable_events? and valid_for_stable_event?( event.path )then
203
+ increment_stable_count( event.path )
204
+ if should_emit_stable?( event.path ) then
205
+ mark_as_invalid_for_stable_event( event.path )
206
+ return true
207
+ end
208
+ end
209
+ return false
210
+ elsif event.removed? then
211
+ mark_as_invalid_for_stable_event( event.path )
212
+ return true
213
+ else
214
+ mark_as_valid_for_stable_event( event.path )
215
+ return true
216
+ end
217
+ end
218
+
219
+ # Is the given path able to have a stable event emitted for it?
220
+ #
221
+ # A stable event may only be emitted for a path that has already had an added
222
+ # or modified event already sent. Also, once a stable event has been emitted
223
+ # for a path, another stable event may not be emitted until it has been
224
+ # modified, or added again.
225
+ #
226
+ # path - the path of the file to check
227
+ #
228
+ # Returns whether or not the path may have a stable event emitted for it.
229
+ def valid_for_stable_event?( path )
230
+ @stable_counts.has_key?( path )
231
+ end
232
+
233
+ # Let it be known that the given path can now have a stable event emitted for
234
+ # it.
235
+ #
236
+ # path - the path to mark as ready
237
+ #
238
+ # Returns nothing
239
+ def mark_as_valid_for_stable_event( path )
240
+ logger.debug "#{path} marked as valid for stable"
241
+ @stable_counts[path] = 0
242
+ end
243
+
244
+ # Mark that the given path is invalid for having a stable event emitted for
245
+ # it.
246
+ #
247
+ # path - the path to mark
248
+ #
249
+ # Returns nothing
250
+ def mark_as_invalid_for_stable_event( path )
251
+ logger.debug "#{path} marked as invalid for stable"
252
+ @stable_counts.delete( path )
253
+ end
254
+
255
+ # Increment the stable count for the given path
256
+ #
257
+ # path - the path of the file to increment its stable count
258
+ #
259
+ # Returns nothing
260
+ def increment_stable_count( path )
261
+ @stable_counts[path] += 1
262
+ end
263
+
264
+ # Is the given path ready to have a stable event emitted?
265
+ #
266
+ # path - the path to report on
267
+ #
268
+ # Returns whether to emit a stable event or not
269
+ def should_emit_stable?( path )
270
+ @stable_counts[path] >= stable_threshold
271
+ end
272
+
273
+ # Is it legal for us to emit stable events at all. This checks the config to
274
+ # see if that is the case.
275
+ #
276
+ # In the @config if the stable threshold is set then we are emitting stable
277
+ # events.
278
+ #
279
+ # Returns whether it is legal to propogate stable events
280
+ def emitting_stable_events?
281
+ stable_threshold
282
+ end
283
+ end
@@ -0,0 +1,228 @@
1
+ #
2
+ # The top level configuration options used by DirectoryWatcher are used by many
3
+ # of the sub components for a variety of purposes. The Configuration represents
4
+ # all those options and other global like instances.
5
+ #
6
+ # The top level DirectoryWatcher class allows the configs to be changed during
7
+ # execution, so all of the dependent classes need to be informed when their
8
+ # options have changed. This class allows that.
9
+ #
10
+ class DirectoryWatcher::Configuration
11
+ # The directory to monitor for events. The glob's will be used in conjunction
12
+ # with this directory to find the full list of globs available.
13
+ attr_reader :dir
14
+
15
+ # The glob of files to monitor. This is an Array of file matching globs
16
+ # be aware that changing the :glob value after watching has started has the
17
+ # potential to cause spurious events if the new globs do not match the old,
18
+ # files will appear to have been deleted.
19
+ #
20
+ # The default is '*'
21
+ attr_reader :glob
22
+
23
+ # The interval at which to do a full scan using the +glob+ to determine Events
24
+ # to send.
25
+ #
26
+ # The default is 30.0 seconds
27
+ attr_reader :interval
28
+
29
+ # Controls the number of intervals a file must remain unchanged before it is
30
+ # considered "stable". When this condition is met, a stable event is
31
+ # generated for the file. If stable is set to +nil+ then stable events
32
+ # will not be generated.
33
+ #
34
+ # The default is nil, indicating no stable events are to be emitted.
35
+ attr_reader :stable
36
+
37
+ # pre_load says if an initial scan using the globs should be done to pre
38
+ # populate the state of the system before sending any events.
39
+ #
40
+ # The default is false
41
+ attr_reader :pre_load
42
+
43
+ # The filename to persist the state of the DirectoryWatcher too upon calling
44
+ # *stop*.
45
+ #
46
+ # The default is nil, indicating that no state is to be persisted.
47
+ attr_reader :persist
48
+
49
+ # The back end scanner to use. The available options are:
50
+ #
51
+ # nil => Use the default, pure ruby Threaded scanner
52
+ # :em => Use the EventMachine based scanner. This requires that the
53
+ # 'eventmachine' gem be installed.
54
+ # :coolio => Use the Cool.io based scanner. This requires that the
55
+ # 'cool.io' gem be installed.
56
+ # :rev => Use the Rev based scanner. This requires that the 'rev' gem be
57
+ # installed.
58
+ #
59
+ # The default is nil, indicating the pure ruby threaded scanner will be used.
60
+ # This option may not be changed once the DirectoryWatcher is allocated.
61
+ #
62
+ attr_reader :scanner
63
+
64
+ # The sorting method to use when emitting a set of Events after a Scan has
65
+ # happened. Since a Scan may produce a number of events, if those Events should
66
+ # be emitted in a particular order, use +sort_by+ to pick which field to sort
67
+ # the events, and +order_by+ to say if those events are to be emitted in
68
+ # :ascending or :descending order.
69
+ #
70
+ # Available options:
71
+ #
72
+ # :path => The default, they will be sorted by full pathname
73
+ # :mtime => Last modified time. They will be sorted by their FileStat mtime
74
+ # :size => The number of bytes in the file.
75
+ #
76
+ attr_accessor :sort_by
77
+
78
+ # When sorting you may pick if the order should be:
79
+ #
80
+ # :ascending => The default, from lowest to highest
81
+ # :descending => from highest to lowest.
82
+ #
83
+ attr_accessor :order_by
84
+
85
+ # The Queue through which the Scanner will send data to the Collector
86
+ #
87
+ attr_reader :collection_queue
88
+
89
+ # The Queue through which the Collector will send data to the Notifier
90
+ #
91
+ attr_reader :notification_queue
92
+
93
+ # The logger through wich every one will log
94
+ #
95
+ attr_reader :logger
96
+
97
+ # Return a Hash of all the default options
98
+ #
99
+ def self.default_options
100
+ {
101
+ :dir => '.',
102
+ :glob => '*',
103
+ :interval => 30.0,
104
+ :stable => nil,
105
+ :pre_load => false,
106
+ :persist => nil,
107
+ :scanner => nil,
108
+ :sort_by => :path,
109
+ :order_by => :ascending,
110
+ :logger => nil,
111
+ }
112
+ end
113
+
114
+ # Create a new Configuration by blending the passed in items with the defaults
115
+ #
116
+ def initialize( options = {} )
117
+ o = self.class.default_options.merge( options )
118
+ @dir = o[:dir]
119
+ @pre_load = o[:pre_load]
120
+ @scanner = o[:scanner]
121
+ @sort_by = o[:sort_by]
122
+ @order_by = o[:order_by]
123
+
124
+ # These have validation rules
125
+ self.persist = o[:persist]
126
+ self.interval = o[:interval]
127
+ self.glob = o[:glob]
128
+ self.stable = o[:stable]
129
+ self.logger = o[:logger]
130
+
131
+ @notification_queue = Queue.new
132
+ @collection_queue = Queue.new
133
+ end
134
+
135
+ # Is pre_load set or not
136
+ #
137
+ def pre_load?
138
+ @pre_load
139
+ end
140
+
141
+ # The class of the scanner
142
+ #
143
+ def scanner_class
144
+ class_name = scanner.to_s.capitalize + 'Scanner'
145
+ DirectoryWatcher.const_get( class_name ) rescue DirectoryWatcher::Scanner
146
+ end
147
+
148
+ # call-seq:
149
+ # glob = '*'
150
+ # glob = ['lib/**/*.rb', 'test/**/*.rb']
151
+ #
152
+ # Sets the glob pattern that will be used when scanning the directory for
153
+ # files. A single glob pattern can be given or an array of glob patterns.
154
+ #
155
+ def glob=( val )
156
+ glob = case val
157
+ when String; [File.join(@dir, val)]
158
+ when Array; val.flatten.map! {|g| File.join(@dir, g)}
159
+ else
160
+ raise(ArgumentError,
161
+ 'expecting a glob pattern or an array of glob patterns')
162
+ end
163
+ glob.uniq!
164
+ @glob = glob
165
+ end
166
+
167
+ # Sets the directory scan interval. The directory will be scanned every
168
+ # _interval_ seconds for changes to files matching the glob pattern.
169
+ # Raises +ArgumentError+ if the interval is zero or negative.
170
+ #
171
+ def interval=( val )
172
+ val = Float(val)
173
+ raise ArgumentError, "interval must be greater than zero" if val <= 0
174
+ @interval = val
175
+ end
176
+
177
+ # Sets the logger instance. This will be used by all classes for logging
178
+ #
179
+ def logger=( val )
180
+ if val then
181
+ if %w[ debug info warn error fatal ].all? { |meth| val.respond_to?( meth ) } then
182
+ @logger = val
183
+ end
184
+ else
185
+ @logger = ::DirectoryWatcher::Logable.default_logger
186
+ end
187
+ end
188
+
189
+ # Sets the number of intervals a file must remain unchanged before it is
190
+ # considered "stable". When this condition is met, a stable event is
191
+ # generated for the file. If stable is set to +nil+ then stable events
192
+ # will not be generated.
193
+ #
194
+ # A stable event will be generated once for a file. Another stable event
195
+ # will only be generated after the file has been modified and then remains
196
+ # unchanged for _stable_ intervals.
197
+ #
198
+ # Example:
199
+ #
200
+ # dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' )
201
+ # dw.interval = 15.0
202
+ # dw.stable = 4
203
+ #
204
+ # In this example, a directory watcher is configured to look for swap files
205
+ # in the /tmp directory. Stable events will be generated every 4 scan
206
+ # intervals iff a swap remains unchanged for that time. In this case the
207
+ # time is 60 seconds (15.0 * 4).
208
+ #
209
+ def stable=( val )
210
+ if val.nil?
211
+ @stable = nil
212
+ else
213
+ val = Integer(val)
214
+ raise ArgumentError, "stable must be greater than zero" if val <= 0
215
+ @stable = val
216
+ end
217
+ return @stable
218
+ end
219
+
220
+ # Sets the name of the file to which the directory watcher state will be
221
+ # persisted when it is stopped. Setting the persist filename to +nil+ will
222
+ # disable this feature.
223
+ #
224
+ def persist=( filename )
225
+ @persist = filename ? filename.to_s : nil
226
+ end
227
+ end
228
+