Channels ▼
RSS

Open Source

Sweetening the Plain Vanilla .NET FileSystemWatcher


Implementing the Actual Watcher

At this point, we're able to configure a watcher, but it won't do anything useful because the implementation of that class is still open on the to-do list. This class is really the meat of this DSL; everything else around it is to enable the syntax, but without the implementation, it's just another way of building hashes of a certain structure.

We left off at the builder initializing the watcher class. All this does is move the data structures from the builder into the watcher implementation. This watcher has a public interface but most of its work is implemented in private methods.

Again, the behavior of the Watcher implementation has been captured in a minimal amount of specs as shown in Listing 5. In addition to the syntax, this Watcher class also has the ability to be started, to be stopped, and to handle registered events.


Watcher

# Syntax specs removed for brevity 

handling events
- should handle events without guards
- should handle events with passing guards
- should not raise events with failing guards
- should trigger events with passing guards
- should not trigger events with failing guards

engine
- should start the watcher
- should stop the watcher
- should dispose the watcher
   
Listing 5: Minimal specs for the Watcher

Because the FileSystemWatcher uses unmanaged resources, it is probably a good idea to wait with creating that instance to the last possible moment. This also gives us the opportunity to manipulate the configuration before we actually start the watcher. There are two main jobs for the watcher class: the first job is to handle the events when they pass the guards and the second main job is to start/stop/dispose the FileSystemWatcher. Let's start with handling events and work our way up from there. This handle method is about the most complex method from this article; the code for this method is shown in Listing 6.


def handle(action, args)
  @handled ||= {}
  path = args_path(action, args)                                          #1
  hand = @handlers[action.to_sym]                                         #2
  hand.each_pair do |filter, handlers|
    handlers.each do |h|
      if not handled?(h, path)                                            #4
        h.call(args)                                                      #5
        @handled[path] ||= []
        @handled[path] << h                                               #6
      end
    end if passes_guard?(filter, path)                                    #3
  end
  nil
end

private
def passes_guard?(guard, path)
  return true if guard == :default
  guard.to_watcher_guard.match(path.gsub(/\\/, "/"))                      #7
end

def handled?(handler, path)
  (@handled[path]||[]).include?(handler)
end

def args_path(action, args)
  (action == :rename ? args.old_full_path : args.full_path)
end

#1 Getting the path for the trigger
#2 Getting the handler registrations
#3 Checking guards
#4 Skipping already handled handlers
#5 Handling the event
#6 Registering event as handled
#7 Calling monkey patched method

Listing 6: The handle method implementation

