directory_watcher 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,12 @@
1
+ == 1.3.0 / 2009-10-21
2
+
3
+ * 2 major enhancements
4
+ - added support for Rev based notifications
5
+ - added support for EventMachine based notifications
6
+
7
+ * 1 minor enhancement
8
+ - pulled out the scanner thread into its own class
9
+
1
10
  == 1.2.0 / 2009-04-12
2
11
 
3
12
  * 2 minor enhancements
data/README.txt CHANGED
@@ -1,4 +1,4 @@
1
- directory_watcher
1
+ Directory Watcher
2
2
  by Tim Pease
3
3
  http://codeforpeople.rubyforge.org/directory_watcher
4
4
 
@@ -27,10 +27,21 @@ watcher.
27
27
 
28
28
  sudo gem install bones
29
29
 
30
+ == NOTES:
31
+
32
+ The support for EventMachine based file notifications is fairly new and
33
+ experimental. Please feel free to experiment and report any issues on the
34
+ github issue tracker.
35
+
36
+ http://github.com/TwP/directory_watcher/issues
37
+
38
+ The support for Rev based file notifications is also fairly new and subject to
39
+ the same disclaimer as the EventMachine functionality.
40
+
30
41
  == LICENSE:
31
42
 
32
43
  MIT License
33
- Copyright (c) 2007 - 2008
44
+ Copyright (c) 2007 - 2009
34
45
 
35
46
  Permission is hereby granted, free of charge, to any person obtaining
