listen 3.2.1 → 3.3.0.pre.2

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,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  module Event
3
5
  class Config
6
+ attr_reader :listener
7
+ attr_reader :event_queue
8
+ attr_reader :min_delay_between_events
9
+
4
10
  def initialize(
5
11
  listener,
6
12
  event_queue,
@@ -15,8 +21,8 @@ module Listen
15
21
  @block = block
16
22
  end
17
23
 
18
- def sleep(*args)
19
- Kernel.sleep(*args)
24
+ def sleep(seconds)
25
+ Kernel.sleep(seconds)
20
26
  end
21
27
 
22
28
  def call(*args)
@@ -27,8 +33,6 @@ module Listen
27
33
  Time.now.to_f
28
34
  end
29
35
 
30
- attr_reader :event_queue
31
-
32
36
  def callable?
33
37
  @block
34
38
  end
@@ -36,20 +40,6 @@ module Listen
36
40
  def optimize_changes(changes)
37
41
  @queue_optimizer.smoosh_changes(changes)
38
42
  end
39
-
40
- attr_reader :min_delay_between_events
41
-
42
- def stopped?
43
- listener.state == :stopped
44
- end
45
-
46
- def paused?
47
- listener.state == :paused
48
- end
49
-
50
- private
51
-
52
- attr_reader :listener
53
43
  end
54
44
  end
55
45
  end
@@ -1,55 +1,61 @@
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'
5
8
 
6
9
  module Listen
7
10
  module Event
8
11
  class Loop
12
+ include Listen::FSM
13
+
9
14
  class Error < RuntimeError
10
- class NotStarted < Error
11
- end
15
+ class NotStarted < Error; end
12
16
  end
13
17
 
18
+ start_state :pre_start
19
+ state :pre_start
20
+ state :starting
21
+ state :started
22
+ state :stopped
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
- return if stopped?
23
- return unless processing?
24
- return unless wait_thread.alive?
25
- _wakeup(:event)
32
+ if started? && @wait_thread&.alive?
33
+ _wakeup(:event)
34
+ end
26
35
  end
27
36
 
28
- def paused?
29
- wait_thread && state == :paused
37
+ def started?
38
+ state == :started
30
39
  end
31
40
 
32
- def processing?
33
- return false if stopped?
34
- return false if paused?
35
- state == :processing
36
- end
41
+ MAX_STARTUP_SECONDS = 5.0
37
42
 
38
- def setup
43
+ def start
39
44
  # TODO: use a Fiber instead?
40
- q = ::Queue.new
41
- @wait_thread = Internals::ThreadPool.add do
42
- _wait_for_changes(q, config)
45
+ return unless state == :pre_start
46
+
47
+ transition! :starting
48
+
49
+ @wait_thread = Listen::Thread.new("wait_thread") do
50
+ _process_changes
43
51
  end
44
52
 
45
- Listen::Logger.debug('Waiting for processing to start...')
46
- Timeout.timeout(5) { q.pop }
47
- end
53
+ Listen.logger.debug("Waiting for processing to start...")
48
54
 
49
- def resume
50
- fail Error::NotStarted if stopped?
51
- return unless wait_thread
52
- _wakeup(:resume)
55
+ wait_for_state(:started, MAX_STARTUP_SECONDS) or
56
+ raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})"
57
+
58
+ Listen.logger.debug('Processing started.')
53
59
  end
54
60
 
55
61
  def pause
@@ -57,60 +63,33 @@ module Listen
57
63
  # fail NotImplementedError
58
64
  end
59
65
 
60
- def teardown
61
- return unless wait_thread
62
- if wait_thread.alive?
63
- _wakeup(:teardown)
64
- wait_thread.join
66
+ def stop
67
+ return if stopped?
68
+ transition! :stopped
69
+
70
+ if @wait_thread.alive?
71
+ @wait_thread.join
65
72
  end
66
73
  @wait_thread = nil
67
74
  end
68
75
 
69
76
  def stopped?
70
- !wait_thread
77
+ state == :stopped
71
78
  end
72
79
 
73
80
  private
74
81
 
75
- attr_reader :config
76
- attr_reader :wait_thread
77
-
78
- attr_accessor :state
79
-
80
- def _wait_for_changes(ready_queue, config)
81
- processor = Event::Processor.new(config, @reasons)
82
+ def _process_changes
83
+ processor = Event::Processor.new(@config, @reasons)
82
84
 
83
- _wait_until_resumed(ready_queue)
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
85
+ transition! :started
99
86
 
