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



