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 +9 -0
- data/README.txt +13 -2
- data/lib/directory_watcher.rb +95 -161
- data/lib/directory_watcher/em_scanner.rb +221 -0
- data/lib/directory_watcher/rev_scanner.rb +183 -0
- data/lib/directory_watcher/scanner.rb +229 -0
- metadata +10 -5
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
|
-
|
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 -
|
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
|
data/lib/directory_watcher.rb
CHANGED
@@ -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.
|
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)
|
225
|
+
Event = Struct.new(:type, :path) {
|
200
226
|
def to_s( ) "#{type} '#{path}'" end
|
201
|
-
|
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)
|
208
|
-
def
|
209
|
-
return unless other.
|
210
|
-
self.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
|
-
|
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
|
-
@
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
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
|
-
@
|
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
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
#
|
493
|
-
#
|
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
|
-
|
547
|
-
|
548
|
-
|
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.
|
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-
|
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.
|
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.
|
80
|
+
rubygems_version: 1.3.5
|
76
81
|
signing_key:
|
77
|
-
specification_version:
|
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
|
|