listen 3.0.8 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CONTRIBUTING.md +10 -3
- data/README.md +248 -79
- data/bin/listen +3 -4
- data/lib/listen/adapter/base.rb +26 -34
- data/lib/listen/adapter/bsd.rb +9 -8
- data/lib/listen/adapter/config.rb +4 -5
- data/lib/listen/adapter/darwin.rb +35 -46
- data/lib/listen/adapter/linux.rb +17 -17
- data/lib/listen/adapter/polling.rb +9 -6
- data/lib/listen/adapter/windows.rb +19 -22
- data/lib/listen/adapter.rb +25 -25
- data/lib/listen/backend.rb +7 -10
- data/lib/listen/change.rb +18 -27
- data/lib/listen/cli.rb +6 -6
- data/lib/listen/directory.rb +15 -9
- data/lib/listen/error.rb +11 -0
- data/lib/listen/event/config.rb +9 -28
- data/lib/listen/event/loop.rb +44 -67
- data/lib/listen/event/processor.rb +41 -37
- data/lib/listen/event/queue.rb +14 -18
- data/lib/listen/file.rb +23 -8
- data/lib/listen/fsm.rb +74 -72
- data/lib/listen/listener/config.rb +5 -9
- data/lib/listen/listener.rb +26 -22
- data/lib/listen/logger.rb +24 -20
- data/lib/listen/monotonic_time.rb +27 -0
- data/lib/listen/options.rb +11 -8
- data/lib/listen/queue_optimizer.rb +15 -18
- data/lib/listen/record/entry.rb +8 -4
- data/lib/listen/record/symlink_detector.rb +9 -7
- data/lib/listen/record.rb +40 -37
- data/lib/listen/silencer/controller.rb +2 -0
- data/lib/listen/silencer.rb +30 -21
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- data/lib/listen.rb +14 -22
- metadata +20 -28
- data/lib/listen/internals/thread_pool.rb +0 -29
    
        data/lib/listen/change.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'listen/file'
         | 
| 2 4 | 
             
            require 'listen/directory'
         | 
| 3 5 |  | 
| @@ -28,51 +30,40 @@ module Listen | |
| 28 30 | 
             
                end
         | 
| 29 31 |  | 
| 30 32 | 
             
                # Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
         | 
| 33 | 
            +
                # rubocop:disable Metrics/MethodLength
         | 
| 34 | 
            +
                # rubocop:disable Metrics/CyclomaticComplexity
         | 
| 35 | 
            +
                # rubocop:disable Metrics/PerceivedComplexity
         | 
| 31 36 | 
             
                def invalidate(type, rel_path, options)
         | 
| 32 37 | 
             
                  watched_dir = Pathname.new(record.root)
         | 
| 33 38 |  | 
| 34 39 | 
             
                  change = options[:change]
         | 
| 35 40 | 
             
                  cookie = options[:cookie]
         | 
| 36 41 |  | 
| 37 | 
            -
                  if !cookie && config.silenced?(rel_path, type)
         | 
| 38 | 
            -
                    Listen | 
| 42 | 
            +
                  if !cookie && @config.silenced?(rel_path, type)
         | 
| 43 | 
            +
                    Listen.logger.debug { "(silenced): #{rel_path.inspect}" }
         | 
| 39 44 | 
             
                    return
         | 
| 40 45 | 
             
                  end
         | 
| 41 46 |  | 
| 42 47 | 
             
                  path = watched_dir + rel_path
         | 
| 43 48 |  | 
| 44 | 
            -
                  Listen | 
| 49 | 
            +
                  Listen.logger.debug do
         | 
| 45 50 | 
             
                    log_details = options[:silence] && 'recording' || change || 'unknown'
         | 
| 46 51 | 
             
                    "#{log_details}: #{type}:#{path} (#{options.inspect})"
         | 
| 47 52 | 
             
                  end
         | 
| 48 53 |  | 
| 49 54 | 
             
                  if change
         | 
| 50 55 | 
             
                    options = cookie ? { cookie: cookie } : {}
         | 
| 51 | 
            -
                    config.queue(type, change, watched_dir, rel_path, options)
         | 
| 52 | 
            -
                   | 
| 53 | 
            -
                     | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
                     | 
| 58 | 
            -
                      change = File.change(record, rel_path)
         | 
