diff --git a/lib/listen/adapter.rb b/lib/listen/adapter.rb index 83255fb1..d3dfc7d8 100644 --- a/lib/listen/adapter.rb +++ b/lib/listen/adapter.rb @@ -5,7 +5,8 @@ module Listen class Adapter - attr_accessor :directories, :latency, :paused + attr_accessor :directories, :callback, :stopped, :paused, + :mutex, :changed_directories, :turnstile, :latency # The list of existing optimized adapters. OPTIMIZED_ADAPTERS = %w[Darwin Linux BSD Windows] @@ -38,8 +39,8 @@ class Adapter # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it # @option options [Float] latency the delay between checking for changes in seconds # - # @yield [changed_dirs, options] callback the callback called when a change happens - # @yieldparam [Array] changed_dirs the changed directories + # @yield [changed_directories, options] callback the callback called when a change happens + # @yieldparam [Array] changed_directories the changed directories # @yieldparam [Hash] options callback options (like recursive: true) # # @return [Listen::Adapter] the chosen adapter @@ -65,6 +66,7 @@ def self.select_and_initialize(directories, options = {}, &callback) Adapters::Polling.new(directories, options, &callback) end + # Initializes the adapter. # # @param [String, Array] directories the directories to watch @@ -72,22 +74,22 @@ def self.select_and_initialize(directories, options = {}, &callback) # @option options [Float] latency the delay between checking for changes in seconds # @option options [Boolean] report_changes whether or not to automatically report changes (run the callback) # - # @yield [changed_dirs, options] callback Callback called when a change happens - # @yieldparam [Array] changed_dirs the changed directories + # @yield [changed_directories, options] callback Callback called when a change happens + # @yieldparam [Array] changed_directories the changed directories # @yieldparam [Hash] options callback options (like recursive: true) # # @return [Listen::Adapter] the adapter # def initialize(directories, options = {}, &callback) - @directories = Array(directories) - @callback = callback - @stopped = true - @paused = false - @mutex = Mutex.new - @changed_dirs = Set.new - @turnstile = Turnstile.new - @latency ||= options[:latency] || DEFAULT_LATENCY - @report_changes = options[:report_changes].nil? ? true : options[:report_changes] + @directories = Array(directories) + @callback = callback + @stopped = true + @paused = false + @mutex = Mutex.new + @changed_directories = Set.new + @turnstile = Turnstile.new + @latency ||= options[:latency] || DEFAULT_LATENCY + @report_changes = options.fetch(:report_changes, true) end # Starts the adapter. @@ -95,8 +97,8 @@ def initialize(directories, options = {}, &callback) # @param [Boolean] blocking whether or not to block the current thread after starting # def start(blocking = true) - @mutex.synchronize do - return unless @stopped + mutex.synchronize do + return unless stopped @stopped = false end end @@ -105,7 +107,7 @@ def start(blocking = true) # def stop @stopped = true - @turnstile.signal # ensure no thread is blocked + turnstile.signal # ensure no thread is blocked end # Pauses the adapter. @@ -125,7 +127,7 @@ def unpause # @return [Boolean] whether the adapter is started or not # def started? - !@stopped + !stopped end # Returns whether the adapter is paused or not. @@ -133,14 +135,14 @@ def started? # @return [Boolean] whether the adapter is paused or not # def paused? - @paused + paused end # Blocks the main thread until the poll thread # runs the callback. # def wait_for_callback - @turnstile.wait unless @paused + turnstile.wait unless paused end # Blocks the main thread until N changes are @@ -150,12 +152,12 @@ def wait_for_changes(threshold = 0) changes = 0 loop do - @mutex.synchronize { changes = @changed_dirs.size } + mutex.synchronize { changes = changed_directories.size } - return if @paused || @stopped + return if paused || stopped return if changes >= threshold - sleep(@latency) + sleep(latency) end end @@ -192,9 +194,10 @@ def self.usable_and_works?(directories, options = {}) # @return [Boolean] whether the adapter works or not # def self.works?(directory, options = {}) - work, test_file = false, "#{directory}/.listen_test" - callback = lambda { |*| work = true } - adapter = self.new(directory, options, &callback) + work = false + test_file = "#{directory}/.listen_test" + callback = lambda { |*| work = true } + adapter = self.new(directory, options, &callback) adapter.start(false) FileUtils.touch(test_file) @@ -214,14 +217,18 @@ def self.works?(directory, options = {}) def report_changes changed_dirs = nil - @mutex.synchronize do - return if @changed_dirs.empty? - changed_dirs = @changed_dirs.to_a - @changed_dirs.clear + mutex.synchronize do + return if @changed_directories.empty? + changed_dirs = @changed_directories.to_a + @changed_directories.clear end - @callback.call(changed_dirs, {}) - @turnstile.signal + callback.call(changed_dirs, {}) + turnstile.signal + end + + def report_changes? + @report_changes end private @@ -243,9 +250,9 @@ def self.warn_polling_fallback(warning, options) # Polls changed directories and reports them back # when there are changes. # - def poll_changed_dirs - until @stopped - sleep(@latency) + def poll_changed_directories + until stopped + sleep(latency) report_changes end end diff --git a/lib/listen/adapters/bsd.rb b/lib/listen/adapters/bsd.rb index 7b151c74..b839a279 100644 --- a/lib/listen/adapters/bsd.rb +++ b/lib/listen/adapters/bsd.rb @@ -16,13 +16,15 @@ class BSD < Adapter # EVENTS = [:delete, :write, :extend, :attrib, :link, :rename, :revoke] + attr_accessor :worker, :worker_thread, :poll_thread + # Initializes the Adapter. # # @see Listen::Adapter#initialize # def initialize(directories, options = {}, &callback) super - @kqueue = init_kqueue + @worker = init_worker end # Starts the adapter. @@ -32,28 +34,27 @@ def initialize(directories, options = {}, &callback) def start(blocking = true) super - @kqueue_thread = Thread.new do - until @stopped - @kqueue.poll - sleep(@latency) + @worker_thread = Thread.new do + until stopped + worker.poll + sleep(latency) end end - @poll_thread = Thread.new { poll_changed_dirs } if @report_changes - - @kqueue_thread.join if blocking + @poll_thread = Thread.new { poll_changed_directories } if report_changes? + worker_thread.join if blocking end # Stops the adapter. # def stop - @mutex.synchronize do - return if @stopped + mutex.synchronize do + return if stopped super end - @kqueue.stop - Thread.kill(@kqueue_thread) if @kqueue_thread - @poll_thread.join if @poll_thread + worker.stop + Thread.kill(worker_thread) if worker_thread + poll_thread.join if poll_thread end # Checks if the adapter is usable on BSD. @@ -72,15 +73,15 @@ def self.usable? # # @return [INotify::Notifier] initialized kqueue # - def init_kqueue + def init_worker require 'find' callback = lambda do |event| path = event.watcher.path - @mutex.synchronize do + mutex.synchronize do # kqueue watches everything, but Listen only needs the # directory where stuffs happens. - @changed_dirs << (File.directory?(path) ? path : File.dirname(path)) + @changed_directories << (File.directory?(path) ? path : File.dirname(path)) # If it is a directory, and it has a write flag, it means a # file has been added so find out which and deal with it. @@ -98,7 +99,7 @@ def init_kqueue end KQueue::Queue.new.tap do |queue| - @directories.each do |directory| + directories.each do |directory| Find.find(directory) do |path| queue.watch_file(path, *EVENTS, &callback) end diff --git a/lib/listen/adapters/darwin.rb b/lib/listen/adapters/darwin.rb index e2837b73..cc1fbdf5 100644 --- a/lib/listen/adapters/darwin.rb +++ b/lib/listen/adapters/darwin.rb @@ -11,6 +11,8 @@ class Darwin < Adapter LAST_SEPARATOR_REGEX = /\/$/ + attr_accessor :worker, :worker_thread, :poll_thread + # Initializes the Adapter. # # @see Listen::Adapter#initialize @@ -27,29 +29,29 @@ def initialize(directories, options = {}, &callback) def start(blocking = true) super - @worker_thread = Thread.new { @worker.run } + @worker_thread = Thread.new { worker.run } # The FSEvent worker needs some time to start up. Turnstiles can't # be used to wait for it as it runs in a loop. # TODO: Find a better way to block until the worker starts. sleep 0.1 - @poll_thread = Thread.new { poll_changed_dirs } if @report_changes + @poll_thread = Thread.new { poll_changed_directories } if report_changes? - @worker_thread.join if blocking + worker_thread.join if blocking end # Stops the adapter. # def stop - @mutex.synchronize do - return if @stopped + mutex.synchronize do + return if stopped super end - @worker.stop - @worker_thread.join if @worker_thread - @poll_thread.join if @poll_thread + worker.stop + Thread.kill(worker_thread) if worker_thread + poll_thread.join if poll_thread end # Checks if the adapter is usable on Mac OSX. @@ -70,11 +72,11 @@ def self.usable? # def init_worker FSEvent.new.tap do |worker| - worker.watch(@directories.dup, latency: @latency) do |changes| - next if @paused + worker.watch(directories.dup, latency: latency) do |changes| + next if paused - @mutex.synchronize do - changes.each { |path| @changed_dirs << path.sub(LAST_SEPARATOR_REGEX, '') } + mutex.synchronize do + changes.each { |path| @changed_directories << path.sub(LAST_SEPARATOR_REGEX, '') } end end end diff --git a/lib/listen/adapters/linux.rb b/lib/listen/adapters/linux.rb index 2195004f..11fa3634 100644 --- a/lib/listen/adapters/linux.rb +++ b/lib/listen/adapters/linux.rb @@ -25,6 +25,8 @@ class Linux < Adapter for information on how to solve this issue. EOS + attr_accessor :worker, :worker_thread, :poll_thread + # Initializes the Adapter. # # @see Listen::Adapter#initialize @@ -43,23 +45,23 @@ def initialize(directories, options = {}, &callback) def start(blocking = true) super - @worker_thread = Thread.new { @worker.run } - @poll_thread = Thread.new { poll_changed_dirs } if @report_changes + @worker_thread = Thread.new { worker.run } + @poll_thread = Thread.new { poll_changed_directories } if report_changes? - @worker_thread.join if blocking + worker_thread.join if blocking end # Stops the adapter. # def stop - @mutex.synchronize do - return if @stopped + mutex.synchronize do + return if stopped super end - @worker.stop - Thread.kill(@worker_thread) if @worker_thread - @poll_thread.join if @poll_thread + worker.stop + Thread.kill(worker_thread) if worker_thread + poll_thread.join if poll_thread end # Checks if the adapter is usable on Linux. @@ -80,7 +82,7 @@ def self.usable? # def init_worker callback = lambda do |event| - if @paused || ( + if paused || ( # Event on root directory event.name == "" ) || ( @@ -94,15 +96,13 @@ def init_worker next end - @mutex.synchronize do - @changed_dirs << File.dirname(event.absolute_name) + mutex.synchronize do + @changed_directories << File.dirname(event.absolute_name) end end INotify::Notifier.new.tap do |worker| - @directories.each do |directory| - worker.watch(directory, *EVENTS, &callback) - end + directories.each { |dir| worker.watch(dir, *EVENTS, &callback) } end end diff --git a/lib/listen/adapters/polling.rb b/lib/listen/adapters/polling.rb index ba40783b..05403e8b 100644 --- a/lib/listen/adapters/polling.rb +++ b/lib/listen/adapters/polling.rb @@ -12,6 +12,8 @@ module Adapters class Polling < Adapter extend DependencyManager + attr_accessor :worker, :poll_thread + # Initialize the Adapter. # # @see Listen::Adapter#initialize @@ -27,21 +29,19 @@ def initialize(directories, options = {}, &callback) # def start(blocking = true) super - @poll_thread = Thread.new { poll } - - @poll_thread.join if blocking + poll_thread.join if blocking end # Stop the adapter. # def stop - @mutex.synchronize do - return if @stopped + mutex.synchronize do + return if stopped super end - @poll_thread.join + poll_thread.join end private @@ -49,13 +49,13 @@ def stop # Poll listener directory for file system changes. # def poll - until @stopped - next if @paused + until stopped + next if paused start = Time.now.to_f - @callback.call(@directories.dup, recursive: true) - @turnstile.signal - nap_time = @latency - (Time.now.to_f - start) + callback.call(directories.dup, recursive: true) + turnstile.signal + nap_time = latency - (Time.now.to_f - start) sleep(nap_time) if nap_time > 0 end rescue Interrupt diff --git a/lib/listen/adapters/windows.rb b/lib/listen/adapters/windows.rb index f89e2cb0..5a72da22 100644 --- a/lib/listen/adapters/windows.rb +++ b/lib/listen/adapters/windows.rb @@ -11,6 +11,8 @@ class Windows < Adapter # Declare the adapter's dependencies dependency 'wdm', '~> 0.1' + attr_accessor :worker, :worker_thread, :poll_thread + # Initializes the Adapter. # # @see Listen::Adapter#initialize @@ -26,29 +28,27 @@ def initialize(directories, options = {}, &callback) # def start(blocking = true) super - - @worker_thread = Thread.new { @worker.run! } + @worker_thread = Thread.new { worker.run! } # Wait for the worker to start. This is needed to avoid a deadlock # when stopping immediately after starting. sleep 0.1 - @poll_thread = Thread.new { poll_changed_dirs } if @report_changes - - @worker_thread.join if blocking + @poll_thread = Thread.new { poll_changed_directories } if report_changes? + worker_thread.join if blocking end # Stops the adapter. # def stop - @mutex.synchronize do - return if @stopped + mutex.synchronize do + return if stopped super end - @worker.stop - @worker_thread.join if @worker_thread - @poll_thread.join if @poll_thread + worker.stop + Thread.kill(worker_thread) if worker_thread + poll_thread.join if poll_thread end # Checks if the adapter is usable on Windows. @@ -69,15 +69,15 @@ def self.usable? # def init_worker callback = Proc.new do |change| - next if @paused + next if paused - @mutex.synchronize do - @changed_dirs << File.dirname(change.path) + mutex.synchronize do + @changed_directories << File.dirname(change.path) end end WDM::Monitor.new.tap do |worker| - @directories.each { |d| worker.watch_recursively(d, &callback) } + directories.each { |dir| worker.watch_recursively(dir, &callback) } end end diff --git a/lib/listen/directory_record.rb b/lib/listen/directory_record.rb index df04f179..80342146 100644 --- a/lib/listen/directory_record.rb +++ b/lib/listen/directory_record.rb @@ -181,10 +181,10 @@ def fetch_changes(directories, options = {}) # @return [String] the relative path # def relative_to_base(path) - return nil unless path[@directory] + return nil unless path[directory] path = path.force_encoding("BINARY") if path.respond_to?(:force_encoding) - path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '') + path.sub(%r{^#{Regexp.quote(directory)}#{File::SEPARATOR}?}, '') end private @@ -201,7 +201,7 @@ def relative_to_base(path) # @option options [Boolean] relative_paths whether or not to use relative paths for changes # def detect_modifications_and_removals(directory, options = {}) - @paths[directory].each do |basename, meta_data| + paths[directory].each do |basename, meta_data| path = File.join(directory, basename) case meta_data.type when 'Dir' @@ -304,7 +304,7 @@ def detect_additions(directory, options = {}) def content_modified?(path) return false unless File.ftype(path) == 'file' @sha1_checksum = sha1_checksum(path) - if @sha1_checksums[path] == @sha1_checksum || !@sha1_checksums.key?(path) + if sha1_checksums[path] == @sha1_checksum || !sha1_checksums.key?(path) update_sha1_checksum(path) false else @@ -339,8 +339,8 @@ def sha1_checksum(path) # @yield [path] an important path # def important_paths - Find.find(@directory) do |path| - next if path == @directory + Find.find(directory) do |path| + next if path == directory if File.directory?(path) # Add a trailing slash to directories when checking if a directory is @@ -375,7 +375,7 @@ def insert_path(path) # @return [Boolean] # def existing_path?(path) - @paths[File.dirname(path)][File.basename(path)] != nil + paths[File.dirname(path)][File.basename(path)] != nil end # Returns the modification time of a file based on the precision defined by the system diff --git a/lib/listen/listener.rb b/lib/listen/listener.rb index 9a3abbe6..4162458d 100644 --- a/lib/listen/listener.rb +++ b/lib/listen/listener.rb @@ -2,7 +2,8 @@ module Listen class Listener - attr_reader :directories, :directories_records, :adapter + + attr_reader :directories, :directories_records, :block, :adapter, :adapter_options, :use_relative_paths # Initializes the directories listener. # @@ -43,13 +44,13 @@ def start(blocking = true) t = Thread.new { build_directories_records } @adapter = initialize_adapter t.join - @adapter.start(blocking) + adapter.start(blocking) end # Stops the listener. # def stop - @adapter.stop + adapter.stop end # Pauses the listener. @@ -57,7 +58,7 @@ def stop # @return [Listen::Listener] the listener # def pause - @adapter.pause + adapter.pause self end @@ -67,7 +68,7 @@ def pause # def unpause build_directories_records - @adapter.unpause + adapter.unpause self end @@ -76,7 +77,7 @@ def unpause # @return [Boolean] adapter paused status # def paused? - !!@adapter && @adapter.paused? + !!adapter && adapter.paused? end # Adds ignoring patterns to the listener. @@ -88,7 +89,7 @@ def paused? # @see Listen::DirectoryRecord#ignore # def ignore(*regexps) - @directories_records.each { |r| r.ignore(*regexps) } + directories_records.each { |r| r.ignore(*regexps) } self end @@ -101,7 +102,7 @@ def ignore(*regexps) # @see Listen::DirectoryRecord#ignore! # def ignore!(*regexps) - @directories_records.each { |r| r.ignore!(*regexps) } + directories_records.each { |r| r.ignore!(*regexps) } self end @@ -114,7 +115,7 @@ def ignore!(*regexps) # @see Listen::DirectoryRecord#filter # def filter(*regexps) - @directories_records.each { |r| r.filter(*regexps) } + directories_records.each { |r| r.filter(*regexps) } self end @@ -127,7 +128,7 @@ def filter(*regexps) # @see Listen::DirectoryRecord#filter! # def filter!(*regexps) - @directories_records.each { |r| r.filter!(*regexps) } + directories_records.each { |r| r.filter!(*regexps) } self end @@ -214,7 +215,7 @@ def change(&block) # modified, added, removed def on_change(directories, options = {}) changes = fetch_records_changes(directories, options) unless changes.values.all? { |paths| paths.empty? } - @block.call(changes[:modified], changes[:added], changes[:removed]) + block.call(changes[:modified], changes[:added], changes[:removed]) end end @@ -222,24 +223,24 @@ def on_change(directories, options = {}) def initialize_directories_and_directories_records(directories) @directories = directories.map { |d| Pathname.new(d).realpath.to_s } - @directories_records = @directories.map { |d| DirectoryRecord.new(d) } + @directories_records = directories.map { |d| DirectoryRecord.new(d) } end def initialize_relative_paths_usage(options) - @use_relative_paths = @directories.one? && options.delete(:relative_paths) { true } + @use_relative_paths = directories.one? && options.delete(:relative_paths) { true } end # Initializes an adapter passing it the callback and adapters' options. # def initialize_adapter - callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) } - Adapter.select_and_initialize(@directories, @adapter_options, &callback) + callback = lambda { |changed_directories, options| self.on_change(changed_directories, options) } + Adapter.select_and_initialize(directories, adapter_options, &callback) end # Build the watched directories' records. # def build_directories_records - @directories_records.each { |r| r.build } + directories_records.each { |r| r.build } end # Returns the sum of all the changes to the directories records @@ -249,10 +250,10 @@ def build_directories_records # @return [Hash] the changes # def fetch_records_changes(directories_to_search, options) - @directories_records.inject({}) do |h, r| + directories_records.inject({}) do |h, r| # directory records skips paths outside their range, so passing the # whole `directories` array is not a problem. - record_changes = r.fetch_changes(directories_to_search, options.merge(relative_paths: @use_relative_paths)) + record_changes = r.fetch_changes(directories_to_search, options.merge(relative_paths: use_relative_paths)) if h.empty? h.merge!(record_changes) diff --git a/lib/listen/turnstile.rb b/lib/listen/turnstile.rb index 926fbe9e..a0dc6963 100644 --- a/lib/listen/turnstile.rb +++ b/lib/listen/turnstile.rb @@ -5,25 +5,26 @@ module Listen # @note Only two threads can be used with this Turnstile # because of the current implementation. class Turnstile + attr_accessor :queue # Initialize the turnstile. # def initialize # Until Ruby offers semahpores, only queues can be used # to implement a turnstile. - @q = Queue.new + @queue = Queue.new end # Blocks the current thread until a signal is received. # def wait - @q.pop if @q.num_waiting == 0 + queue.pop if queue.num_waiting == 0 end # Unblocks the waiting thread if any. # def signal - @q.push(:dummy) if @q.num_waiting == 1 + queue.push(:dummy) if queue.num_waiting == 1 end end diff --git a/spec/listen/adapters/polling_spec.rb b/spec/listen/adapters/polling_spec.rb index 9b339ca5..d630a65c 100644 --- a/spec/listen/adapters/polling_spec.rb +++ b/spec/listen/adapters/polling_spec.rb @@ -13,7 +13,7 @@ describe '#poll' do let(:listener) { mock(Listen::Listener) } - let(:callback) { lambda { |changed_dirs, options| @called = true; listener.on_change(changed_dirs, options) } } + let(:callback) { lambda { |changed_directories, options| @called = true; listener.on_change(changed_directories, options) } } after { subject.stop } diff --git a/spec/support/adapter_helper.rb b/spec/support/adapter_helper.rb index f1d46bdb..21f7171e 100644 --- a/spec/support/adapter_helper.rb +++ b/spec/support/adapter_helper.rb @@ -4,7 +4,9 @@ # @param [String] path the path to watch # def watch(listener, expected_changes, *paths) - callback = lambda { |changed_dirs, options| @called = true; listener.on_change(changed_dirs) } + sleep 0.01 # allow file/creation to be done (!) + + callback = lambda { |changed_directories, options| @called = true; listener.on_change(changed_directories) } @adapter = Listen::Adapter.select_and_initialize(paths, { report_changes: false, latency: test_latency }, &callback) forced_stop = false