As you can see in the listing, the handle method actually does a bunch of things. The first notable item is the call to args_path (#1), which will return the path that originally triggered the event because a rename has different event arguments as the rest of the events. Then, we grab the registered handlers for this action (#2). They are the direct result of the configuration we did earlier with the builder. After getting the registered handlers, we iterate over them so we can try calling the handlers. The first thing we do in the loop is work out if this particular handler passes the guards for this path (#3). If this handler passes the guards, we're going to check if this instance has already been handled (#4). When this last check also succeeds, we can finally call the handler (#5) and register this handler as one of the handled events (#6).

We did glance over the private methods that have been defined in the code listing. There is one thing there I'd like to draw your attention to. In the passes_guard? method, there is a call to the method to_watcher_guard (#7). The code for that method is shown in Listing 7 and the method is supposed to take care of converting glob-like patterns (spec/**/*.rb?) to regular expressions so they can be used as watcher guards.


class String
  
  alias :old_gsub :gsub
  def gsub(pattern, replacement=nil, &b)
    if pattern.is_a? Hash                                                 #1
      pattern.inject(self.dup) { |memo, k| memo.old_gsub(k.first, k.last) }
    else
      self.old_gsub(pattern, replacement, &b)
    end
  end
  
  def to_watcher_guard
    re_str = self.gsub /(\/)/ => '\/',                                    #2
                       /\./ => '\.', 
                       /\?/ => '.?', 
                       /\*/ => ".*", 
                       /\.\*\.\*\\\// => "(.*\\/)?"
    /#{re_str}/
  end
  
end

class Regexp
  
  def to_watcher_guard
    self
  end
end

#1 Augmenting behavior of gsub
#2 Very basic glob pattern parser

Listing 7: Converting glob like patterns

We want to augment the behavior of the String class so we're going to take advantage of the fact that classes are open in Ruby. I want to get to the point that I can provide a hash of gsub replacement instructions so that I can avoid writing something like my_string.gsub(/a/, '1').gsub(/b/, '2')…gsub(/z/, '26'). To do that, I first created an alias for the gsub method with the name old_gsub. This allows me to create a new gsub method and call that aliased method to get the old behavior back. Our gsub method checks if the first parameter is a hash (#1) and, if it is, it iterates the pairs and execute the old gsub method passing the result to the next iteration.

We've also got a second method called to_watcher_guard defined on both the String and the Regexp class. People can define guards as either a glob pattern or a regular expression. And the passes_guard? method from Listing 6 calls this to_watcher_guard #2 method on the registered guards for a particular event. This method takes care of converting a string value to a regular expression by:

  • Normalizing \ paths to / paths
  • Escaping '.' characters
  • Replacing the single character wildcard '?' with a regular expression for an optional character '.?'
  • Replacing the multicharacter wildcard * with a regular expression for optional multiple characters '.*'
  • Replacing the recurse in subdirectories wildcard ** with a regular expression for optional folders

So far, our DSL execution engine checks the guards on the individual handlers, but we also have to get in front of those and check global guards first. The code for that functionality is shown in Listing 8.


def trigger(action, args)
  @handled = {}
  return nil unless guards_pass_for action, args                          #A
  handle action, args
end

private 
def guards_pass_for(action, args)
  return true if filters.empty? 
  filters.all? { |g| passes_guard?(g, args_path(action, args)) }
end

#A Checking global guards

Listing 8: Checking the global guards

There isn't that much interesting happening in Listing 8. We check the guards by iterating over the filters collection and, if they pass, we invoke the handle method from Listing 6. This brings us to the last chunk of functionality that's open for discussion: starting and stopping the watcher. Listing 9 holds the last bit of code to complete the watcher implementation.


ACTION_MAP = {                                                            #1
  :changed => :change, 
  :created => :create, 
  :deleted => :delete, 
  :renamed => :rename, 
  :error   => :error 
} 

def start
  @watcher ||= init_watcher
  unless @watcher.enable_raising_events
    @watcher.enable_raising_events = true 
  end
end

def stop
  if @watcher and @watcher.enable_raising_events
    @watcher.enable_raising_events = false                                #4
  end
end

def dispose
  self.stop
  @watcher.dispose if @watcher                                            #5
  @watcher = nil
end

private
def init_watcher
  watcher = System::IO::FileSystemWatcher.new @path                       #2
  watcher.include_subdirectories = @subdirs
  setup_internal_handlers watcher
  @watcher = watcher
end

def setup_internal_handlers(watcher)
  ACTION_MAP.each_pair do |event, action|                                 #3
    nothing_registered = (@handlers[action]||{}).empty?
    unless nothing_registered  
      watcher.send("#{event}") { |_, args| trigger action, args } 
    end
  end
end

#1 Defining action name map
#2 Creating FileSystemWatcher object
#3 Setting up handlers on the watcher
#4 Stopping the FileSystemWatcher
#5 Disposing the FileSystemWatcher

Listing 9: Starting and stopping the watcher

This code first defines a constant value that holds a Hash of the .NET event name and the action name we create when the user of the DSL defines an event handler (#1). The start method first sets up an instance of the CLR object System.IO.FileSystemWatcher (#2) and then configures this instance with the include subdirectories value we get from the DSL. Next, it sets up the configured event handlers. So, if there is a handler defined for create, it will attach a block to the Created event on the FileSystemWatcher instance (#3). This event handler invokes the trigger method, which we discussed in Listing 8. The start method then sets the enabling_raising_events property to rue, which starts the watcher. The stop method doesn't do anything else except set the enable_raising_events property to False (#4).

The dispose method takes care of cleaning up the resources for the watcher. It first stops the FileSystemWatcher instance, then we dispose the FileSystemWatcher instance (#5) and set its instance variable to nil so it's hopefully completely gone.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video