listen 3.2.1 → 3.5.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.
@@ -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::Logger.debug('Processing stopped')
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 = _timestamp
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(:waiting_until_latency, diff)
47
+ _sleep(diff)
42
48
  end
43
49
  end
44
50
 
45
51
  def _wait_until_no_longer_paused
46
- # TODO: may not be a good idea?
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
- return unless config.stopped?
52
-
53
- _flush_wakeup_reasons
54
- raise Stopped
56
+ if @listener.stopped?
57
+ _flush_wakeup_reasons
58
+ raise Stopped
59
+ end
55
60
  end
56
61
 
57
- def _sleep(_local_reason, *args)
62
+ def _sleep(seconds)
58
63
  _check_stopped
59
- sleep_duration = config.sleep(*args)
64
+ config.sleep(seconds)
60
65
  _check_stopped
61
66
 
62
67
  _flush_wakeup_reasons do |reason|
63
- next unless reason == :event
64
- _remember_time_of_first_unprocessed_event unless config.paused?
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
- @first_unprocessed_event_time ||= _timestamp
75
+ @_remember_time_of_first_unprocessed_event ||= MonotonicTime.now
72
76
  end
73
77
 
74
78
  def _reset_no_unprocessed_events
75
- @first_unprocessed_event_time = nil
79
+ @_remember_time_of_first_unprocessed_event = nil
76
80
  end
77
81
 
78
82
  def _deadline
79
- @first_unprocessed_event_time + @latency
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
- # TODO: long sleep may not be a good idea?
84
- _sleep(:waiting_for_events) while config.event_queue.empty?
85
- @first_unprocessed_event_time ||= _timestamp
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
- reasons = @reasons
90
- until reasons.empty?
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
- callable = config.callable?
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 = _timestamp
115
- config.call(*result)
116
- Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  require 'forwardable'
@@ -17,9 +19,8 @@ module Listen
17
19
  end
18
20
  end
19
21
 
20
- def initialize(config, &block)
22
+ def initialize(config)
21
23
  @event_queue = ::Queue.new
22
- @block = block
23
24
  @config = config
24
25
  end
25
26
 
@@ -29,23 +30,21 @@ module Listen
29
30
  fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
30
31
  fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
31
32
 
32
- dir = _safe_relative_from_cwd(dir)
33
- event_queue.public_send(:<<, [type, change, dir, path, options])
34
-
35
- block.call(args) if block
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]
36
39
  end
37
40
 
38
- delegate empty?: :event_queue
39
- delegate pop: :event_queue
41
+ delegate empty?: :@event_queue
42
+ delegate pop: :@event_queue
43
+ delegate close: :@event_queue
40
44
 
41
45
  private
42
46
 
43
- attr_reader :event_queue
44
- attr_reader :block
45
- attr_reader :config
46
-
47
47
  def _safe_relative_from_cwd(dir)
48
- return dir unless config.relative?
49
48
  dir.relative_path_from(Pathname.pwd)
50
49
  rescue ArgumentError
51
50
  dir
data/lib/listen/file.rb CHANGED
@@ -1,12 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
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,6 +30,11 @@ 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
39
  return unless inaccurate_mac_time?(lstat)
30
40
 
@@ -64,9 +74,12 @@ module Listen
64
74
  record.unset_path(rel_path)
65
75
  :removed
66
76
  rescue
67
- Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
77
+ Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
68
78
  raise
69
79
  end
80
+ # rubocop:enable Metrics/MethodLength
81
+ # rubocop:enable Metrics/CyclomaticComplexity
82
+ # rubocop:enable Metrics/PerceivedComplexity
70
83
 
71
84
  def self.inaccurate_mac_time?(stat)
72
85
  # 'mac' means Modified/Accessed/Created
data/lib/listen/fsm.rb CHANGED
@@ -1,120 +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 default state
13
- # Passing a state name sets the default state
14
- def default_state(new_default = nil)
15
- if new_default
16
- @default_state = new_default.to_sym
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?(@default_state) ? @default_state : DEFAULT_STATE
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
- # Obtain the valid states for this FSM
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(*args, &block)
31
- if args.last.is_a? Hash
32
- # Stringify keys :/
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
- # Be kind and call super if you must redefine initialize
47
- def initialize
48
- @state = self.class.default_state
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
- # Obtain the current state of the FSM
49
+ # Current state of the FSM, stored as a symbol
52
50
  attr_reader :state
53
51
 
54
- def transition(state_name)
55
- new_state = validate_and_sanitize_new_state(state_name)
56
- return unless new_state
57
- transition_with_callbacks!(new_state)
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
- # Immediate state transition with no checks, or callbacks. "Dangerous!"
61
- def transition!(state_name)
62
- @state = state_name
63
- end
67
+ private
64
68
 
65
- protected
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
- def validate_and_sanitize_new_state(state_name)
68
- state_name = state_name.to_sym
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
- return if current_state_name == state_name
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?(state_name)
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
- " to '#{state_name}', only to: #{valid}"
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[state_name]
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!(state_name)
90
- transition! state_name.name
91
- state_name.call(self)
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 = nil, &block)
115
+ def initialize(name, transitions, &block)
114
116
  @name = name
115
117
  @block = block
116
- @transitions = nil
117
- @transitions = Array(transitions).map(&:to_sym) if transitions
118
+ @transitions = if transitions
119
+ Array(transitions).map(&:to_sym)
120
+ end
118
121
  end
119
122
 
120
123
  def call(obj)
@@ -122,10 +125,8 @@ module Listen
122
125
  end
123
126
 
124
127
  def valid_transition?(new_state)
125
- # All transitions are allowed unless expressly
126
- return true unless @transitions
127
-
128
- @transitions.include? new_state.to_sym
128
+ # All transitions are allowed if none are expressly declared
129
+ !@transitions || @transitions.include?(new_state.to_sym)
129
130
  end
130
131
  end
131
132
  end