listen 3.2.1 → 3.3.0
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/README.md +110 -65
- data/lib/listen.rb +15 -20
- data/lib/listen/adapter.rb +6 -8
- data/lib/listen/adapter/base.rb +17 -29
- data/lib/listen/adapter/bsd.rb +3 -1
- data/lib/listen/adapter/config.rb +2 -0
- data/lib/listen/adapter/darwin.rb +10 -12
- data/lib/listen/adapter/linux.rb +7 -4
- data/lib/listen/adapter/polling.rb +2 -0
- data/lib/listen/adapter/windows.rb +6 -4
- data/lib/listen/backend.rb +2 -0
- data/lib/listen/change.rb +5 -3
- data/lib/listen/cli.rb +3 -2
- data/lib/listen/directory.rb +4 -2
- data/lib/listen/event/config.rb +8 -18
- data/lib/listen/event/loop.rb +43 -64
- data/lib/listen/event/processor.rb +31 -25
- data/lib/listen/event/queue.rb +4 -5
- data/lib/listen/file.rb +9 -2
- data/lib/listen/fsm.rb +69 -71
- data/lib/listen/listener.rb +24 -23
- data/lib/listen/listener/config.rb +2 -0
- data/lib/listen/logger.rb +27 -24
- data/lib/listen/options.rb +3 -1
- data/lib/listen/queue_optimizer.rb +2 -0
- data/lib/listen/record.rb +16 -2
- data/lib/listen/record/entry.rb +3 -1
- data/lib/listen/record/symlink_detector.rb +2 -0
- data/lib/listen/silencer.rb +2 -0
- data/lib/listen/silencer/controller.rb +2 -0
- data/lib/listen/thread.rb +52 -0
- data/lib/listen/version.rb +3 -1
- metadata +7 -23
- data/lib/listen/internals/thread_pool.rb +0 -29
@@ -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
|
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(
|
45
|
+
_sleep(diff)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
49
|
def _wait_until_no_longer_paused
|
46
|
-
|
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
|
54
|
+
return unless @listener.stopped?
|
52
55
|
|
53
56
|
_flush_wakeup_reasons
|
54
57
|
raise Stopped
|
55
58
|
end
|
56
59
|
|
57
|
-
def _sleep(
|
60
|
+
def _sleep(seconds)
|
58
61
|
_check_stopped
|
59
|
-
|
62
|
+
config.sleep(seconds)
|
60
63
|
_check_stopped
|
61
64
|
|
62
65
|
_flush_wakeup_reasons do |reason|
|
63
|
-
|
64
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
90
|
-
|
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,22 +101,25 @@ 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
|
-
|
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]]
|
112
114
|
return if result.all?(&:empty?)
|
113
115
|
|
114
116
|
block_start = _timestamp
|
115
|
-
|
116
|
-
Listen::
|
117
|
+
exception_note = " (exception)"
|
118
|
+
::Listen::Thread.rescue_and_log('_process_changes') do
|
119
|
+
config.call(*result)
|
120
|
+
exception_note = nil
|
121
|
+
end
|
122
|
+
Listen.logger.debug "Callback#{exception_note} took #{_timestamp - block_start} sec"
|
117
123
|
end
|
118
124
|
|
119
125
|
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
|
|
@@ -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)
|
data/lib/listen/file.rb
CHANGED
@@ -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
|
74
|
+
Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
|
68
75
|
raise
|
69
76
|
end
|
70
77
|
|
data/lib/listen/fsm.rb
CHANGED
@@ -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
|
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
|
+
@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
|
-
|
61
|
-
def transition!(state_name)
|
62
|
-
@state = state_name
|
63
|
-
end
|
64
|
+
private
|
64
65
|
|
65
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
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?(
|
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
|
-
|
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[
|
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!(
|
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
|
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
|
112
|
+
def initialize(name, transitions, &block)
|
114
113
|
@name = name
|
115
114
|
@block = block
|
116
|
-
@transitions =
|
117
|
-
|
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
|
126
|
-
|
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
|
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'
|
@@ -19,7 +21,6 @@ require 'listen/listener/config'
|
|
19
21
|
|
20
22
|
module Listen
|
21
23
|
class Listener
|
22
|
-
# TODO: move the state machine's methods private
|
23
24
|
include Listen::FSM
|
24
25
|
|
25
26
|
# Initializes the directories listener.
|
@@ -38,7 +39,7 @@ module Listen
|
|
38
39
|
@config = Config.new(options)
|
39
40
|
|
40
41
|
eq_config = Event::Queue::Config.new(@config.relative?)
|
41
|
-
queue = Event::Queue.new(eq_config)
|
42
|
+
queue = Event::Queue.new(eq_config)
|
42
43
|
|
43
44
|
silencer = Silencer.new
|
44
45
|
rules = @config.silencer_rules
|
@@ -57,41 +58,42 @@ module Listen
|
|
57
58
|
|
58
59
|
@processor = Event::Loop.new(pconfig)
|
59
60
|
|
60
|
-
|
61
|
+
initialize_fsm
|
61
62
|
end
|
62
63
|
|
63
|
-
|
64
|
+
start_state :initializing
|
64
65
|
|
65
66
|
state :initializing, to: [:backend_started, :stopped]
|
66
67
|
|
67
|
-
state :backend_started, to: [:
|
68
|
-
backend.start
|
69
|
-
end
|
70
|
-
|
71
|
-
state :frontend_ready, to: [:processing_events, :stopped] do
|
72
|
-
processor.setup
|
68
|
+
state :backend_started, to: [:processing_events, :stopped] do
|
69
|
+
@backend.start
|
73
70
|
end
|
74
71
|
|
75
72
|
state :processing_events, to: [:paused, :stopped] do
|
76
|
-
processor.
|
73
|
+
@processor.start
|
77
74
|
end
|
78
75
|
|
79
76
|
state :paused, to: [:processing_events, :stopped] do
|
80
|
-
processor.pause
|
77
|
+
@processor.pause
|
81
78
|
end
|
82
79
|
|
83
80
|
state :stopped, to: [:backend_started] do
|
84
|
-
backend.stop #
|
85
|
-
processor.
|
81
|
+
@backend.stop # halt events ASAP
|
82
|
+
@processor.stop
|
86
83
|
end
|
87
84
|
|
88
85
|
# Starts processing events and starts adapters
|
89
86
|
# or resumes invoking callbacks if paused
|
90
87
|
def start
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
88
|
+
case state
|
89
|
+
when :initializing
|
90
|
+
transition :backend_started
|
91
|
+
transition :processing_events
|
92
|
+
when :paused
|
93
|
+
transition :processing_events
|
94
|
+
else
|
95
|
+
raise ArgumentError, "cannot start from state #{state.inspect}"
|
96
|
+
end
|
95
97
|
end
|
96
98
|
|
97
99
|
# Stops both listening for events and processing them
|
@@ -113,6 +115,10 @@ module Listen
|
|
113
115
|
state == :paused
|
114
116
|
end
|
115
117
|
|
118
|
+
def stopped?
|
119
|
+
state == :stopped
|
120
|
+
end
|
121
|
+
|
116
122
|
def ignore(regexps)
|
117
123
|
@silencer_controller.append_ignores(regexps)
|
118
124
|
end
|
@@ -124,10 +130,5 @@ module Listen
|
|
124
130
|
def only(regexps)
|
125
131
|
@silencer_controller.replace_with_only(regexps)
|
126
132
|
end
|
127
|
-
|
128
|
-
private
|
129
|
-
|
130
|
-
attr_reader :processor
|
131
|
-
attr_reader :backend
|
132
133
|
end
|
133
134
|
end
|