listen 3.2.1 → 3.3.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.
- checksums.yaml +4 -4
- data/README.md +110 -65
- data/bin/listen +3 -4
- data/lib/listen.rb +15 -20
- data/lib/listen/adapter.rb +9 -11
- data/lib/listen/adapter/base.rb +20 -32
- 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 +11 -6
- data/lib/listen/adapter/polling.rb +3 -1
- 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/event/config.rb +9 -20
- data/lib/listen/event/loop.rb +41 -65
- data/lib/listen/event/processor.rb +37 -31
- data/lib/listen/event/queue.rb +12 -13
- data/lib/listen/file.rb +15 -2
- data/lib/listen/fsm.rb +69 -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/options.rb +11 -8
- data/lib/listen/queue_optimizer.rb +9 -12
- data/lib/listen/record.rb +42 -31
- data/lib/listen/record/entry.rb +4 -2
- data/lib/listen/record/symlink_detector.rb +6 -4
- 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 +6 -22
- data/lib/listen/internals/thread_pool.rb +0 -29
data/lib/listen/event/config.rb
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Listen
|
|
2
4
|
module Event
|
|
3
5
|
class Config
|
|
6
|
+
attr_reader :listener, :event_queue, :min_delay_between_events
|
|
7
|
+
|
|
4
8
|
def initialize(
|
|
5
9
|
listener,
|
|
6
10
|
event_queue,
|
|
7
11
|
queue_optimizer,
|
|
8
12
|
wait_for_delay,
|
|
9
|
-
&block
|
|
13
|
+
&block
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
@listener = listener
|
|
12
17
|
@event_queue = event_queue
|
|
@@ -15,20 +20,18 @@ module Listen
|
|
|
15
20
|
@block = block
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
def sleep(
|
|
19
|
-
Kernel.sleep(
|
|
23
|
+
def sleep(seconds)
|
|
24
|
+
Kernel.sleep(seconds)
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def call(*args)
|
|
23
|
-
@block
|
|
28
|
+
@block&.call(*args)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def timestamp
|
|
27
32
|
Time.now.to_f
|
|
28
33
|
end
|
|
29
34
|
|
|
30
|
-
attr_reader :event_queue
|
|
31
|
-
|
|
32
35
|
def callable?
|
|
33
36
|
@block
|
|
34
37
|
end
|
|
@@ -36,20 +39,6 @@ module Listen
|
|
|
36
39
|
def optimize_changes(changes)
|
|
37
40
|
@queue_optimizer.smoosh_changes(changes)
|
|
38
41
|
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
42
|
end
|
|
54
43
|
end
|
|
55
44
|
end
|
data/lib/listen/event/loop.rb
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
_wakeup(:event)
|
|
32
|
+
if started? && @wait_thread&.alive?
|
|
33
|
+
_wakeup(:event)
|
|
34
|
+
end
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
def
|
|
29
|
-
|
|
37
|
+
def started?
|
|
38
|
+
state == :started
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
|
|
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
|
|
43
|
+
def start
|
|
39
44
|
# TODO: use a Fiber instead?
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
Listen.logger.debug("Waiting for processing to start...")
|
|
54
|
+
|
|
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})"
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
fail Error::NotStarted if stopped?
|
|
51
|
-
return unless wait_thread
|
|
52
|
-
_wakeup(:resume)
|
|
58
|
+
Listen.logger.debug('Processing started.')
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def pause
|
|
@@ -57,60 +63,30 @@ module Listen
|
|
|
57
63
|
# fail NotImplementedError
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
wait_thread.join
|
|
65
|
-
end
|
|
66
|
+
def stop
|
|
67
|
+
transition! :stopped
|
|
68
|
+
|
|
69
|
+
@wait_thread&.join
|
|
66
70
|
@wait_thread = nil
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def stopped?
|
|
70
|
-
|
|
74
|
+
state == :stopped
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
private
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
attr_accessor :state
|
|
79
|
+
def _process_changes
|
|
80
|
+
processor = Event::Processor.new(@config, @reasons)
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
processor = Event::Processor.new(config, @reasons)
|
|
82
|
-
|
|
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
|
|
82
|
+
transition! :started
|
|
99
83
|
|
|
100
|
-
|
|
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)
|
|
84
|
+
processor.loop_for(@config.min_delay_between_events)
|
|
109
85
|
end
|
|
110
86
|
|
|
111
87
|
def _wakeup(reason)
|
|
112
88
|
@reasons << reason
|
|
113
|
-
wait_thread.wakeup
|
|
89
|
+
@wait_thread.wakeup
|
|
114
90
|
end
|
|
115
91
|
end
|
|
116
92
|
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
|
|
26
|
+
Listen.logger.debug('Processing stopped')
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
@@ -38,57 +42,56 @@ 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
if @listener.stopped?
|
|
55
|
+
_flush_wakeup_reasons
|
|
56
|
+
raise Stopped
|
|
57
|
+
end
|
|
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
|
|
71
|
-
@
|
|
73
|
+
@_remember_time_of_first_unprocessed_event ||= _timestamp
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def _reset_no_unprocessed_events
|
|
75
|
-
@
|
|
77
|
+
@_remember_time_of_first_unprocessed_event = nil
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def _deadline
|
|
79
|
-
@
|
|
81
|
+
@_remember_time_of_first_unprocessed_event + @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
|
+
@_remember_time_of_first_unprocessed_event ||= _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
|
|
|
@@ -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
|