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
@@ -9,174 +9,108 @@ if DirectoryWatcher::HAVE_COOLIO
|
|
9
9
|
|
10
10
|
# The CoolioScanner uses the Coolio loop to monitor changes to files in the
|
11
11
|
# watched directory. This scanner is more efficient than the pure Ruby
|
12
|
-
# scanner because it relies on the operating system kernel
|
12
|
+
# scanner because it relies on the operating system kernel notifications
|
13
13
|
# instead of a periodic polling and stat of every file in the watched
|
14
14
|
# directory (the technique used by the Scanner class).
|
15
15
|
#
|
16
|
-
|
17
|
-
|
16
|
+
# Coolio cannot notify us when a file is added to the watched
|
17
|
+
# directory; therefore, added files are only picked up when we apply the
|
18
|
+
# glob pattern to the directory. This is done at the configured interval.
|
19
|
+
#
|
20
|
+
class DirectoryWatcher::CoolioScanner < DirectoryWatcher::EventableScanner
|
18
21
|
# call-seq:
|
19
|
-
# CoolioScanner.new
|
20
|
-
#
|
21
|
-
# Create a Coolio based scanner that will generate file events and pass
|
22
|
-
# those events (as an array) to the given _block_.
|
22
|
+
# CoolioScanner.new( config )
|
23
23
|
#
|
24
|
-
def initialize(
|
25
|
-
super(
|
26
|
-
@watchers = {}
|
24
|
+
def initialize( config )
|
25
|
+
super(config)
|
27
26
|
end
|
28
27
|
|
29
|
-
#
|
30
|
-
# will
|
28
|
+
# Called by EventablScanner#start to start the loop up and attach the periodic
|
29
|
+
# timer that will poll the globs for new files.
|
31
30
|
#
|
32
|
-
def
|
33
|
-
return if
|
34
|
-
|
35
|
-
@
|
36
|
-
|
37
|
-
|
38
|
-
@files.keys.each do |fn|
|
39
|
-
if test ?e, fn
|
40
|
-
_watch_file fn
|
41
|
-
next
|
42
|
-
end
|
43
|
-
|
44
|
-
@files.delete fn
|
45
|
-
@events << ::DirectoryWatcher::Event.new(:removed, fn)
|
46
|
-
end
|
47
|
-
|
48
|
-
@timer.attach(coolio_loop)
|
49
|
-
coolio_loop.run
|
31
|
+
def start_loop_with_attached_scan_timer
|
32
|
+
return if @loop_thread
|
33
|
+
@timer = ScanTimer.new( self )
|
34
|
+
@loop_thread = Thread.new {
|
35
|
+
@timer.attach(event_loop)
|
36
|
+
event_loop.run
|
50
37
|
}
|
51
38
|
end
|
52
39
|
|
53
|
-
#
|
54
|
-
#
|
40
|
+
# Called by EventableScanner#stop to stop the loop as part of the shutdown
|
41
|
+
# process.
|
55
42
|
#
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
@watchers.each_value {|w| w.detach}
|
63
|
-
@watchers.clear
|
64
|
-
|
65
|
-
notify
|
66
|
-
|
67
|
-
@thread._coolio_loop.stop rescue nil
|
68
|
-
@thread.kill # for some reason the rev loop is not returning after stopping
|
69
|
-
@thread = nil
|
43
|
+
def stop_loop
|
44
|
+
if @loop_thread then
|
45
|
+
event_loop.stop rescue nil
|
46
|
+
@loop_thread.kill
|
47
|
+
@loop_thread = nil
|
48
|
+
end
|
70
49
|
end
|
71
50
|
|
72
|
-
#
|
51
|
+
# Return the cool.io loop object.
|
73
52
|
#
|
74
|
-
# This
|
75
|
-
#
|
76
|
-
# modified or deleted and notifies the directory watcher accordingly.
|
53
|
+
# This is used during the startup, shutdown process and for the Watcher to
|
54
|
+
# attach and detach from the event loop
|
77
55
|
#
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
if stat
|
83
|
-
if @files[fn] != stat
|
84
|
-
@files[fn] = stat
|
85
|
-
@events << ::DirectoryWatcher::Event.new(:modified, fn)
|
86
|
-
end
|
56
|
+
def event_loop
|
57
|
+
if @loop_thread then
|
58
|
+
@loop_thread._coolio_loop
|
87
59
|
else
|
88
|
-
|
89
|
-
@watchers.delete fn
|
90
|
-
@files.delete fn
|
91
|
-
@events << ::DirectoryWatcher::Event.new(:removed, fn)
|
60
|
+
Thread.current._coolio_loop
|
92
61
|
end
|
93
|
-
|
94
|
-
notify
|
95
62
|
end
|
96
63
|
|
97
|
-
#
|
98
|
-
# the Coolio loop. This method will check for added files and stable files
|
99
|
-
# and notify the directory watcher accordingly.
|
100
|
-
#
|
101
|
-
def _on_timer
|
102
|
-
_find_added
|
103
|
-
_find_stable
|
104
|
-
notify
|
105
|
-
end
|
106
|
-
# :startdoc:
|
107
|
-
|
108
|
-
|
109
|
-
private
|
110
|
-
|
111
|
-
# From the list of files in the watched directory, find those that we are
|
112
|
-
# not currently watching and add them to the watch list. Generate "added"
|
113
|
-
# events for those newly found files.
|
64
|
+
# :stopdoc:
|
114
65
|
#
|
115
|
-
|
116
|
-
cur = list_files
|
117
|
-
prev = @files.keys
|
118
|
-
added = cur - prev
|
119
|
-
|
120
|
-
added.each do |fn|
|
121
|
-
@files[fn] = _watch_file(fn).stat
|
122
|
-
@events << ::DirectoryWatcher::Event.new(:added, fn)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Iterate over the FileStat instances looking for those with non-nil
|
127
|
-
# stable counts. Decrement these counts and generate "stable" events for
|
128
|
-
# those files whose count reaches zero.
|
66
|
+
# Watch files using the Coolio StatWatcher.
|
129
67
|
#
|
130
|
-
|
131
|
-
@files.each do |fn, stat|
|
132
|
-
next if stat.stable.nil?
|
133
|
-
stat.stable -= 1
|
134
|
-
if stat.stable <= 0
|
135
|
-
@events << ::DirectoryWatcher::Event.new(:stable, fn)
|
136
|
-
stat.stable = nil
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# Create and return a new Watcher instance for the given filename _fn_.
|
68
|
+
# This class is required by EventableScanner to institute file watching.
|
142
69
|
#
|
143
|
-
|
144
|
-
|
145
|
-
w.attach(@thread ? @thread._coolio_loop : Thread.current._coolio_loop)
|
146
|
-
@watchers[fn] = w
|
147
|
-
end
|
148
|
-
|
149
|
-
# :stopdoc:
|
70
|
+
# The coolio +on_change+ callback is converted to the appropriate +on_removed+
|
71
|
+
# and +on_modified+ callbacks for the EventableScanner.
|
150
72
|
#
|
151
73
|
class Watcher < Coolio::StatWatcher
|
152
|
-
def
|
153
|
-
|
154
|
-
@scanner = scanner
|
74
|
+
def self.watch(fn, scanner )
|
75
|
+
new(fn, scanner)
|
155
76
|
end
|
156
77
|
|
157
|
-
def
|
158
|
-
|
78
|
+
def initialize( fn, scanner )
|
79
|
+
# for file watching, we want to make sure this happens at a reasonable
|
80
|
+
# value, so set it to 0 if the scanner.interval is > 5 seconds. This will
|
81
|
+
# make it use the system value, and allow us to test.
|
82
|
+
i = scanner.interval < 5 ? scanner.interval : 0
|
83
|
+
super(fn, i)
|
84
|
+
@scanner = scanner
|
85
|
+
attach(scanner.event_loop)
|
159
86
|
end
|
160
87
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
88
|
+
# Cool.io uses on_change so we convert that to the appropriate
|
89
|
+
# EventableScanner calls.
|
90
|
+
#
|
91
|
+
def on_change( prev_stat, current_stat )
|
92
|
+
logger.debug "on_change called"
|
93
|
+
if File.exist?(path) then
|
94
|
+
@scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(path, current_stat.mtime, current_stat.size))
|
95
|
+
else
|
96
|
+
@scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(path))
|
97
|
+
end
|
165
98
|
end
|
166
99
|
end
|
167
100
|
|
168
|
-
|
101
|
+
# Periodically execute a Scan. Hook this into the EventableScanner#on_scan
|
102
|
+
#
|
103
|
+
class ScanTimer< Coolio::TimerWatcher
|
169
104
|
def initialize( scanner )
|
170
105
|
super(scanner.interval, true)
|
171
106
|
@scanner = scanner
|
172
107
|
end
|
173
108
|
|
174
109
|
def on_timer( *args )
|
175
|
-
@scanner.
|
110
|
+
@scanner.on_scan
|
176
111
|
end
|
177
112
|
end
|
178
113
|
# :startdoc:
|
179
|
-
|
180
114
|
end # class DirectoryWatcher::CoolioScanner
|
181
115
|
|
182
116
|
end # if DirectoryWatcher::HAVE_COOLIO
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
begin
|
3
2
|
require 'eventmachine'
|
4
3
|
DirectoryWatcher::HAVE_EM = true
|
@@ -7,13 +6,19 @@ rescue LoadError
|
|
7
6
|
end
|
8
7
|
|
9
8
|
if DirectoryWatcher::HAVE_EM
|
10
|
-
[:epoll, :kqueue].each {|poll| break if EventMachine.send(poll)}
|
11
9
|
|
10
|
+
# Set up the appropriate polling options
|
11
|
+
[:epoll, :kqueue].each do |poll|
|
12
|
+
if EventMachine.send("#{poll}?") then
|
13
|
+
EventMachine.send("#{poll}=", true )
|
14
|
+
break
|
15
|
+
end
|
16
|
+
end
|
12
17
|
|
13
18
|
# The EmScanner uses the EventMachine reactor loop to monitor changes to
|
14
19
|
# files in the watched directory. This scanner is more efficient than the
|
15
20
|
# pure Ruby scanner because it relies on the operating system kernel
|
16
|
-
#
|
21
|
+
# notifications instead of a periodic polling and stat of every file in the
|
17
22
|
# watched directory (the technique used by the Scanner class).
|
18
23
|
#
|
19
24
|
# EventMachine cannot notify us when a file is added to the watched
|
@@ -27,194 +32,117 @@ if DirectoryWatcher::HAVE_EM
|
|
27
32
|
#
|
28
33
|
# * New files are detected only when the watched directory is polled at the
|
29
34
|
# configured interval.
|
30
|
-
#
|
31
|
-
class DirectoryWatcher::EmScanner <
|
32
|
-
|
35
|
+
#
|
36
|
+
class DirectoryWatcher::EmScanner < DirectoryWatcher::EventableScanner
|
33
37
|
# call-seq:
|
34
|
-
# EmScanner.new
|
35
|
-
#
|
36
|
-
# Create an EventMachine based scanner that will generate file events and
|
37
|
-
# pass those events (as an array) to the given _block_.
|
38
|
+
# EmScanner.new( configuration )
|
38
39
|
#
|
39
|
-
def initialize(
|
40
|
-
super(
|
41
|
-
@timer = nil
|
42
|
-
@run_loop = lambda {_run_loop}
|
43
|
-
@watchers = {}
|
40
|
+
def initialize( config )
|
41
|
+
super(config)
|
44
42
|
end
|
45
43
|
|
46
|
-
#
|
47
|
-
#
|
44
|
+
# Called by EventablScanner#start to start the loop up and attach the periodic
|
45
|
+
# timer that will poll the globs for new files.
|
48
46
|
#
|
49
|
-
def
|
50
|
-
|
51
|
-
end
|
52
|
-
|
53
|
-
# Start the EventMachine scanner. If the scanner has already been started
|
54
|
-
# this method will return without taking any action.
|
55
|
-
#
|
56
|
-
# If the EventMachine reactor is not running, it will be started by this
|
57
|
-
# method.
|
58
|
-
#
|
59
|
-
def start
|
60
|
-
return if running?
|
61
|
-
|
47
|
+
def start_loop_with_attached_scan_timer
|
48
|
+
return if @loop_thread
|
62
49
|
unless EventMachine.reactor_running?
|
63
|
-
@
|
50
|
+
@loop_thread = Thread.new {EventMachine.run}
|
64
51
|
Thread.pass until EventMachine.reactor_running?
|
65
52
|
end
|
66
53
|
|
67
|
-
@
|
68
|
-
if test ?e, fn
|
69
|
-
_watch_file fn
|
70
|
-
next
|
71
|
-
end
|
72
|
-
|
73
|
-
@files.delete fn
|
74
|
-
@events << ::DirectoryWatcher::Event.new(:removed, fn)
|
75
|
-
end
|
76
|
-
|
77
|
-
_run_loop
|
54
|
+
@timer = ScanTimer.new(self)
|
78
55
|
end
|
79
56
|
|
80
|
-
#
|
81
|
-
#
|
57
|
+
# Called by EventableScanner#stop to stop the loop as part of the shutdown
|
58
|
+
# process.
|
82
59
|
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
@timer = nil
|
92
|
-
|
93
|
-
@watchers.each_value {|w| w.stop_watching if w.active?}
|
94
|
-
@watchers.clear
|
95
|
-
|
96
|
-
notify
|
60
|
+
def stop_loop
|
61
|
+
if @loop_thread then
|
62
|
+
EventMachine.next_tick do
|
63
|
+
EventMachine.stop_event_loop
|
64
|
+
end
|
65
|
+
@loop_thread.kill
|
66
|
+
@loop_thread = nil
|
67
|
+
end
|
97
68
|
end
|
98
69
|
|
99
|
-
#
|
100
|
-
# join( limit = nil )
|
70
|
+
# :stopdoc:
|
101
71
|
#
|
102
|
-
# This is
|
72
|
+
# This is our tailored implementation of the EventMachine FileWatch class.
|
73
|
+
# It receives notifications of file events and provides a mechanism to
|
74
|
+
# translate the EventMachine events into objects to send to the Scanner that
|
75
|
+
# it is initialized with.
|
103
76
|
#
|
104
|
-
|
105
|
-
end
|
106
|
-
|
107
|
-
# :stopdoc:
|
77
|
+
# The Watcher only responds to modified and deleted events.
|
108
78
|
#
|
109
|
-
# This
|
110
|
-
# occured on the file. The scanner determines if the file has been
|
111
|
-
# modified or deleted and notifies the directory watcher accordingly.
|
79
|
+
# This class is required by EventableScanner to institute file watching.
|
112
80
|
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
if stat
|
118
|
-
_watch_file fn unless watcher.active?
|
119
|
-
@files[fn] = stat
|
120
|
-
@events << ::DirectoryWatcher::Event.new(:modified, fn)
|
121
|
-
else
|
122
|
-
if watcher.active?
|
123
|
-
watcher.stop_watching
|
124
|
-
@watchers.delete fn
|
125
|
-
end
|
126
|
-
@files.delete fn
|
127
|
-
@events << ::DirectoryWatcher::Event.new(:removed, fn)
|
81
|
+
class Watcher < EventMachine::FileWatch
|
82
|
+
def self.watch( path, scanner )
|
83
|
+
EventMachine.watch_file path, Watcher, scanner
|
128
84
|
end
|
129
85
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
# EventMachine cannot notify us when new files are added to the watched
|
138
|
-
# directory. The event loop will run at the configured interval and look
|
139
|
-
# for files that have been added or files that have become stable.
|
140
|
-
#
|
141
|
-
def _run_loop
|
142
|
-
start = Time.now.to_f
|
143
|
-
|
144
|
-
_find_added
|
145
|
-
_find_stable
|
146
|
-
|
147
|
-
notify
|
148
|
-
|
149
|
-
nap_time = @interval - (Time.now.to_f - start)
|
150
|
-
nap_time = 0.001 unless nap_time > 0
|
151
|
-
@timer = EventMachine.add_timer nap_time, @run_loop
|
152
|
-
end
|
86
|
+
# Initialize the Watcher using with the given scanner
|
87
|
+
# Post initialization, EventMachine will set @path
|
88
|
+
#
|
89
|
+
def initialize( scanner )
|
90
|
+
@scanner = scanner
|
91
|
+
end
|
153
92
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
added
|
93
|
+
# EventMachine callback for when a watched file is deleted. We convert this
|
94
|
+
# to a FileStat object for a removed file.
|
95
|
+
#
|
96
|
+
def file_deleted
|
97
|
+
@scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(@path))
|
98
|
+
end
|
99
|
+
# Event Machine also sends events on file_moved which we'll just consider a
|
100
|
+
# file deleted and the file added event will be picked up by the next scan
|
101
|
+
alias :file_moved :file_deleted
|
162
102
|
|
163
|
-
|
164
|
-
|
165
|
-
|
103
|
+
# EventMachine callback for when a watched file is modified. We convert this
|
104
|
+
# to a FileStat object and send it to the collector
|
105
|
+
def file_modified
|
106
|
+
stat = File.stat @path
|
107
|
+
@scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(@path, stat.mtime, stat.size))
|
166
108
|
end
|
167
|
-
end
|
168
109
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
stat.stable -= 1
|
177
|
-
if stat.stable <= 0
|
178
|
-
@events << ::DirectoryWatcher::Event.new(:stable, fn)
|
179
|
-
stat.stable = nil
|
110
|
+
# Detach the watcher from the event loop.
|
111
|
+
#
|
112
|
+
# Required by EventableScanner as part of the shutdown process.
|
113
|
+
#
|
114
|
+
def detach
|
115
|
+
EventMachine.next_tick do
|
116
|
+
stop_watching
|
180
117
|
end
|
181
118
|
end
|
182
119
|
end
|
183
120
|
|
184
|
-
#
|
121
|
+
# Periodically execute a Scan.
|
185
122
|
#
|
186
|
-
|
187
|
-
@watchers[fn] = EventMachine.watch_file fn, Watcher, self
|
188
|
-
end
|
189
|
-
|
190
|
-
# :stopdoc:
|
123
|
+
# This object is used by EventableScanner to during shutdown.
|
191
124
|
#
|
192
|
-
|
193
|
-
# It receives notifications of file events and provides a mechanism to
|
194
|
-
# translate the EventMachine events into DirectoryWatcher events.
|
195
|
-
#
|
196
|
-
class Watcher < EventMachine::FileWatch
|
125
|
+
class ScanTimer
|
197
126
|
def initialize( scanner )
|
198
127
|
@scanner = scanner
|
199
|
-
@
|
128
|
+
@timer = EventMachine::PeriodicTimer.new( @scanner.interval, method(:on_scan) )
|
200
129
|
end
|
201
130
|
|
202
|
-
def
|
203
|
-
|
204
|
-
stat = File.stat @path
|
205
|
-
::DirectoryWatcher::FileStat.new(stat.mtime, stat.size, @scanner.stable)
|
131
|
+
def on_scan
|
132
|
+
@scanner.on_scan
|
206
133
|
end
|
207
134
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
135
|
+
# Detach the watcher from the event loop.
|
136
|
+
#
|
137
|
+
# Required by EventableScanner as part of the shutdown process.
|
138
|
+
#
|
139
|
+
def detach
|
140
|
+
EventMachine.next_tick do
|
141
|
+
@timer.cancel
|
142
|
+
end
|
143
|
+
end
|
215
144
|
end
|
216
145
|
# :startdoc:
|
217
|
-
|
218
146
|
end # class DirectoryWatcher::EmScanner
|
219
147
|
end # if HAVE_EM
|
220
148
|
|