From 598f850124f65b8f50a8cda72b1bea62789aad5c Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Wed, 2 Sep 2009 22:42:59 -0700 Subject: [PATCH] Upgrade to FSSM version 0.0.4 --- lib/vendor/fssm.rb | 23 ++-- lib/vendor/fssm/backends/fsevents.rb | 88 ++++-------- lib/vendor/fssm/backends/polling.rb | 36 ++--- lib/vendor/fssm/cache.rb | 193 +++++++++++++++++++++++++++ lib/vendor/fssm/ext.rb | 5 + lib/vendor/fssm/fsevents.rb | 129 ++++++++++++++++++ lib/vendor/fssm/monitor.rb | 16 ++- lib/vendor/fssm/path.rb | 45 ++++--- lib/vendor/fssm/state.rb | 53 ++++---- lib/vendor/fssm/support.rb | 10 +- 10 files changed, 450 insertions(+), 148 deletions(-) create mode 100644 lib/vendor/fssm/cache.rb create mode 100644 lib/vendor/fssm/fsevents.rb diff --git a/lib/vendor/fssm.rb b/lib/vendor/fssm.rb index d04b32aa..e73e3fed 100644 --- a/lib/vendor/fssm.rb +++ b/lib/vendor/fssm.rb @@ -1,30 +1,37 @@ +dir = File.dirname(__FILE__) +$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir) + module FSSM FileNotFoundError = Class.new(StandardError) CallbackError = Class.new(StandardError) - + class << self def monitor(*args, &block) monitor = FSSM::Monitor.new context = args.empty? ? monitor : monitor.path(*args) - if block && block.arity == 0 - context.instance_eval(&block) - elsif block && block.arity == 1 - block.call(context) + + if block_given? + if block.arity == 1 + block.call(context) + else + context.instance_eval(&block) + end end + monitor.run end end end -$:.unshift(File.dirname(__FILE__)) +require 'thread' require 'pathname' + require 'fssm/ext' require 'fssm/support' +require 'fssm/cache' require 'fssm/path' require 'fssm/state' require 'fssm/monitor' require "fssm/backends/#{FSSM::Support.backend.downcase}" FSSM::Backends::Default = FSSM::Backends.const_get(FSSM::Support.backend) -$:.shift - diff --git a/lib/vendor/fssm/backends/fsevents.rb b/lib/vendor/fssm/backends/fsevents.rb index 0790a922..3c457153 100644 --- a/lib/vendor/fssm/backends/fsevents.rb +++ b/lib/vendor/fssm/backends/fsevents.rb @@ -1,78 +1,36 @@ +require 'fssm/fsevents' + module FSSM::Backends - class FSEvents - def initialize(options={}) - @streams = [] - @handlers = {} - @allocator = options[:allocator] || OSX::KCFAllocatorDefault - @context = options[:context] || nil - @since = options[:since] || OSX::KFSEventStreamEventIdSinceNow - @latency = options[:latency] || 0.0 - @flags = options[:flags] || 0 + class FSEvents + def initialize + @handlers = {} + @fsevents = [] end def add_path(path, preload=true) - @handlers["#{path}"] = FSSM::State.new(path, preload) + handler = FSSM::State.new(path, preload) + @handlers["#{path}"] = handler - cb = lambda do |stream, context, number, paths, flags, ids| - paths.regard_as('*') - watched = OSX.FSEventStreamCopyPathsBeingWatched(stream).first - @handlers["#{watched}"].refresh - # TODO: support this level of granularity - # number.times do |n| - # @handlers["#{watched}"].refresh_path(paths[n]) - # end - end - - @streams << create_stream(cb, "#{path}") - end - - def run - @streams.each do |stream| - schedule_stream(stream) - start_stream(stream) - end - - begin - OSX.CFRunLoopRun - rescue Interrupt - @streams.each do |stream| - stop_stream(stream) - invalidate_stream(stream) - release_stream(stream) + fsevent = Rucola::FSEvents.new("#{path}") do |events| + events.each do |event| + handler.refresh(event.path) end end + fsevent.create_stream + fsevent.start + @fsevents << fsevent end - private - - def create_stream(callback, paths) - paths = [paths] unless paths.is_a?(Array) - OSX.FSEventStreamCreate(@allocator, callback, @context, paths, @since, @latency, @flags) + def run + begin + OSX.CFRunLoopRun + rescue Interrupt + @fsevents.each do |fsev| + fsev.stop + end + end end - - def schedule_stream(stream, options={}) - run_loop = options[:run_loop] || OSX.CFRunLoopGetCurrent - loop_mode = options[:loop_mode] || OSX::KCFRunLoopDefaultMode - - OSX.FSEventStreamScheduleWithRunLoop(stream, run_loop, loop_mode) - end - - def start_stream(stream) - OSX.FSEventStreamStart(stream) - end - - def stop_stream(stream) - OSX.FSEventStreamStop(stream) - end - - def invalidate_stream(stream) - OSX.FSEventStreamInvalidate(stream) - end - - def release_stream(stream) - OSX.FSEventStreamRelease(stream) - end - + end end diff --git a/lib/vendor/fssm/backends/polling.rb b/lib/vendor/fssm/backends/polling.rb index 08797757..41981447 100644 --- a/lib/vendor/fssm/backends/polling.rb +++ b/lib/vendor/fssm/backends/polling.rb @@ -1,24 +1,24 @@ module FSSM::Backends class Polling - def initialize(options={}) - @handlers = [] - @latency = options[:latency] || 1 - end - - def add_path(path, preload=true) - @handlers << FSSM::State.new(path, preload) - end - - def run - begin - loop do - start = Time.now.to_f - @handlers.each {|handler| handler.refresh} - nap_time = @latency - (Time.now.to_f - start) - sleep nap_time if nap_time > 0 + def initialize(options={}) + @handlers = [] + @latency = options[:latency] || 1 + end + + def add_path(path, preload=true) + @handlers << FSSM::State.new(path, preload) + end + + def run + begin + loop do + start = Time.now.to_f + @handlers.each {|handler| handler.refresh} + nap_time = @latency - (Time.now.to_f - start) + sleep nap_time if nap_time > 0 + end + rescue Interrupt end - rescue Interrupt end end end -end diff --git a/lib/vendor/fssm/cache.rb b/lib/vendor/fssm/cache.rb new file mode 100644 index 00000000..adeff963 --- /dev/null +++ b/lib/vendor/fssm/cache.rb @@ -0,0 +1,193 @@ +class FSSM::Cache + module Common + include Enumerable + + def initialize + @children = Hash.new + end + + def each(prefix='./', &block) + @children.each do |segment, node| + cprefix = Pathname.for(prefix.dup).join(segment) + block.call(cprefix, node) + node.each(cprefix, &block) + end + end + + protected + + def with_lock + @mutex.lock + yield + @mutex.unlock + end + + def descendant(path) + recurse_on_key(path, false) + end + + def descendant!(path) + recurse_on_key(path, true) + end + + def child(segment) + has_child?(segment) ? @children["#{segment}"] : nil + end + + def child!(segment) + (@children["#{segment}"] ||= Node.new) + end + + def has_child?(segment) + @children.include?("#{segment}") + end + + def remove_child(segment) + @children.delete("#{segment}") + end + + def remove_children + @children.clear + end + + def recurse_on_key(key, create) + key = sanitize_key(key) + node = self + + until key.empty? + segment = key.shift + node = create ? node.child!(segment) : node.child(segment) + return nil unless node + end + + node + end + + def key_for_path(path) + Pathname.for(path).names + end + + def relative_path(path) + sanitize_path(path, false) + end + + def absolute_path(path) + sanitize_path(path, true) + end + + def sanitize_path(path, absolute) + if path.is_a?(Array) + first = absolute ? '/' : path.shift + path = path.inject(Pathname.new("#{first}")) do |pathname, segment| + pathname.join("#{segment}") + end + path + else + path = Pathname.for(path) + absolute ? path.expand_path : path + end + end + end + + class Node + include Common + + attr_accessor :mtime + attr_accessor :ftype + + def <=>(other) + self.mtime <=> other.mtime + end + + def from_path(path) + path = absolute_path(path) + @mtime = path.mtime + @ftype = path.ftype + end + + protected + + def sanitize_key(key) + key_for_path(relative_path(key)) + end + end + + include Common + + def initialize + @mutex = Mutex.new + super + end + + def clear + @mutex.lock + @children.clear + @mutex.unlock + end + + def set(path) + unset(path) + node = descendant!(path) + node.from_path(path) + node.mtime + end + + def unset(path='/') + key = sanitize_key(path) + + if key.empty? + self.clear + return nil + end + + segment = key.pop + node = descendant(key) + + return unless node + + @mutex.lock + node.remove_child(segment) + @mutex.unlock + + nil + end + + def files + ftype('file') + end + + def directories + ftype('directory') + end + + protected + + def each(&block) + prefix='/' + super(prefix, &block) + end + + def ftype(ft) + inject({}) do |hash, entry| + path, node = entry + hash["#{path}"] = node.mtime if node.ftype == ft + hash + end + end + + def descendant(path) + node = recurse_on_key(path, false) + node + end + + def descendant!(path) + @mutex.lock + node = recurse_on_key(path, true) + @mutex.unlock + node + end + + def sanitize_key(key) + key_for_path(absolute_path(key)) + end +end diff --git a/lib/vendor/fssm/ext.rb b/lib/vendor/fssm/ext.rb index 50146174..fb6ca938 100644 --- a/lib/vendor/fssm/ext.rb +++ b/lib/vendor/fssm/ext.rb @@ -4,4 +4,9 @@ class Pathname path.is_a?(Pathname) ? path : new(path) end end + + def names + prefix, names = split_names(@path) + names + end end diff --git a/lib/vendor/fssm/fsevents.rb b/lib/vendor/fssm/fsevents.rb new file mode 100644 index 00000000..848c6ae2 --- /dev/null +++ b/lib/vendor/fssm/fsevents.rb @@ -0,0 +1,129 @@ +OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework' + +module Rucola + class FSEvents + class FSEvent + attr_reader :fsevents_object + attr_reader :id + attr_reader :path + def initialize(fsevents_object, id, path) + @fsevents_object, @id, @path = fsevents_object, id, path + end + + # Returns an array of the files/dirs in the path that the event occurred in. + # The files are sorted by the modification time, the first entry is the last modified file. + def files + Dir.glob("#{File.expand_path(path)}/*").sort_by {|f| File.mtime(f) }.reverse + end + + # Returns the last modified file in the path that the event occurred in. + def last_modified_file + files.first + end + end + + class StreamError < StandardError; end + + attr_reader :paths + attr_reader :stream + + attr_accessor :allocator + attr_accessor :context + attr_accessor :since + attr_accessor :latency + attr_accessor :flags + + # Initializes a new FSEvents `watchdog` object and starts watching the directories you specify for events. The + # block is used as a handler for events, which are passed as the block's argument. This method is the easiest + # way to start watching some directories if you don't care about the details of setting up the event stream. + # + # Rucola::FSEvents.start_watching('/tmp') do |events| + # events.each { |event| log.debug("#{event.files.inspect} were changed.") } + # end + # + # Rucola::FSEvents.start_watching('/var/log/system.log', '/var/log/secure.log', :since => last_id, :latency => 5) do + # Growl.notify("Something was added to your log files!") + # end + # + # Note that the method also returns the FSEvents object. This enables you to control the event stream if you want to. + # + # fsevents = Rucola::FSEvents.start_watching('/Volumes') do |events| + # events.each { |event| Growl.notify("Volume changes: #{event.files.to_sentence}") } + # end + # fsevents.stop + def self.start_watching(*params, &block) + fsevents = new(*params, &block) + fsevents.create_stream + fsevents.start + fsevents + end + + # Creates a new FSEvents `watchdog` object. You can specify a list of paths to watch and options to control the + # behaviour of the watchdog. The block you pass serves as a callback when an event is generated on one of the + # specified paths. + # + # fsevents = FSEvents.new('/etc/passwd') { Mailer.send_mail("Someone touched the password file!") } + # fsevents.create_stream + # fsevents.start + # + # fsevents = FSEvents.new('/home/upload', :since => UploadWatcher.last_event_id) do |events| + # events.each do |event| + # UploadWatcher.last_event_id = event.id + # event.files.each do |file| + # UploadWatcher.logfile.append("#{file} was changed") + # end + # end + # end + # + # *:since: The service will report events that have happened after the supplied event ID. Never use 0 because that + # will cause every fsevent since the "beginning of time" to be reported. Use OSX::KFSEventStreamEventIdSinceNow + # if you want to receive events that have happened after this call. (Default: OSX::KFSEventStreamEventIdSinceNow). + # You can find the ID's passed with :since in the events passed to your block. + # *:latency: Number of seconds to wait until an FSEvent is reported, this allows the service to bundle events. (Default: 0.0) + # + # Please refer to the Cocoa documentation for the rest of the options. + def initialize(*params, &block) + raise ArgumentError, 'No callback block was specified.' unless block_given? + + options = params.last.kind_of?(Hash) ? params.pop : {} + @paths = params.flatten + + paths.each { |path| raise ArgumentError, "The specified path (#{path}) does not exist." unless File.exist?(path) } + + @allocator = options[:allocator] || OSX::KCFAllocatorDefault + @context = options[:context] || nil + @since = options[:since] || OSX::KFSEventStreamEventIdSinceNow + @latency = options[:latency] || 0.0 + @flags = options[:flags] || 0 + @stream = options[:stream] || nil + + @user_callback = block + @callback = Proc.new do |stream, client_callback_info, number_of_events, paths_pointer, event_flags, event_ids| + paths_pointer.regard_as('*') + events = [] + number_of_events.times {|i| events << Rucola::FSEvents::FSEvent.new(self, event_ids[i], paths_pointer[i]) } + @user_callback.call(events) + end + end + + # Create the stream. + # Raises a Rucola::FSEvents::StreamError if the stream could not be created. + def create_stream + @stream = OSX.FSEventStreamCreate(@allocator, @callback, @context, @paths, @since, @latency, @flags) + raise(StreamError, 'Unable to create FSEvents stream.') unless @stream + OSX.FSEventStreamScheduleWithRunLoop(@stream, OSX.CFRunLoopGetCurrent, OSX::KCFRunLoopDefaultMode) + end + + # Start the stream. + # Raises a Rucola::FSEvents::StreamError if the stream could not be started. + def start + raise(StreamError, 'Unable to start FSEvents stream.') unless OSX.FSEventStreamStart(@stream) + end + + # Stop the stream. + # You can resume it by calling `start` again. + def stop + OSX.FSEventStreamStop(@stream) + end + end +end diff --git a/lib/vendor/fssm/monitor.rb b/lib/vendor/fssm/monitor.rb index bda386d0..b7cd0f99 100644 --- a/lib/vendor/fssm/monitor.rb +++ b/lib/vendor/fssm/monitor.rb @@ -3,18 +3,22 @@ class FSSM::Monitor @options = options @backend = FSSM::Backends::Default.new end - + def path(*args, &block) path = FSSM::Path.new(*args) - if block && block.arity == 0 - path.instance_eval(&block) - elsif block && block.arity == 1 - block.call(path) + + if block_given? + if block.arity == 1 + block.call(path) + else + path.instance_eval(&block) + end end + @backend.add_path(path) path end - + def run @backend.run end diff --git a/lib/vendor/fssm/path.rb b/lib/vendor/fssm/path.rb index 8b781338..16290ec4 100644 --- a/lib/vendor/fssm/path.rb +++ b/lib/vendor/fssm/path.rb @@ -3,45 +3,48 @@ class FSSM::Path set_path(path || '.') set_glob(glob || '**/*') init_callbacks - if block && block.arity == 0 - self.instance_eval(&block) - elsif block && block.arity == 1 - block.call(self) + + if block_given? + if block.arity == 1 + block.call(self) + else + self.instance_eval(&block) + end end end - + def to_s @path.to_s end - + def to_pathname @path end - + def glob(value=nil) return @glob if value.nil? set_glob(value) end - + def create(callback_or_path=nil, &block) callback_action(:create, (block_given? ? block : callback_or_path)) end - + def update(callback_or_path=nil, &block) callback_action(:update, (block_given? ? block : callback_or_path)) end - + def delete(callback_or_path=nil, &block) callback_action(:delete, (block_given? ? block : callback_or_path)) end - + private - + def init_callbacks do_nothing = lambda {|base, relative|} @callbacks = Hash.new(do_nothing) end - + def callback_action(type, arg=nil) if arg.is_a?(Proc) set_callback(type, arg) @@ -51,37 +54,37 @@ class FSSM::Path run_callback(type, arg) end end - + def set_callback(type, arg) raise ArgumentError, "Proc expected" unless arg.is_a?(Proc) @callbacks[type] = arg end - + def get_callback(type) @callbacks[type] end - + def run_callback(type, arg) base, relative = split_path(arg) - + begin @callbacks[type].call(base, relative) rescue Exception => e raise FSSM::CallbackError, "#{type} - #{base.join(relative)}: #{e.message}", e.backtrace end end - + def split_path(path) path = Pathname.for(path) - [@path, (path.relative? ? path : path.relative_path_from(@path))] + [@path, (path.relative? ? path : path.relative_path_from(@path))] end - + def set_path(path) path = Pathname.for(path) raise FSSM::FileNotFoundError, "#{path}" unless path.exist? @path = path.realpath end - + def set_glob(glob) @glob = glob.is_a?(Array) ? glob : [glob] end diff --git a/lib/vendor/fssm/state.rb b/lib/vendor/fssm/state.rb index fe514c3f..1c1ee23c 100644 --- a/lib/vendor/fssm/state.rb +++ b/lib/vendor/fssm/state.rb @@ -1,46 +1,53 @@ +require 'yaml' class FSSM::State def initialize(path, preload=true) @path = path - @snapshot = {} - snapshot if preload + @cache = FSSM::Cache.new + snapshot(@path.to_pathname) if preload end - - def refresh - previous = @snapshot - current = snapshot - + + def refresh(base=nil) + previous, current = recache(base || @path.to_pathname) + deleted(previous, current) created(previous, current) - modified(previous, current) + modified(previous, current) end - + private - + def created(previous, current) (current.keys - previous.keys).each {|created| @path.create(created)} end - + def deleted(previous, current) (previous.keys - current.keys).each {|deleted| @path.delete(deleted)} end - + def modified(previous, current) (current.keys & previous.keys).each do |file| @path.update(file) if (current[file] <=> previous[file]) != 0 end end - - def snapshot - snap = {} - @path.glob.each {|glob| add_glob(snap, glob)} - @snapshot = snap + + def recache(base) + base = Pathname.for(base) + previous = @cache.files + snapshot(base) + current = @cache.files + [previous, current] end - - def add_glob(snap, glob) - Pathname.glob(@path.to_pathname.join(glob)).each do |fn| - next unless fn.file? - snap["#{fn}"] = fn.mtime + + def snapshot(base) + base = Pathname.for(base) + @cache.unset(base) + @path.glob.each {|glob| add_glob(base, glob)} + end + + def add_glob(base, glob) + Pathname.glob(base.join(glob)).each do |fn| + @cache.set(fn) end end - + end diff --git a/lib/vendor/fssm/support.rb b/lib/vendor/fssm/support.rb index fe6aa896..68128b60 100644 --- a/lib/vendor/fssm/support.rb +++ b/lib/vendor/fssm/support.rb @@ -1,17 +1,13 @@ module FSSM::Support class << self - # def backend - # (mac? && carbon_core?) ? 'FSEvents' : 'Polling' - # end - def backend - 'Polling' + (mac? && carbon_core?) ? 'FSEvents' : 'Polling' end - + def mac? @@mac ||= RUBY_PLATFORM =~ /darwin/i end - + def carbon_core? @@carbon_core ||= begin require 'osx/foundation'