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.
@@ -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