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,55 @@
1
+ class DirectoryWatcher
2
+ # Paths contains helpful methods to determine paths of files inside the
3
+ # DirectoryWatcher library
4
+ #
5
+ module Paths
6
+ # The root directory of the project is considered the parent directory of
7
+ # the 'lib' directory.
8
+ #
9
+ # Returns The full expanded path of the parent directory of 'lib' going up
10
+ # the path from the current file. Trailing File::SEPARATOR is guaranteed
11
+ #
12
+ def root_dir
13
+ path_parts = ::File.expand_path(__FILE__).split(::File::SEPARATOR)
14
+ lib_index = path_parts.rindex("lib")
15
+ return path_parts[0...lib_index].join(::File::SEPARATOR) + ::File::SEPARATOR
16
+ end
17
+
18
+ # Return a path relative to the 'lib' directory in this project
19
+ #
20
+ def lib_path(*args,&block)
21
+ sub_path('lib', *args, &block)
22
+ end
23
+
24
+ # Return a path relative to the 'root' directory in the project
25
+ #
26
+ def path(*args,&block)
27
+ sub_path('', *args, &block)
28
+ end
29
+
30
+ # Calculate the full expanded path of the item with respect to a sub path of
31
+ # 'root_dir'
32
+ #
33
+ def sub_path(sub,*args,&block)
34
+ rv = ::File.join(root_dir, sub) + ::File::SEPARATOR
35
+ rv = ::File.join(rv, *args) if args
36
+ if block
37
+ with_load_path( rv ) do
38
+ rv = block.call
39
+ end
40
+ end
41
+ return rv
42
+ end
43
+
44
+ # Execute a block in the context of a path added to $LOAD_PATH
45
+ #
46
+ def with_load_path(path, &block)
47
+ $LOAD_PATH.unshift path
48
+ block.call
49
+ ensure
50
+ $LOAD_PATH.shift
51
+ end
52
+
53
+ extend self
54
+ end
55
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  begin
3
2
  require 'rev'
4
3
  DirectoryWatcher::HAVE_REV = true
@@ -8,177 +7,115 @@ end
8
7
 
9
8
  if DirectoryWatcher::HAVE_REV
10
9
 
10
+ # Deprecated:
11
+ #
11
12
  # The RevScanner uses the Rev loop to monitor changes to files in the
12
13
  # watched directory. This scanner is more efficient than the pure Ruby
13
- # scanner because it relies on the operating system kernel notifictions
14
+ # scanner because it relies on the operating system kernel notifications
14
15
  # instead of a periodic polling and stat of every file in the watched
15
16
  # directory (the technique used by the Scanner class).
16
17
  #
17
- class DirectoryWatcher::RevScanner < ::DirectoryWatcher::Scanner
18
-
18
+ # The RevScanner is essentially the exact same as the CoolioScanner with class
19
+ # names changed and using _rev_loop instead of _coolio_loop. Unfortunately the
20
+ # RevScanner cannot be a sub class of CoolioScanner because of C-extension
21
+ # reasons between the rev and coolio gems
22
+ #
23
+ # Rev cannot notify us when a file is added to the watched
24
+ # directory; therefore, added files are only picked up when we apply the
25
+ # glob pattern to the directory. This is done at the configured interval.
26
+ #
27
+ class DirectoryWatcher::RevScanner < ::DirectoryWatcher::EventableScanner
19
28
  # call-seq:
20
- # RevScanner.new { |events| block }
21
- #
22
- # Create a Rev based scanner that will generate file events and pass
23
- # those events (as an array) to the given _block_.
29
+ # RevScanner.new( glob, interval, collection_queue )
24
30
  #
25
- def initialize( &block )
26
- super(&block)
27
- @watchers = {}
31
+ def initialize( glob, interval, collection_queue )
32
+ super(glob, interval, collection_queue)
28
33
  end
29
34
 
30
- # Start the Rev scanner loop. If the scanner is already running, this method
31
- # will return without taking any action.
35
+ # Called by EventablScanner#start to start the loop up and attach the periodic
36
+ # timer that will poll the globs for new files.
32
37
  #
