Use FSSM by Travis Tilley to monitor for filesystem changes. On mac this will use filesystem events instead of polling. Fixes an infinite looping issue when compilation errors occur.

This commit is contained in:
Chris Eppstein 2009-08-30 11:37:24 -07:00
parent d40f72acbb
commit 005f6d4f36
9 changed files with 342 additions and 8 deletions

View File

@ -14,17 +14,30 @@ module Compass
puts "" puts ""
exit 0 exit 0
end end
recompile
puts ">>> Compass is watching for changes. Press Ctrl-C to Stop." puts ">>> Compass is watching for changes. Press Ctrl-C to Stop."
loop do
# TODO: Make this efficient by using filesystem monitoring. require File.join(Compass.lib_directory, 'vendor', 'fssm')
compiler = new_compiler_instance(:quiet => true)
remove_obsolete_css(compiler) FSSM.monitor do |monitor|
recompile(compiler) Compass.configuration.sass_load_paths.each do |load_path|
sleep 1 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
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 sass_files = compiler.sass_files
deleted_sass_files = (last_sass_files || []) - sass_files deleted_sass_files = (last_sass_files || []) - sass_files
deleted_sass_files.each do |deleted_sass_file| deleted_sass_files.each do |deleted_sass_file|
@ -34,7 +47,8 @@ module Compass
self.last_sass_files = sass_files self.last_sass_files = sass_files
end end
def recompile(compiler) def recompile(base = nil, relative = nil)
compiler = new_compiler_instance(:quiet => true)
if file = compiler.out_of_date? if file = compiler.out_of_date?
begin begin
puts ">>> Change detected to: #{file}" puts ">>> Change detected to: #{file}"

30
lib/vendor/fssm.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/backends/fsevents.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/backends/polling.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/ext.rb vendored Normal file
View File

@ -0,0 +1,7 @@
class Pathname
class << self
def for(path)
path.is_a?(Pathname) ? path : new(path)
end
end
end

21
lib/vendor/fssm/monitor.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/path.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/state.rb vendored Normal file
View File

@ -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
lib/vendor/fssm/support.rb vendored Normal file
View File

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