Class DirectoryWatcher
In: lib/directory_watcher.rb
Parent: Object

Synopsis

A class for watching files within a directory and generating events when those files change.

Details

A directory watcher is an Observable object that sends events to registered observers when file changes are detected within the directory being watched.

The directory watcher operates by scanning the directory at some interval and creating a list of the files it finds. File events are detected by comparing the current file list with the file list from the previous scan interval. Three types of events are supported — added, modified, and removed.

An added event is generated when the file appears in the current file list but not in the previous scan interval file list. A removed event is generated when the file appears in the previous scan interval file list but not in the current file list. A modified event is generated when the file appears in the current and the previous interval file list, but the file modification time or the file size differs between the two lists.

The file events are collected into an array, and all registered observers receive all file events for each scan interval. It is up to the individual observers to filter the events they are interested in.

File Selection

The directory watcher uses glob patterns to select the files to scan. The default glob pattern will select all regular files in the directory of interest ’*’.

Here are a few useful glob examples:

    '*'               => all files in the current directory
    '**/*'            => all files in all subdirectories
    '**/*.rb'         => all ruby files
    'ext/**/*.{h,c}'  => all C source code files

Note: file events will never be generated for directories. Only regular files are included in the file scan.

Stable Files

A fourth file event is supported but not enabled by default — the stable event. This event is generated after a file has been added or modified and then remains unchanged for a certain number of scan intervals.

To enable the generation of this event the stable count must be configured. This is the number of scan intervals a file must remain unchanged (based modification time and file size) before it is considered stable.

To disable this event the stable count should be set to nil.

Usage

Learn by Doing — here are a few different ways to configure and use a directory watcher.

Basic

This basic recipe will watch all files in the current directory and generate the three default events. We‘ll register an observer that simply prints the events to standard out.

   require 'directory_watcher'

   dw = DirectoryWatcher.new '.'
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.start
   gets      # when the user hits "enter" the script will terminate
   dw.stop

Suppress Initial "added" Events

This little twist will suppress the initial "added" events that are generated the first time the directory is scanned. This is done by pre-loading the watcher with files — i.e. telling the watcher to scan for files before actually starting the scan loop.

   require 'directory_watcher'

   dw = DirectoryWatcher.new '.', :pre_load => true
   dw.glob = '**/*.rb'
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.start
   gets      # when the user hits "enter" the script will terminate
   dw.stop

There is one catch with this recipe. The glob pattern must be specified before the pre-load takes place. The glob pattern can be given as an option to the constructor:

   dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :pre_load => true

The other option is to use the reset method:

   dw = DirectoryWatcher.new '.'
   dw.glob = '**/*.rb'
   dw.reset true     # the +true+ flag causes the watcher to pre-load
                     # the files

Generate "stable" Events

In order to generate stable events, the stable count must be specified. In this example the interval is set to 5.0 seconds and the stable count is set to 2. Stable events will only be generated for files after they have remain unchanged for 10 seconds (5.0 * 2).

   require 'directory_watcher'

   dw = DirectoryWatcher.new '.', :glob => '**/*.rb'
   dw.interval = 5.0
   dw.stable = 2
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.start
   gets      # when the user hits "enter" the script will terminate
   dw.stop

Persisting State

A directory watcher can be configured to persist its current state to a file when it is stopped and to load state from that same file when it starts. Setting the persist value to a filename will enable this feature.

   require 'directory_watcher'

   dw = DirectoryWatcher.new '.', :glob => '**/*.rb'
   dw.interval = 5.0
   dw.persist = "dw_state.yml"
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.start  # loads state from dw_state.yml
   gets      # when the user hits "enter" the script will terminate
   dw.stop   # stores state to dw_state.yml

Running Once

Instead of using the built in run loop, the directory watcher can be run one or many times using the run_once method. The state of the directory watcher can be loaded and dumped if so desired.

   dw = DirectoryWatcher.new '.', :glob => '**/*.rb'
   dw.persist = "dw_state.yml"
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.load!       # loads state from dw_state.yml
   dw.run_once
   sleep 5.0
   dw.run_once
   dw.persist!    # stores state to dw_state.yml

Scanning Strategies

By default DirectoryWatcher uses a thread that scans the directory being watched for files and calls "stat" on each file. The stat information is used to determine which files have been modified, added, removed, etc. This approach is fairly intensive for short intervals and/or directories with many files.

DirectoryWatcher supports using Rev () or EventMachine () instead of a busy polling thread. These libraries use system level kernel hooks to receive notifications of file system changes. This makes DirectoryWorker much more efficient.

