directory_watcher 1.4.1 → 1.5.1

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.
@@ -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
@@ -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 - 2009
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://gemcutter.org/gems/directory_watcher'
14
- ignore_file '.gitignore'
16
+ url 'http://rubygems.org/gems/directory_watcher'
17
+
18
+ spec.opts << "--color" << "--format documentation"
15
19
 
16
- depend_on 'bones-git', :development => true
17
- depend_on 'rev', :development => true
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
 
@@ -4,9 +4,15 @@
4
4
  # See DirectoryWatcher for detailed documentation and usage.
5
5
  #
6
6
 
7
- require 'observer'
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
- # An +Event+ structure contains the _type_ of the event and the file _path_
226
- # to which the event pertains. The type can be one of the following:
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
- if Kernel.test(?e, @dir)
325
- unless Kernel.test(?d, @dir)
326
- raise ArgumentError, "'#{@dir}' is not a directory"
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 @dir
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 = case val
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
- @scanner.glob
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
- val = Float(val)
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
- @scanner.interval
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
- if val.nil?
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
- @scanner.stable
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
- @persist = filename ? filename.to_s : nil
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(@persist, 'w') {|fd| fd.write YAML.dump(@scanner.files)} if @persist
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
- @scanner.files = YAML.load_file(@persist) if @persist and test(?f, @persist)
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(@persist) if @persist and test(?f, @persist)
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.run_once
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
- DirectoryWatcher.libpath {
587
- require 'directory_watcher/scanner'
588
- require 'directory_watcher/coolio_scanner'
589
- require 'directory_watcher/em_scanner'
590
- require 'directory_watcher/rev_scanner'
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