33
- def start
34
- return if running?
35
-
36
- @timer = Timer.new self
37
- @thread = Thread.new {
38
- rev_loop = Thread.current._rev_loop
39
- @files.keys.each do |fn|
40
- if test ?e, fn
41
- _watch_file fn
42
- next
43
- end
44
-
45
- @files.delete fn
46
- @events << ::DirectoryWatcher::Event.new(:removed, fn)
47
- end
48
-
49
- @timer.attach rev_loop
50
- rev_loop.run
38
+ def start_loop_with_attached_scan_timer
39
+ return if @loop_thread
40
+ @timer = ScanTimer.new( self )
41
+ @loop_thread = Thread.new {
42
+ @timer.attach(event_loop)
43
+ event_loop.run
51
44
  }
52
45
  end
53
46
 
54
- # Stop the Rev scanner loop. If the scanner is already stopped, this method
55
- # will return without taking any action.
47
+ # Called by EventableScanner#stop to stop the loop as part of the shutdown
48
+ # process.
56
49
  #
57
- def stop
58
- return unless running?
59
-
60
- @timer.detach
61
- @timer = nil
62
-
63
- @watchers.each_value {|w| w.detach}
64
- @watchers.clear
65
-
66
- notify
67
-
68
- @thread._rev_loop.stop rescue nil
69
- @thread.kill # for some reason the rev loop is not returning after stopping
70
- @thread = nil
50
+ def stop_loop
51
+ if @loop_thread then
52
+ event_loop.stop rescue nil
53
+ @loop_thread.kill
54
+ @loop_thread = nil
55
+ end
71
56
  end
72
57
 
73
- # :stopdoc:
58
+ # Return the rev loop object
74
59
  #
75
- # This callback is invoked by a Watcher instance when some change has
76
- # occured on the file. The scanner determines if the file has been
77
- # modified or deleted and notifies the directory watcher accordingly.
60
+ # This is used during the startup, shutdown process and for the Watcher to
61
+ # attach and detach from the event loop
78
62
  #
79
- def _on_change( watcher )
80
- fn = watcher.path
81
- stat = watcher.stat
82
-
83
- if stat
84
- if @files[fn] != stat
85
- @files[fn] = stat
86
- @events << ::DirectoryWatcher::Event.new(:modified, fn)
87
- end
63
+ def event_loop
64
+ if @loop_thread then
65
+ @loop_thread._rev_loop
88
66
  else
89
- watcher.detach
90
- @watchers.delete fn
91
- @files.delete fn
92
- @events << ::DirectoryWatcher::Event.new(:removed, fn)
67
+ Thread.current._rev_loop
93
68
  end
94
-
95
- notify
96
- end
97
-
98
- # This callback is invoked by the Timer instance when it is triggered by
99
- # the Rev loop. This method will check for added files and stable files
100
- # and notify the directory watcher accordingly.
101
- #
102
- def _on_timer
103
- _find_added
104
- _find_stable
105
- notify
106
69
  end
107
- # :startdoc:
108
-
109
-
110
- private
111
70
 
112
- # From the list of files in the watched directory, find those that we are
113
- # not currently watching and add them to the watch list. Generate "added"
114
- # events for those newly found files.
71
+ # :stopdoc:
115
72
  #
116
- def _find_added
117
- cur = list_files
118
- prev = @files.keys
119
- added = cur - prev
120
-
121
- added.each do |fn|
122
- @files[fn] = _watch_file(fn).stat
123
- @events << ::DirectoryWatcher::Event.new(:added, fn)
124
- end
125
- end
126
-
127
- # Iterate over the FileStat instances looking for those with non-nil
128
- # stable counts. Decrement these counts and generate "stable" events for
129
- # those files whose count reaches zero.
73
+ # Watch files using the Rev::StatWatcher.
130
74
  #
131
- def _find_stable
132
- @files.each do |fn, stat|
133
- next if stat.stable.nil?
134
- stat.stable -= 1
135
- if stat.stable <= 0
136
- @events << ::DirectoryWatcher::Event.new(:stable, fn)
137
- stat.stable = nil
138
- end
75
+ # The rev +on_change+ callback is converted to the appropriate +on_removed+
76
+ # and +on_modified+ callbacks for the EventableScanner.
77
+ class Watcher < ::Rev::StatWatcher
78
+ def self.watch(fn, scanner )
79
+ new(fn, scanner)
139
80
  end
140
- end
141
81
 
142
- # Create and return a new Watcher instance for the given filename _fn_.
143
- #
144
- def _watch_file( fn )
145
- w = Watcher.new(fn, self)
146
- w.attach(@thread ? @thread._rev_loop : Thread.current._rev_loop)
147
- @watchers[fn] = w
148
- end
149
-
150
- # :stopdoc:
151
- #
152
- class Watcher < Rev::StatWatcher
153
82
  def initialize( fn, scanner )
154
- super(fn, scanner.interval)
83
+ # for file watching, we want to make sure this happens at a reasonable
84
+ # value, so set it to 0 if the scanner.interval is > 5 seconds. This will
85
+ # make it use the system value, and allow us to test.
86
+ i = scanner.interval < 5 ? scanner.interval : 0
87
+ super(fn, i)
155
88
  @scanner = scanner
89
+ attach(scanner.event_loop)
156
90
  end
157
91
 
92
+ # Rev uses on_change so we convert that to the appropriate
93
+ # EventableScanner calls. Unlike Coolio, Rev's on_change() takes no
94
+ # parameters
95
+ #
158
96
  def on_change
159
- @scanner._on_change self
160
- end
161
-
162
- def stat
163
- return unless test ?e, path
164
- stat = File.stat path
165
- ::DirectoryWatcher::FileStat.new(stat.mtime, stat.size, @scanner.stable)
97
+ if File.exist?(path) then
98
+ @scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(path))
99
+ else
100
+ stat = File.stat(path)
101
+ @scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(path, stat.mtime, stat.size))
102
+ end
166
103
  end