| 59 | 
            -
                      return if !change || options[:silence]
         | 
| 60 | 
            -
                      config.queue(:file, change, watched_dir, rel_path)
         | 
| 61 | 
            -
                    end
         | 
| 56 | 
            +
                    @config.queue(type, change, watched_dir, rel_path, options)
         | 
| 57 | 
            +
                  elsif type == :dir
         | 
| 58 | 
            +
                    # NOTE: POSSIBLE RECURSION
         | 
| 59 | 
            +
                    # TODO: fix - use a queue instead
         | 
| 60 | 
            +
                    Directory.scan(self, rel_path, options)
         | 
| 61 | 
            +
                  elsif (change = File.change(record, rel_path)) && !options[:silence]
         | 
| 62 | 
            +
                    @config.queue(:file, change, watched_dir, rel_path)
         | 
| 62 63 | 
             
                  end
         | 
| 63 | 
            -
                rescue RuntimeError => ex
         | 
| 64 | 
            -
                  msg = format(
         | 
| 65 | 
            -
                    '%s#%s crashed %s:%s',
         | 
| 66 | 
            -
                    self.class,
         | 
| 67 | 
            -
                    __method__,
         | 
| 68 | 
            -
                    exinspect,
         | 
| 69 | 
            -
                    ex.backtrace * "\n")
         | 
| 70 | 
            -
                  Listen::Logger.error(msg)
         | 
| 71 | 
            -
                  raise
         | 
| 72 64 | 
             
                end
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                 | 
| 75 | 
            -
             | 
| 76 | 
            -
                attr_reader :config
         | 
| 65 | 
            +
                # rubocop:enable Metrics/MethodLength
         | 
| 66 | 
            +
                # rubocop:enable Metrics/CyclomaticComplexity
         | 
| 67 | 
            +
                # rubocop:enable Metrics/PerceivedComplexity
         | 
| 77 68 | 
             
              end
         | 
| 78 69 | 
             
            end
         | 
    
        data/lib/listen/cli.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'thor'
         | 
| 2 4 | 
             
            require 'listen'
         | 
| 3 5 | 
             
            require 'logger'
         | 
| @@ -33,15 +35,16 @@ module Listen | |
| 33 35 |  | 
| 34 36 | 
             
              class Forwarder
         | 
| 35 37 | 
             
                attr_reader :logger
         | 
| 38 | 
            +
             | 
| 36 39 | 
             
                def initialize(options)
         | 
| 37 40 | 
             
                  @options = options
         | 
| 38 | 
            -
                  @logger = ::Logger.new(STDOUT)
         | 
| 39 | 
            -
                  @logger.level = ::Logger::INFO
         | 
| 41 | 
            +
                  @logger = ::Logger.new(STDOUT, level: ::Logger::INFO)
         | 
| 40 42 | 
             
                  @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
         | 
| 41 43 | 
             
                end
         | 
| 42 44 |  | 
| 43 45 | 
             
                def start
         | 
| 44 46 | 
             
                  logger.info 'Starting listen...'
         | 
| 47 | 
            +
             | 
| 45 48 | 
             
                  directory = @options[:directory]
         | 
| 46 49 | 
             
                  relative = @options[:relative]
         | 
| 47 50 | 
             
                  callback = proc do |modified, added, removed|
         | 
| @@ -52,10 +55,7 @@ module Listen | |
| 52 55 | 
             
                    end
         | 
| 53 56 | 
             
                  end
         | 
| 54 57 |  | 
| 55 | 
            -
                  listener = Listen.to(
         | 
| 56 | 
            -
                    directory,
         | 
| 57 | 
            -
                    relative: relative,
         | 
| 58 | 
            -
                    &callback)
         | 
| 58 | 
            +
                  listener = Listen.to(directory, relative: relative, &callback)
         | 
| 59 59 |  | 
| 60 60 | 
             
                  listener.start
         | 
| 61 61 |  | 
    
        data/lib/listen/directory.rb
    CHANGED
    
    | @@ -1,8 +1,11 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'set'
         | 
| 2 4 |  | 
| 3 5 | 
             
            module Listen
         | 
| 4 6 | 
             
              # TODO: refactor (turn it into a normal object, cache the stat, etc)
         | 
| 5 7 | 
             
              class Directory
         | 
