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.
@@ -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 notifictions
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
- class DirectoryWatcher::CoolioScanner < ::DirectoryWatcher::Scanner
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 { |events| block }
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( &block )
25
- super(&block)
26
- @watchers = {}
24
+ def initialize( config )
25
+ super(config)
27
26
  end
28
27
 
29
- # Start the Coolio scanner loop. If the scanner is already running, this method
30
- # will return without taking any action.
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 start
33
- return if running?
34
-
35
- @timer = Timer.new self
36
- @thread = Thread.new {
37
- coolio_loop = Thread.current._coolio_loop
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
- # Stop the Coolio scanner loop. If the scanner is already stopped, this method
54
- # will return without taking any action.
40
+ # Called by EventableScanner#stop to stop the loop as part of the shutdown
41
+ # process.
55
42
  #
56
- def stop
57
- return unless running?
58
-
59
- @timer.detach
60
- @timer = nil
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
- # :stopdoc:
51
+ # Return the cool.io loop object.
73
52
  #
74
- # This callback is invoked by a Watcher instance when some change has
75
- # occured on the file. The scanner determines if the file has been
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 _on_change( watcher )
79
- fn = watcher.path
80
- stat = watcher.stat
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
- watcher.detach
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
- # This callback is invoked by the Timer instance when it is triggered by
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
- def _find_added
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
- def _find_stable
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
- def _watch_file( fn )
144
- w = Watcher.new(fn, self)
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 initialize( fn, scanner )
153
- super(fn, scanner.interval)
154
- @scanner = scanner
74
+ def self.watch(fn, scanner )
75
+ new(fn, scanner)
155
76
  end
156
77
 
157
- def on_change( *args )
158
- @scanner._on_change self
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
- def stat
162
- return unless test ?e, path
163
- stat = File.stat path
164
- ::DirectoryWatcher::FileStat.new(stat.mtime, stat.size, @scanner.stable)
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
- class Timer < Coolio::TimerWatcher
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._on_timer
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
- # notifictions instead of a periodic polling and stat of every file in the
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 < ::DirectoryWatcher::Scanner
32
-
35
+ #
36
+ class DirectoryWatcher::EmScanner < DirectoryWatcher::EventableScanner
33
37
  # call-seq:
34
- # EmScanner.new { |events| block }
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( &block )
40
- super(&block)
41
- @timer = nil
42
- @run_loop = lambda {_run_loop}
43
- @watchers = {}
40
+ def initialize( config )
41
+ super(config)
44
42
  end
45
43
 
46
- # Returns +true+ if the scanner is currently running. Returns +false+ if
47
- # this is not the case.
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 running?
50
- !@timer.nil?
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
- @thread = Thread.new {EventMachine.run}
50
+ @loop_thread = Thread.new {EventMachine.run}
64
51
  Thread.pass until EventMachine.reactor_running?
65
52
  end
66
53
 
67
- @files.keys.each do |fn|
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
- # Stop the EventMachine scanner. If the scanner is already stopped this
81
- # method will return without taking any action.
57
+ # Called by EventableScanner#stop to stop the loop as part of the shutdown
58
+ # process.
82
59
  #
83
- # The EventMachine reactor will _not_ be stopped by this method. It is up
84
- # to the user to stop the reactor using the EventMachine#stop_event_loop
85
- # method.
86
- #
87
- def stop
88
- return unless running?
89
-
90
- EventMachine.cancel_timer @timer rescue nil
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
- # call-seq:
100
- # join( limit = nil )
70
+ # :stopdoc:
101
71
  #
102
- # This is a no-op method for the EventMachine file scanner.
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
- def join( limit = nil )
105
- end
106
-
107
- # :stopdoc:
77
+ # The Watcher only responds to modified and deleted events.
108
78
  #
109
- # This callback is invoked by a Watcher instance when some event has
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
- def _event!( watcher )
114
- fn = watcher.path
115
- stat = watcher.stat
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
- notify
131
- end
132
- # :startdoc:
133
-
134
-
135
- private
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
- # From the list of files in the watched directory, find those that we are
155
- # not currently watching and add them to the watch list. Generate "added"
156
- # events for those newly found files.
157
- #
158
- def _find_added
159
- cur = list_files
160
- prev = @files.keys
161
- added = cur - prev
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
- added.each do |fn|
164
- @files[fn] = _watch_file(fn).stat
165
- @events << ::DirectoryWatcher::Event.new(:added, fn)
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
- # Iterate over the FileStat instances looking for those with non-nil
170
- # stable counts. Decrement these counts and generate "stable" events for
171
- # those files whose count reaches zero.
172
- #
173
- def _find_stable
174
- @files.each do |fn, stat|
175
- next if stat.stable.nil?
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
- # Create and return a new Watcher instance for the given filename _fn_.
121
+ # Periodically execute a Scan.
185
122
  #
186
- def _watch_file( fn )
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
- # This is our tailored implementation of the EventMachine FileWatch class.
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
- @active = true
128
+ @timer = EventMachine::PeriodicTimer.new( @scanner.interval, method(:on_scan) )
200
129
  end
201
130
 
202
- def stat
203
- return unless test ?e, @path
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
- def active?() @active; end
209
- def event!() @scanner._event!(self); end
210
- def unbind() @active = false; end
211
- def file_deleted() EventMachine.next_tick {event!}; end
212
-
213
- alias :file_modified :event!
214
- alias :file_moved :event!
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