directory_watcher 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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