Skip to content

Commit

Permalink
Use FSSM by Travis Tilley to monitor for filesystem changes. On mac t…
Browse files Browse the repository at this point in the history
…his will use filesystem events instead of polling. Fixes an infinite looping issue when compilation errors occur.
  • Loading branch information
chriseppstein committed Aug 30, 2009
1 parent d40f72a commit 005f6d4
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 8 deletions.
30 changes: 22 additions & 8 deletions lib/compass/commands/watch_project.rb
Expand Up @@ -14,17 +14,30 @@ def perform
puts ""
exit 0
end

recompile

puts ">>> Compass is watching for changes. Press Ctrl-C to Stop."
loop do
# TODO: Make this efficient by using filesystem monitoring.
compiler = new_compiler_instance(:quiet => true)
remove_obsolete_css(compiler)
recompile(compiler)
sleep 1

require File.join(Compass.lib_directory, 'vendor', 'fssm')

FSSM.monitor do |monitor|
Compass.configuration.sass_load_paths.each do |load_path|
monitor.path load_path do |path|
path.glob '**/*.sass'

path.update &method(:recompile)
path.delete {|base, relative| remove_obsolete_css(base,relative); recompile(base, relative)}
path.create &method(:recompile)
end
end

end

end

def remove_obsolete_css(compiler)
def remove_obsolete_css(base = nil, relative = nil)
compiler = new_compiler_instance(:quiet => true)
sass_files = compiler.sass_files
deleted_sass_files = (last_sass_files || []) - sass_files
deleted_sass_files.each do |deleted_sass_file|
Expand All @@ -34,7 +47,8 @@ def remove_obsolete_css(compiler)
self.last_sass_files = sass_files
end

def recompile(compiler)
def recompile(base = nil, relative = nil)
compiler = new_compiler_instance(:quiet => true)
if file = compiler.out_of_date?
begin
puts ">>> Change detected to: #{file}"
Expand Down
30 changes: 30 additions & 0 deletions lib/vendor/fssm.rb
@@ -0,0 +1,30 @@
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)
end
monitor.run
end
end
end

$:.unshift(File.dirname(__FILE__))
require 'pathname'
require 'fssm/ext'
require 'fssm/support'
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

78 changes: 78 additions & 0 deletions lib/vendor/fssm/backends/fsevents.rb
@@ -0,0 +1,78 @@
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
end

def add_path(path, preload=true)
@handlers["#{path}"] = FSSM::State.new(path, preload)

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)
end
end

end

private

def create_stream(callback, paths)
paths = [paths] unless paths.is_a?(Array)
OSX.FSEventStreamCreate(@allocator, callback, @context, paths, @since, @latency, @flags)
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
24 changes: 24 additions & 0 deletions lib/vendor/fssm/backends/polling.rb
@@ -0,0 +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
end
rescue Interrupt
end
end
end
end
7 changes: 7 additions & 0 deletions lib/vendor/fssm/ext.rb
@@ -0,0 +1,7 @@
class Pathname
class << self
def for(path)
path.is_a?(Pathname) ? path : new(path)
end
end
end
21 changes: 21 additions & 0 deletions lib/vendor/fssm/monitor.rb
@@ -0,0 +1,21 @@
class FSSM::Monitor
def initialize(options={})
@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)
end
@backend.add_path(path)
path
end

def run
@backend.run
end
end
88 changes: 88 additions & 0 deletions lib/vendor/fssm/path.rb
@@ -0,0 +1,88 @@
class FSSM::Path
def initialize(path=nil, glob=nil, &block)
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)
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)
elsif arg.nil?
get_callback(type)
else
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))]
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
end
46 changes: 46 additions & 0 deletions lib/vendor/fssm/state.rb
@@ -0,0 +1,46 @@
class FSSM::State
def initialize(path, preload=true)
@path = path
@snapshot = {}
snapshot if preload
end

def refresh
previous = @snapshot
current = snapshot

deleted(previous, current)
created(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
end

def add_glob(snap, glob)
Pathname.glob(@path.to_pathname.join(glob)).each do |fn|
next unless fn.file?
snap["#{fn}"] = fn.mtime
end
end

end
26 changes: 26 additions & 0 deletions lib/vendor/fssm/support.rb
@@ -0,0 +1,26 @@
module FSSM::Support
class << self
# def backend
# (mac? && carbon_core?) ? 'FSEvents' : 'Polling'
# end

def backend
'Polling'
end

def mac?
@@mac ||= RUBY_PLATFORM =~ /darwin/i
end

def carbon_core?
@@carbon_core ||= begin
require 'osx/foundation'
OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
true
rescue LoadError
false
end
end

end
end

0 comments on commit 005f6d4

Please sign in to comment.