167
104
  end
168
105
 
169
- class Timer < Rev::TimerWatcher
106
+ # Periodically execute a Scan. Hook this into the EventableScanner#on_scan
107
+ #
108
+ class ScanTimer< Rev::TimerWatcher
170
109
  def initialize( scanner )
171
110
  super(scanner.interval, true)
172
111
  @scanner = scanner
173
112
  end
174
113
 
175
- def on_timer
176
- @scanner._on_timer
114
+ def on_timer( *args )
115
+ @scanner.on_scan
177
116
  end
178
117
  end
179
- # :startdoc:
180
118
 
181
119
  end # class DirectoryWatcher::RevScanner
182
- end # if DirectoryWatcher::HAVE_REV
183
120
 
184
- # EOF
121
+ end # if DirectoryWatcher::HAVE_REV
@@ -0,0 +1,72 @@
1
+ # A Scan is the scan of a full directory structure with the ability to iterate
2
+ # over the results, or return them as a full dataset
3
+ #
4
+ # results = Scan.new( globs ).run
5
+ #
6
+ class DirectoryWatcher::Scan
7
+
8
+ def initialize( globs = Array.new )
9
+ @globs = [ globs ].flatten
10
+ @results = Array.new
11
+ end
12
+
13
+ # Run the entire scan and collect all the results. The Scan will only ever
14
+ # be run once.
15
+ #
16
+ # Return the array of FileStat results
17
+ def run
18
+ results
19
+ end
20
+
21
+ # Return the results of the scan. If the scan has not been run yet, then run
22
+ # it
23
+ def results
24
+ @results = collect_all_stats if @results.empty?
25
+ return @results
26
+ end
27
+
28
+ #######
29
+ private
30
+ #######
31
+
32
+ # Collect all the Stats into an Array and return them
33
+ #
34
+ def collect_all_stats
35
+ r = []
36
+ each { |stat| r << stat }
37
+ return r
38
+ end
39
+
40
+ # Iterate over each glob, yielding it
41
+ #
42
+ def each_glob( &block )
43
+ @globs.each do |glob|
44
+ yield glob
45
+ end
46
+ end
47
+
48
+ # Iterate over each item that matches the glob.
49
+ # The item yielded is a ::DirectoryWatcher::FileStat object.
50
+ #
51
+ def each( &block )
52
+ each_glob do |glob|
53
+ Dir.glob(glob).each do |fn|
54
+ if stat = file_stat( fn ) then
55
+ yield stat if block_given?
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Return the stat of of the file in question. If the item is not a file,
62
+ # then return the value of the passed in +if_not_file+
63
+ #
64
+ def file_stat( fn, if_not_file = false )
65
+ stat = File.stat fn
66
+ return if_not_file unless stat.file?
67
+ return DirectoryWatcher::FileStat.new( fn, stat.mtime, stat.size )
68
+ rescue SystemCallError => e
69
+ # swallow
70
+ $stderr.puts "Error Stating #{fn} : #{e}"
71
+ end
72
+ end
@@ -0,0 +1,22 @@
1
+ # ScanAndQueue creates a Scan from its input globs and then sends that Scan to
2
+ # its Queue.
3
+ #
4
+ # Every time scan_and_queue is called a new scan is created an sent to the
5
+ # queue.
6
+ class DirectoryWatcher::ScanAndQueue
7
+
8
+ def initialize( glob, queue )
9
+ @globs = glob
10
+ @queue =queue
11
+ end
12
+
13
+ # Create and run a Scan and submit it to the Queue.
14
+ #
15
+ # Returns the Scan that was run
16
+ def scan_and_queue
17
+ scan = ::DirectoryWatcher::Scan.new( @globs )
18
+ scan.run
19
+ @queue.enq scan
20
+ return scan
21
+ end
22
+ end
@@ -1,8 +1,6 @@
1
-
2
1
  # The Scanner is responsible for polling the watched directory at a regular
