directory_watcher 1.4.1 → 1.5.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.
@@ -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
+