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,72 @@
1
+ # An +Event+ structure contains the _type_ of the event and the file _path_
2
+ # to which the event pertains. The type can be one of the following:
3
+ #
4
+ # :added => file has been added to the directory
5
+ # :modified => file has been modified (either mtime or size or both
6
+ # have changed)
7
+ # :removed => file has been removed from the directory
8
+ # :stable => file has stabilized since being added or modified
9
+ #
10
+ class DirectoryWatcher::Event
11
+
12
+ attr_reader :type
13
+ attr_reader :path
14
+ attr_reader :stat
15
+
16
+ # Create one of the 4 types of events given the two stats
17
+ #
18
+ # The rules are:
19
+ #
20
+ # :added => old_stat will be nil and new_stat will exist
21
+ # :removed => old_stat will exist and new_stat will be nil
22
+ # :modified => old_stat != new_stat
23
+ # :stable => old_stat == new_stat and
24
+ #
25
+ def self.from_stats( old_stat, new_stat )
26
+ if old_stat != new_stat then
27
+ return DirectoryWatcher::Event.new( :removed, new_stat.path ) if new_stat.removed?
28
+ return DirectoryWatcher::Event.new( :added, new_stat.path, new_stat ) if old_stat.nil?
29
+ return DirectoryWatcher::Event.new( :modified, new_stat.path, new_stat )
30
+ else
31
+ return DirectoryWatcher::Event.new( :stable, new_stat.path, new_stat )
32
+ end
33
+ end
34
+
35
+ # Create a new Event with one of the 4 types and the path of the file.
36
+ #
37
+ def initialize( type, path, stat = nil )
38
+ @type = type
39
+ @path = path
40
+ @stat = stat
41
+ end
42
+
43
+ # Is the event a modified event.
44
+ #
45
+ def modified?
46
+ type == :modified
47
+ end
48
+
49
+ # Is the event an added event.
50
+ #
51
+ def added?
52
+ type == :added
53
+ end
54
+
55
+ # Is the event a removed event.
56
+ #
57
+ def removed?
58
+ type == :removed
59
+ end
60
+
61
+ # Is the event a stable event.
62
+ #
63
+ def stable?
64
+ type == :stable
65
+ end
66
+
67
+ # Convert the Event to a nice string format
68
+ #
69
+ def to_s( )
70
+ "<#{self.class} type: #{type} path: '#{path}'>"
71
+ end
72
+ end
@@ -0,0 +1,242 @@
1
+ # An Eventable Scanner is one that can be utilized by something that has an
2
+ # Event Loop. It is intended to be subclassed by classes that implement the
3
+ # specific event loop semantics for say EventMachine or Cool.io.
4
+ #
5
+ # The Events that the EventableScanner is programmed for are:
6
+ #
7
+ # on_scan - this should be called every +interval+ times
8
+ # on_modified - If the event loop can monitor individual files then this should
9
+ # be called when the file is modified
10
+ # on_removed - Similar to on_modified but called when a file is removed.
11
+ #
12
+ # Sub classes are required to implement the following:
13
+ #
14
+ # start_loop_with_attached_scan_timer() - Instance Method
15
+ # This method is to start up the loop, if necessary assign to @loop_thread
16
+ # instance variable the Thread that is controlling the event loop.
17
+ #
18
+ # This method must also assign an object to @timer which is what does the
19
+ # periodic scanning of the globs. This object must respond to +detach()+ so
20
+ # that it may be detached from the event loop.
21
+ #
22
+ # stop_loop() - Instance Method
23
+ # This method must shut down the event loop, or detach these classes from
24
+ # the event loop if we just attached to an existing event loop.
25
+ #
26
+ # Watcher - An Embedded class
27
+ # This is a class that must have a class method +watcher(path,scanner)+
28
+ # which is used to instantiate a file watcher. The Watcher instance must
29
+ # respond to +detach()+ so that it may be independently detached from the
30
+ # event loop.
31
+ #
32
+ class DirectoryWatcher::EventableScanner
33
+ include DirectoryWatcher::Logable
34
+
35
+ # A Hash of Watcher objects.
36
+ attr_reader :watchers
37
+
38
+ # call-seq:
39
+ # EventableScanner.new( config )
40
+ #
41
+ # config - the Configuration instances
42
+ #
43
+ def initialize( config )
44
+ @config = config
45
+ @scan_and_queue = DirectoryWatcher::ScanAndQueue.new(config.glob, config.collection_queue)
46
+ @watchers = {}
47
+ @stopping = false
48
+ @timer = nil
49
+ @loop_thread = nil
50
+ @paused = false
51
+ end
52
+
53
+ # The queue on which to put FileStat and Scan items.
54
+ #
55
+ def collection_queue
56
+ @config.collection_queue
57
+ end
58
+
59
+ # The interval at which to scan
60
+ #
61
+ def interval
62
+ @config.interval
63
+ end
64
+
65
+ # Returns +true+ if the scanner is currently running. Returns +false+ if
66
+ # this is not the case.
67
+ #
68
+ def running?
69
+ return !@stopping if @timer
70
+ return false
71
+ end
72
+
73
+ # Start up the scanner. If the scanner is already running, nothing happens.
74
+ #
75
+ def start
76
+ return if running?
77
+ logger.debug "starting scanner"
78
+ start_loop_with_attached_scan_timer
79
+ end
80
+
81
+ # Stop the scanner. If the scanner is not running, nothing happens.
82
+ #
83
+ def stop
84
+ return unless running?
85
+ logger.debug "stoping scanner"
86
+ @stopping = true
87
+ teardown_timer_and_watches
88
+ @stopping = false
89
+ stop_loop
90
+ end
91
+
92
+ # Pause the scanner.
93
+ #
94
+ # Pausing the scanner does not stop the scanning per se, it stops items from
95
+ # being sent to the collection queue
96
+ #
97
+ def pause
98
+ logger.debug "pausing scanner"
99
+ @paused = true
100
+ end
101
+
102
+ # Resume the scanner.
103
+ #
104
+ # This removes the blockage on sending items to the collection queue.
105
+ #
106
+ def resume
107
+ logger.debug "resuming scanner"
108
+ @paused = false
109
+ end
110
+
111
+ # Is the Scanner currently paused.
112
+ #
113
+ def paused?
114
+ @paused
115
+ end
116
+
117
+ # EventableScanners do not join
118
+ #
119
+ def join( limit = nil )
120
+ end
121
+
122
+ # Do a single scan and send those items to the collection queue.
123
+ #
124
+ def run
125
+ logger.debug "running scan and queue"
126
+ @scan_and_queue.scan_and_queue
127
+ end
128
+
129
+ # Setting maximum iterations means hooking into the periodic timer event and
130
+ # counting the number of times it is going on. This also resets the current
131
+ # iterations count
132
+ #
133
+ def maximum_iterations=(value)
134
+ unless value.nil?
135
+ value = Integer(value)
136
+ raise ArgumentError, "maximum iterations must be >= 1" unless value >= 1
137
+ end
138
+ @iterations = 0
139
+ @maximum_iterations = value
140
+ end
141
+ attr_reader :maximum_iterations
142
+ attr_reader :iterations
143
+
144
+ # Have we completed up to the maximum_iterations?
145
+ #
146
+ def finished_iterations?
147
+ self.iterations >= self.maximum_iterations
148
+ end
149
+
150
+ # This callback is invoked by the Timer instance when it is triggered by
151
+ # the Loop. This method will check for added files and stable files
152
+ # and notify the directory watcher accordingly.
153
+ #
154
+ def on_scan
155
+ logger.debug "on_scan called"
156
+ scan_and_watch_files
157
+ progress_towards_maximum_iterations
158
+ end
159
+
160
+ # This callback is invoked by the Watcher instance when it is triggered by the
161
+ # loop for file modifications.
162
+ #
163
+ def on_modified(watcher, new_stat)
164
+ logger.debug "on_modified called"
165
+ queue_item(new_stat)
166
+ end
167
+
168
+ # This callback is invoked by the Watcher instance when it is triggered by the
169
+ # loop for file removals
170
+ #
171
+ def on_removed(watcher, new_stat)
172
+ logger.debug "on_removed called"
173
+ unwatch_file(watcher.path)
174
+ queue_item(new_stat)
175
+ end
176
+
177
+ #######
178
+ private
179
+ #######
180
+
181
+ # Send the given item to the collection queue
182
+ #
183
+ def queue_item( item )
184
+ if paused? then
185
+ logger.debug "Not queueing item, we're paused"
186
+ else
187
+ logger.debug "enqueuing #{item} to #{collection_queue}"
188
+ collection_queue.enq item
189
+ end
190
+ end
191
+
192
+
193
+ # Run a single scan and turn on watches for all the files found in that scan
194
+ # that do not already have watchers on them.
195
+ #
196
+ def scan_and_watch_files
197
+ logger.debug "scanning and watching files"
198
+ scan = @scan_and_queue.scan_and_queue
199
+ scan.results.each do |stat|
200
+ watch_file(stat.path)
201
+ end
202
+ end
203
+
204
+ # Remove the timer and the watches from the event loop
205
+ #
206
+ def teardown_timer_and_watches
207
+ @timer.detach rescue nil
208
+ @timer = nil
209
+
210
+ @watchers.each_value {|w| w.detach}
211
+ @watchers.clear
212
+ end
213
+
214
+ # Create and return a new Watcher instance for the given filename _fn_.
215
+ # A watcher will only be created once for a particular fn.
216
+ #
217
+ def watch_file( fn )
218
+ unless @watchers[fn] then
219
+ logger.debug "Watching file #{fn}"
220
+ w = self.class::Watcher.watch(fn, self)
221
+ @watchers[fn] = w
222
+ end
223
+ end
224
+
225
+ # Remove the watcher instance from our tracking
226
+ #
227
+ def unwatch_file( fn )
228
+ logger.debug "Unwatching file #{fn}"
229
+ @watchers.delete(fn)
230
+ end
231
+
232
+ # Make progress towards maximum iterations. And if we get there, then stop
233
+ # monitoring files.
234
+ #
235
+ def progress_towards_maximum_iterations
236
+ if maximum_iterations then
237
+ @iterations += 1
238
+ stop if finished_iterations?
239
+ end
240
+ end
241
+ # :startdoc:
242
+ end # class DirectoryWatcher::Eventablecanner
@@ -0,0 +1,65 @@
1
+ # FileStat contains file system information about a single file including:
2
+ #
3
+ # path - The fully expanded path of the file
4
+ # mtime - The last modified time of the file, as a Time object
5
+ # size - The size of the file, in bytes.
6
+ #
7
+ # The FileStat object can also say if the file is removed of not.
8
+ #
9
+ class DirectoryWatcher::FileStat
10
+
11
+ # The fully expanded path of the file
12
+ attr_reader :path
13
+
14
+ # The last modified time of the file
15
+ attr_accessor :mtime
16
+
17
+ # The size of the file in bytes
18
+ attr_accessor :size
19
+
20
+ # Create an instance of FileStat that will make sure that the instance method
21
+ # +removed?+ returns true when called on it.
22
+ #
23
+ def self.for_removed_path( path )
24
+ ::DirectoryWatcher::FileStat.new(path, nil, nil)
25
+ end
26
+
27
+ # Create a new instance of FileStat with the given path, mtime and size
28
+ #
29
+ def initialize( path, mtime, size )
30
+ @path = path
31
+ @mtime = mtime
32
+ @size = size
33
+ end
34
+
35
+ # Is the file represented by this FileStat to be considered removed?
36
+ #
37
+ # FileStat doesn't actually go to the file system and check, it assumes if the
38
+ # FileStat was initialized with a nil mtime or a nil size then that data
39
+ # wasn't available, and therefore must indicate that the file is no longer in
40
+ # existence.
41
+ #
42
+ def removed?
43
+ @mtime.nil? || @size.nil?
44
+ end
45
+
46
+ # Compare this FileStat to another object.
47
+ #
48
+ # This will only return true when all of the following are true:
49
+ #
50
+ # 1) The other object is also a FileStat object
51
+ # 2) The other object's mtime is equal to this mtime
52
+ # 3) The other object's msize is equal to this size
53
+ #
54
+ def eql?( other )
55
+ return false unless other.instance_of? self.class
56
+ self.mtime == other.mtime and self.size == other.size
57
+ end
58
+ alias :== :eql?
59
+
60
+ # Create a nice string based representation of this instance.
61
+ #
62
+ def to_s
63
+ "<#{self.class.name} path: #{path} mtime: #{mtime} size: #{size}>"
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ class DirectoryWatcher
2
+
3
+ # This is the implementation of a logger that does nothing.
4
+ # It has all the debug, info, warn, error, fatal methods, but they do nothing
5
+ class NullLogger
6
+ def debug( msg ); end
7
+ def info( msg ); end
8
+ def warn( msg ); end
9
+ def error( msg ); end
10
+ def fatal( msg ); end
11
+ end
12
+
13
+ module Logable
14
+ def logger
15
+ @config.logger
16
+ end
17
+
18
+ def self.default_logger
19
+ require 'logging'
20
+ Logging::Logger[DirectoryWatcher]
21
+ rescue LoadError
22
+ NullLogger.new
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,49 @@
1
+ # A Notifier pull Event instances from the give queue and sends them to all of
2
+ # the Observers it knows about.
3
+ #
4
+ class DirectoryWatcher::Notifier
5
+ include DirectoryWatcher::Threaded
6
+ include DirectoryWatcher::Logable
7
+
8
+ # Create a new Notifier that pulls events off the given notification_queue from the
9
+ # config, and sends them to the listed observers.
10
+ #
11
+ def initialize( config, observers )
12
+ @config = config
13
+ @observers = observers
14
+ self.interval = 0.01 # yes this is a fast loop
15
+ end
16
+
17
+ # Notify all the observers of all the available events in the queue.
18
+ # If there are 2 or more events in a row that are the same, then they are
19
+ # collapsed into a single event.
20
+ #
21
+ def run
22
+ previous_event = nil
23
+ until queue.empty? do
24
+ event = queue.deq
25
+ next if previous_event == event
26
+ @observers.each do |observer, func|
27
+ send_event_to_observer( observer, func, event )
28
+ end
29
+ previous_event = event
30
+ end
31
+ end
32
+
33
+ #######
34
+ private
35
+ #######
36
+
37
+ def queue
38
+ @config.notification_queue
39
+ end
40
+
41
+ # Send the given event to the given observer using the given function.
42
+ #
43
+ # Capture any exceptions that have, swallow them and send them to stderr.
44
+ def send_event_to_observer( observer, func, event )
45
+ observer.send(func, event)
46
+ rescue Exception => e
47
+ $stderr.puts "Called #{observer}##{func}(#{event}) and all I got was this lousy exception #{e}"
48
+ end
49
+ end