36
47
  a copy of this software and associated documentation files (the
@@ -57,7 +57,7 @@ require 'yaml'
57
57
  # *stable* event. This event is generated after a file has been added or
58
58
  # modified and then remains unchanged for a certain number of scan
59
59
  # intervals.
60
- #
60
+ #
61
61
  # To enable the generation of this event the +stable+ count must be
62
62
  # configured. This is the number of scan intervals a file must remain
63
63
  # unchanged (based modification time and file size) before it is considered
@@ -167,25 +167,51 @@ require 'yaml'
167
167
  # dw.run_once
168
168
  # dw.persist! # stores state to dw_state.yml
169
169
  #
170
+ # === Scanning Strategies
171
+ #
172
+ # By default DirectoryWatcher uses a thread that scans the directory being
173
+ # watched for files and calls "stat" on each file. The stat information is
174
+ # used to determine which files have been modified, added, removed, etc.
175
+ # This approach is fairly intensive for short intervals and/or directories
176
+ # with many files.
177
+ #
178
+ # DirectoryWatcher supports using Rev () or EventMachine () instead of a
179
+ # busy polling thread. These libraries use system level kernel hooks to
180
+ # receive notifications of file system changes. This makes DirectoryWorker
181
+ # much more efficient.
182
+ #
183
+ # This example will use Rev to generate file notifications.
184
+ #
185
+ # dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :scanner => :rev
186
+ # dw.add_observer {|*args| args.each {|event| puts event}}
187
+ #
188
+ # dw.start
189
+ # gets # when the user hits "enter" the script will terminate
190
+ # dw.stop
191
+ #
192
+ # The scanner cannot be changed after the DirectoryWatcher has been
193
+ # created. To use an EventMachine scanner, pass :em as the :scanner
194
+ # option.
195
+ #
170
196
  # == Contact
171
197
  #
172
198
  # A lot of discussion happens about Ruby in general on the ruby-talk
173
199
  # mailing list (http://www.ruby-lang.org/en/ml.html), and you can ask
174
200
  # any questions you might have there. I monitor the list, as do many
175
201
  # other helpful Rubyists, and you're sure to get a quick answer. Of
176
- # course, you're also welcome to email me (Tim Pease) directly at the
202
+ # course, you're also welcome to email me (Tim Pease) directly at the
177
203
  # at tim.pease@gmail.com, and I'll do my best to help you out.
178
- #
204
+ #
179
205
  # (the above paragraph was blatantly stolen from Nathaniel Talbott's
180
206
  # Test::Unit documentation)
181
- #
207
+ #
182
208
  # == Author
183
209
  #
184
210
  # Tim Pease
185
211
  #
186
212
  class DirectoryWatcher
187
213
 
188
- VERSION = '1.2.0' # :nodoc:
214
+ VERSION = '1.3.0' # :nodoc:
189
215
 
190
216
  # An +Event+ structure contains the _type_ of the event and the file _path_
191
217
  # to which the event pertains. The type can be one of the following:
@@ -196,20 +222,21 @@ class DirectoryWatcher
196
222
  # :removed => file has been removed from the directory
197
223
  # :stable => file has stabilized since being added or modified
198
224
  #
199
- Event = Struct.new(:type, :path) do
225
+ Event = Struct.new(:type, :path) {
200
226
  def to_s( ) "#{type} '#{path}'" end
201
- end
227
+ }
202
228
 
203
229
  # :stopdoc:
204
230
  # A persistable file stat structure used internally by the directory
205
231
  # watcher.
206
232
  #
207
- FileStat = Struct.new(:mtime, :size, :stable) do
208
- def <=>( other )
209
- return unless other.is_a? ::DirectoryWatcher::FileStat
210
- self.mtime <=> other.mtime
233
+ FileStat = Struct.new(:mtime, :size, :stable) {
234
+ def eql?( other )
235
+ return false unless other.instance_of? FileStat
236
+ self.mtime == other.mtime and self.size == other.size
211
237
  end
212
- end
238
+ alias :== :eql?
239
+ }
213
240
  # :startdoc:
214
241
 
215
242
  # call-seq:
@@ -232,6 +259,8 @@ class DirectoryWatcher
232
259
  # :persist => file the state will be persisted to and restored
233
260
  # from the file when the directory watcher is
234
261
  # stopped and started (respectively)
262
+ # :scanner => nil the directory scanning strategy to use with
263
+ # the directory watcher (either :em :rev or nil)
235
264
  #
236
265
  # The default glob pattern will scan all files in the configured directory.
237
266
  # Setting the :stable option to +nil+ will prevent stable events from being
@@ -239,6 +268,7 @@ class DirectoryWatcher
239
268
  #
240
269
  def initialize( directory, opts = {} )
241
270
  @dir = directory
271
+ @observer_peers = {}
242
272
 
243
273
  if Kernel.test(?e, @dir)
244
274
  unless Kernel.test(?d, @dir)
@@ -248,15 +278,16 @@ class DirectoryWatcher
248
278
  Dir.mkdir @dir
249
279
  end
250
280
 
281
+ klass = opts[:scanner].to_s.capitalize + 'Scanner'
282
+ klass = DirectoryWatcher.const_get klass rescue Scanner
283
+ @scanner = klass.new {|events| notify_observers(events)}
284
+
251
285
  self.glob = opts[:glob] || '*'
252
286
  self.interval = opts[:interval] || 30
253
287
  self.stable = opts[:stable] || nil
254
288
  self.persist = opts[:persist]
255
289
 
256
- @files = (opts[:pre_load] ? scan_files : Hash.new)
257
- @events = []
258
- @thread = nil
259
- @observer_peers = {}
290
+ @scanner.reset opts[:pre_load]
260
291
  end
261
292
 
262
293
  # call-seq:
@@ -314,15 +345,15 @@ class DirectoryWatcher
314
345
  # files. A single glob pattern can be given or an array of glob patterns.
315
346
  #
316
347
  def glob=( val )
317
- @glob = case val
318
- when String; [File.join(@dir, val)]
319
- when Array; val.flatten.map! {|g| File.join(@dir, g)}
320
- else
321
- raise(ArgumentError,
322
- 'expecting a glob pattern or an array of glob patterns')
323
- end
324
- @glob.uniq!
325
- val
348
+ glob = case val
349
+ when String; [File.join(@dir, val)]
350
+ when Array; val.flatten.map! {|g| File.join(@dir, g)}
351
+ else
352
+ raise(ArgumentError,
353
+ 'expecting a glob pattern or an array of glob patterns')
354
+ end
355
+ glob.uniq!
356
+ @scanner.glob = glob
326
357
  end
327
358
  attr_reader :glob
328
359
 
@@ -333,9 +364,14 @@ class DirectoryWatcher
333
364
  def interval=( val )
334
365
  val = Float(val)
335
366
  raise ArgumentError, "interval must be greater than zero" if val <= 0
336
- @interval = Float(val)
367
+ @scanner.interval = Float(val)
368
+ end
369
+
370
+ # Returns the directory scan interval in seconds.
371
+ #
372
+ def interval
373
+ @scanner.interval
337
374
  end
338
- attr_reader :interval
339
375
 
340
376
  # Sets the number of intervals a file must remain unchanged before it is
341
377
  # considered "stable". When this condition is met, a stable event is
@@ -359,15 +395,21 @@ class DirectoryWatcher
359
395
  #
360
396
  def stable=( val )
361
397
  if val.nil?
362
- @stable = nil
398
+ @scanner.stable = nil
363
399
  return
364
400
  end
365
401
 
366
402
  val = Integer(val)
367
403
  raise ArgumentError, "stable must be greater than zero" if val <= 0
368
- @stable = val
404
+ @scanner.stable = val
405
+ end
406
+
407
+ # Returs the number of intervals a file must remain unchanged before it is
408
+ # considered "stable".
409
+ #
410
+ def stable
411
+ @scanner.stable
369
412
  end
370
- attr_reader :stable
371
413
 
372
414
  # Sets the name of the file to which the directory watcher state will be
373
415
  # persisted when it is stopped. Setting the persist filename to +nil+ will
@@ -384,7 +426,7 @@ class DirectoryWatcher
384
426
  #
385
427
  def persist!
386
428
  return if running?
387
- File.open(@persist, 'w') {|fd| fd.write YAML.dump(@files)} if @persist
429
+ File.open(@persist, 'w') {|fd| fd.write YAML.dump(@scanner.files)} if @persist
388
430
  self
389
431
  end
390
432
 
@@ -394,7 +436,7 @@ class DirectoryWatcher
394
436
  #
395
437
  def load!
396
438
  return if running?
397
- @files = YAML.load_file(@persist) if @persist and test(?f, @persist)
439
+ @scanner.files = YAML.load_file(@persist) if @persist and test(?f, @persist)
398
440
  self
399
441
  end
400
442
 
@@ -402,7 +444,7 @@ class DirectoryWatcher
402
444
  # +false+ if this is not the case.
403
445
  #
404
446
  def running?
405
- !@thread.nil?
447
+ @scanner.running?
406
448
  end
407
449
 
408
450
  # Start the directory watcher scanning thread. If the directory watcher is
@@ -412,8 +454,7 @@ class DirectoryWatcher
412
454
  return if running?
413
455
 
414
456
  load!
415
- @stop = false
416
- @thread = Thread.new(self) {|dw| dw.__send__ :run_loop}
457
+ @scanner.start
417
458
  self
418
459
  end
419
460
 
@@ -423,12 +464,9 @@ class DirectoryWatcher
423
464
  def stop
424
465
  return unless running?
425
466
 
426
- @stop = true
427
- @thread.wakeup if @thread.status == 'sleep'
428
- @thread.join
467
+ @scanner.stop
429
468
  self
430
469
  ensure
431
- @thread = nil
432
470
  persist!
433
471
  end
434
472
 
@@ -443,11 +481,11 @@ class DirectoryWatcher
443
481
  # generated.
444
482
  #
445
483
  def reset( pre_load = false )
446
- was_running = running?
484
+ was_running = @scanner.running?
447
485
 
448
486
  stop if was_running
449
487
  File.delete(@persist) if @persist and test(?f, @persist)
450
- @files = (pre_load ? scan_files : Hash.new)
488
+ @scanner.reset pre_load
451
489
  start if was_running
452
490
  end
453
491
 
@@ -463,142 +501,38 @@ class DirectoryWatcher
463
501
  # with +nil+.
464
502
  #
465
503
  def join( limit = nil )
466
- return unless running?
467
- @thread.join limit
504
+ @scanner.join limit
468
505
  end
469
506
 
470
507
  # Performs exactly one scan of the directory for file changes and notifies
471
508
  # the observers.
472
- #
473
- # This method wil not persist file changes if that options is configured.
474
- # The user must call persist! explicitly when using the run_once method.
475
509
  #
476
510
  def run_once
477
- files = scan_files
478
- keys = [files.keys, @files.keys] # current files, previous files
479
-
480
- find_added(files, *keys)
481
- find_modified(files, *keys)
482
- find_removed(*keys)
483
-
484
- notify_observers
485
- @files = files # store the current file list for the next iteration
511
+ @scanner.run_once
486
512
  self
487
513
  end
488
514
 
489
515
 
490
516
  private
491
517
 
492
- # Using the configured glob pattern, scan the directory for all files and
493
- # return a hash with the filenames as keys and +File::Stat+ objects as the
494
- # values. The +File::Stat+ objects contain the mtime and size of the file.
495
- #
496
- def scan_files
497
- files = {}
498
- @glob.each do |glob|
499
- Dir.glob(glob).each do |fn|
500
- begin
501
- stat = File.stat fn
502
- next unless stat.file?
503
- files[fn] = DirectoryWatcher::FileStat.new(stat.mtime, stat.size)
504
- rescue SystemCallError; end
505
- end
506
- end
507
- files
508
- end
509
-
510
- # Calling this method will enter the directory watcher's run loop. The
511
- # calling thread will not return until the +stop+ method is called.
512
- #
513
- # The run loop is responsible for scanning the directory for file changes,
514
- # and then dispatching events to registered listeners.
515
- #
516
- def run_loop
517
- until @stop
518
- start = Time.now.to_f
519
-
520
- run_once
521
-
522
- nap_time = @interval - (Time.now.to_f - start)
523
- sleep nap_time if nap_time > 0
524
- end
525
- end
526
-
527
- # call-seq:
528
- # find_added( files, cur, prev )
529
- #
530
- # Taking the list of current files, _cur_, and the list of files found
531
- # previously, _prev_, figure out which files have been added and generate
532
- # a new file added event for each.
533
- #
534
- def find_added( files, cur, prev )
535
- added = cur - prev
536
- added.each do |fn|
537
- files[fn].stable = @stable
538
- @events << Event.new(:added, fn)
539
- end
540
- self
541
- end
542
-
543
- # call-seq:
544
- # find_removed( cur, prev )
518
+ # Invoke the update method of each registered observer in turn passing the
519
+ # list of file events to each.
545
520
  #
546
- # Taking the list of current files, _cur_, and the list of files found
547
- # previously, _prev_, figure out which files have been removed and
548
- # generate a new file removed event for each.
549
- #
550
- def find_removed( cur, prev )
551
- removed = prev - cur
552
- removed.each {|fn| @events << Event.new(:removed, fn)}
553
- self
554
- end
555
-
556
- # call-seq:
557
- # find_modified( files, cur, prev )
558
- #
559
- # Taking the list of current files, _cur_, and the list of files found
560
- # previously, _prev_, find those that are common between them and determine
561
- # if any have been modified. Generate a new file modified event for each
562
- # modified file. Also, by looking at the stable count in the _files_ hash,
563
- # figure out if any files have become stable since being added or modified.
564
- # Generate a new stable event for each stabilized file.
565
- #
566
- def find_modified( files, cur, prev )
567
- (cur & prev).each do |key|
568
- cur_stat, prev_stat = files[key], @files[key]
569
-
570
- # if the modification time or the file size differs from the last
571
- # time it was seen, then create a :modified event
572
- if (cur_stat <=> prev_stat) != 0 or cur_stat.size != prev_stat.size
573
- @events << Event.new(:modified, key)
574
- cur_stat.stable = @stable
575
-
576
- # otherwise, if the count is not nil see if we need to create a
577
- # :stable event
578
- elsif !prev_stat.stable.nil?
579
- cur_stat.stable = prev_stat.stable - 1
580
- if cur_stat.stable == 0
581
- @events << Event.new(:stable, key)
582
- cur_stat.stable = nil
583
- end
584
- end
585
- end
586
- self
587
- end
588
-
589
- # If there are queued files events, then invoke the update method of each
590
- # registered observer in turn passing the list of file events to each.
591
- # The file events array is cleared at the end of this method call.
592
- #
593
- def notify_observers
594
- unless @events.empty?
595
- @observer_peers.each do |observer, func|
596
- begin; observer.send(func, *@events); rescue Exception; end
597
- end
598
- @events.clear
521
+ def notify_observers( events )
522
+ @observer_peers.each do |observer, func|
523
+ begin; observer.send(func, *events); rescue Exception; end
599
524
  end
600
525
  end
601
526
 
602
527
  end # class DirectoryWatcher
603
528
 
529
+ begin
530
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
531
+ require 'directory_watcher/scanner'
532
+ require 'directory_watcher/em_scanner'
533
+ require 'directory_watcher/rev_scanner'
534
+ ensure
535
+ $LOAD_PATH.shift
536
+ end
537
+
604
538
  # EOF
@@ -0,0 +1,221 @@
1
+
2
+ begin
3
+ require 'eventmachine'
4
+ DirectoryWatcher::HAVE_EM = true
5
+ rescue LoadError
6
+ DirectoryWatcher::HAVE_EM = false
7
+ end
8
+
9
+ if DirectoryWatcher::HAVE_EM
10
+ [:epoll, :kqueue].each {|poll| break if EventMachine.send(poll)}
11
+
12
+
13
+ # The EmScanner uses the EventMachine reactor loop to monitor changes to
14
+ # files in the watched directory. This scanner is more efficient than the
15
+ # 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
17
+ # watched directory (the technique used by the Scanner class).
18
+ #
19
+ # EventMachine cannot notify us when a file is added to the watched
20
+ # directory; therefore, added files are only picked up when we apply the
21
+ # glob pattern to the directory. This is done at the configured interval.
22
+ #
23
+ # Notes:
24
+ #
25
+ # * Kqueue does not generate notifications when "touch" is used to update
26
+ # a file's timestamp. This applies to Mac and BSD systems.
27
+ #
28
+ # * New files are detected only when the watched directory is polled at the
29
+ # configured interval.
30
+ #
31
+ class DirectoryWatcher::EmScanner < ::DirectoryWatcher::Scanner
32
+
33
+ # 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
+ #
39
+ def initialize( &block )
40
+ super(&block)
41
+ @timer = nil
42
+ @run_loop = lambda {_run_loop}
43
+ @watchers = {}
44
+ end
45
+
46
+ # Returns +true+ if the scanner is currently running. Returns +false+ if
47
+ # this is not the case.
48
+ #
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
+
62
+ unless EventMachine.reactor_running?
63
+ @thread = Thread.new {EventMachine.run}
64
+ Thread.pass until EventMachine.reactor_running?
65
+ end
66
+
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
78
+ end
79
+
80
+ # Stop the EventMachine scanner. If the scanner is already stopped this
81
+ # method will return without taking any action.
82
+ #
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
97
+ end
98
+
99
+ # call-seq:
100
+ # join( limit = nil )
101
+ #
102
+ # This is a no-op method for the EventMachine file scanner.
103
+ #
104
+ def join( limit = nil )
105
+ end
106
+
107
+ # :stopdoc:
108
+ #
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.
112
+ #
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)
128
+ end
129
+
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
153
+
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
162
+
163
+ added.each do |fn|
164
+ @files[fn] = _watch_file(fn).stat
165
+ @events << ::DirectoryWatcher::Event.new(:added, fn)
166
+ end
167
+ end
168
+
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
180
+ end
181
+ end
182
+ end
183
+
184
+ # Create and return a new Watcher instance for the given filename _fn_.
185
+ #
186
+ def _watch_file( fn )
187
+ @watchers[fn] = EventMachine.watch_file fn, Watcher, self
188
+ end
189
+
190
+ # :stopdoc:
191
+ #
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
197
+ def initialize( scanner )
198
+ @scanner = scanner
199
+ @active = true
200
+ end
201
+
202
+ def stat
203
+ return unless test ?e, @path
204
+ stat = File.stat @path
205
+ ::DirectoryWatcher::FileStat.new(stat.mtime, stat.size, @scanner.stable)
206
+ end
207
+
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!
215
+ end
216
+ # :startdoc:
217
+
218
+ end # class DirectoryWatcher::EmScanner
219
+ end # if HAVE_EM
220
+
221
+ # EOF
@@ -0,0 +1,183 @@
1
+
2
+ begin
3
+ require 'rev'
4
+ DirectoryWatcher::HAVE_REV = true
5
+ rescue LoadError
6
+ DirectoryWatcher::HAVE_REV = false
7
+ end
8
+
9
+ if DirectoryWatcher::HAVE_REV
10
+
11
+ # The RevScanner uses the Rev loop to monitor changes to files in the
12
+ # watched directory. This scanner is more efficient than the pure Ruby
13
+ # scanner because it relies on the operating system kernel notifictions
14
+ # instead of a periodic polling and stat of every file in the watched
15
+ # directory (the technique used by the Scanner class).
16
+ #
17
+ class DirectoryWatcher::RevScanner < ::DirectoryWatcher::Scanner
18
+
19
+ # 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_.
24
+ #
25
+ def initialize( &block )
26
+ super(&block)
27
+ @watchers = {}
28
+ end
29
+
30
+ # Start the Rev scanner loop. If the scanner is already running, this method
31
+ # will return without taking any action.
32
+ #
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
51
+ }
52
+ end
53
+
54
+ # Stop the Rev scanner loop. If the scanner is already stopped, this method
55
+ # will return without taking any action.
56
+ #
57
+ def stop
58
+ return unless running?
59
+
60
+ @thread._rev_loop.stop rescue nil
61
+ @thread = nil
62
+
63
+ @timer.detach
64
+ @timer = nil
65
+
66
+ @watchers.each_value {|w| w.detach}
67
+ @watchers.clear
68
+
69
+ notify
70
+ end
71
+
72
+ # :stopdoc:
73
+ #
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.
77
+ #
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
87
+ else
88
+ watcher.detach
89
+ @watchers.delete fn
90
+ @files.delete fn
91
+ @events << ::DirectoryWatcher::Event.new(:removed, fn)
92
+ end
93
+
94
+ notify
95
+ end
96
+
97
+ # This callback is invoked by the Timer instance when it is triggered by
98
+ # the Rev 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.
114
+ #
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.
129
+ #
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_.
142
+ #
143
+ def _watch_file( fn )
144
+ w = Watcher.new(fn, self)
145
+ w.attach(@thread ? @thread._rev_loop : Thread.current._rev_loop)
146
+ @watchers[fn] = w
147
+ end
148
+
149
+ # :stopdoc:
150
+ #
151
+ class Watcher < Rev::StatWatcher
152
+ def initialize( fn, scanner )
153
+ super(fn, scanner.interval)
154
+ @scanner = scanner
155
+ end
156
+
157
+ def on_change
158
+ @scanner._on_change self
159
+ end
160
+
161
+ def stat
162
+ return unless test ?e, path
163
+ stat = File.stat path
164
+ ::DirectoryWatcher::FileStat.new(stat.mtime, stat.size, @scanner.stable)
165
+ end
166
+ end
167
+
168
+ class Timer < Rev::TimerWatcher
169
+ def initialize( scanner )
170
+ super(scanner.interval, true)
171
+ @scanner = scanner
172
+ end
173
+
174
+ def on_timer
175
+ @scanner._on_timer
176
+ end
177
+ end
178
+ # :startdoc:
179
+
180
+ end # class DirectoryWatcher::RevScanner
181
+ end # if DirectoryWatcher::HAVE_REV
182
+
183
+ # EOF
@@ -0,0 +1,229 @@
1
+
2
+ # 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.
6
+ #
7
+ # The Scanner is a pure Ruby class, and as such it works across all Ruby
8
+ # interpreters on the major platforms. This also means that it can be
9
+ # processor intensive for large numbers of files or very fast update
10
+ # intervals. Your mileage will vary, but it is something to keep an eye on.
11
+ #
12
+ 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
156
+
157
+ # call-seq:
158
+ # find_added( files, cur, prev )
159
+ #
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.
163
+ #
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 )
175
+ #
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.
179
+ #
180
+ def find_removed( cur, prev )
181
+ removed = prev - cur
182
+ removed.each {|fn| @events << ::DirectoryWatcher::Event.new(:removed, fn)}
183
+ self
184
+ end
185
+
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
217
+ end
218
+
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.
222
+ #
223
+ def notify
224
+ @notify.call(@events) unless @events.empty?
225
+ ensure
226
+ @events.clear
227
+ end
228
+
229
+ end # class DirectoryWatcher::Scanner
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: directory_watcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pease
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-12 00:00:00 -06:00
12
+ date: 2009-10-21 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,7 +20,7 @@ dependencies:
20
20
  requirements:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: 2.5.0
