directory_watcher 1.4.1 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/History.txt +10 -0
- data/README.txt +1 -1
- data/Rakefile +16 -5
- data/lib/directory_watcher.rb +166 -139
- data/lib/directory_watcher/collector.rb +283 -0
- data/lib/directory_watcher/configuration.rb +228 -0
- data/lib/directory_watcher/coolio_scanner.rb +61 -127
- data/lib/directory_watcher/em_scanner.rb +81 -153
- data/lib/directory_watcher/event.rb +72 -0
- data/lib/directory_watcher/eventable_scanner.rb +242 -0
- data/lib/directory_watcher/file_stat.rb +65 -0
- data/lib/directory_watcher/logable.rb +26 -0
- data/lib/directory_watcher/notifier.rb +49 -0
- data/lib/directory_watcher/paths.rb +55 -0
- data/lib/directory_watcher/rev_scanner.rb +68 -131
- data/lib/directory_watcher/scan.rb +72 -0
- data/lib/directory_watcher/scan_and_queue.rb +22 -0
- data/lib/directory_watcher/scanner.rb +26 -209
- data/lib/directory_watcher/threaded.rb +277 -0
- data/lib/directory_watcher/version.rb +8 -0
- data/spec/directory_watcher_spec.rb +37 -0
- data/spec/paths_spec.rb +7 -0
- data/spec/scanner_scenarios.rb +236 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/utility_classes.rb +117 -0
- data/version.txt +1 -1
- metadata +123 -23
- data/bin/dw +0 -2
data/.gitignore
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# git-ls-files --others --exclude-from=.git/info/exclude
|
2
|
+
# Lines that start with '#' are comments.
|
3
|
+
# For a project mostly in C, the following would be a good set of
|
4
|
+
# exclude patterns (uncomment them if you want to use them):
|
5
|
+
# *.[oa]
|
6
|
+
# *~
|
7
|
+
announcement.txt
|
8
|
+
coverage
|
9
|
+
doc
|
10
|
+
pkg
|
11
|
+
tags
|
12
|
+
temp.dir
|
13
|
+
tmp.rb
|
14
|
+
tmp.yml
|
15
|
+
vendor
|
16
|
+
script
|
17
|
+
spec/scratch
|
data/History.txt
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
== Next Version / 2011-XX-XX
|
2
|
+
|
3
|
+
Major Enhancements
|
4
|
+
- tests!
|
5
|
+
- major refactor
|
6
|
+
|
7
|
+
Minor Enhancement
|
8
|
+
- events generated from full scans may be sorted by mtime or size
|
9
|
+
- stat information is propagated into the event
|
10
|
+
|
1
11
|
== 1.4.1 / 2011-08-29
|
2
12
|
|
3
13
|
Minor Enhancements
|
data/README.txt
CHANGED
@@ -41,7 +41,7 @@ the same disclaimer as the EventMachine functionality.
|
|
41
41
|
== LICENSE:
|
42
42
|
|
43
43
|
MIT License
|
44
|
-
Copyright (c) 2007 -
|
44
|
+
Copyright (c) 2007 - 2013
|
45
45
|
|
46
46
|
Permission is hereby granted, free of charge, to any person obtaining
|
47
47
|
a copy of this software and associated documentation files (the
|
data/Rakefile
CHANGED
@@ -5,16 +5,27 @@ rescue LoadError
|
|
5
5
|
abort '### please install the "bones" gem ###'
|
6
6
|
end
|
7
7
|
|
8
|
+
task :default => 'spec:run'
|
9
|
+
#task 'gem:release' => 'spec:run'
|
10
|
+
|
8
11
|
Bones {
|
9
12
|
name 'directory_watcher'
|
10
13
|
summary 'A class for watching files within a directory and generating events when those files change'
|
11
|
-
authors 'Tim Pease'
|
14
|
+
authors ['Tim Pease', 'Jeremy Hinegardner']
|
12
15
|
email 'tim.pease@gmail.com'
|
13
|
-
url 'http://
|
14
|
-
|
16
|
+
url 'http://rubygems.org/gems/directory_watcher'
|
17
|
+
|
18
|
+
spec.opts << "--color" << "--format documentation"
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
# these are optional dependencies for runtime, adding one of them will provide
|
21
|
+
# additional Scanner backends.
|
22
|
+
depend_on 'rev' , :development => true
|
18
23
|
depend_on 'eventmachine', :development => true
|
24
|
+
depend_on 'cool.io' , :development => true
|
25
|
+
|
26
|
+
depend_on 'bones-git' , '~> 1.2.4', :development => true
|
27
|
+
depend_on 'bones-rspec', '~> 2.0.1', :development => true
|
28
|
+
depend_on 'rspec' , '~> 2.7.0', :development => true
|
29
|
+
depend_on 'logging' , '~> 1.6.1', :development => true
|
19
30
|
}
|
20
31
|
|
data/lib/directory_watcher.rb
CHANGED
@@ -4,9 +4,15 @@
|
|
4
4
|
# See DirectoryWatcher for detailed documentation and usage.
|
5
5
|
#
|
6
6
|
|
7
|
-
require '
|
7
|
+
require 'set'
|
8
|
+
require 'thread'
|
8
9
|
require 'yaml'
|
9
10
|
|
11
|
+
require 'directory_watcher/paths'
|
12
|
+
require 'directory_watcher/version'
|
13
|
+
require 'directory_watcher/configuration'
|
14
|
+
require 'directory_watcher/logable'
|
15
|
+
|
10
16
|
# == Synopsis
|
11
17
|
#
|
12
18
|
# A class for watching files within a directory and generating events when
|
@@ -167,6 +173,21 @@ require 'yaml'
|
|
167
173
|
# dw.run_once
|
168
174
|
# dw.persist! # stores state to dw_state.yml
|
169
175
|
#
|
176
|
+
# === Ordering of Events
|
177
|
+
#
|
178
|
+
# In the case, particularly in the initial scan, or in cases where the Scanner
|
179
|
+
# may be doing a large pass over the monitored locations, many events may be
|
180
|
+
# generated all at once. In the default case, these will be emitted in the order
|
181
|
+
# in which they are observed, which tends to be alphabetical, but it not
|
182
|
+
# guaranteed. If you wish the events to be order by modified time, or file size
|
183
|
+
# this may be done by setting the +sort_by+ and/or the +order_by+ options.
|
184
|
+
#
|
185
|
+
# dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :sort_by => :mtime
|
186
|
+
# dw.add_observer {|*args| args.each {|event| puts event}}
|
187
|
+
# dw.start
|
188
|
+
# gets # when the user hits "enter" the script will terminate
|
189
|
+
# dw.stop
|
190
|
+
#
|
170
191
|
# === Scanning Strategies
|
171
192
|
#
|
172
193
|
# By default DirectoryWatcher uses a thread that scans the directory being
|
@@ -221,74 +242,12 @@ require 'yaml'
|
|
221
242
|
# Tim Pease
|
222
243
|
#
|
223
244
|
class DirectoryWatcher
|
245
|
+
extend Paths
|
246
|
+
extend Version
|
247
|
+
include Logable
|
224
248
|
|
225
|
-
#
|
226
|
-
|
227
|
-
#
|
228
|
-
# :added => file has been added to the directory
|
229
|
-
# :modified => file has been modified (either mtime or size or both
|
230
|
-
# have changed)
|
231
|
-
# :removed => file has been removed from the directory
|
232
|
-
# :stable => file has stabilized since being added or modified
|
233
|
-
#
|
234
|
-
Event = Struct.new(:type, :path) {
|
235
|
-
def to_s( ) "#{type} '#{path}'" end
|
236
|
-
}
|
237
|
-
|
238
|
-
# :stopdoc:
|
239
|
-
# A persistable file stat structure used internally by the directory
|
240
|
-
# watcher.
|
241
|
-
#
|
242
|
-
FileStat = Struct.new(:mtime, :size, :stable) {
|
243
|
-
def eql?( other )
|
244
|
-
return false unless other.instance_of? FileStat
|
245
|
-
self.mtime == other.mtime and self.size == other.size
|
246
|
-
end
|
247
|
-
alias :== :eql?
|
248
|
-
}
|
249
|
-
|
250
|
-
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
251
|
-
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
252
|
-
# :startdoc:
|
253
|
-
|
254
|
-
# Returns the version string for the library.
|
255
|
-
#
|
256
|
-
def self.version
|
257
|
-
@version ||= File.read(path('version.txt')).strip
|
258
|
-
end
|
259
|
-
|
260
|
-
# Returns the library path for the module. If any arguments are given,
|
261
|
-
# they will be joined to the end of the libray path using
|
262
|
-
# <tt>File.join</tt>.
|
263
|
-
#
|
264
|
-
def self.libpath( *args, &block )
|
265
|
-
rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
266
|
-
if block
|
267
|
-
begin
|
268
|
-
$LOAD_PATH.unshift LIBPATH
|
269
|
-
rv = block.call
|
270
|
-
ensure
|
271
|
-
$LOAD_PATH.shift
|
272
|
-
end
|
273
|
-
end
|
274
|
-
return rv
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns the lpath for the module. If any arguments are given, they
|
278
|
-
# will be joined to the end of the path using <tt>File.join</tt>.
|
279
|
-
#
|
280
|
-
def self.path( *args, &block )
|
281
|
-
rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
282
|
-
if block
|
283
|
-
begin
|
284
|
-
$LOAD_PATH.unshift PATH
|
285
|
-
rv = block.call
|
286
|
-
ensure
|
287
|
-
$LOAD_PATH.shift
|
288
|
-
end
|
289
|
-
end
|
290
|
-
return rv
|
291
|
-
end
|
249
|
+
# access the configuration of the DirectoryWatcher
|
250
|
+
attr_reader :config
|
292
251
|
|
293
252
|
# call-seq:
|
294
253
|
# DirectoryWatcher.new( directory, options )
|
@@ -312,33 +271,51 @@ class DirectoryWatcher
|
|
312
271
|
# stopped and started (respectively)
|
313
272
|
# :scanner => nil the directory scanning strategy to use with
|
314
273
|
# the directory watcher (either :coolio, :em, :rev or nil)
|
274
|
+
# :sort_by => :path the sort order of the scans, when there are
|
275
|
+
# multiple events ready for deliver. This can be
|
276
|
+
# one of:
|
277
|
+
#
|
278
|
+
# :path => default, order by file name
|
279
|
+
# :mtime => order by last modified time
|
280
|
+
# :size => order by file size
|
281
|
+
# :order_by => :ascending The direction in which the sorted items are
|
282
|
+
# sorted. Either :ascending or :descending
|
283
|
+
# :logger => nil An object that responds to the debug, info, warn,
|
284
|
+
# error and fatal methods. Using the default will
|
285
|
+
# use Logging gem if it is available and then fall
|
286
|
+
# back to NullLogger
|
315
287
|
#
|
316
288
|
# The default glob pattern will scan all files in the configured directory.
|
317
289
|
# Setting the :stable option to +nil+ will prevent stable events from being
|
318
290
|
# generated.
|
319
291
|
#
|
292
|
+
# Additional information about the available options is documented in the
|
293
|
+
# Configuration class.
|
294
|
+
#
|
320
295
|
def initialize( directory, opts = {} )
|
321
|
-
@dir = directory
|
322
296
|
@observer_peers = {}
|
297
|
+
@config = Configuration.new( opts.merge( :dir => directory ) )
|
298
|
+
|
299
|
+
setup_dir(config.dir)
|
323
300
|
|
324
|
-
|
325
|
-
|
326
|
-
|
301
|
+
@notifier = Notifier.new(config, @observer_peers)
|
302
|
+
@collector = Collector.new(config)
|
303
|
+
@scanner = config.scanner_class.new(config)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Setup the directory existence.
|
307
|
+
#
|
308
|
+
# Raise an error if the item passed in does exist but is not a directory
|
309
|
+
#
|
310
|
+
# Returns nothing
|
311
|
+
def setup_dir( dir )
|
312
|
+
if Kernel.test(?e, dir)
|
313
|
+
unless Kernel.test(?d, dir)
|
314
|
+
raise ArgumentError, "'#{dir}' is not a directory"
|
327
315
|
end
|
328
316
|
else
|
329
|
-
Dir.mkdir
|
317
|
+
Dir.mkdir dir
|
330
318
|
end
|
331
|
-
|
332
|
-
klass = opts[:scanner].to_s.capitalize + 'Scanner'
|
333
|
-
klass = DirectoryWatcher.const_get klass rescue Scanner
|
334
|
-
@scanner = klass.new {|events| notify_observers(events)}
|
335
|
-
|
336
|
-
self.glob = opts[:glob] || '*'
|
337
|
-
self.interval = opts[:interval] || 30
|
338
|
-
self.stable = opts[:stable] || nil
|
339
|
-
self.persist = opts[:persist]
|
340
|
-
|
341
|
-
@scanner.reset opts[:pre_load]
|
342
319
|
end
|
343
320
|
|
344
321
|
# call-seq:
|
@@ -365,6 +342,7 @@ class DirectoryWatcher
|
|
365
342
|
raise NoMethodError, "observer does not respond to `#{func.to_s}'"
|
366
343
|
end
|
367
344
|
|
345
|
+
logger.debug "Added observer"
|
368
346
|
@observer_peers[observer] = func
|
369
347
|
observer
|
370
348
|
end
|
@@ -396,21 +374,11 @@ class DirectoryWatcher
|
|
396
374
|
# files. A single glob pattern can be given or an array of glob patterns.
|
397
375
|
#
|
398
376
|
def glob=( val )
|
399
|
-
glob =
|
400
|
-
when String; [File.join(@dir, val)]
|
401
|
-
when Array; val.flatten.map! {|g| File.join(@dir, g)}
|
402
|
-
else
|
403
|
-
raise(ArgumentError,
|
404
|
-
'expecting a glob pattern or an array of glob patterns')
|
405
|
-
end
|
406
|
-
glob.uniq!
|
407
|
-
@scanner.glob = glob
|
377
|
+
config.glob = val
|
408
378
|
end
|
409
379
|
|
410
|
-
# Returns the array of glob patterns used to monitor files in the directory.
|
411
|
-
#
|
412
380
|
def glob
|
413
|
-
|
381
|
+
config.glob
|
414
382
|
end
|
415
383
|
|
416
384
|
# Sets the directory scan interval. The directory will be scanned every
|
@@ -418,15 +386,11 @@ class DirectoryWatcher
|
|
418
386
|
# Raises +ArgumentError+ if the interval is zero or negative.
|
419
387
|
#
|
420
388
|
def interval=( val )
|
421
|
-
|
422
|
-
raise ArgumentError, "interval must be greater than zero" if val <= 0
|
423
|
-
@scanner.interval = val
|
389
|
+
config.interval = val
|
424
390
|
end
|
425
391
|
|
426
|
-
# Returns the directory scan interval in seconds.
|
427
|
-
#
|
428
392
|
def interval
|
429
|
-
|
393
|
+
config.interval
|
430
394
|
end
|
431
395
|
|
432
396
|
# Sets the number of intervals a file must remain unchanged before it is
|
@@ -450,21 +414,11 @@ class DirectoryWatcher
|
|
450
414
|
# time is 60 seconds (15.0 * 4).
|
451
415
|
#
|
452
416
|
def stable=( val )
|
453
|
-
|
454
|
-
@scanner.stable = nil
|
455
|
-
return
|
456
|
-
end
|
457
|
-
|
458
|
-
val = Integer(val)
|
459
|
-
raise ArgumentError, "stable must be greater than zero" if val <= 0
|
460
|
-
@scanner.stable = val
|
417
|
+
config.stable = val
|
461
418
|
end
|
462
419
|
|
463
|
-
# Returs the number of intervals a file must remain unchanged before it is
|
464
|
-
# considered "stable".
|
465
|
-
#
|
466
420
|
def stable
|
467
|
-
|
421
|
+
config.stable
|
468
422
|
end
|
469
423
|
|
470
424
|
# Sets the name of the file to which the directory watcher state will be
|
@@ -472,9 +426,12 @@ class DirectoryWatcher
|
|
472
426
|
# disable this feature.
|
473
427
|
#
|
474
428
|
def persist=( filename )
|
475
|
-
|
429
|
+
config.persist = filename
|
430
|
+
end
|
431
|
+
|
432
|
+
def persist
|
433
|
+
config.persist
|
476
434
|
end
|
477
|
-
attr_reader :persist
|
478
435
|
|
479
436
|
# Write the current state of the directory watcher to the persist file.
|
480
437
|
# This method will do nothing if the directory watcher is running or if
|
@@ -482,8 +439,16 @@ class DirectoryWatcher
|
|
482
439
|
#
|
483
440
|
def persist!
|
484
441
|
return if running?
|
485
|
-
File.open(
|
442
|
+
File.open(persist, 'w') { |fd| @collector.dump_stats(fd) } if persist?
|
486
443
|
self
|
444
|
+
rescue => e
|
445
|
+
logger.error "Failure to write to persitence file #{persist.inspect} : #{e}"
|
446
|
+
end
|
447
|
+
|
448
|
+
# Is persistence done on this DirectoryWatcher
|
449
|
+
#
|
450
|
+
def persist?
|
451
|
+
config.persist
|
487
452
|
end
|
488
453
|
|
489
454
|
# Loads the state of the directory watcher from the persist file. This
|
@@ -492,7 +457,7 @@ class DirectoryWatcher
|
|
492
457
|
#
|
493
458
|
def load!
|
494
459
|
return if running?
|
495
|
-
|
460
|
+
File.open(persist, 'r') { |fd| @collector.load_stats(fd) } if persist? and test(?f, persist)
|
496
461
|
self
|
497
462
|
end
|
498
463
|
|
@@ -506,26 +471,93 @@ class DirectoryWatcher
|
|
506
471
|
# Start the directory watcher scanning thread. If the directory watcher is
|
507
472
|
# already running, this method will return without taking any action.
|
508
473
|
#
|
474
|
+
# Start returns one the scanner and the notifier say they are running
|
475
|
+
#
|
509
476
|
def start
|
477
|
+
logger.debug "start (running -> #{running?})"
|
510
478
|
return self if running?
|
511
479
|
|
512
480
|
load!
|
481
|
+
logger.debug "starting notifier #{@notifier.object_id}"
|
482
|
+
@notifier.start
|
483
|
+
Thread.pass until @notifier.running?
|
484
|
+
|
485
|
+
logger.debug "starting collector"
|
486
|
+
@collector.start
|
487
|
+
Thread.pass until @collector.running?
|
488
|
+
|
489
|
+
logger.debug "starting scanner"
|
513
490
|
@scanner.start
|
491
|
+
Thread.pass until @scanner.running?
|
492
|
+
|
514
493
|
self
|
515
494
|
end
|
516
495
|
|
496
|
+
# Pauses the scanner.
|
497
|
+
#
|
498
|
+
def pause
|
499
|
+
@scanner.pause
|
500
|
+
end
|
501
|
+
|
502
|
+
# Resume the emitting of events
|
503
|
+
#
|
504
|
+
def resume
|
505
|
+
@scanner.resume
|
506
|
+
end
|
507
|
+
|
517
508
|
# Stop the directory watcher scanning thread. If the directory watcher is
|
518
509
|
# already stopped, this method will return without taking any action.
|
519
510
|
#
|
511
|
+
# Stop returns once the scanner and notifier say they are no longer running
|
520
512
|
def stop
|
513
|
+
logger.debug "stop (running -> #{running?})"
|
521
514
|
return self unless running?
|
522
515
|
|
516
|
+
logger.debug"stopping scanner"
|
523
517
|
@scanner.stop
|
518
|
+
Thread.pass while @scanner.running?
|
519
|
+
|
520
|
+
logger.debug"stopping collector"
|
521
|
+
@collector.stop
|
522
|
+
Thread.pass while @collector.running?
|
523
|
+
|
524
|
+
logger.debug"stopping notifier"
|
525
|
+
@notifier.stop
|
526
|
+
Thread.pass while @notifier.running?
|
527
|
+
|
524
528
|
self
|
525
529
|
ensure
|
526
530
|
persist!
|
527
531
|
end
|
528
532
|
|
533
|
+
# Sets the maximum number of scans the scanner is to make on the directory
|
534
|
+
#
|
535
|
+
def maximum_iterations=( value )
|
536
|
+
@scanner.maximum_iterations = value
|
537
|
+
end
|
538
|
+
|
539
|
+
# Returns the maximum number of scans the directory scanner will perform
|
540
|
+
#
|
541
|
+
def maximum_iterations
|
542
|
+
@scanner.maximum_iterations
|
543
|
+
end
|
544
|
+
|
545
|
+
# Returns the number of scans of the directory scanner it has
|
546
|
+
# completed thus far.
|
547
|
+
#
|
548
|
+
# This will always report 0 unless a maximum number of scans has been set
|
549
|
+
#
|
550
|
+
def scans
|
551
|
+
@scanner.iterations
|
552
|
+
end
|
553
|
+
|
554
|
+
# Returns true if the maximum number of scans has been reached.
|
555
|
+
#
|
556
|
+
def finished_scans?
|
557
|
+
return true if maximum_iterations and (scans >= maximum_iterations)
|
558
|
+
return false
|
559
|
+
end
|
560
|
+
|
529
561
|
# call-seq:
|
530
562
|
# reset( pre_load = false )
|
531
563
|
#
|
@@ -540,7 +572,7 @@ class DirectoryWatcher
|
|
540
572
|
was_running = @scanner.running?
|
541
573
|
|
542
574
|
stop if was_running
|
543
|
-
File.delete(
|
575
|
+
File.delete(config.persist) if persist? and test(?f, config.persist)
|
544
576
|
@scanner.reset pre_load
|
545
577
|
start if was_running
|
546
578
|
self
|
@@ -565,29 +597,24 @@ class DirectoryWatcher
|
|
565
597
|
# the observers.
|
566
598
|
#
|
567
599
|
def run_once
|
568
|
-
@scanner.
|
600
|
+
@scanner.run
|
601
|
+
@collector.start unless running?
|
602
|
+
@notifier.start unless running?
|
569
603
|
self
|
570
604
|
end
|
571
|
-
|
572
|
-
|
573
|
-
private
|
574
|
-
|
575
|
-
# Invoke the update method of each registered observer in turn passing the
|
576
|
-
# list of file events to each.
|
577
|
-
#
|
578
|
-
def notify_observers( events )
|
579
|
-
@observer_peers.each do |observer, func|
|
580
|
-
begin; observer.send(func, *events); rescue Exception; end
|
581
|
-
end
|
582
|
-
end
|
583
|
-
|
584
605
|
end # class DirectoryWatcher
|
585
606
|
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
607
|
+
require 'directory_watcher/file_stat'
|
608
|
+
require 'directory_watcher/scan'
|
609
|
+
require 'directory_watcher/event'
|
610
|
+
require 'directory_watcher/threaded'
|
611
|
+
require 'directory_watcher/collector'
|
612
|
+
require 'directory_watcher/notifier'
|
613
|
+
require 'directory_watcher/scan_and_queue'
|
614
|
+
require 'directory_watcher/scanner'
|
615
|
+
require 'directory_watcher/eventable_scanner'
|
616
|
+
require 'directory_watcher/coolio_scanner'
|
617
|
+
require 'directory_watcher/em_scanner'
|
618
|
+
require 'directory_watcher/rev_scanner'
|
592
619
|
|
593
620
|
# EOF
|