| 8 | 
            +
                # rubocop:disable Metrics/MethodLength
         | 
| 6 9 | 
             
                def self.scan(snapshot, rel_path, options)
         | 
| 7 10 | 
             
                  record = snapshot.record
         | 
| 8 11 | 
             
                  dir = Pathname.new(record.root)
         | 
| @@ -14,7 +17,7 @@ module Listen | |
| 14 17 | 
             
                  path = dir + rel_path
         | 
| 15 18 | 
             
                  current = Set.new(_children(path))
         | 
| 16 19 |  | 
| 17 | 
            -
                  Listen | 
| 20 | 
            +
                  Listen.logger.debug do
         | 
| 18 21 | 
             
                    format('%s: %s(%s): %s -> %s',
         | 
| 19 22 | 
             
                           (options[:silence] ? 'Recording' : 'Scanning'),
         | 
| 20 23 | 
             
                           rel_path, options.inspect, previous.inspect, current.inspect)
         | 
| @@ -33,25 +36,28 @@ module Listen | |
| 33 36 | 
             
                  end
         | 
| 34 37 |  | 
| 35 38 | 
             
                  # TODO: this is not tested properly
         | 
| 36 | 
            -
                  previous = previous.reject { |entry, _| current.include? | 
| 39 | 
            +
                  previous = previous.reject { |entry, _| current.include?(path + entry) }
         | 
| 37 40 |  | 
| 38 41 | 
             
                  _async_changes(snapshot, Pathname.new(rel_path), previous, options)
         | 
| 39 | 
            -
             | 
| 40 42 | 
             
                rescue Errno::ENOENT, Errno::EHOSTDOWN
         | 
| 41 43 | 
             
                  record.unset_path(rel_path)
         | 
| 42 44 | 
             
                  _async_changes(snapshot, Pathname.new(rel_path), previous, options)
         | 
| 43 | 
            -
             | 
| 44 45 | 
             
                rescue Errno::ENOTDIR
         | 
| 45 46 | 
             
                  # TODO: path not tested
         | 
| 46 47 | 
             
                  record.unset_path(rel_path)
         | 
| 47 48 | 
             
                  _async_changes(snapshot, path, previous, options)
         | 
| 48 49 | 
             
                  _change(snapshot, :file, rel_path, options)
         | 
| 49 50 | 
             
                rescue
         | 
| 50 | 
            -
                  Listen | 
| 51 | 
            -
                    format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
         | 
| 52 | 
            -
                  end
         | 
| 51 | 
            +
                  Listen.logger.warn { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") }
         | 
| 53 52 | 
             
                  raise
         | 
| 54 53 | 
             
                end
         | 
| 54 | 
            +
                # rubocop:enable Metrics/MethodLength
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def self.ascendant_of?(base, other)
         | 
| 57 | 
            +
                  other.ascend do |ascendant|
         | 
| 58 | 
            +
                    break true if base == ascendant
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 55 61 |  | 
| 56 62 | 
             
                def self._async_changes(snapshot, path, previous, options)
         | 
| 57 63 | 
             
                  fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
         | 
| @@ -80,8 +86,8 @@ module Listen | |
| 80 86 | 
             
                  # https://github.com/jruby/jruby/issues/3840
         | 
| 81 87 | 
             
                  exists = path.exist?
         | 
| 82 88 | 
             
                  directory = path.directory?
         | 
| 83 | 
            -
                   | 
| 84 | 
            -
                   | 
| 89 | 
            +
                  exists && !directory and raise Errno::ENOTDIR, path.to_s
         | 
| 90 | 
            +
                  path.children
         | 
| 85 91 | 
             
                end
         | 
| 86 92 | 
             
              end
         | 
| 87 93 | 
             
            end
         | 
    
        data/lib/listen/error.rb
    ADDED
    
    | @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Besides programming error exceptions like ArgumentError,
         | 
| 4 | 
            +
            # all public interface exceptions should be declared here and inherit from Listen::Error.
         | 
| 5 | 
            +
            module Listen
         | 
| 6 | 
            +
              class Error < RuntimeError
         | 
| 7 | 
            +
                class NotStarted < Error; end
         | 
| 8 | 
            +
                class SymlinkLoop < Error; end
         | 
| 9 | 
            +
                class INotifyMaxWatchesExceeded < Error; end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
            end
         | 
    
        data/lib/listen/event/config.rb
    CHANGED
    
    | @@ -1,12 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 4 | 
             
              module Event
         | 
| 3 5 | 
             
                class Config
         | 
| 6 | 
            +
                  attr_reader :listener, :event_queue, :min_delay_between_events
         | 
| 7 | 
            +
             | 
| 4 8 | 
             
                  def initialize(
         | 
| 5 9 | 
             
                    listener,
         | 
| 6 10 | 
             
                    event_queue,
         | 
| 7 11 | 
             
                    queue_optimizer,
         | 
| 8 12 | 
             
                    wait_for_delay,
         | 
| 9 | 
            -
                    &block | 
| 13 | 
            +
                    &block
         | 
| 14 | 
            +
                  )
         | 
| 10 15 |  | 
| 11 16 | 
             
                    @listener = listener
         | 
| 12 17 | 
             
                    @event_queue = event_queue
         | 
| @@ -15,20 +20,12 @@ module Listen | |
| 15 20 | 
             
                    @block = block
         | 
| 16 21 | 
             
                  end
         | 
| 17 22 |  | 
| 18 | 
            -
                  def sleep( | 
| 19 | 
            -
                    Kernel.sleep( | 
| 23 | 
            +
                  def sleep(seconds)
         | 
| 24 | 
            +
                    Kernel.sleep(seconds)
         | 
| 20 25 | 
             
                  end
         | 
| 21 26 |  | 
| 22 27 | 
             
                  def call(*args)
         | 
| 23 | 
            -
                    @block | 
| 24 | 
            -
                  end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                  def timestamp
         | 
| 27 | 
            -
                    Time.now.to_f
         | 
| 28 | 
            -
                  end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                  def event_queue
         | 
| 31 | 
            -
                    @event_queue
         | 
| 28 | 
            +
                    @block&.call(*args)
         | 
| 32 29 | 
             
                  end
         | 
| 33 30 |  | 
| 34 31 | 
             
                  def callable?
         | 
| @@ -38,22 +35,6 @@ module Listen | |
| 38 35 | 
             
                  def optimize_changes(changes)
         | 
| 39 36 | 
             
                    @queue_optimizer.smoosh_changes(changes)
         | 
| 40 37 | 
             
                  end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                  def min_delay_between_events
         | 
| 43 | 
            -
                    @min_delay_between_events
         | 
| 44 | 
            -
                  end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  def stopped?
         | 
| 47 | 
            -
                    listener.state == :stopped
         | 
| 48 | 
            -
                  end
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                  def paused?
         | 
| 51 | 
            -
                    listener.state == :paused
         | 
| 52 | 
            -
                  end
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                  private
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                  attr_reader :listener
         | 
| 57 38 | 
             
                end
         | 
| 58 39 | 
             
              end
         | 
| 59 40 | 
             
            end
         | 
    
        data/lib/listen/event/loop.rb
    CHANGED
    
    | @@ -1,55 +1,62 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'thread'
         | 
| 2 4 |  | 
| 3 5 | 
             
            require 'timeout'
         | 
| 4 6 | 
             
            require 'listen/event/processor'
         | 
| 7 | 
            +
            require 'listen/thread'
         | 
| 8 | 
            +
            require 'listen/error'
         | 
| 5 9 |  | 
| 6 10 | 
             
            module Listen
         | 
| 7 11 | 
             
              module Event
         | 
| 8 12 | 
             
                class Loop
         | 
| 9 | 
            -
                   | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
                   | 
| 13 | 
            +
                  include Listen::FSM
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  Error = ::Listen::Error
         | 
| 16 | 
            +
                  NotStarted = ::Listen::Error::NotStarted # for backward compatibility
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  start_state :pre_start
         | 
| 19 | 
            +
                  state :pre_start
         | 
| 20 | 
            +
                  state :starting
         | 
| 21 | 
            +
                  state :started
         | 
| 22 | 
            +
                  state :stopped
         | 
| 13 23 |  | 
| 14 24 | 
             
                  def initialize(config)
         | 
| 15 25 | 
             
                    @config = config
         | 
| 16 26 | 
             
                    @wait_thread = nil
         | 
| 17 | 
            -
                    @state = :paused
         | 
| 18 27 | 
             
                    @reasons = ::Queue.new
         | 
| 28 | 
            +
                    initialize_fsm
         | 
| 19 29 | 
             
                  end
         | 
| 20 30 |  | 
| 21 31 | 
             
                  def wakeup_on_event
         | 
| 22 | 
            -
                     | 
| 23 | 
            -
             | 
| 24 | 
            -
                     | 
| 25 | 
            -
                    _wakeup(:event)
         | 
| 32 | 
            +
                    if started? && @wait_thread&.alive?
         | 
| 33 | 
            +
                      _wakeup(:event)
         | 
| 34 | 
            +
                    end
         | 
| 26 35 | 
             
                  end
         | 
| 27 36 |  | 
| 28 | 
            -
                  def  | 
| 29 | 
            -
                     | 
| 37 | 
            +
                  def started?
         | 
| 38 | 
            +
                    state == :started
         | 
| 30 39 | 
             
                  end
         | 
| 31 40 |  | 
| 32 | 
            -
                   | 
| 33 | 
            -
                    return false if stopped?
         | 
| 34 | 
            -
                    return false if paused?
         | 
| 35 | 
            -
                    state == :processing
         | 
| 36 | 
            -
                  end
         | 
| 41 | 
            +
                  MAX_STARTUP_SECONDS = 5.0
         | 
| 37 42 |  | 
| 38 | 
            -
                   | 
| 43 | 
            +
                  # @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS
         | 
| 44 | 
            +
                  def start
         | 
| 39 45 | 
             
                    # TODO: use a Fiber instead?
         | 
| 40 | 
            -
                     | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 46 | 
            +
                    return unless state == :pre_start
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    transition! :starting
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    @wait_thread = Listen::Thread.new("wait_thread") do
         | 
| 51 | 
            +
                      _process_changes
         | 
| 43 52 | 
             
                    end
         | 
| 44 53 |  | 
| 45 | 
            -
                    Listen | 
| 46 | 
            -
                    Timeout.timeout(5) { q.pop }
         | 
| 47 | 
            -
                  end
         | 
| 54 | 
            +
                    Listen.logger.debug("Waiting for processing to start...")
         | 
| 48 55 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
                     | 
| 56 | 
            +
                    wait_for_state(:started, timeout: MAX_STARTUP_SECONDS) or
         | 
| 57 | 
            +
                      raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})"
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    Listen.logger.debug('Processing started.')
         | 
| 53 60 | 
             
                  end
         | 
| 54 61 |  | 
| 55 62 | 
             
                  def pause
         | 
| @@ -57,60 +64,30 @@ module Listen | |
| 57 64 | 
             
                    # fail NotImplementedError
         | 
| 58 65 | 
             
                  end
         | 
| 59 66 |  | 
| 60 | 
            -
                  def  | 
| 61 | 
            -
                     | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
                      wait_thread.join
         | 
| 65 | 
            -
                    end
         | 
| 67 | 
            +
                  def stop
         | 
| 68 | 
            +
                    transition! :stopped
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    @wait_thread&.join
         | 
| 66 71 | 
             
                    @wait_thread = nil
         | 
| 67 72 | 
             
                  end
         | 
| 68 73 |  | 
| 69 74 | 
             
                  def stopped?
         | 
| 70 | 
            -
                     | 
| 75 | 
            +
                    state == :stopped
         | 
| 71 76 | 
             
                  end
         | 
| 72 77 |  | 
| 73 78 | 
             
                  private
         | 
| 74 79 |  | 
| 75 | 
            -
                   | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
                  attr_accessor :state
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                  def _wait_for_changes(ready_queue, config)
         | 
| 81 | 
            -
                    processor = Event::Processor.new(config, @reasons)
         | 
| 80 | 
            +
                  def _process_changes
         | 
| 81 | 
            +
                    processor = Event::Processor.new(@config, @reasons)
         | 
| 82 82 |  | 
| 83 | 
            -
                     | 
| 84 | 
            -
                    processor.loop_for(config.min_delay_between_events)
         | 
| 85 | 
            -
                  rescue StandardError => ex
         | 
| 86 | 
            -
                    _nice_error(ex)
         | 
| 87 | 
            -
                  end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                  def _sleep(*args)
         | 
| 90 | 
            -
                    Kernel.sleep(*args)
         | 
| 91 | 
            -
                  end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                  def _wait_until_resumed(ready_queue)
         | 
| 94 | 
            -
                    self.state = :paused
         | 
| 95 | 
            -
                    ready_queue << :ready
         | 
| 96 | 
            -
                    sleep
         | 
| 97 | 
            -
                    self.state = :processing
         | 
| 98 | 
            -
                  end
         | 
| 83 | 
            +
                    transition! :started
         | 
| 99 84 |  | 
| 100 | 
            -
             | 
| 101 | 
            -
                    indent = "\n -- "
         | 
| 102 | 
            -
                    msg = format(
         | 
| 103 | 
            -
                      'exception while processing events: %s Backtrace:%s%s',
         | 
| 104 | 
            -
                      ex,
         | 
| 105 | 
            -
                      indent,
         | 
| 106 | 
            -
                      ex.backtrace * indent
         | 
| 107 | 
            -
                    )
         | 
| 108 | 
            -
                    Listen::Logger.error(msg)
         | 
| 85 | 
            +
                    processor.loop_for(@config.min_delay_between_events)
         | 
| 109 86 | 
             
                  end
         | 
| 110 87 |  | 
| 111 88 | 
             
                  def _wakeup(reason)
         | 
| 112 89 | 
             
                    @reasons << reason
         | 
| 113 | 
            -
                    wait_thread.wakeup
         | 
| 90 | 
            +
                    @wait_thread.wakeup
         | 
| 114 91 | 
             
                  end
         | 
| 115 92 | 
             
                end
         | 
| 116 93 | 
             
              end
         | 
| @@ -1,8 +1,13 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'listen/monotonic_time'
         | 
| 4 | 
            +
             | 
| 1 5 | 
             
            module Listen
         | 
| 2 6 | 
             
              module Event
         | 
| 3 7 | 
             
                class Processor
         | 
| 4 8 | 
             
                  def initialize(config, reasons)
         | 
| 5 9 | 
             
                    @config = config
         | 
| 10 | 
            +
                    @listener = config.listener
         | 
| 6 11 | 
             
                    @reasons = reasons
         | 
| 7 12 | 
             
                    _reset_no_unprocessed_events
         | 
| 8 13 | 
             
                  end
         | 
| @@ -13,13 +18,14 @@ module Listen | |
| 13 18 | 
             
                    @latency = latency
         | 
| 14 19 |  | 
| 15 20 | 
             
                    loop do
         | 
| 16 | 
            -
                      _wait_until_events
         | 
| 21 | 
            +
                      event = _wait_until_events
         | 
| 22 | 
            +
                      _check_stopped
         | 
| 17 23 | 
             
                      _wait_until_events_calm_down
         | 
| 18 24 | 
             
                      _wait_until_no_longer_paused
         | 
| 19 | 
            -
                      _process_changes
         | 
| 25 | 
            +
                      _process_changes(event)
         | 
| 20 26 | 
             
                    end
         | 
| 21 27 | 
             
                  rescue Stopped
         | 
| 22 | 
            -
                    Listen | 
| 28 | 
            +
                    Listen.logger.debug('Processing stopped')
         | 
| 23 29 | 
             
                  end
         | 
| 24 30 |  | 
| 25 31 | 
             
                  private
         | 
| @@ -29,7 +35,7 @@ module Listen | |
| 29 35 |  | 
| 30 36 | 
             
                  def _wait_until_events_calm_down
         | 
| 31 37 | 
             
                    loop do
         | 
| 32 | 
            -
                      now =  | 
| 38 | 
            +
                      now = MonotonicTime.now
         | 
| 33 39 |  | 
| 34 40 | 
             
                      # Assure there's at least latency between callbacks to allow
         | 
| 35 41 | 
             
                      # for accumulating changes
         | 
| @@ -38,82 +44,80 @@ module Listen | |
| 38 44 |  | 
| 39 45 | 
             
                      # give events a bit of time to accumulate so they can be
         | 
| 40 46 | 
             
                      # compressed/optimized
         | 
| 41 | 
            -
                      _sleep( | 
| 47 | 
            +
                      _sleep(diff)
         | 
| 42 48 | 
             
                    end
         | 
| 43 49 | 
             
                  end
         | 
| 44 50 |  | 
| 45 51 | 
             
                  def _wait_until_no_longer_paused
         | 
| 46 | 
            -
                     | 
| 47 | 
            -
                    _sleep(:waiting_for_unpause) while config.paused?
         | 
| 52 | 
            +
                    @listener.wait_for_state(*(Listener.states.keys - [:paused]))
         | 
| 48 53 | 
             
                  end
         | 
| 49 54 |  | 
| 50 55 | 
             
                  def _check_stopped
         | 
| 51 | 
            -
                     | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                     | 
| 56 | 
            +
                    if @listener.stopped?
         | 
| 57 | 
            +
                      _flush_wakeup_reasons
         | 
| 58 | 
            +
                      raise Stopped
         | 
| 59 | 
            +
                    end
         | 
| 55 60 | 
             
                  end
         | 
| 56 61 |  | 
| 57 | 
            -
                  def _sleep( | 
| 62 | 
            +
                  def _sleep(seconds)
         | 
| 58 63 | 
             
                    _check_stopped
         | 
| 59 | 
            -
                     | 
| 64 | 
            +
                    config.sleep(seconds)
         | 
| 60 65 | 
             
                    _check_stopped
         | 
| 61 66 |  | 
| 62 67 | 
             
                    _flush_wakeup_reasons do |reason|
         | 
| 63 | 
            -
                       | 
| 64 | 
            -
             | 
| 68 | 
            +
                      if reason == :event && !@listener.paused?
         | 
| 69 | 
            +
                        _remember_time_of_first_unprocessed_event
         | 
| 70 | 
            +
                      end
         | 
| 65 71 | 
             
                    end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                    sleep_duration
         | 
| 68 72 | 
             
                  end
         | 
| 69 73 |  | 
| 70 74 | 
             
                  def _remember_time_of_first_unprocessed_event
         | 
| 71 | 
            -
                    @ | 
| 75 | 
            +
                    @_remember_time_of_first_unprocessed_event ||= MonotonicTime.now
         | 
| 72 76 | 
             
                  end
         | 
| 73 77 |  | 
| 74 78 | 
             
                  def _reset_no_unprocessed_events
         | 
| 75 | 
            -
                    @ | 
| 79 | 
            +
                    @_remember_time_of_first_unprocessed_event = nil
         | 
| 76 80 | 
             
                  end
         | 
| 77 81 |  | 
| 78 82 | 
             
                  def _deadline
         | 
| 79 | 
            -
                    @ | 
| 83 | 
            +
                    @_remember_time_of_first_unprocessed_event + @latency
         | 
| 80 84 | 
             
                  end
         | 
| 81 85 |  | 
| 86 | 
            +
                  # blocks until event is popped
         | 
| 87 | 
            +
                  # returns the event or `nil` when the event_queue is closed
         | 
| 82 88 | 
             
                  def _wait_until_events
         | 
| 83 | 
            -
                     | 
| 84 | 
            -
             | 
| 85 | 
            -
                     | 
| 89 | 
            +
                    config.event_queue.pop.tap do |_event|
         | 
| 90 | 
            +
                      @_remember_time_of_first_unprocessed_event ||= MonotonicTime.now
         | 
| 91 | 
            +
                    end
         | 
| 86 92 | 
             
                  end
         | 
| 87 93 |  | 
| 88 94 | 
             
                  def _flush_wakeup_reasons
         | 
| 89 | 
            -
                     | 
| 90 | 
            -
             | 
| 91 | 
            -
                      reason = reasons.pop
         | 
| 95 | 
            +
                    until @reasons.empty?
         | 
| 96 | 
            +
                      reason = @reasons.pop
         | 
| 92 97 | 
             
                      yield reason if block_given?
         | 
| 93 98 | 
             
                    end
         | 
| 94 99 | 
             
                  end
         | 
| 95 100 |  | 
| 96 | 
            -
                  def _timestamp
         | 
| 97 | 
            -
                    config.timestamp
         | 
| 98 | 
            -
                  end
         | 
| 99 | 
            -
             | 
| 100 101 | 
             
                  # for easier testing without sleep loop
         | 
| 101 | 
            -
                  def _process_changes
         | 
| 102 | 
            +
                  def _process_changes(event)
         | 
| 102 103 | 
             
                    _reset_no_unprocessed_events
         | 
| 103 104 |  | 
| 104 | 
            -
                    changes = []
         | 
| 105 | 
            +
                    changes = [event]
         | 
| 105 106 | 
             
                    changes << config.event_queue.pop until config.event_queue.empty?
         | 
| 106 107 |  | 
| 107 | 
            -
                     | 
| 108 | 
            -
                    return unless callable
         | 
| 108 | 
            +
                    return unless config.callable?
         | 
| 109 109 |  | 
| 110 110 | 
             
                    hash = config.optimize_changes(changes)
         | 
| 111 111 | 
             
                    result = [hash[:modified], hash[:added], hash[:removed]]
         | 
| 112 112 | 
             
                    return if result.all?(&:empty?)
         | 
| 113 113 |  | 
| 114 | 
            -
                    block_start =  | 
| 115 | 
            -
                     | 
| 116 | 
            -
                    Listen:: | 
| 114 | 
            +
                    block_start = MonotonicTime.now
         | 
| 115 | 
            +
                    exception_note = " (exception)"
         | 
| 116 | 
            +
                    ::Listen::Thread.rescue_and_log('_process_changes') do
         | 
| 117 | 
            +
                      config.call(*result)
         | 
| 118 | 
            +
                      exception_note = nil
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
                    Listen.logger.debug "Callback#{exception_note} took #{MonotonicTime.now - block_start} sec"
         | 
| 117 121 | 
             
                  end
         | 
| 118 122 |  | 
| 119 123 | 
             
                  attr_reader :config
         | 
    
        data/lib/listen/event/queue.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'thread'
         | 
| 2 4 |  | 
| 3 5 | 
             
            require 'forwardable'
         | 
| @@ -5,6 +7,8 @@ require 'forwardable' | |
| 5 7 | 
             
            module Listen
         | 
| 6 8 | 
             
              module Event
         | 
| 7 9 | 
             
                class Queue
         | 
| 10 | 
            +
                  extend Forwardable
         | 
| 11 | 
            +
             | 
| 8 12 | 
             
                  class Config
         | 
| 9 13 | 
             
                    def initialize(relative)
         | 
| 10 14 | 
             
                      @relative = relative
         | 
| @@ -15,9 +19,8 @@ module Listen | |
| 15 19 | 
             
                    end
         | 
| 16 20 | 
             
                  end
         | 
| 17 21 |  | 
| 18 | 
            -
                  def initialize(config | 
| 22 | 
            +
                  def initialize(config)
         | 
| 19 23 | 
             
                    @event_queue = ::Queue.new
         | 
| 20 | 
            -
                    @block = block
         | 
| 21 24 | 
             
                    @config = config
         | 
| 22 25 | 
             
                  end
         | 
| 23 26 |  | 
| @@ -27,28 +30,21 @@ module Listen | |
| 27 30 | 
             
                    fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
         | 
| 28 31 | 
             
                    fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
         | 
| 29 32 |  | 
| 30 | 
            -
                    dir =  | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
                  def empty?
         | 
| 37 | 
            -
                    event_queue.empty?
         | 
| 33 | 
            +
                    dir = if @config.relative?
         | 
| 34 | 
            +
                      _safe_relative_from_cwd(dir)
         | 
| 35 | 
            +
                    else
         | 
| 36 | 
            +
                      dir
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                    @event_queue << [type, change, dir, path, options]
         | 
| 38 39 | 
             
                  end
         | 
| 39 40 |  | 
| 40 | 
            -
                   | 
| 41 | 
            -
             | 
| 42 | 
            -
                   | 
| 41 | 
            +
                  delegate empty?: :@event_queue
         | 
| 42 | 
            +
                  delegate pop: :@event_queue
         | 
| 43 | 
            +
                  delegate close: :@event_queue
         | 
| 43 44 |  | 
| 44 45 | 
             
                  private
         | 
| 45 46 |  | 
| 46 | 
            -
                  attr_reader :event_queue
         | 
| 47 | 
            -
                  attr_reader :block
         | 
| 48 | 
            -
                  attr_reader :config
         | 
| 49 | 
            -
             | 
| 50 47 | 
             
                  def _safe_relative_from_cwd(dir)
         | 
| 51 | 
            -
                    return dir unless config.relative?
         | 
| 52 48 | 
             
                    dir.relative_path_from(Pathname.pwd)
         | 
| 53 49 | 
             
                  rescue ArgumentError
         | 
| 54 50 | 
             
                    dir
         |