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/file.rb
    CHANGED
    
    | @@ -1,12 +1,17 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'digest'
         | 
| 2 4 |  | 
| 3 5 | 
             
            module Listen
         | 
| 4 6 | 
             
              class File
         | 
| 7 | 
            +
                # rubocop:disable Metrics/MethodLength
         | 
| 8 | 
            +
                # rubocop:disable Metrics/CyclomaticComplexity
         | 
| 9 | 
            +
                # rubocop:disable Metrics/PerceivedComplexity
         | 
| 5 10 | 
             
                def self.change(record, rel_path)
         | 
| 6 11 | 
             
                  path = Pathname.new(record.root) + rel_path
         | 
| 7 12 | 
             
                  lstat = path.lstat
         | 
| 8 13 |  | 
| 9 | 
            -
                  data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
         | 
| 14 | 
            +
                  data = { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
         | 
| 10 15 |  | 
| 11 16 | 
             
                  record_data = record.file_data(rel_path)
         | 
| 12 17 |  | 
| @@ -25,8 +30,13 @@ module Listen | |
| 25 30 | 
             
                    return :modified
         | 
| 26 31 | 
             
                  end
         | 
| 27 32 |  | 
| 33 | 
            +
                  if data[:size] != record_data[:size]
         | 
| 34 | 
            +
                    record.update_file(rel_path, data)
         | 
| 35 | 
            +
                    return :modified
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 28 38 | 
             
                  return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
         | 
| 29 | 
            -
                  return unless  | 
| 39 | 
            +
                  return unless inaccurate_mac_time?(lstat)
         | 
| 30 40 |  | 
| 31 41 | 
             
                  # Check if change happened within 1 second (maybe it's even
         | 
| 32 42 | 
             
                  # too much, e.g. 0.3-0.5 could be sufficient).
         | 
| @@ -43,7 +53,7 @@ module Listen | |
| 43 53 | 
             
                  # then at ???14.998, but the fstat time would be ???14.0 in
         | 
| 44 54 | 
             
                  # both cases).
         | 
| 45 55 | 
             
                  #
         | 
| 46 | 
            -
                  # If change  | 
| 56 | 
            +
                  # If change happened at ???14.999997, the mtime is 14.0, so for
         | 
| 47 57 | 
             
                  # an mtime=???14.0 we assume it could even be almost ???15.0
         | 
| 48 58 | 
             
                  #
         | 
| 49 59 | 
             
                  # So if Time.now.to_f is ???15.999998 and stat reports mtime
         | 
| @@ -57,16 +67,21 @@ module Listen | |
| 57 67 | 
             
                  #
         | 
| 58 68 | 
             
                  return if data[:mtime].to_i + 2 <= Time.now.to_f
         | 
| 59 69 |  | 
| 60 | 
            -
                   | 
| 61 | 
            -
                  record.update_file(rel_path, data.merge( | 
| 62 | 
            -
                   | 
| 70 | 
            +
                  sha = Digest::SHA256.file(path).digest
         | 
| 71 | 
            +
                  record.update_file(rel_path, data.merge(sha: sha))
         | 
| 72 | 
            +
                  if record_data[:sha] && sha != record_data[:sha]
         | 
| 73 | 
            +
                    :modified
         | 
| 74 | 
            +
                  end
         | 
| 63 75 | 
             
                rescue SystemCallError
         | 
| 64 76 | 
             
                  record.unset_path(rel_path)
         | 
| 65 77 | 
             
                  :removed
         | 
| 66 78 | 
             
                rescue
         | 
| 67 | 
            -
                  Listen | 
| 79 | 
            +
                  Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
         | 
| 68 80 | 
             
                  raise
         | 
| 69 81 | 
             
                end
         | 
| 82 | 
            +
                # rubocop:enable Metrics/MethodLength
         | 
| 83 | 
            +
                # rubocop:enable Metrics/CyclomaticComplexity
         | 
| 84 | 
            +
                # rubocop:enable Metrics/PerceivedComplexity
         | 
| 70 85 |  | 
| 71 86 | 
             
                def self.inaccurate_mac_time?(stat)
         | 
| 72 87 | 
             
                  # 'mac' means Modified/Accessed/Created
         | 
    
        data/lib/listen/fsm.rb
    CHANGED
    
    | @@ -1,119 +1,123 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            # Code copied from https://github.com/celluloid/celluloid-fsm
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            require 'thread'
         | 
| 6 | 
            +
             | 
| 2 7 | 
             
            module Listen
         | 
| 3 8 | 
             
              module FSM
         | 
| 4 | 
            -
                DEFAULT_STATE = :default # Default state name unless one is explicitly set
         | 
| 5 | 
            -
             | 
| 6 9 | 
             
                # Included hook to extend class methods
         | 
| 7 10 | 
             
                def self.included(klass)
         | 
| 8 11 | 
             
                  klass.send :extend, ClassMethods
         | 
| 9 12 | 
             
                end
         | 
| 10 13 |  | 
| 11 14 | 
             
                module ClassMethods
         | 
| 12 | 
            -
                  # Obtain or set the  | 
| 13 | 
            -
                  # Passing a state name sets the  | 
| 14 | 
            -
                  def  | 
| 15 | 
            -
                    if  | 
| 16 | 
            -
                       | 
| 15 | 
            +
                  # Obtain or set the start state
         | 
| 16 | 
            +
                  # Passing a state name sets the start state
         | 
| 17 | 
            +
                  def start_state(new_start_state = nil)
         | 
| 18 | 
            +
                    if new_start_state
         | 
| 19 | 
            +
                      new_start_state.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_start_state.inspect})"
         | 
| 20 | 
            +
                      @start_state = new_start_state
         | 
| 17 21 | 
             
                    else
         | 
| 18 | 
            -
                      defined?(@ | 
| 22 | 
            +
                      defined?(@start_state) or raise ArgumentError, "`start_state :<state>` must be declared before `new`"
         | 
| 23 | 
            +
                      @start_state
         | 
| 19 24 | 
             
                    end
         | 
| 20 25 | 
             
                  end
         | 
| 21 26 |  | 
| 22 | 
            -
                  #  | 
| 27 | 
            +
                  # The valid states for this FSM, as a hash with state name symbols as keys and State objects as values.
         | 
| 23 28 | 
             
                  def states
         | 
| 24 29 | 
             
                    @states ||= {}
         | 
| 25 30 | 
             
                  end
         | 
| 26 31 |  | 
| 27 | 
            -
                  # Declare an FSM state and optionally provide a callback block to fire
         | 
| 32 | 
            +
                  # Declare an FSM state and optionally provide a callback block to fire on state entry
         | 
| 28 33 | 
             
                  # Options:
         | 
| 29 34 | 
             
                  # * to: a state or array of states this state can transition to
         | 
| 30 | 
            -
                  def state( | 
| 31 | 
            -
                     | 
| 32 | 
            -
             | 
| 33 | 
            -
                      options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
         | 
| 34 | 
            -
                    else
         | 
| 35 | 
            -
                      options = {}
         | 
| 36 | 
            -
                    end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                    args.each do |name|
         | 
| 39 | 
            -
                      name = name.to_sym
         | 
| 40 | 
            -
                      default_state name if options['default']
         | 
| 41 | 
            -
                      states[name] = State.new(name, options['to'], &block)
         | 
| 42 | 
            -
                    end
         | 
| 35 | 
            +
                  def state(state_name, to: nil, &block)
         | 
| 36 | 
            +
                    state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{state_name.inspect})"
         | 
| 37 | 
            +
                    states[state_name] = State.new(state_name, to, &block)
         | 
| 43 38 | 
             
                  end
         | 
| 44 39 | 
             
                end
         | 
| 45 40 |  | 
| 46 | 
            -
                #  | 
| 47 | 
            -
                def  | 
| 48 | 
            -
                  @ | 
| 41 | 
            +
                # Note: including classes must call initialize_fsm from their initialize method.
         | 
| 42 | 
            +
                def initialize_fsm
         | 
| 43 | 
            +
                  @fsm_initialized = true
         | 
| 44 | 
            +
                  @state = self.class.start_state
         | 
| 45 | 
            +
                  @mutex = ::Mutex.new
         | 
| 46 | 
            +
                  @state_changed = ::ConditionVariable.new
         | 
| 49 47 | 
             
                end
         | 
| 50 48 |  | 
| 51 | 
            -
                #  | 
| 49 | 
            +
                # Current state of the FSM, stored as a symbol
         | 
| 52 50 | 
             
                attr_reader :state
         | 
| 53 51 |  | 
| 54 | 
            -
                 | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 52 | 
            +
                # checks for one of the given states to wait for
         | 
| 53 | 
            +
                # if not already, waits for a state change (up to timeout seconds--`nil` means infinite)
         | 
| 54 | 
            +
                # returns truthy iff the transition to one of the desired state has occurred
         | 
| 55 | 
            +
                def wait_for_state(*wait_for_states, timeout: nil)
         | 
| 56 | 
            +
                  wait_for_states.each do |state|
         | 
| 57 | 
            +
                    state.is_a?(Symbol) or raise ArgumentError, "states must be symbols (got #{state.inspect})"
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                  @mutex.synchronize do
         | 
| 60 | 
            +
                    if !wait_for_states.include?(@state)
         | 
| 61 | 
            +
                      @state_changed.wait(@mutex, timeout)
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                    wait_for_states.include?(@state)
         | 
| 64 | 
            +
                  end
         | 
| 58 65 | 
             
                end
         | 
| 59 66 |  | 
| 60 | 
            -
                 | 
| 61 | 
            -
                def transition!(state_name)
         | 
| 62 | 
            -
                  @state = state_name
         | 
| 63 | 
            -
                end
         | 
| 67 | 
            +
                private
         | 
| 64 68 |  | 
| 65 | 
            -
                 | 
| 69 | 
            +
                def transition(new_state_name)
         | 
| 70 | 
            +
                  new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
         | 
| 71 | 
            +
                  if (new_state = validate_and_sanitize_new_state(new_state_name))
         | 
| 72 | 
            +
                    transition_with_callbacks!(new_state)
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 66 75 |  | 
| 67 | 
            -
                 | 
| 68 | 
            -
             | 
| 76 | 
            +
                # Low-level, immediate state transition with no checks or callbacks.
         | 
| 77 | 
            +
                def transition!(new_state_name)
         | 
| 78 | 
            +
                  new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
         | 
| 79 | 
            +
                  @fsm_initialized or raise ArgumentError, "FSM not initialized. You must call initialize_fsm from initialize!"
         | 
| 80 | 
            +
                  @mutex.synchronize do
         | 
| 81 | 
            +
                    yield if block_given?
         | 
| 82 | 
            +
                    @state = new_state_name
         | 
| 83 | 
            +
                    @state_changed.broadcast
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                end
         | 
| 69 86 |  | 
| 70 | 
            -
             | 
| 87 | 
            +
                def validate_and_sanitize_new_state(new_state_name)
         | 
| 88 | 
            +
                  return nil if @state == new_state_name
         | 
| 71 89 |  | 
| 72 | 
            -
                  if current_state && !current_state.valid_transition?( | 
| 90 | 
            +
                  if current_state && !current_state.valid_transition?(new_state_name)
         | 
| 73 91 | 
             
                    valid = current_state.transitions.map(&:to_s).join(', ')
         | 
| 74 | 
            -
                    msg = "#{self.class} can't change state from '#{@state}'" | 
| 75 | 
            -
             | 
| 76 | 
            -
                    fail ArgumentError, msg
         | 
| 92 | 
            +
                    msg = "#{self.class} can't change state from '#{@state}' to '#{new_state_name}', only to: #{valid}"
         | 
| 93 | 
            +
                    raise ArgumentError, msg
         | 
| 77 94 | 
             
                  end
         | 
| 78 95 |  | 
| 79 | 
            -
                  new_state = states[ | 
| 80 | 
            -
             | 
| 81 | 
            -
                  unless new_state
         | 
| 82 | 
            -
                    return if state_name == default_state
         | 
| 83 | 
            -
                    fail ArgumentError, "invalid state for #{self.class}: #{state_name}"
         | 
| 96 | 
            +
                  unless (new_state = self.class.states[new_state_name])
         | 
| 97 | 
            +
                    new_state_name == self.class.start_state or raise ArgumentError, "invalid state for #{self.class}: #{new_state_name}"
         | 
| 84 98 | 
             
                  end
         | 
| 85 99 |  | 
| 86 100 | 
             
                  new_state
         | 
| 87 101 | 
             
                end
         | 
| 88 102 |  | 
| 89 | 
            -
                def transition_with_callbacks!( | 
| 90 | 
            -
                  transition!  | 
| 91 | 
            -
                   | 
| 92 | 
            -
                end
         | 
| 93 | 
            -
             | 
| 94 | 
            -
                def states
         | 
| 95 | 
            -
                  self.class.states
         | 
| 96 | 
            -
                end
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                def default_state
         | 
| 99 | 
            -
                  self.class.default_state
         | 
| 103 | 
            +
                def transition_with_callbacks!(new_state)
         | 
| 104 | 
            +
                  transition! new_state.name
         | 
| 105 | 
            +
                  new_state.call(self)
         | 
| 100 106 | 
             
                end
         | 
| 101 107 |  | 
| 102 108 | 
             
                def current_state
         | 
| 103 | 
            -
                  states[@state]
         | 
| 104 | 
            -
                end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                def current_state_name
         | 
| 107 | 
            -
                  current_state && current_state.name || ''
         | 
| 109 | 
            +
                  self.class.states[@state]
         | 
| 108 110 | 
             
                end
         | 
| 109 111 |  | 
| 110 112 | 
             
                class State
         | 
| 111 113 | 
             
                  attr_reader :name, :transitions
         | 
| 112 114 |  | 
| 113 | 
            -
                  def initialize(name, transitions | 
| 114 | 
            -
                    @name | 
| 115 | 
            -
                    @ | 
| 116 | 
            -
                    @transitions =  | 
| 115 | 
            +
                  def initialize(name, transitions, &block)
         | 
| 116 | 
            +
                    @name = name
         | 
| 117 | 
            +
                    @block = block
         | 
| 118 | 
            +
                    @transitions = if transitions
         | 
| 119 | 
            +
                      Array(transitions).map(&:to_sym)
         | 
| 120 | 
            +
                    end
         | 
| 117 121 | 
             
                  end
         | 
| 118 122 |  | 
| 119 123 | 
             
                  def call(obj)
         | 
| @@ -121,10 +125,8 @@ module Listen | |
| 121 125 | 
             
                  end
         | 
| 122 126 |  | 
| 123 127 | 
             
                  def valid_transition?(new_state)
         | 
| 124 | 
            -
                    # All transitions are allowed  | 
| 125 | 
            -
                     | 
| 126 | 
            -
             | 
| 127 | 
            -
                    @transitions.include? new_state.to_sym
         | 
| 128 | 
            +
                    # All transitions are allowed if none are expressly declared
         | 
| 129 | 
            +
                    !@transitions || @transitions.include?(new_state.to_sym)
         | 
| 128 130 | 
             
                  end
         | 
| 129 131 | 
             
                end
         | 
| 130 132 | 
             
              end
         | 
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 4 | 
             
              class Listener
         | 
| 3 5 | 
             
                class Config
         | 
| @@ -10,7 +12,7 @@ module Listen | |
| 10 12 | 
             
                    # Backend selecting options
         | 
| 11 13 | 
             
                    force_polling: false,
         | 
| 12 14 | 
             
                    polling_fallback_message: nil
         | 
| 13 | 
            -
                  }
         | 
| 15 | 
            +
                  }.freeze
         | 
| 14 16 |  | 
| 15 17 | 
             
                  def initialize(opts)
         | 
| 16 18 | 
             
                    @options = DEFAULTS.merge(opts)
         | 
| @@ -23,13 +25,7 @@ module Listen | |
| 23 25 | 
             
                    @relative
         | 
| 24 26 | 
             
                  end
         | 
| 25 27 |  | 
| 26 | 
            -
                   | 
| 27 | 
            -
                    @min_delay_between_events
         | 
| 28 | 
            -
                  end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                  def silencer_rules
         | 
| 31 | 
            -
                    @silencer_rules
         | 
| 32 | 
            -
                  end
         | 
| 28 | 
            +
                  attr_reader :min_delay_between_events, :silencer_rules
         | 
| 33 29 |  | 
| 34 30 | 
             
                  def adapter_instance_options(klass)
         | 
| 35 31 | 
             
                    valid_keys = klass.const_get('DEFAULTS').keys
         | 
| @@ -37,7 +33,7 @@ module Listen | |
| 37 33 | 
             
                  end
         | 
| 38 34 |  | 
| 39 35 | 
             
                  def adapter_select_options
         | 
| 40 | 
            -
                    valid_keys = %w | 
| 36 | 
            +
                    valid_keys = %w[force_polling polling_fallback_message].map(&:to_sym)
         | 
| 41 37 | 
             
                    Hash[@options.select { |key, _| valid_keys.include?(key) }]
         | 
| 42 38 | 
             
                  end
         | 
| 43 39 | 
             
                end
         | 
    
        data/lib/listen/listener.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'English'
         | 
| 2 4 |  | 
| 3 5 | 
             
            require 'listen/version'
         | 
| @@ -31,13 +33,14 @@ module Listen | |
| 31 33 | 
             
                # @yieldparam [Array<String>] added the list of added files
         | 
| 32 34 | 
             
                # @yieldparam [Array<String>] removed the list of removed files
         | 
| 33 35 | 
             
                #
         | 
| 36 | 
            +
                # rubocop:disable Metrics/MethodLength
         | 
| 34 37 | 
             
                def initialize(*dirs, &block)
         | 
| 35 38 | 
             
                  options = dirs.last.is_a?(Hash) ? dirs.pop : {}
         | 
| 36 39 |  | 
| 37 40 | 
             
                  @config = Config.new(options)
         | 
| 38 41 |  | 
| 39 42 | 
             
                  eq_config = Event::Queue::Config.new(@config.relative?)
         | 
| 40 | 
            -
                  queue = Event::Queue.new(eq_config) | 
| 43 | 
            +
                  queue = Event::Queue.new(eq_config)
         | 
| 41 44 |  | 
| 42 45 | 
             
                  silencer = Silencer.new
         | 
| 43 46 | 
             
                  rules = @config.silencer_rules
         | 
| @@ -56,41 +59,43 @@ module Listen | |
| 56 59 |  | 
| 57 60 | 
             
                  @processor = Event::Loop.new(pconfig)
         | 
| 58 61 |  | 
| 59 | 
            -
                   | 
| 62 | 
            +
                  initialize_fsm
         | 
| 60 63 | 
             
                end
         | 
| 64 | 
            +
                # rubocop:enable Metrics/MethodLength
         | 
| 61 65 |  | 
| 62 | 
            -
                 | 
| 66 | 
            +
                start_state :initializing
         | 
| 63 67 |  | 
| 64 68 | 
             
                state :initializing, to: [:backend_started, :stopped]
         | 
| 65 69 |  | 
| 66 | 
            -
                state :backend_started, to: [: | 
| 67 | 
            -
                  backend.start
         | 
| 68 | 
            -
                end
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                state :frontend_ready, to: [:processing_events, :stopped] do
         | 
| 71 | 
            -
                  processor.setup
         | 
| 70 | 
            +
                state :backend_started, to: [:processing_events, :stopped] do
         | 
| 71 | 
            +
                  @backend.start
         | 
| 72 72 | 
             
                end
         | 
| 73 73 |  | 
| 74 74 | 
             
                state :processing_events, to: [:paused, :stopped] do
         | 
| 75 | 
            -
                  processor. | 
| 75 | 
            +
                  @processor.start
         | 
| 76 76 | 
             
                end
         | 
| 77 77 |  | 
| 78 78 | 
             
                state :paused, to: [:processing_events, :stopped] do
         | 
| 79 | 
            -
                  processor.pause
         | 
| 79 | 
            +
                  @processor.pause
         | 
| 80 80 | 
             
                end
         | 
| 81 81 |  | 
| 82 82 | 
             
                state :stopped, to: [:backend_started] do
         | 
| 83 | 
            -
                  backend.stop #  | 
| 84 | 
            -
                  processor. | 
| 83 | 
            +
                  @backend.stop # halt events ASAP
         | 
| 84 | 
            +
                  @processor.stop
         | 
| 85 85 | 
             
                end
         | 
| 86 86 |  | 
| 87 87 | 
             
                # Starts processing events and starts adapters
         | 
| 88 88 | 
             
                # or resumes invoking callbacks if paused
         | 
| 89 89 | 
             
                def start
         | 
| 90 | 
            -
                   | 
| 91 | 
            -
                   | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 90 | 
            +
                  case state
         | 
| 91 | 
            +
                  when :initializing
         | 
| 92 | 
            +
                    transition :backend_started
         | 
| 93 | 
            +
                    transition :processing_events
         | 
| 94 | 
            +
                  when :paused
         | 
| 95 | 
            +
                    transition :processing_events
         | 
| 96 | 
            +
                  else
         | 
| 97 | 
            +
                    raise ArgumentError, "cannot start from state #{state.inspect}"
         | 
| 98 | 
            +
                  end
         | 
| 94 99 | 
             
                end
         | 
| 95 100 |  | 
| 96 101 | 
             
                # Stops both listening for events and processing them
         | 
| @@ -112,6 +117,10 @@ module Listen | |
| 112 117 | 
             
                  state == :paused
         | 
| 113 118 | 
             
                end
         | 
| 114 119 |  | 
| 120 | 
            +
                def stopped?
         | 
| 121 | 
            +
                  state == :stopped
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 115 124 | 
             
                def ignore(regexps)
         | 
| 116 125 | 
             
                  @silencer_controller.append_ignores(regexps)
         | 
| 117 126 | 
             
                end
         | 
| @@ -123,10 +132,5 @@ module Listen | |
| 123 132 | 
             
                def only(regexps)
         | 
| 124 133 | 
             
                  @silencer_controller.replace_with_only(regexps)
         | 
| 125 134 | 
             
                end
         | 
| 126 | 
            -
             | 
| 127 | 
            -
                private
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                attr_reader :processor
         | 
| 130 | 
            -
                attr_reader :backend
         | 
| 131 135 | 
             
              end
         | 
| 132 136 | 
             
            end
         | 
    
        data/lib/listen/logger.rb
    CHANGED
    
    | @@ -1,32 +1,36 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 | 
            -
               | 
| 3 | 
            -
                @logger ||= nil
         | 
| 4 | 
            -
              end
         | 
| 4 | 
            +
              @logger = nil
         | 
| 5 5 |  | 
| 6 | 
            -
               | 
| 7 | 
            -
             | 
| 8 | 
            -
              end
         | 
| 6 | 
            +
              # Listen.logger will always be present.
         | 
| 7 | 
            +
              # If you don't want logging, set Listen.logger = ::Logger.new('/dev/null', level: ::Logger::UNKNOWN)
         | 
| 9 8 |  | 
| 10 | 
            -
               | 
| 11 | 
            -
                 | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 9 | 
            +
              class << self
         | 
| 10 | 
            +
                attr_writer :logger
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def logger
         | 
| 13 | 
            +
                  @logger ||= default_logger
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                private
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def default_logger
         | 
| 19 | 
            +
                  level =
         | 
| 20 | 
            +
                    case ENV['LISTEN_GEM_DEBUGGING'].to_s
         | 
| 21 | 
            +
                    when /debug|2/i
         | 
| 16 22 | 
             
                      ::Logger::DEBUG
         | 
| 17 | 
            -
                    when /true|yes|1/i
         | 
| 23 | 
            +
                    when /info|true|yes|1/i
         | 
| 18 24 | 
             
                      ::Logger::INFO
         | 
| 25 | 
            +
                    when /warn/i
         | 
| 26 | 
            +
                      ::Logger::WARN
         | 
| 27 | 
            +
                    when /fatal/i
         | 
| 28 | 
            +
                      ::Logger::FATAL
         | 
| 19 29 | 
             
                    else
         | 
| 20 30 | 
             
                      ::Logger::ERROR
         | 
| 21 31 | 
             
                    end
         | 
| 22 | 
            -
                end
         | 
| 23 | 
            -
              end
         | 
| 24 32 |  | 
| 25 | 
            -
             | 
| 26 | 
            -
                [:fatal, :error, :warn, :info, :debug].each do |meth|
         | 
| 27 | 
            -
                  define_singleton_method(meth) do |*args, &block|
         | 
| 28 | 
            -
                    Listen.logger.public_send(meth, *args, &block) if Listen.logger
         | 
| 29 | 
            -
                  end
         | 
| 33 | 
            +
                  ::Logger.new(STDERR, level: level)
         | 
| 30 34 | 
             
                end
         | 
| 31 35 | 
             
              end
         | 
| 32 36 | 
             
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Listen
         | 
| 4 | 
            +
              module MonotonicTime
         | 
| 5 | 
            +
                class << self
         | 
| 6 | 
            +
                  if defined?(Process::CLOCK_MONOTONIC)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    def now
         | 
| 9 | 
            +
                      Process.clock_gettime(Process::CLOCK_MONOTONIC)
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  elsif defined?(Process::CLOCK_MONOTONIC_RAW)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    def now
         | 
| 15 | 
            +
                      Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW)
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  else
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    def now
         | 
| 21 | 
            +
                      Time.now.to_f
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/listen/options.rb
    CHANGED
    
    | @@ -1,23 +1,26 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 4 | 
             
              class Options
         | 
| 3 5 | 
             
                def initialize(opts, defaults)
         | 
| 4 6 | 
             
                  @options = {}
         | 
| 5 7 | 
             
                  given_options = opts.dup
         | 
| 6 | 
            -
                  defaults. | 
| 8 | 
            +
                  defaults.each_key do |key|
         | 
| 7 9 | 
             
                    @options[key] = given_options.delete(key) || defaults[key]
         | 
| 8 10 | 
             
                  end
         | 
| 9 11 |  | 
| 10 | 
            -
                   | 
| 12 | 
            +
                  given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}"
         | 
| 13 | 
            +
                end
         | 
| 11 14 |  | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
                   | 
| 15 | 
            +
                # rubocop:disable Lint/MissingSuper
         | 
| 16 | 
            +
                def respond_to_missing?(name, *_)
         | 
| 17 | 
            +
                  @options.has_key?(name)
         | 
| 15 18 | 
             
                end
         | 
| 16 19 |  | 
| 17 20 | 
             
                def method_missing(name, *_)
         | 
| 18 | 
            -
                   | 
| 19 | 
            -
                   | 
| 20 | 
            -
                  fail NameError, msg
         | 
| 21 | 
            +
                  respond_to_missing?(name) or raise NameError, "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
         | 
| 22 | 
            +
                  @options[name]
         | 
| 21 23 | 
             
                end
         | 
| 24 | 
            +
                # rubocop:enable Lint/MissingSuper
         | 
| 22 25 | 
             
              end
         | 
| 23 26 | 
             
            end
         | 
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 4 | 
             
              class QueueOptimizer
         | 
| 3 5 | 
             
                class Config
         | 
| @@ -60,7 +62,7 @@ module Listen | |
| 60 62 | 
             
                  actions << :added if actions.delete(:moved_to)
         | 
| 61 63 | 
             
                  actions << :removed if actions.delete(:moved_from)
         | 
| 62 64 |  | 
| 63 | 
            -
                  modified = actions. | 
| 65 | 
            +
                  modified = actions.find { |x| x == :modified }
         | 
| 64 66 | 
             
                  _calculate_add_remove_difference(actions, path, modified)
         | 
| 65 67 | 
             
                end
         | 
| 66 68 |  | 
| @@ -88,11 +90,9 @@ module Listen | |
| 88 90 | 
             
                # editor rename() call (e.g. Kate and Sublime)
         | 
| 89 91 | 
             
                def _reinterpret_related_changes(cookies)
         | 
| 90 92 | 
             
                  table = { moved_to: :added, moved_from: :removed }
         | 
| 91 | 
            -
                  cookies. | 
| 92 | 
            -
                     | 
| 93 | 
            -
             | 
| 94 | 
            -
                      to_dir, to_file = data
         | 
| 95 | 
            -
                      [[:modified, to_dir, to_file]]
         | 
| 93 | 
            +
                  cookies.flat_map do |_, changes|
         | 
| 94 | 
            +
                    if (editor_modified = editor_modified?(changes))
         | 
| 95 | 
            +
                      [[:modified, *editor_modified]]
         | 
| 96 96 | 
             
                    else
         | 
| 97 97 | 
             
                      not_silenced = changes.reject do |type, _, _, path, _|
         | 
| 98 98 | 
             
                        config.silenced?(Pathname(path), type)
         | 
| @@ -101,32 +101,29 @@ module Listen | |
| 101 101 | 
             
                        [table.fetch(change, change), dir, path]
         | 
| 102 102 | 
             
                      end
         | 
| 103 103 | 
             
                    end
         | 
| 104 | 
            -
                  end | 
| 104 | 
            +
                  end
         | 
| 105 105 | 
             
                end
         | 
| 106 106 |  | 
| 107 | 
            -
                def  | 
| 107 | 
            +
                def editor_modified?(changes)
         | 
| 108 108 | 
             
                  return unless changes.size == 2
         | 
| 109 109 |  | 
| 110 | 
            -
                  from_type =  | 
| 111 | 
            -
                  to_type =  | 
| 110 | 
            +
                  from_type = from = nil
         | 
| 111 | 
            +
                  to_type = to_dir = to = nil
         | 
| 112 112 |  | 
| 113 113 | 
             
                  changes.each do |data|
         | 
| 114 114 | 
             
                    case data[1]
         | 
| 115 115 | 
             
                    when :moved_from
         | 
| 116 | 
            -
                      from_type,  | 
| 116 | 
            +
                      from_type, _from_change, _, from, = data
         | 
| 117 117 | 
             
                    when :moved_to
         | 
| 118 | 
            -
                      to_type,  | 
| 119 | 
            -
                    else
         | 
| 120 | 
            -
                      return nil
         | 
| 118 | 
            +
                      to_type, _to_change, to_dir, to, = data
         | 
| 121 119 | 
             
                    end
         | 
| 122 120 | 
             
                  end
         | 
| 123 121 |  | 
| 124 | 
            -
                  return unless from && to
         | 
| 125 | 
            -
             | 
| 126 122 | 
             
                  # Expect an ignored moved_from and non-ignored moved_to
         | 
| 127 123 | 
             
                  # to qualify as an "editor modify"
         | 
| 128 | 
            -
                   | 
| 129 | 
            -
             | 
| 124 | 
            +
                  if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type)
         | 
| 125 | 
            +
                    [to_dir, to]
         | 
| 126 | 
            +
                  end
         | 
| 130 127 | 
             
                end
         | 
| 131 128 | 
             
              end
         | 
| 132 129 | 
             
            end
         | 
    
        data/lib/listen/record/entry.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Listen
         | 
| 2 4 | 
             
              # @private api
         | 
| 3 5 | 
             
              class Record
         | 
| @@ -6,21 +8,23 @@ module Listen | |
| 6 8 | 
             
                  # file: "/home/me/watched_dir", "app/models", "foo.rb"
         | 
| 7 9 | 
             
                  # dir, "/home/me/watched_dir", "."
         | 
| 8 10 | 
             
                  def initialize(root, relative, name = nil)
         | 
| 9 | 
            -
                    @root | 
| 11 | 
            +
                    @root = root
         | 
| 12 | 
            +
                    @relative = relative
         | 
| 13 | 
            +
                    @name = name
         | 
| 10 14 | 
             
                  end
         | 
| 11 15 |  | 
| 12 16 | 
             
                  attr_reader :root, :relative, :name
         | 
| 13 17 |  | 
| 14 18 | 
             
                  def children
         | 
| 15 19 | 
             
                    child_relative = _join
         | 
| 16 | 
            -
                    (_entries(sys_path) - %w | 
| 20 | 
            +
                    (_entries(sys_path) - %w[. ..]).map do |name|
         | 
| 17 21 | 
             
                      Entry.new(@root, child_relative, name)
         | 
| 18 22 | 
             
                    end
         | 
| 19 23 | 
             
                  end
         | 
| 20 24 |  | 
| 21 25 | 
             
                  def meta
         | 
| 22 26 | 
             
                    lstat = ::File.lstat(sys_path)
         | 
| 23 | 
            -
                    { mtime: lstat.mtime.to_f, mode: lstat.mode }
         | 
| 27 | 
            +
                    { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
         | 
| 24 28 | 
             
                  end
         | 
| 25 29 |  | 
| 26 30 | 
             
                  # record hash is e.g.
         | 
| @@ -54,7 +58,7 @@ module Listen | |
| 54 58 | 
             
                    # https://github.com/jruby/jruby/issues/3840
         | 
| 55 59 | 
             
                    exists = ::File.exist?(dir)
         | 
| 56 60 | 
             
                    directory = ::File.directory?(dir)
         | 
| 57 | 
            -
                    return Dir.entries(dir) unless  | 
| 61 | 
            +
                    return Dir.entries(dir) unless exists && !directory
         | 
| 58 62 | 
             
                    raise Errno::ENOTDIR, dir
         | 
| 59 63 | 
             
                  end
         | 
| 60 64 | 
             
                end
         |