This example will use Rev to generate file notifications.

   dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :scanner => :rev
   dw.add_observer {|*args| args.each {|event| puts event}}

   dw.start
   gets      # when the user hits "enter" the script will terminate
   dw.stop

The scanner cannot be changed after the DirectoryWatcher has been created. To use an EventMachine scanner, pass :em as the :scanner option.

Contact

A lot of discussion happens about Ruby in general on the ruby-talk mailing list (www.ruby-lang.org/en/ml.html), and you can ask any questions you might have there. I monitor the list, as do many other helpful Rubyists, and you‘re sure to get a quick answer. Of course, you‘re also welcome to email me (Tim Pease) directly at the at tim.pease@gmail.com, and I‘ll do my best to help you out.

(the above paragraph was blatantly stolen from Nathaniel Talbott‘s Test::Unit documentation)

Author

Tim Pease

Methods

Classes and Modules

Class DirectoryWatcher::EmScanner
Class DirectoryWatcher::RevScanner
Class DirectoryWatcher::Scanner

Constants

Event = Struct.new(:type, :path) { def to_s( ) "#{type} '#{path}'" end   An Event structure contains the type of the event and the file path to which the event pertains. The type can be one of the following:
   :added      =>  file has been added to the directory
   :modified   =>  file has been modified (either mtime or size or both
                   have changed)
   :removed    =>  file has been removed from the directory
   :stable     =>  file has stabilized since being added or modified

Attributes

glob  [R] 
persist  [R] 

Public Class methods

Create a new DirectoryWatcher that will generate events when file changes are detected in the given directory. If the directory does not exist, it will be created. The following options can be passed to this method:

   :glob      =>  '*'      file glob pattern to restrict scanning
   :interval  =>  30.0     the directory scan interval (in seconds)
   :stable    =>  nil      the number of intervals a file must remain
                           unchanged for it to be considered "stable"
   :pre_load  =>  false    setting this option to true will pre-load the
                           file list effectively skipping the initial
                           round of file added events that would normally
                           be generated (glob pattern must also be
                           specified otherwise odd things will happen)
   :persist   =>  file     the state will be persisted to and restored
                           from the file when the directory watcher is
                           stopped and started (respectively)
   :scanner   =>  nil      the directory scanning strategy to use with
                           the directory watcher (either :em :rev or nil)

The default glob pattern will scan all files in the configured directory. Setting the :stable option to nil will prevent stable events from being generated.

[Source]

# File lib/directory_watcher.rb, line 269
  def initialize( directory, opts = {} )
    @dir = directory
    @observer_peers = {}

    if Kernel.test(?e, @dir)
      unless Kernel.test(?d, @dir)
        raise ArgumentError, "'#{@dir}' is not a directory"
      end
    else
      Dir.mkdir @dir
    end

    klass = opts[:scanner].to_s.capitalize + 'Scanner'
    klass = DirectoryWatcher.const_get klass rescue Scanner
    @scanner = klass.new {|events| notify_observers(events)}

    self.glob = opts[:glob] || '*'
    self.interval = opts[:interval] || 30
    self.stable = opts[:stable] || nil
    self.persist = opts[:persist]

    @scanner.reset opts[:pre_load]
  end

Public Instance methods

Adds the given observer as an observer on this directory watcher. The observer will now receive file events when they are generated. The second optional argument specifies a method to notify updates, of which the default value is update.

Optionally, a block can be passed as the observer. The block will be executed with the file events passed as the arguments. A reference to the underlying Proc object will be returned for use with the delete_observer method.

[Source]

# File lib/directory_watcher.rb, line 307
  def add_observer( observer = nil, func = :update, &block )
    unless block.nil?
      observer = block.to_proc
      func = :call
    end

    unless observer.respond_to? func
      raise NoMethodError, "observer does not respond to `#{func.to_s}'"
    end

    @observer_peers[observer] = func
    observer
  end

Return the number of observers associated with this directory watcher..

[Source]

# File lib/directory_watcher.rb, line 336
  def count_observers
    @observer_peers.size
  end

Delete observer as an observer of this directory watcher. It will no longer receive notifications.

[Source]

# File lib/directory_watcher.rb, line 324
  def delete_observer( observer )
    @observer_peers.delete observer
  end

Delete all observers associated with the directory watcher.

[Source]

# File lib/directory_watcher.rb, line 330
  def delete_observers
    @observer_peers.clear
  end

Sets the glob pattern that will be used when scanning the directory for files. A single glob pattern can be given or an array of glob patterns.

[Source]

# File lib/directory_watcher.rb, line 347
  def glob=( val )
    glob = case val
           when String; [File.join(@dir, val)]
           when Array; val.flatten.map! {|g| File.join(@dir, g)}
           else
             raise(ArgumentError,
                   'expecting a glob pattern or an array of glob patterns')
           end
    glob.uniq!
    @scanner.glob = glob
  end

Returns the directory scan interval in seconds.

[Source]

# File lib/directory_watcher.rb, line 372
  def interval
    @scanner.interval
  end

Sets the directory scan interval. The directory will be scanned every interval seconds for changes to files matching the glob pattern. Raises ArgumentError if the interval is zero or negative.

[Source]

# File lib/directory_watcher.rb, line 364
  def interval=( val )
    val = Float(val)
    raise ArgumentError, "interval must be greater than zero" if val <= 0
    @scanner.interval = Float(val)
  end

If the directory watcher is running, the calling thread will suspend execution and run the directory watcher thread. This method does not return until the directory watcher is stopped or until limit seconds have passed.

If the directory watcher is not running, this method returns immediately with nil.

[Source]

# File lib/directory_watcher.rb, line 503
  def join( limit = nil )
    @scanner.join limit
  end

Loads the state of the directory watcher from the persist file. This method will do nothing if the directory watcher is running or if the persist file is not configured.

[Source]

# File lib/directory_watcher.rb, line 437
  def load!
    return if running?
    @scanner.files = YAML.load_file(@persist) if @persist and test(?f, @persist)
    self
  end

Write the current state of the directory watcher to the persist file. This method will do nothing if the directory watcher is running or if the persist file is not configured.

[Source]

# File lib/directory_watcher.rb, line 427
  def persist!
    return if running?
    File.open(@persist, 'w') {|fd| fd.write YAML.dump(@scanner.files)} if @persist
    self
  end

Sets the name of the file to which the directory watcher state will be persisted when it is stopped. Setting the persist filename to nil will disable this feature.

[Source]

# File lib/directory_watcher.rb, line 418
  def persist=( filename )
    @persist = filename ? filename.to_s : nil
  end

Reset the directory watcher state by clearing the stored file list. If the directory watcher is running, it will be stopped, the file list cleared, and then restarted. Passing true to this method will cause the file list to be pre-loaded after it has been cleared effectively skipping the initial round of file added events that would normally be generated.

[Source]

# File lib/directory_watcher.rb, line 483
  def reset( pre_load = false )
    was_running = @scanner.running?

    stop if was_running
    File.delete(@persist) if @persist and test(?f, @persist)
    @scanner.reset pre_load
    start if was_running
  end

Performs exactly one scan of the directory for file changes and notifies the observers.

[Source]

# File lib/directory_watcher.rb, line 510
  def run_once
    @scanner.run_once
    self
  end

Returns true if the directory watcher is currently running. Returns false if this is not the case.

[Source]

# File lib/directory_watcher.rb, line 446
  def running?
    @scanner.running?
  end

Returs the number of intervals a file must remain unchanged before it is considered "stable".

[Source]

# File lib/directory_watcher.rb, line 410
  def stable
    @scanner.stable
  end

Sets the number of intervals a file must remain unchanged before it is considered "stable". When this condition is met, a stable event is generated for the file. If stable is set to nil then stable events will not be generated.

A stable event will be generated once for a file. Another stable event will only be generated after the file has been modified and then remains unchanged for stable intervals.

Example:

    dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' )
    dw.interval = 15.0
    dw.stable = 4

In this example, a directory watcher is configured to look for swap files in the /tmp directory. Stable events will be generated every 4 scan intervals iff a swap remains unchanged for that time. In this case the time is 60 seconds (15.0 * 4).

[Source]

# File lib/directory_watcher.rb, line 396
  def stable=( val )
    if val.nil?
      @scanner.stable = nil
      return
    end

    val = Integer(val)
    raise ArgumentError, "stable must be greater than zero" if val <= 0
    @scanner.stable = val
  end

Start the directory watcher scanning thread. If the directory watcher is already running, this method will return without taking any action.

[Source]

# File lib/directory_watcher.rb, line 453
  def start
    return if running?

    load!
    @scanner.start
    self
  end

Stop the directory watcher scanning thread. If the directory watcher is already stopped, this method will return without taking any action.

[Source]

# File lib/directory_watcher.rb, line 464
  def stop
    return unless running?

    @scanner.stop
    self
  ensure
    persist!
  end

[Validate]