3
- # interval and generating events when files are modified, added or removed.
4
- # These events are passed to the DirectoryWatcher which notifies the
5
- # registered observers.
2
+ # interval and generating a Scan which it will then send down the collection
3
+ # queue to the Collector.
6
4
  #
7
5
  # The Scanner is a pure Ruby class, and as such it works across all Ruby
8
6
  # interpreters on the major platforms. This also means that it can be
@@ -10,220 +8,39 @@
10
8
  # intervals. Your mileage will vary, but it is something to keep an eye on.
11
9
  #
12
10
  class DirectoryWatcher::Scanner
13
-
14
- attr_accessor :glob
15
- attr_accessor :interval
16
- attr_accessor :stable
17
- attr_accessor :files
18
-
19
- # call-seq:
20
- # Scanner.new { |events| block }
21
- #
22
- # Create a thread-based scanner that will generate file events and pass
23
- # those events (as an array) to the given _block_.
24
- #
25
- def initialize( &block )
26
- @events = []
27
- @thread = nil
28
- @notify = block;
29
- end
30
-
31
- # Returns +true+ if the scanner is currently running. Returns +false+ if
32
- # this is not the case.
33
- #
34
- def running?
35
- !@thread.nil?
36
- end
37
-
38
- # Start the scanner thread. If the scanner is already running, this method
39
- # will return without taking any action.
40
- #
41
- def start
42
- return if running?
43
-
44
- @stop = false
45
- @thread = Thread.new(self) {|scanner| scanner.__send__ :run_loop}
46
- self
47
- end
48
-
49
- # Stop the scanner thread. If the scanner is already stopped, this method
50
- # will return without taking any action.
51
- #
52
- def stop
53
- return unless running?
54
-
55
- @stop = true
56
- @thread.wakeup if @thread.status == 'sleep'
57
- @thread.join
58
- self
59
- ensure
60
- @thread = nil
61
- end
62
-
63
- # call-seq:
64
- # reset( pre_load = false )
65
- #
66
- # Reset the scanner state by clearing the stored file list. Passing +true+
67
- # to this method will cause the file list to be pre-loaded after it has
68
- # been cleared effectively skipping the initial round of file added events
69
- # that would normally be generated.
70
- #
71
- def reset( pre_load = false )
72
- @events.clear
73
- @files = (pre_load ? scan_files : Hash.new)
74
- end
75
-
76
- # call-seq:
77
- # join( limit = nil )
78
- #
79
- # If the scanner thread is running, the calling thread will suspend
80
- # execution and run the scanner thread. This method does not return until
81
- # the scanner thread is stopped or until _limit_ seconds have passed.
82
- #
83
- # If the scanner thread is not running, this method returns immediately
84
- # with +nil+.
85
- #
86
- def join( limit = nil )
87
- return unless running?
88
- @thread.join limit
89
- end
90
-
91
- # Performs exactly one scan of the directory for file changes and notifies
92
- # the observers.
93
- #
94
- def run_once
95
- files = scan_files
96
- keys = [files.keys, @files.keys] # current files, previous files
97
-
98
- find_added(files, *keys)
99
- find_modified(files, *keys)
100
- find_removed(*keys)
101
-
102
- notify
103
- @files = files # store the current file list for the next iteration
104
- self
105
- end
106
-
107
-
108
- private
109
-
110
- # Using the configured glob pattern, scan the directory for all files and
111
- # return a hash with the filenames as keys and +FileStat+ objects as the
112
- # values. The +FileStat+ objects contain the mtime and size of the file.
113
- #
114
- def scan_files
115
- files = {}
116
- @glob.each do |glob|
117
- Dir.glob(glob).each do |fn|
118
- begin
119
- stat = File.stat fn
120
- next unless stat.file?
121
- files[fn] = ::DirectoryWatcher::FileStat.new(stat.mtime, stat.size)
122
- rescue SystemCallError; end
123
- end
124
- end
125
- files
126
- end
127
-
128
- # Using the configured glob pattern, scan the directory for all files and
129
- # return an array of the filenames found.
130
- #
131
- def list_files
132
- files = []
133
- @glob.each do |glob|
134
- Dir.glob(glob).each {|fn| files << fn if test ?f, fn}
135
- end
136
- files
137
- end
138
-
139
-
140
- # Calling this method will enter the scanner's run loop. The
141
- # calling thread will not return until the +stop+ method is called.
142
- #
143
- # The run loop is responsible for scanning the directory for file changes,
144
- # and then dispatching events to registered listeners.
145
- #
146
- def run_loop
147
- until @stop
148
- start = Time.now.to_f
149
-
150
- run_once
151
-
152
- nap_time = @interval - (Time.now.to_f - start)
153
- sleep nap_time if nap_time > 0
154
- end
155
- end
11
+ include DirectoryWatcher::Threaded
12
+ include DirectoryWatcher::Logable
156
13
 
