Refactorized listeners support

Added polling fallback
Removed sys-uname dependency
This commit is contained in:
Thibaud Guillaume-Gentil 2010-10-17 21:42:40 +02:00
parent a0ecca82e5
commit 3f922a0667
22 changed files with 450 additions and 181 deletions

View File

@ -1,3 +1,12 @@
source "http://rubygems.org"
gemspec
require 'rbconfig'
if Config::CONFIG['target_os'] =~ /darwin/i
gem 'rb-fsevent', '>= 0.3.2'
end
if Config::CONFIG['target_os'] =~ /linux/i
gem 'rb-inotify', '>= 0.5.1'
end

View File

@ -4,17 +4,17 @@ Guard is a command line tool to easly handle events on files modifications.
== Features
- FSEvent support on Mac OS X (without RubyCocoa!)
- Inotify support on Linux (beta)
- Super fast change detection
- FSEvent support on Mac OS X 10.5+ (without RubyCocoa!, please install {rb-fsevent, >= 0.3.2}[https://rubygems.org/gems/rb-fsevent])
- Inotify support on Linux (beta, please install {rb-inotify, >= 0.5.1}[https://rubygems.org/gems/rb-inotify])
- Polling for others (help us to support more systems)
- Super fast change detection (when polling not used)
- Automatic files modifications detection (even new files are detected)
- Growl notification (please install {growlnotify}[http://growl.info/documentation/growlnotify.php])
- Libnotify notification
- Tested on Ruby 1.8.7 & 1.9.2.
== Install
Only Mac OS X (10.5+) & Linux are supported. Tested on Ruby 1.8.7 & 1.9.2.
Install the gem:
gem install guard
@ -35,9 +35,13 @@ Just launch Guard inside your ruby/rails project with:
guard
Shell can be cleared after each change with:
guard -c
Options list is available with:
guard help
guard help [TASK]
Signal handlers are used to interact with Guard:
@ -65,7 +69,7 @@ Add it to your Gemfile (inside test group):
gem '<guard-name>'
Add guard definition to your Guardfile with:
Add guard definition to your Guardfile by running this command:
guard init <guard-name>

View File

@ -8,7 +8,7 @@ task :default => :spec
namespace(:spec) do
desc "Run all specs on multiple ruby versions (requires rvm)"
task(:portability) do
%w[1.8.7 1.9.2].each do |version|
%w[1.8.7 1.9.2 jruby].each do |version|
system <<-BASH
bash -c 'source ~/.rvm/scripts/rvm;
rvm #{version};

View File

@ -1,25 +0,0 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'rb-inotify'
folders = Array.new
notifier = INotify::Notifier.new
notifier.watch(ARGV.first || '.', :modify, :recursive) do |event|
dir = File.expand_path(File.dirname(event.absolute_name)) + '/'
if !folders.include?(dir)
folders << dir
end
end
while true do
notifier.process
if !folders.empty?
$stdout.puts folders.join(' ')
$stdout.flush
folders.clear
end
sleep(0.1)
end

View File

@ -1,18 +0,0 @@
# Workaround to make Rubygems believe it builds a native gem
require 'mkmf'
create_makefile('none')
if `uname -s`.chomp == 'Darwin'
gem_root = File.expand_path(File.join('..'))
darwin_verion = `uname -r`.to_i
sdk_verion = { 9 => '10.5', 10 => '10.6', 11 => '10.7' }[darwin_verion]
raise "Darwin #{darwin_verion} is not supported" unless sdk_verion
# Compile the actual fsevent_watch binary
system("CFLAGS='-isysroot /Developer/SDKs/MacOSX#{sdk_verion}.sdk -mmacosx-version-min=#{sdk_verion}' /usr/bin/gcc -framework CoreServices -o '#{gem_root}/bin/fsevent_watch' fsevent/fsevent_watch.c")
unless File.executable?("#{gem_root}/bin/fsevent_watch")
raise "Compilation of fsevent_watch failed (see README)"
end
end

View File

@ -1,44 +0,0 @@
#include <CoreServices/CoreServices.h>
void callback(ConstFSEventStreamRef streamRef,
void *clientCallBackInfo,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[]
) {
// Print modified dirs
int i;
char **paths = eventPaths;
for (i = 0; i < numEvents; i++) {
printf("%s", paths[i]);
printf(" ");
}
printf("\n");
fflush(stdout);
}
int main (int argc, const char * argv[]) {
// Create event stream
CFStringRef pathToWatch = CFStringCreateWithCString(kCFAllocatorDefault, argv[1], kCFStringEncodingUTF8);
CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&pathToWatch, 1, NULL);
void *callbackInfo = NULL;
FSEventStreamRef stream;
CFAbsoluteTime latency = 0.1;
stream = FSEventStreamCreate(
kCFAllocatorDefault,
callback,
callbackInfo,
pathsToWatch,
kFSEventStreamEventIdSinceNow,
latency,
kFSEventStreamCreateFlagNone
);
// Add stream to run loop
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
CFRunLoopRun();
return 2;
}

View File

@ -15,20 +15,17 @@ Gem::Specification.new do |s|
s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = 'guard'
s.add_development_dependency 'rspec', '~> 2.0.0.rc'
s.add_development_dependency 'guard-rspec', '~> 0.1.0'
s.add_development_dependency 'rspec', '~> 2.0.0'
s.add_development_dependency 'guard-rspec', '~> 0.1.3'
s.add_dependency 'bundler', '~> 1.0.2'
s.add_dependency 'thor', '~> 0.14.3'
s.add_dependency 'sys-uname', '~> 0.8.4'
# Mac OS X
s.add_dependency 'growl', '~> 1.0.3'
# Linux
s.add_dependency 'rb-inotify', '~> 0.8.1'
s.add_dependency 'libnotify', '~> 0.1.3'
s.files = Dir.glob('{bin,images,lib,ext}/**/*') + %w[LICENSE README.rdoc]
s.extensions = ['ext/extconf.rb']
s.files = Dir.glob('{bin,images,lib}/**/*') + %w[LICENSE README.rdoc]
s.executable = 'guard'
s.require_path = 'lib'
end

View File

@ -14,7 +14,7 @@ module Guard
def start(options = {})
@options = options
@listener = Listener.new
@listener = Listener.init
@guards = []
Dsl.evaluate_guardfile
@ -59,7 +59,7 @@ module Guard
def run
listener.stop
UI.clear if options[:clear]
UI. clear if options[:clear]
yield
listener.start
end

View File

@ -1,51 +1,39 @@
require 'sys/uname'
require 'rbconfig'
module Guard
autoload :Darwin, 'guard/listeners/darwin'
autoload :Linux, 'guard/listeners/linux'
autoload :Polling, 'guard/listeners/polling'
class Listener
attr_reader :last_event, :callback, :pipe
attr_reader :last_event
def self.init
if mac? && Darwin.usable?
Darwin.new
elsif linux? && Linux.usable?
Linux.new
else
UI.info "Using polling (Please help us to support your system better than that.)"
Polling.new
end
end
def initialize
update_last_event
end
def on_change(&block)
@callback = block
end
def start
@pipe = case Sys::Uname.sysname
when 'Darwin'
IO.popen("#{bin_path}/fsevent_watch .")
when 'Linux'
IO.popen("#{bin_path}/inotify_watch .")
end
watch_change
end
def stop
Process.kill("HUP", pipe.pid) if pipe
end
private
def watch_change
while !pipe.eof?
if line = pipe.readline
modified_dirs = line.split(" ")
files = modified_files(modified_dirs)
update_last_event
callback.call(files)
end
end
end
def modified_files(dirs)
files = potentially_modified_files(dirs).select { |file| recent_file?(file) }
def modified_files(dirs, options = {})
files = potentially_modified_files(dirs, options).select { |path| File.file?(path) && recent_file?(path) }
files.map! { |file| file.gsub("#{Dir.pwd}/", '') }
end
def potentially_modified_files(dirs)
Dir.glob(dirs.map { |dir| "#{dir}*" })
def potentially_modified_files(dirs, options = {})
match = options[:all] ? "**/*" : "*"
Dir.glob(dirs.map { |dir| "#{dir}#{match}" })
end
def recent_file?(file)
@ -58,8 +46,12 @@ module Guard
@last_event = Time.now
end
def bin_path
File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'bin'))
def self.mac?
Config::CONFIG['target_os'] =~ /darwin/i
end
def self.linux?
Config::CONFIG['target_os'] =~ /linux/i
end
end

View File

@ -0,0 +1,40 @@
module Guard
class Darwin < Listener
attr_reader :fsevent
def initialize
super
@fsevent = FSEvent.new
end
def on_change(&callback)
@fsevent.watch Dir.pwd do |modified_dirs|
files = modified_files(modified_dirs)
update_last_event
callback.call(files)
end
end
def start
@fsevent.run
end
def stop
@fsevent.stop
end
def self.usable?
require 'rb-fsevent'
if !defined?(FSEvent::VERSION) || Gem::Version.new(FSEvent::VERSION) < Gem::Version.new('0.3.2')
UI.info "Please update rb-fsevent (>= 0.3.2)"
false
else
true
end
rescue LoadError
UI.info "Please install rb-fsevent gem for Mac OSX FSEvents support"
false
end
end
end

View File

@ -0,0 +1,58 @@
module Guard
class Linux < Listener
attr_reader :inotify, :files, :latency, :callback
def initialize
@inotify = INotify::Notifier.new
@files = []
@latency = 0.5
end
def on_change(&callback)
@callback = callback
inotify.watch(Dir.pwd, :recursive, :attrib, :modify, :create) do |event|
unless event.name == "" # Event on root directory
@files << event.absolute_name
end
end
end
def start
@stop = false
watch_change
end
def stop
@stop = true
inotify.stop
end
def self.usable?
require 'rb-inotify'
if !defined?(INotify::VERSION) || Gem::Version.new(INotify::VERSION) < Gem::Version.new('0.5.1')
UI.info "Please update rb-inotify (>= 0.5.1)"
false
else
true
end
rescue LoadError
UI.info "Please install rb-inotify gem for Linux inotify support"
false
end
private
def watch_change
while !@stop
inotify.process
unless files.empty?
files.map! { |file| file.gsub("#{Dir.pwd}/", '') }
callback.call(files)
files.clear
end
sleep latency
end
end
end
end

View File

@ -0,0 +1,37 @@
module Guard
class Polling < Listener
attr_reader :callback, :latency
def initialize
super
@latency = 1.5
end
def on_change(&callback)
@callback = callback
end
def start
@stop = false
watch_change
end
def stop
@stop = true
end
private
def watch_change
while !@stop
start = Time.now.to_f
files = modified_files([Dir.pwd + '/'], :all => true)
update_last_event
callback.call(files) unless files.empty?
nap_time = latency - (Time.now.to_f - start)
sleep(nap_time) if nap_time > 0
end
end
end
end

View File

@ -1,10 +1,10 @@
require 'sys/uname'
require 'rbconfig'
require 'pathname'
case Sys::Uname.sysname
when 'Darwin'
case Config::CONFIG['target_os']
when /darwin/i
require 'growl'
when 'Linux'
when /linux/i
require 'libnotify'
end
@ -15,10 +15,10 @@ module Guard
unless ENV["GUARD_ENV"] == "test"
image = options[:image] || :success
title = options[:title] || "Guard"
case Sys::Uname.sysname
when 'Darwin'
case Config::CONFIG['target_os']
when /darwin/i
Growl.notify message, :title => title, :icon => image_path(image), :name => "Guard"
when 'Linux'
when /linux/i
Libnotify.show :body => message, :summary => title, :icon_path => image_path(image)
end
end

0
spec/fixtures/folder1/file1.txt vendored Normal file
View File

View File

View File

@ -1,25 +1,31 @@
require 'spec_helper'
describe Guard::Listener do
subject { described_class.new }
subject { described_class }
its(:last_event) { should < Time.now }
describe "start" do
let(:pipe_mock) { mock("pipe", :eof? => true) }
describe "init" do
it "should use fsevent_watch on Mac OS X" do
Sys::Uname.stub(:sysname).and_return('Darwin')
IO.should_receive(:popen).with(/.*\/fsevent_watch\s\./).and_return(pipe_mock)
subject.start
before(:each) { @target_os = Config::CONFIG['target_os'] }
after(:each) { Config::CONFIG['target_os'] = @target_os }
it "should use darwin listener on Mac OS X" do
Config::CONFIG['target_os'] = 'darwin10.4.0'
Guard::Darwin.should_receive(:new)
subject.init
end
it "should use inotify_watch on Linux" do
Sys::Uname.stub(:sysname).and_return('Linux')
IO.should_receive(:popen).with(/.*\/inotify_watch\s\./).and_return(pipe_mock)
subject.start
it "should use polling listener on Windows" do
Config::CONFIG['target_os'] = 'win32'
Guard::Polling.should_receive(:new)
subject.init
end
# it "should use inotify_watch on Linux" do
# # Sys::Uname.stub(:sysname).and_return('Linux')
# IO.should_receive(:popen).with(/.*\/inotify_watch\s\./).and_return(pipe_mock)
# subject.start
# end
end
end

View File

@ -0,0 +1,73 @@
require 'spec_helper'
require 'guard/listeners/darwin'
describe Guard::Darwin do
subject { Guard::Darwin }
if linux?
it "should not be usable on linux" do
subject.should_not be_usable
end
end
if mac?
it "should be usable on 10.6" do
subject.should be_usable
end
describe "watch" do
before(:each) do
@results = []
@listener = Guard::Darwin.new
@listener.on_change do |files|
@results += files
end
end
it "should catch new file" do
file = @fixture_path.join("newfile.rb")
File.exists?(file).should be_false
start
FileUtils.touch file
stop
File.delete file
@results.should == ['spec/fixtures/newfile.rb']
end
it "should catch file update" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
FileUtils.touch file
stop
@results.should == ['spec/fixtures/folder1/file1.txt']
end
it "should catch files update" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/folder2/file2.txt")
File.exists?(file1).should be_true
File.exists?(file2).should be_true
start
FileUtils.touch file1
FileUtils.touch file2
stop
@results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
end
end
end
private
def start
sleep 1
Thread.new { @listener.start }
sleep 1
end
def stop
sleep 1
@listener.stop
end
end

View File

@ -0,0 +1,73 @@
require 'spec_helper'
require 'guard/listeners/linux'
describe Guard::Linux do
subject { Guard::Linux }
if mac?
it "should not be usable on 10.6" do
subject.should_not be_usable
end
end
if linux?
it "should be usable on linux" do
subject.should_not be_usable
end
describe "watch" do
before(:each) do
@results = []
@listener = Guard::Linux.new
@listener.on_change do |files|
@results += files
end
end
it "should catch new file" do
file = @fixture_path.join("newfile.rb")
File.exists?(file).should be_false
start
FileUtils.touch file
stop
File.delete file
@results.should == ['spec/fixtures/newfile.rb']
end
it "should catch file update" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
FileUtils.touch file
stop
@results.should == ['spec/fixtures/folder1/file1.txt']
end
it "should catch files update" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/folder2/file2.txt")
File.exists?(file1).should be_true
File.exists?(file2).should be_true
start
FileUtils.touch file1
FileUtils.touch file2
stop
@results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
end
end
end
private
def start
sleep 1
Thread.new { @listener.start }
sleep 1
end
def stop
sleep 1
@listener.stop
end
end

View File

@ -0,0 +1,57 @@
require 'spec_helper'
require 'guard/listeners/polling'
describe Guard::Polling do
before(:each) do
@results = []
@listener = Guard::Polling.new
@listener.on_change do |files|
@results += files
end
end
it "should catch new file" do
file = @fixture_path.join("newfile.rb")
File.exists?(file).should be_false
start
FileUtils.touch file
stop
File.delete file
@results.should == ['spec/fixtures/newfile.rb']
end
it "should catch file update" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
FileUtils.touch file
stop
@results.should == ['spec/fixtures/folder1/file1.txt']
end
it "should catch files update" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/folder2/file2.txt")
File.exists?(file1).should be_true
File.exists?(file2).should be_true
start
FileUtils.touch file1
FileUtils.touch file2
stop
@results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
end
private
def start
Thread.new { @listener.start }
sleep 1
end
def stop
sleep 3
@listener.stop
end
end

View File

@ -6,25 +6,28 @@ describe Guard::Notifier do
describe "notify" do
before(:each) { ENV["GUARD_ENV"] = 'special_test' }
it "should use Growl on Mac OS X" do
Sys::Uname.stub(:sysname).and_return('Darwin')
Growl.should_receive(:notify).with("great",
:title => "Guard",
:icon => Pathname.new(File.dirname(__FILE__)).join('../../images/success.png').to_s,
:name => "Guard"
)
subject.notify 'great', :title => 'Guard'
if mac?
it "should use Growl on Mac OS X" do
Growl.should_receive(:notify).with("great",
:title => "Guard",
:icon => Pathname.new(File.dirname(__FILE__)).join('../../images/success.png').to_s,
:name => "Guard"
)
subject.notify 'great', :title => 'Guard'
end
end
# it "should use Libnotify on Linux" do
# Sys::Uname.stub(:sysname).and_return('Linux')
# Libnotify.should_receive(:show).with(
# :body => "great",
# :summary => 'Guard',
# :icon_path => 'image/path'
# )
# subject.notify 'great', 'Guard', 'image/path'
# end
if linux?
it "should use Libnotify on Linux" do
Sys::Uname.stub(:sysname).and_return('Linux')
Libnotify.should_receive(:show).with(
:body => "great",
:summary => 'Guard',
:icon_path => 'image/path'
)
subject.notify 'great', 'Guard', 'image/path'
end
end
after(:each) { ENV["GUARD_ENV"] = 'test' }
end

View File

@ -2,16 +2,16 @@ require 'rubygems'
require 'guard'
require 'rspec'
fixture_path = Pathname.new(File.expand_path('../fixtures/', __FILE__))
Dir["#{File.expand_path('..', __FILE__)}/support/**/*.rb"].each { |f| require f }
puts "Please do not update/create files while tests are running."
RSpec.configure do |config|
config.color_enabled = true
config.before(:each) do
ENV["GUARD_ENV"] = 'test'
@fixture_path = fixture_path
@fixture_path = Pathname.new(File.expand_path('../fixtures/', __FILE__))
end
config.after(:all) do
end
end

View File

@ -0,0 +1,7 @@
def mac?
Config::CONFIG['target_os'] =~ /darwin/i
end
def linux?
Config::CONFIG['target_os'] =~ /linux/i
end