diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 11af3b7..8180089 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,8 @@ +== Jan 19, 2011 [by thibaudgg] + +Features: +- The whole directory are now watched after that run_on_change was launched on all guards to detect new files modifications. + == Dec 17, 2010 [by netzpirat] Features: @@ -14,7 +19,6 @@ Features: Features: - It's now possible to return an enumerable in the 'watch' optional blocks in the Guardfile. -- Listener now continue to watch changed files even when guards plugin are running. Specs: - Guard::Watcher diff --git a/lib/guard.rb b/lib/guard.rb index 5c6460f..b5a6c04 100644 --- a/lib/guard.rb +++ b/lib/guard.rb @@ -1,17 +1,17 @@ require 'bundler' module Guard - + autoload :UI, 'guard/ui' autoload :Dsl, 'guard/dsl' autoload :Interactor, 'guard/interactor' autoload :Listener, 'guard/listener' autoload :Watcher, 'guard/watcher' autoload :Notifier, 'guard/notifier' - + class << self attr_accessor :options, :guards, :listener - + # initialize this singleton def setup(options = {}) @options = options @@ -19,41 +19,41 @@ module Guard @guards = [] self end - + def start(options = {}) setup(options) - + Interactor.init_signal_traps Dsl.evaluate_guardfile(options) - + if guards.empty? UI.error "No guards found in Guardfile, please add at least one." else - UI.info "Guard is now watching at '#{Dir.pwd}'" - guards.each { |g| supervised_task(g, :start) } - - Thread.new { listener.start } - wait_for_changes_and_launch_guards - end - end - - def wait_for_changes_and_launch_guards - loop do - if !running? && !listener.changed_files.empty? - changed_files = listener.get_and_clear_changed_files - if Watcher.match_files?(guards, changed_files) - run do - guards.each do |guard| - paths = Watcher.match_files(guard, changed_files) - supervised_task(guard, :run_on_change, paths) unless paths.empty? - end - end + listener.on_change do |files| + if Watcher.match_files?(guards, files) + run { run_on_change_for_all_guards(files) } end end - sleep 0.2 + + UI.info "Guard is now watching at '#{Dir.pwd}'" + guards.each { |guard| supervised_task(guard, :start) } + listener.start end end - + + def run_on_change_for_all_guards(files) + guards.each do |guard| + paths = Watcher.match_files(guard, files) + supervised_task(guard, :run_on_change, paths) unless paths.empty? + end + # Reparse the whole directory to catch new files modified during the guards run + new_modified_files = listener.modified_files([Dir.pwd + '/'], :all => true) + listener.update_last_event + unless new_modified_files.empty? + run_on_change_for_all_guards(new_modified_files) + end + end + # Let a guard execute its task but # fire it if his work leads to a system failure def supervised_task(guard, task_to_supervise, *args) @@ -64,26 +64,22 @@ module Guard UI.info("Guard #{guard.class.name} has just been fired") return $! end - + def run - @run = true + listener.stop UI.clear if options[:clear] begin yield rescue Interrupt end - @run = false + listener.start end - - def running? - @run == true - end - + def add_guard(name, watchers = [], options = {}) guard_class = get_guard_class(name) @guards << guard_class.new(watchers, options) end - + def get_guard_class(name) require "guard/#{name.downcase}" klasses = [] @@ -94,12 +90,12 @@ module Guard rescue LoadError UI.error "Could not find gem 'guard-#{name}', please add it in your Gemfile." end - + def locate_guard(name) `gem open guard-#{name} --latest --command echo`.chomp rescue UI.error "Could not find 'guard-#{name}' gem path." end - + end end \ No newline at end of file diff --git a/lib/guard/guard.rb b/lib/guard/guard.rb index 5ec6542..a6b546c 100644 --- a/lib/guard/guard.rb +++ b/lib/guard/guard.rb @@ -1,11 +1,11 @@ module Guard class Guard attr_accessor :watchers, :options - + def initialize(watchers = [], options = {}) @watchers, @options = watchers, options end - + # Guardfile template needed inside guard gem def self.init(name) if ::Guard::Dsl.guardfile_include?(name) @@ -21,35 +21,35 @@ module Guard ::Guard::UI.info "#{name} guard added to Guardfile, feel free to edit it" end end - + # ================ # = Guard method = # ================ - + # Call once when guard starts # Please override initialize method to init stuff def start true end - + # Call once when guard quit def stop true end - + # Should be mainly used for "reload" (really!) actions like reloading passenger/spork/bundler/... def reload true end - + # Should be principally used for long action like running all specs/tests/... def run_all true end - + def run_on_change(paths) true end - + end end \ No newline at end of file diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb index c441abc..89d90f1 100644 --- a/lib/guard/listener.rb +++ b/lib/guard/listener.rb @@ -1,14 +1,14 @@ require 'rbconfig' module Guard - + autoload :Darwin, 'guard/listeners/darwin' autoload :Linux, 'guard/listeners/linux' autoload :Polling, 'guard/listeners/polling' - + class Listener - attr_accessor :last_event, :changed_files - + attr_reader :last_event + def self.select_and_init if mac? && Darwin.usable? Darwin.new @@ -19,47 +19,107 @@ module Guard Polling.new end end - + def initialize - @changed_files = [] update_last_event end - - def get_and_clear_changed_files - files = changed_files.dup - changed_files.clear - files.uniq + + def update_last_event + @last_event = Time.now end - - private - - def find_changed_files(dirs, options = {}) - files = potentially_changed_files(dirs, options).select { |path| File.file?(path) && changed_file?(path) } + + 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_changed_files(dirs, options = {}) + + private + + def potentially_modified_files(dirs, options = {}) match = options[:all] ? "**/*" : "*" Dir.glob(dirs.map { |dir| "#{dir}#{match}" }) end - - def changed_file?(file) + + def recent_file?(file) File.mtime(file) >= last_event rescue false end - - def update_last_event - @last_event = Time.now - end - + + def self.mac? Config::CONFIG['target_os'] =~ /darwin/i end - + def self.linux? Config::CONFIG['target_os'] =~ /linux/i end - + end -end \ No newline at end of file +end + +# require 'rbconfig' +# +# module Guard +# +# autoload :Darwin, 'guard/listeners/darwin' +# autoload :Linux, 'guard/listeners/linux' +# autoload :Polling, 'guard/listeners/polling' +# +# class Listener +# attr_accessor :last_event, :changed_files +# +# def self.select_and_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 +# @changed_files = [] +# update_last_event +# end +# +# def get_and_clear_changed_files +# files = changed_files.dup +# changed_files.clear +# files.uniq +# end +# +# private +# +# def find_changed_files(dirs, options = {}) +# files = potentially_changed_files(dirs, options).select { |path| File.file?(path) && changed_file?(path) } +# files.map! { |file| file.gsub("#{Dir.pwd}/", '') } +# end +# +# def potentially_changed_files(dirs, options = {}) +# match = options[:all] ? "**/*" : "*" +# Dir.glob(dirs.map { |dir| "#{dir}#{match}" }) +# end +# +# def changed_file?(file) +# File.mtime(file) >= last_event +# rescue +# false +# end +# +# def update_last_event +# @last_event = Time.now +# end +# +# def self.mac? +# Config::CONFIG['target_os'] =~ /darwin/i +# end +# +# def self.linux? +# Config::CONFIG['target_os'] =~ /linux/i +# end +# +# end +# end \ No newline at end of file diff --git a/lib/guard/listeners/darwin.rb b/lib/guard/listeners/darwin.rb index a7a943b..c6eb055 100644 --- a/lib/guard/listeners/darwin.rb +++ b/lib/guard/listeners/darwin.rb @@ -1,28 +1,28 @@ module Guard class Darwin < Listener attr_reader :fsevent - + def initialize super @fsevent = FSEvent.new - fsevent.watch Dir.pwd do |modified_dirs| - @changed_files += find_changed_files(modified_dirs) + 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 - # keep relaunching fsevent.run if not stopped by Guard - while @stop != true - fsevent.run - end + @fsevent.run end - + def stop - @stop = true - fsevent.stop + @fsevent.stop end - + def self.usable? require 'rb-fsevent' if !defined?(FSEvent::VERSION) || Gem::Version.new(FSEvent::VERSION) < Gem::Version.new('0.3.9') @@ -35,6 +35,6 @@ module Guard UI.info "Please install rb-fsevent gem for Mac OSX FSEvents support" false end - + end -end \ No newline at end of file +end diff --git a/lib/guard/listeners/linux.rb b/lib/guard/listeners/linux.rb index aa4e622..d4d13ad 100644 --- a/lib/guard/listeners/linux.rb +++ b/lib/guard/listeners/linux.rb @@ -1,25 +1,35 @@ module Guard class Linux < Listener - attr_reader :inotify - + attr_reader :inotify, :files, :latency, :callback + def initialize super + @inotify = INotify::Notifier.new + @files = [] + @latency = 0.5 + end + + def start + @stop = false + watch_change unless watch_change? + end + + def stop + @stop = true + sleep latency + end + + def on_change(&callback) + @callback = callback inotify.watch(Dir.pwd, :recursive, :modify, :create, :delete, :move) do |event| unless event.name == "" # Event on root directory - @changed_files << event.absolute_name.gsub("#{Dir.pwd}/", '') + @files << event.absolute_name end end + rescue Interrupt end - - def start - inotify.run - end - - def stop - inotify.stop - end - + def self.usable? require 'rb-inotify' if !defined?(INotify::VERSION) || Gem::Version.new(INotify::VERSION.join('.')) < Gem::Version.new('0.5.1') @@ -32,6 +42,32 @@ module Guard UI.info "Please install rb-inotify gem for Linux inotify support" false end - + + def watch_change? + !!@watch_change + end + + private + + def watch_change + @watch_change = true + while !@stop + if Config::CONFIG['build'] =~ /java/ || IO.select([inotify.to_io], [], [], latency) + break if @stop + + sleep latency + inotify.process + update_last_event + + unless files.empty? + files.map! { |file| file.gsub("#{Dir.pwd}/", '') } + callback.call(files) + files.clear + end + end + end + @watch_change = false + end + end -end \ No newline at end of file +end diff --git a/lib/guard/listeners/polling.rb b/lib/guard/listeners/polling.rb index e542d8d..e0dd5ca 100644 --- a/lib/guard/listeners/polling.rb +++ b/lib/guard/listeners/polling.rb @@ -1,34 +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 - if files = find_changed_files([Dir.pwd + '/'], :all => true) - update_last_event - @changed_files += files - end + 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 +end diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb index b7e9454..7fd8a79 100644 --- a/spec/guard/listener_spec.rb +++ b/spec/guard/listener_spec.rb @@ -2,25 +2,25 @@ require 'spec_helper' describe Guard::Listener do subject { described_class } - + describe ".select_and_init" do 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.stub(:usable?).and_return(true) Guard::Darwin.should_receive(:new) subject.select_and_init end - + it "should use polling listener on Windows" do Config::CONFIG['target_os'] = 'win32' Guard::Polling.stub(:usable?).and_return(true) Guard::Polling.should_receive(:new) subject.select_and_init end - + it "should use linux listener on Linux" do Config::CONFIG['target_os'] = 'linux' Guard::Linux.stub(:usable?).and_return(true) @@ -28,16 +28,16 @@ describe Guard::Listener do subject.select_and_init end end - - describe "#get_and_clear_changed_files" do - subject { Guard::Listener.new } - - it "should return uniq changed files and clear it" do - subject.changed_files = ["foo", "bar", "bar"] - subject.get_and_clear_changed_files.should == ["foo", "bar"] - subject.changed_files.should be_empty + + describe "#update_last_event" do + subject { described_class.new } + + it "should update last_event with time.now" do + time = Time.now + subject.update_last_event + subject.last_event.should >= time 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 index 96badfb..fa06f0b 100644 --- a/spec/guard/listeners/darwin_spec.rb +++ b/spec/guard/listeners/darwin_spec.rb @@ -3,21 +3,27 @@ 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 - subject { Guard::Darwin.new } - + 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 @@ -25,18 +31,18 @@ describe Guard::Darwin do FileUtils.touch file stop File.delete file - subject.changed_files.should == ['spec/fixtures/newfile.rb'] + @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 - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt'] + @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") @@ -46,22 +52,22 @@ describe Guard::Darwin do FileUtils.touch file1 FileUtils.touch file2 stop - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] + @results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] end end end - + private - + def start sleep 1 - Thread.new { subject.start } + Thread.new { @listener.start } sleep 1 end - + def stop sleep 1 - subject.stop + @listener.stop end - -end + +end \ No newline at end of file diff --git a/spec/guard/listeners/linux_spec.rb b/spec/guard/listeners/linux_spec.rb index 786276a..ad45285 100644 --- a/spec/guard/listeners/linux_spec.rb +++ b/spec/guard/listeners/linux_spec.rb @@ -4,21 +4,51 @@ 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 be_usable end - + + describe "start" do + before(:each) do + @listener = Guard::Linux.new + end + + it "should call watch_change if first start" do + @listener.should_receive(:watch_change) + start + end + + it "should not call watch_change if start after stop" do + @listener.stub!(:stop) + start + stop + @listener.should be_watch_change + @listener.should_not_receive(:watch_change) + start + @listener.unstub!(:stop) + stop + @listener.should_not be_watch_change + end + + end + describe "watch" do - subject { Guard::Linux.new } - + 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 @@ -26,18 +56,18 @@ describe Guard::Linux do FileUtils.touch file stop File.delete file - subject.changed_files.should == ['spec/fixtures/newfile.rb'] + @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 File.open(file, 'w') {|f| f.write('') } stop - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt'] + @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") @@ -47,9 +77,9 @@ describe Guard::Linux do File.open(file1, 'w') {|f| f.write('') } File.open(file2, 'w') {|f| f.write('') } stop - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] + @results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] end - + it "should catch deleted file" do file = @fixture_path.join("folder1/file1.txt") File.exists?(file).should be_true @@ -57,9 +87,9 @@ describe Guard::Linux do File.delete file stop FileUtils.touch file - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt'] + @results.should == ['spec/fixtures/folder1/file1.txt'] end - + it "should catch moved file" do file1 = @fixture_path.join("folder1/file1.txt") file2 = @fixture_path.join("folder1/movedfile1.txt") @@ -69,32 +99,32 @@ describe Guard::Linux do FileUtils.mv file1, file2 stop FileUtils.mv file2, file1 - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/movedfile1.txt'] + @results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/movedfile1.txt'] + end + + it "should not process change if stopped" do + file = @fixture_path.join("folder1/file1.txt") + File.exists?(file).should be_true + start + @listener.inotify.should_not_receive(:process) + stop + File.open(file, 'w') {|f| f.write('') } end - - # it "should not process change if stopped" do - # file = @fixture_path.join("folder1/file1.txt") - # File.exists?(file).should be_true - # start - # subject.changed_files.inotify.should_not_receive(:process) - # stop - # File.open(file, 'w') {|f| f.write('') } - # end end end - + private - + def start sleep 1 - Thread.new { subject.start } + Thread.new { @listener.start } sleep 1 end - + def stop sleep 1 - subject.stop + @listener.stop sleep 1 end - -end + +end \ No newline at end of file diff --git a/spec/guard/listeners/polling_spec.rb b/spec/guard/listeners/polling_spec.rb index f94f400..3ec91a7 100644 --- a/spec/guard/listeners/polling_spec.rb +++ b/spec/guard/listeners/polling_spec.rb @@ -2,8 +2,15 @@ require 'spec_helper' require 'guard/listeners/polling' describe Guard::Polling do - subject { Guard::Polling.new } - + + 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 @@ -11,18 +18,18 @@ describe Guard::Polling do FileUtils.touch file stop File.delete file - subject.changed_files.should == ['spec/fixtures/newfile.rb'] + @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 - subject.changed_files.should == ['spec/fixtures/folder1/file1.txt'] + @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") @@ -32,19 +39,19 @@ describe Guard::Polling do FileUtils.touch file1 FileUtils.touch file2 stop - subject.changed_files.sort.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] + @results.sort.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] end - + private - + def start - Thread.new { subject.start } + Thread.new { @listener.start } sleep 1 end - + def stop sleep 1 - subject.stop + @listener.stop end - -end + +end \ No newline at end of file