157
14
  # call-seq:
158
- # find_added( files, cur, prev )
15
+ # Scanner.new( configuration )
159
16
  #
160
- # Taking the list of current files, _cur_, and the list of files found
161
- # previously, _prev_, figure out which files have been added and generate
162
- # a new file added event for each.
17
+ # From the Configuration instance passed in Scanner uses:
163
18
  #
164
- def find_added( files, cur, prev )
165
- added = cur - prev
166
- added.each do |fn|
167
- files[fn].stable = @stable
168
- @events << ::DirectoryWatcher::Event.new(:added, fn)
169
- end
170
- self
171
- end
172
-
173
- # call-seq:
174
- # find_removed( cur, prev )
19
+ # glob - Same as that in DirectoryWatcher
20
+ # interval - Same as that in DirectoryWatcher
21
+ # collection_queue - The Queue to send the Scans too.
22
+ # the other end of this queue is connected to a Collector
175
23
  #
176
- # Taking the list of current files, _cur_, and the list of files found
177
- # previously, _prev_, figure out which files have been removed and
178
- # generate a new file removed event for each.
24
+ # The Scanner is not generally used out side of a DirectoryWatcher so this is
25
+ # more of an internal API
179
26
  #
180
- def find_removed( cur, prev )
181
- removed = prev - cur
182
- removed.each {|fn| @events << ::DirectoryWatcher::Event.new(:removed, fn)}
183
- self
27
+ #def initialize( glob, interval, collection_queue )
28
+ def initialize( config )
29
+ @config = config
30
+ @scan_and_queue = ::DirectoryWatcher::ScanAndQueue.new( @config.glob, @config.collection_queue )
184
31
  end
185
32
 
186
- # call-seq:
187
- # find_modified( files, cur, prev )
188
- #
189
- # Taking the list of current files, _cur_, and the list of files found
190
- # previously, _prev_, find those that are common between them and determine
191
- # if any have been modified. Generate a new file modified event for each
192
- # modified file. Also, by looking at the stable count in the _files_ hash,
193
- # figure out if any files have become stable since being added or modified.
194
- # Generate a new stable event for each stabilized file.
195
- #
196
- def find_modified( files, cur, prev )
197
- (cur & prev).each do |key|
198
- cur_stat, prev_stat = files[key], @files[key]
199
-
200
- # if the modification time or the file size differs from the last
201
- # time it was seen, then create a :modified event
202
- if cur_stat != prev_stat
203
- @events << ::DirectoryWatcher::Event.new(:modified, key)
204
- cur_stat.stable = @stable
205
-
206
- # otherwise, if the count is not nil see if we need to create a
207
- # :stable event
208
- elsif !prev_stat.stable.nil?
209
- cur_stat.stable = prev_stat.stable - 1
210
- if cur_stat.stable <= 0
211
- @events << ::DirectoryWatcher::Event.new(:stable, key)
212
- cur_stat.stable = nil
213
- end
214
- end
215
- end
216
- self
33
+ # Set the interval before starting the loop.
34
+ # This allows for interval to be set AFTER the DirectoryWatcher instance is
35
+ # allocated but before it is started.
36
+ def before_starting
37
+ self.interval = @config.interval
217
38
  end
218
39
 
219
- # If there are queued files events, then invoke the notify block given
220
- # when the scanner was created. The file events array is cleared at the
221
- # end of this method call.
40
+ # Performs exactly one scan of the directory and sends the
41
+ # results to the Collector
222
42
  #
223
- def notify
224
- @notify.call(@events) unless @events.empty?
225
- ensure
226
- @events.clear
43
+ def run
44
+ @scan_and_queue.scan_and_queue
227
45
  end
228
-
229
- end # class DirectoryWatcher::Scanner
46
+ end