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 +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
|
|