100
- def _nice_error(ex)
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)
87
+ processor.loop_for(@config.min_delay_between_events)
109
88
  end
110
89
 
111
90
  def _wakeup(reason)
112
91
  @reasons << reason
113
- wait_thread.wakeup
92
+ @wait_thread.wakeup
114
93
  end
115
94
  end
116
95
  end
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  module Event
3
5
  class Processor
4
6
  def initialize(config, reasons)
5
7
  @config = config
8
+ @listener = config.listener
6
9
  @reasons = reasons
7
10
  _reset_no_unprocessed_events
8
11
  end
@@ -13,13 +16,14 @@ module Listen
13
16
  @latency = latency
14
17
 
15
18
  loop do
16
- _wait_until_events
19
+ event = _wait_until_events
20
+ _check_stopped
17
21
  _wait_until_events_calm_down
18
22
  _wait_until_no_longer_paused
19
- _process_changes
23
+ _process_changes(event)
20
24
  end
21
25
  rescue Stopped
22
- Listen::Logger.debug('Processing stopped')
26
+ Listen.logger.debug('Processing stopped')
23
27
  end
24
28
 
25
29
  private
@@ -38,33 +42,31 @@ module Listen
38
42
 
39
43
  # give events a bit of time to accumulate so they can be
40
44
  # compressed/optimized
41
- _sleep(:waiting_until_latency, diff)
45
+ _sleep(diff)
42
46
  end
43
47
  end
44
48
 
45
49
  def _wait_until_no_longer_paused
46
- # TODO: may not be a good idea?
47
- _sleep(:waiting_for_unpause) while config.paused?
50
+ @listener.wait_for_state(*(Listener.states.keys - [:paused]))
48
51
  end
49
52
 
50
53
  def _check_stopped
51
- return unless config.stopped?
54
+ return unless @listener.stopped?
52
55
 
53
56
  _flush_wakeup_reasons
54
57
  raise Stopped
55
58
  end
56
59
 
57
- def _sleep(_local_reason, *args)
60
+ def _sleep(seconds)
58
61
  _check_stopped
59
- sleep_duration = config.sleep(*args)
62
+ config.sleep(seconds)
60
63
  _check_stopped
61
64
 
62
65
  _flush_wakeup_reasons do |reason|
63
- next unless reason == :event
64
- _remember_time_of_first_unprocessed_event unless config.paused?
66
+ if reason == :event && !@listener.paused?
67
+ _remember_time_of_first_unprocessed_event
68
+ end
65
69
  end
66
-
67
- sleep_duration
68
70
  end
69
71
 
70
72
  def _remember_time_of_first_unprocessed_event
@@ -79,16 +81,17 @@ module Listen
79
81
  @first_unprocessed_event_time + @latency
80
82
  end
81
83
 
84
+ # blocks until event is popped
85
+ # returns the event or `nil` when the event_queue is closed
82
86
  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
87
+ config.event_queue.pop.tap do |_event|
88
+ @first_unprocessed_event_time ||= _timestamp
89
+ end
86
90
  end
87
91
 
88
92
  def _flush_wakeup_reasons
89
- reasons = @reasons
90
- until reasons.empty?
91
- reason = reasons.pop
93
+ until @reasons.empty?
94
+ reason = @reasons.pop
92
95
  yield reason if block_given?
93
96
  end
94
97
  end
@@ -98,14 +101,13 @@ module Listen
98
101
  end
99
102
 
100
103
  # for easier testing without sleep loop
101
- def _process_changes
104
+ def _process_changes(event)
102
105
  _reset_no_unprocessed_events
103
106
 
104
- changes = []
107
+ changes = [event]
105
108
  changes << config.event_queue.pop until config.event_queue.empty?
106
109
 
107
- callable = config.callable?
108
- return unless callable
110
+ return unless config.callable?
109
111
 
110
112
  hash = config.optimize_changes(changes)
111
113
  result = [hash[:modified], hash[:added], hash[:removed]]
@@ -113,7 +115,7 @@ module Listen
113
115
 
114
116
  block_start = _timestamp
115
117
  config.call(*result)
116
- Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
118
+ Listen.logger.debug "Callback took #{_timestamp - block_start} sec"
117
119
  end
118
120
 
119
121
  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
 
@@ -31,17 +32,15 @@ module Listen
31
32
 
32
33
  dir = _safe_relative_from_cwd(dir)
33
34
  event_queue.public_send(:<<, [type, change, dir, path, options])
