diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb index c401932..148c3ac 100644 --- a/lib/guard/listener.rb +++ b/lib/guard/listener.rb @@ -8,40 +8,60 @@ module Guard autoload :Windows, 'guard/listeners/windows' autoload :Polling, 'guard/listeners/polling' + # The Listener is the base class for all listener + # implementations. + # class Listener - DefaultIgnorePaths = %w[. .. .bundle .git log tmp vendor] + DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor] + attr_accessor :changed_files attr_reader :directory, :ignore_paths, :locked - def self.select_and_init(*a) + # Select the appropriate listener implementation for the + # current OS and initializes it. + # + # @param [Array] args the arguments for the listener + # @return [Guard::Listener] the chosen listener + # + def self.select_and_init(*args) if mac? && Darwin.usable? - Darwin.new(*a) + Darwin.new(*args) elsif linux? && Linux.usable? - Linux.new(*a) + Linux.new(*args) elsif windows? && Windows.usable? - Windows.new(*a) + Windows.new(*args) else - UI.info "Using polling (Please help us to support your system better than that.)" - Polling.new(*a) + UI.info 'Using polling (Please help us to support your system better than that).' + Polling.new(*args) end end - def initialize(directory = Dir.pwd, options = {}) + # Initialize the listener. + # + # @param [String] directory the root directory to listen to + # @param [Hash] options the listener options + # @option options [Boolean] relativize_paths use only relative paths + # @option options [Array] ignore_paths the paths to ignore by the listener + # + def initialize(directory = Dir.pwd, options = { }) @directory = directory.to_s - @sha1_checksums_hash = {} + @sha1_checksums_hash = { } @relativize_paths = options.fetch(:relativize_paths, true) @changed_files = [] @locked = false - @ignore_paths = DefaultIgnorePaths - @ignore_paths |= options[:ignore_paths] if options[:ignore_paths] + @ignore_paths = DEFAULT_IGNORE_PATHS + @ignore_paths |= options[:ignore_paths] if options[:ignore_paths] update_last_event start_reactor end + # Start the listener thread. + # def start_reactor return if ENV["GUARD_ENV"] == 'test' + Thread.new do loop do if @changed_files != [] && !@locked @@ -55,75 +75,114 @@ module Guard end end + # Start watching the root directory. + # def start watch(@directory) end + # Stop listening for events. + # def stop end + # Lock the listener to ignore change events. + # def lock @locked = true end + # Unlock the listener to listen again to change events. + # def unlock @locked = false end + # Clear the list of changed files. + # def clear_changed_files @changed_files.clear end + # Store a listener callback. + # + # @param [Block] callback the callback to store + # def on_change(&callback) @callback = callback end + # Updates the timestamp of the last event. + # def update_last_event @last_event = Time.now end - def modified_files(dirs, options = {}) + def modified_files(dirs, options = { }) last_event = @last_event update_last_event files = potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) } relativize_paths(files) end - def worker - raise NotImplementedError, "should respond to #watch" - end - - # register a directory to watch. must be implemented by the subclasses + # Register a directory to watch. + # Must be implemented by the subclasses. + # + # @param [String] directory the directory to watch + # def watch(directory) raise NotImplementedError, "do whatever you want here, given the directory as only argument" end + # Get all files that are in the watched directory. + # + # @return [Array] the list of files + # def all_files potentially_modified_files([@directory], :all => true) end - # scopes all given paths to the current #directory + # Scopes all given paths to the current directory. + # + # @param [Array] paths the paths to change + # @return [Array] all paths now relative to the current dir + # def relativize_paths(paths) return paths unless relativize_paths? paths.map do |path| - path.gsub(%r{^#{@directory}/}, '') + path.gsub(%r{^#{ @directory }/}, '') end end + # Use relative paths? + # + # @return [Boolean] whether to use relative or absolute paths + # def relativize_paths? !!@relativize_paths end - # return children of the passed dirs that are not in the ignore_paths list + # Removes ignored paths from the directory list. + # + # @param [Array] dirs the directory to listen to + # @param [Array] ignore_paths the paths to ignore + # @return children of the passed dirs that are not in the ignore_paths list + # def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths) Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path| ignore_paths.include?(File.basename(path)) end end - private + private - def potentially_modified_files(dirs, options={}) + # Gets a list of files that are in the modified firectories. + # + # @param [Array] dirs the list of directories + # @param [Hash] options the options + # @option options [Symbol] all whether to include all files + # + def potentially_modified_files(dirs, options = {}) paths = exclude_ignored_paths(dirs) if options[:all] @@ -131,7 +190,7 @@ module Guard if File.file?(path) array << path else - array += Dir.glob("#{path}/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) } + array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) } end array end @@ -140,9 +199,17 @@ module Guard end end + # Test if the file content has changed. + # # Depending on the filesystem, mtime/ctime is probably only precise to the second, so round # both values down to the second for the comparison. + # # ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX + # + # @param [String] path the file path + # @param [Time] last_event the time of the last event + # @return [Boolean] Whether the file content has changed or not. + # def file_modified?(path, last_event) ctime = File.ctime(path).to_i mtime = File.mtime(path).to_i @@ -158,6 +225,12 @@ module Guard false end + # Tests if the file content has been modified by + # comparing the SHA1 checksum. + # + # @param [String] path the file path + # @param [String] sha1_checksum the checksum of the file + # def file_content_modified?(path, sha1_checksum) if @sha1_checksums_hash[path] != sha1_checksum set_sha1_checksums_hash(path, sha1_checksum) @@ -167,22 +240,44 @@ module Guard end end + # Set the current checksum of a file. + # + # @param [String] path the file path + # @param [String] sha1_checksum the checksum of the file + # def set_sha1_checksums_hash(path, sha1_checksum) @sha1_checksums_hash[path] = sha1_checksum end + # Calculates the SHA1 checksum of a file. + # + # @param [String] path the path to the file + # @return [String] the SHA1 checksum + # def sha1_checksum(path) Digest::SHA1.file(path).to_s end + # Test if the OS is Mac OS X. + # + # @return [Boolean] Whether the OS is Mac OS X + # def self.mac? RbConfig::CONFIG['target_os'] =~ /darwin/i end + # Test if the OS is Linux. + # + # @return [Boolean] Whether the OS is Linux + # def self.linux? RbConfig::CONFIG['target_os'] =~ /linux/i end + # Test if the OS is Windows. + # + # @return [Boolean] Whether the OS is Windows + # def self.windows? RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i end diff --git a/lib/guard/listeners/darwin.rb b/lib/guard/listeners/darwin.rb index 07fafc6..420680b 100644 --- a/lib/guard/listeners/darwin.rb +++ b/lib/guard/listeners/darwin.rb @@ -1,41 +1,60 @@ module Guard + + # Listener implementation for Mac OS X FSEvents. + # class Darwin < Listener + # Initialize the Listener. + # def initialize(*) super @fsevent = FSEvent.new end - def worker - @fsevent - end - + # Start the listener. + # def start super worker.run end + # Stop the listener. + # def stop super worker.stop end + # Check if the listener is usable on the current OS. + # + # @return [Boolean] whether usable or not + # def self.usable? require 'rb-fsevent' if !defined?(FSEvent::VERSION) || (defined?(Gem::Version) && Gem::Version.new(FSEvent::VERSION) < Gem::Version.new('0.4.0')) - UI.info "Please update rb-fsevent (>= 0.4.0)" + UI.info 'Please update rb-fsevent (>= 0.4.0)' false else true end rescue LoadError - UI.info "Please install rb-fsevent gem for Mac OSX FSEvents support" + UI.info 'Please install rb-fsevent gem for Mac OSX FSEvents support' false end - private + private + # Get the listener worker. + # + def worker + @fsevent + end + + # Watch the given directory for file changes. + # + # @param [String] directory the directory to watch + # def watch(directory) worker.watch(directory) do |modified_dirs| files = modified_files(modified_dirs) diff --git a/lib/guard/listeners/linux.rb b/lib/guard/listeners/linux.rb index 68453cb..ba31076 100644 --- a/lib/guard/listeners/linux.rb +++ b/lib/guard/listeners/linux.rb @@ -1,6 +1,11 @@ module Guard + + # Listener implementation for Linux inotify. + # class Linux < Listener + # Initialize the Listener. + # def initialize(*) super @inotify = INotify::Notifier.new @@ -8,44 +13,53 @@ module Guard @latency = 0.5 end + # Start the listener. + # def start @stop = false super watch_change unless watch_change? end + # Stop the listener. + # def stop super @stop = true sleep(@latency) end + # Check if the listener is usable on the current OS. + # + # @return [Boolean] whether usable or not + # def self.usable? require 'rb-inotify' if !defined?(INotify::VERSION) || (defined?(Gem::Version) && Gem::Version.new(INotify::VERSION.join('.')) < Gem::Version.new('0.8.5')) - UI.info "Please update rb-inotify (>= 0.8.5)" + UI.info 'Please update rb-inotify (>= 0.8.5)' false else true end rescue LoadError - UI.info "Please install rb-inotify gem for Linux inotify support" + UI.info 'Please install rb-inotify gem for Linux inotify support' false end - def watch_change? - !!@watch_change - end - - private + private + # Get the listener worker. + # def worker @inotify end + # Watch the given directory for file changes. + # + # @param [String] directory the directory to watch + # def watch(directory) - # The event selection is based on https://github.com/guard/guard/wiki/Analysis-of-inotify-events-for-different-editors worker.watch(directory, :recursive, :attrib, :create, :move_self, :close_write) do |event| unless event.name == "" # Event on root directory @files << event.absolute_name @@ -54,6 +68,16 @@ module Guard rescue Interrupt end + # Test if inotify is watching for changes. + # + # @return [Boolean] whether inotify is active or not + # + def watch_change? + !!@watch_change + end + + # Watch for file system changes. + # def watch_change @watch_change = true until @stop diff --git a/lib/guard/listeners/polling.rb b/lib/guard/listeners/polling.rb index 894bff9..ffc6508 100644 --- a/lib/guard/listeners/polling.rb +++ b/lib/guard/listeners/polling.rb @@ -1,24 +1,46 @@ module Guard + + # Polling listener that works cross-plattform and + # has no dependencies. This is the listener that + # uses the most CPU processing power and has higher + # File IO that the other implementations. + # class Polling < Listener + # Initialize the Listener. + # def initialize(*) super @latency = 1.5 end + # Start the listener. + # def start @stop = false super watch_change end + # Stop the listener. + # def stop super @stop = true end - private + # Watch the given directory for file changes. + # + # @param [String] directory the directory to watch + # + def watch(directory) + @existing = all_files + end + private + + # Watch for file system changes. + # def watch_change until @stop start = Time.now.to_f @@ -29,9 +51,5 @@ module Guard end end - def watch(directory) - @existing = all_files - end - end end diff --git a/lib/guard/listeners/windows.rb b/lib/guard/listeners/windows.rb index 1f74d14..f7be41e 100644 --- a/lib/guard/listeners/windows.rb +++ b/lib/guard/listeners/windows.rb @@ -1,35 +1,48 @@ module Guard + + # Listener implementation for Windows fchange. + # class Windows < Listener + # Initialize the Listener. + # def initialize(*) super @fchange = FChange::Notifier.new end + # Start the listener. + # def start super worker.run end + # Stop the listener. + # def stop super worker.stop end + # Check if the listener is usable on the current OS. + # + # @return [Boolean] whether usable or not + # def self.usable? require 'rb-fchange' true rescue LoadError - UI.info "Please install rb-fchange gem for Windows file events support" + UI.info 'Please install rb-fchange gem for Windows file events support' false end - private - - def worker - @fchange - end + private + # Watch the given directory for file changes. + # + # @param [String] directory the directory to watch + # def watch(directory) worker.watch(directory, :all_events, :recursive) do |event| paths = [File.expand_path(event.watcher.path)] @@ -38,5 +51,11 @@ module Guard end end + # Get the listener worker. + # + def worker + @fchange + end + end end diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb index 639e761..011b829 100644 --- a/spec/guard/listener_spec.rb +++ b/spec/guard/listener_spec.rb @@ -172,7 +172,7 @@ describe Guard::Listener do describe "#ignore_paths" do it "defaults to the default ignore paths" do - subject.new.ignore_paths.should == Guard::Listener::DefaultIgnorePaths + subject.new.ignore_paths.should == Guard::Listener::DEFAULT_IGNORE_PATHS end it "can be added to via :ignore_paths option" do