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,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