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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +2 -2
- data/README.md +220 -70
- data/bin/listen +3 -4
- data/lib/listen.rb +15 -20
- data/lib/listen/adapter.rb +9 -11
- data/lib/listen/adapter/base.rb +22 -34
- data/lib/listen/adapter/bsd.rb +6 -5
- data/lib/listen/adapter/config.rb +3 -4
- data/lib/listen/adapter/darwin.rb +11 -13
- data/lib/listen/adapter/linux.rb +14 -9
- data/lib/listen/adapter/polling.rb +8 -5
- data/lib/listen/adapter/windows.rb +15 -17
- data/lib/listen/backend.rb +2 -0
- data/lib/listen/change.rb +14 -21
- data/lib/listen/cli.rb +6 -6
- data/lib/listen/directory.rb +8 -8
- data/lib/listen/error.rb +10 -0
- data/lib/listen/event/config.rb +9 -24
- data/lib/listen/event/loop.rb +44 -67
- data/lib/listen/event/processor.rb +41 -37
- data/lib/listen/event/queue.rb +12 -13
- data/lib/listen/file.rb +15 -2
- data/lib/listen/fsm.rb +72 -71
- data/lib/listen/listener.rb +26 -23
- data/lib/listen/listener/config.rb +4 -4
- 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 +9 -12
- data/lib/listen/record.rb +30 -31
- data/lib/listen/record/entry.rb +4 -2
- data/lib/listen/record/symlink_detector.rb +10 -8
- data/lib/listen/silencer.rb +12 -8
- data/lib/listen/silencer/controller.rb +2 -0
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- metadata +14 -23
- data/lib/listen/internals/thread_pool.rb +0 -29
@@ -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'
|
@@ -17,9 +19,8 @@ module Listen
|
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
|
-
def initialize(config
|
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 =
|
33
|
-
|
34
|
-
|
35
|
-
|
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?:
|
39
|
-
delegate pop:
|
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
|
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
|
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
|
115
|
+
def initialize(name, transitions, &block)
|
114
116
|
@name = name
|
115
117
|
@block = block
|
116
|
-
@transitions =
|
117
|
-
|
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
|
126
|
-
|
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
|