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.
- data/.gitignore +17 -0
- data/History.txt +10 -0
- data/README.txt +1 -1
- data/Rakefile +16 -5
- data/lib/directory_watcher.rb +166 -139
- data/lib/directory_watcher/collector.rb +283 -0
- data/lib/directory_watcher/configuration.rb +228 -0
- data/lib/directory_watcher/coolio_scanner.rb +61 -127
- data/lib/directory_watcher/em_scanner.rb +81 -153
- data/lib/directory_watcher/event.rb +72 -0
- data/lib/directory_watcher/eventable_scanner.rb +242 -0
- data/lib/directory_watcher/file_stat.rb +65 -0
- data/lib/directory_watcher/logable.rb +26 -0
- data/lib/directory_watcher/notifier.rb +49 -0
- data/lib/directory_watcher/paths.rb +55 -0
- data/lib/directory_watcher/rev_scanner.rb +68 -131
- data/lib/directory_watcher/scan.rb +72 -0
- data/lib/directory_watcher/scan_and_queue.rb +22 -0
- data/lib/directory_watcher/scanner.rb +26 -209
- data/lib/directory_watcher/threaded.rb +277 -0
- data/lib/directory_watcher/version.rb +8 -0
- data/spec/directory_watcher_spec.rb +37 -0
- data/spec/paths_spec.rb +7 -0
- data/spec/scanner_scenarios.rb +236 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/utility_classes.rb +117 -0
- data/version.txt +1 -1
- metadata +123 -23
- data/bin/dw +0 -2
@@ -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
|