diff --git a/Gemfile b/Gemfile index c80ee36..e542d54 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/README.rdoc b/README.rdoc index bbd6c2c..c5f8cff 100644 --- a/README.rdoc +++ b/README.rdoc @@ -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 '' -Add guard definition to your Guardfile with: +Add guard definition to your Guardfile by running this command: guard init diff --git a/Rakefile b/Rakefile index bdb2660..12b7b7a 100644 --- a/Rakefile +++ b/Rakefile @@ -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}; diff --git a/bin/inotify_watch b/bin/inotify_watch deleted file mode 100755 index 65b6eb7..0000000 --- a/bin/inotify_watch +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ext/extconf.rb b/ext/extconf.rb deleted file mode 100644 index cdadaa7..0000000 --- a/ext/extconf.rb +++ /dev/null @@ -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 diff --git a/ext/fsevent/fsevent_watch.c b/ext/fsevent/fsevent_watch.c deleted file mode 100644 index c77c2ef..0000000 --- a/ext/fsevent/fsevent_watch.c +++ /dev/null @@ -1,44 +0,0 @@ -#include - -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; -} \ No newline at end of file diff --git a/guard.gemspec b/guard.gemspec index 6010728..6dcae44 100644 --- a/guard.gemspec +++ b/guard.gemspec @@ -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 \ No newline at end of file diff --git a/lib/guard.rb b/lib/guard.rb index 0df26bd..e0846c4 100644 --- a/lib/guard.rb +++ b/lib/guard.rb @@ -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 diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb index bc3487e..92af49e 100644 --- a/lib/guard/listener.rb +++ b/lib/guard/listener.rb @@ -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 diff --git a/lib/guard/listeners/darwin.rb b/lib/guard/listeners/darwin.rb new file mode 100644 index 0000000..55aa84a --- /dev/null +++ b/lib/guard/listeners/darwin.rb @@ -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 \ No newline at end of file diff --git a/lib/guard/listeners/linux.rb b/lib/guard/listeners/linux.rb new file mode 100644 index 0000000..ce7ead3 --- /dev/null +++ b/lib/guard/listeners/linux.rb @@ -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 \ No newline at end of file diff --git a/lib/guard/listeners/polling.rb b/lib/guard/listeners/polling.rb new file mode 100644 index 0000000..3e70cb9 --- /dev/null +++ b/lib/guard/listeners/polling.rb @@ -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 \ No newline at end of file diff --git a/lib/guard/notifier.rb b/lib/guard/notifier.rb index cdc483e..8ee0c86 100644 --- a/lib/guard/notifier.rb +++ b/lib/guard/notifier.rb @@ -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 diff --git a/spec/fixtures/folder1/file1.txt b/spec/fixtures/folder1/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/folder1/folder2/file2.txt b/spec/fixtures/folder1/folder2/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb index 1dc4386..430e426 100644 --- a/spec/guard/listener_spec.rb +++ b/spec/guard/listener_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/guard/listeners/darwin_spec.rb b/spec/guard/listeners/darwin_spec.rb new file mode 100644 index 0000000..9871ace --- /dev/null +++ b/spec/guard/listeners/darwin_spec.rb @@ -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 diff --git a/spec/guard/listeners/linux_spec.rb b/spec/guard/listeners/linux_spec.rb new file mode 100644 index 0000000..55e1a26 --- /dev/null +++ b/spec/guard/listeners/linux_spec.rb @@ -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 diff --git a/spec/guard/listeners/polling_spec.rb b/spec/guard/listeners/polling_spec.rb new file mode 100644 index 0000000..7cc5d88 --- /dev/null +++ b/spec/guard/listeners/polling_spec.rb @@ -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 diff --git a/spec/guard/notifier_spec.rb b/spec/guard/notifier_spec.rb index 8f670ac..17dbf5e 100644 --- a/spec/guard/notifier_spec.rb +++ b/spec/guard/notifier_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fe8b17..fd68ace 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 \ No newline at end of file diff --git a/spec/support/platform_helper.rb b/spec/support/platform_helper.rb new file mode 100644 index 0000000..2b8bfb5 --- /dev/null +++ b/spec/support/platform_helper.rb @@ -0,0 +1,7 @@ +def mac? + Config::CONFIG['target_os'] =~ /darwin/i +end + +def linux? + Config::CONFIG['target_os'] =~ /linux/i +end \ No newline at end of file