34
-
35
- block.call(args) if block
36
35
  end
37
36
 
38
37
  delegate empty?: :event_queue
39
38
  delegate pop: :event_queue
39
+ delegate close: :event_queue
40
40
 
41
41
  private
42
42
 
43
43
  attr_reader :event_queue
44
- attr_reader :block
45
44
  attr_reader :config
46
45
 
47
46
  def _safe_relative_from_cwd(dir)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
2
4
 
3
5
  module Listen
@@ -6,7 +8,7 @@ module Listen
6
8
  path = Pathname.new(record.root) + rel_path
7
9
  lstat = path.lstat
8
10
 
9
- data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
11
+ data = { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
10
12
 
11
13
  record_data = record.file_data(rel_path)
12
14
 
@@ -25,6 +27,11 @@ module Listen
25
27
  return :modified
26
28
  end
27
29
 
30
+ if data[:size] != record_data[:size]
31
+ record.update_file(rel_path, data)
32
+ return :modified
33
+ end
34
+
28
35
  return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
29
36
  return unless inaccurate_mac_time?(lstat)
30
37
 
@@ -64,7 +71,7 @@ module Listen
64
71
  record.unset_path(rel_path)
65
72
  :removed
66
73
  rescue
67
- Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
74
+ Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
68
75
  raise
69
76
  end
70
77
 
@@ -1,120 +1,120 @@
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
+ @mutex.synchronize do
57
+ if !wait_for_states.include?(@state)
58
+ @state_changed.wait(@mutex, timeout)
59
+ end
60
+ wait_for_states.include?(@state)
61
+ end
58
62
  end
59
63
 
60
- # Immediate state transition with no checks, or callbacks. "Dangerous!"
61
- def transition!(state_name)
62
- @state = state_name
63
- end
64
+ private
64
65
 
65
- protected
66
+ def transition(new_state_name)
67
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
68
+ if (new_state = validate_and_sanitize_new_state(new_state_name))
69
+ transition_with_callbacks!(new_state)
70
+ end
71
+ end
66
72
 
67
- def validate_and_sanitize_new_state(state_name)
68
- state_name = state_name.to_sym
73
+ # Low-level, immediate state transition with no checks or callbacks.
74
+ def transition!(new_state_name)
75
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
76
+ @fsm_initialized or raise ArgumentError, "FSM not initialized. You must call initialize_fsm from initialize!"
77
+ @mutex.synchronize do
78
+ yield if block_given?
79
+ @state = new_state_name
80
+ @state_changed.broadcast
81
+ end
82
+ end
69
83
 
70
- return if current_state_name == state_name
84
+ def validate_and_sanitize_new_state(new_state_name)
85
+ return nil if @state == new_state_name
71
86
 
72
- if current_state && !current_state.valid_transition?(state_name)
87
+ if current_state && !current_state.valid_transition?(new_state_name)
73
88
  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
89
+ msg = "#{self.class} can't change state from '#{@state}' to '#{new_state_name}', only to: #{valid}"
90
+ raise ArgumentError, msg
77
91
  end
78
92
 
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}"
93
+ unless (new_state = self.class.states[new_state_name])
94
+ new_state_name == self.class.start_state or raise ArgumentError, "invalid state for #{self.class}: #{new_state_name}"
84
95
  end
85
96
 
86
97
  new_state
87
98
  end
88
99
 
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
100
+ def transition_with_callbacks!(new_state)
101
+ transition! new_state.name
102
+ new_state.call(self)
100
103
  end
101
104
 
102
105
  def current_state
103
- states[@state]
104
- end
105
-
106
- def current_state_name
107
- current_state && current_state.name || ''
106
+ self.class.states[@state]
108
107
  end
109
108
 
110
109
  class State
111
110
  attr_reader :name, :transitions
112
111
 
113
- def initialize(name, transitions = nil, &block)
112
+ def initialize(name, transitions, &block)
114
113
  @name = name
115
114
  @block = block
116
- @transitions = nil
117
- @transitions = Array(transitions).map(&:to_sym) if transitions
115
+ @transitions = if transitions
116
+ Array(transitions).map(&:to_sym)
117
+ end
118
118
  end
119
119
 
120
120
  def call(obj)
@@ -122,10 +122,8 @@ module Listen
122
122
  end
123
123
 
124
124
  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
125
+ # All transitions are allowed if none are expressly declared
126
+ !@transitions || @transitions.include?(new_state.to_sym)
129
127
  end
130
128
  end
131
129
  end