23
+ version: 2.5.1
24
24
  version:
25
25
  description: ""
26
26
  email: tim.pease@gmail.com
@@ -36,6 +36,9 @@ files:
36
36
  - README.txt
37
37
  - Rakefile
38
38
  - lib/directory_watcher.rb
39
+ - lib/directory_watcher/em_scanner.rb
40
+ - lib/directory_watcher/rev_scanner.rb
41
+ - lib/directory_watcher/scanner.rb
39
42
  - tasks/ann.rake
40
43
  - tasks/bones.rake
41
44
  - tasks/gem.rake
@@ -51,6 +54,8 @@ files:
51
54
  - tasks/zentest.rake
52
55
  has_rdoc: true
53
56
  homepage: http://codeforpeople.rubyforge.org/directory_watcher
57
+ licenses: []
58
+
54
59
  post_install_message:
55
60
  rdoc_options:
56
61
  - --main
@@ -72,9 +77,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
77
  requirements: []
73
78
 
74
79
  rubyforge_project: codeforpeople
75
- rubygems_version: 1.3.1
80
+ rubygems_version: 1.3.5
76
81
  signing_key:
77
- specification_version: 2
82
+ specification_version: 3
78
83
  summary: A class for watching files within a directory and generating events when those files change
79
84
  test_files: []
80
85