listen 3.0.8 → 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 +5 -5
- data/CONTRIBUTING.md +10 -3
- data/README.md +241 -70
- data/bin/listen +3 -4
- data/lib/listen/adapter/base.rb +25 -33
- data/lib/listen/adapter/bsd.rb +9 -8
- data/lib/listen/adapter/config.rb +4 -5
- data/lib/listen/adapter/darwin.rb +35 -46
- data/lib/listen/adapter/linux.rb +16 -13
- data/lib/listen/adapter/polling.rb +9 -6
- data/lib/listen/adapter/windows.rb +19 -22
- data/lib/listen/adapter.rb +25 -25
- data/lib/listen/backend.rb +7 -10
- data/lib/listen/change.rb +18 -27
- data/lib/listen/cli.rb +6 -6
- data/lib/listen/directory.rb +14 -8
- data/lib/listen/error.rb +10 -0
- data/lib/listen/event/config.rb +9 -28
- data/lib/listen/event/loop.rb +44 -67
- data/lib/listen/event/processor.rb +41 -37
- data/lib/listen/event/queue.rb +14 -18
- data/lib/listen/file.rb +16 -3
- data/lib/listen/fsm.rb +74 -72
- data/lib/listen/listener/config.rb +5 -9
- data/lib/listen/listener.rb +26 -22
- 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 +15 -18
- data/lib/listen/record/entry.rb +8 -4
- data/lib/listen/record/symlink_detector.rb +9 -7
- data/lib/listen/record.rb +30 -33
- data/lib/listen/silencer/controller.rb +2 -0
- data/lib/listen/silencer.rb +16 -9
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- data/lib/listen.rb +14 -22
- metadata +20 -28
- data/lib/listen/internals/thread_pool.rb +0 -29
data/lib/listen/directory.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
2
4
|
|
3
5
|
module Listen
|
4
6
|
# TODO: refactor (turn it into a normal object, cache the stat, etc)
|
5
7
|
class Directory
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
6
9
|
def self.scan(snapshot, rel_path, options)
|
7
10
|
record = snapshot.record
|
8
11
|
dir = Pathname.new(record.root)
|
@@ -14,7 +17,7 @@ module Listen
|
|
14
17
|
path = dir + rel_path
|
15
18
|
current = Set.new(_children(path))
|
16
19
|
|
17
|
-
Listen
|
20
|
+
Listen.logger.debug do
|
18
21
|
format('%s: %s(%s): %s -> %s',
|
19
22
|
(options[:silence] ? 'Recording' : 'Scanning'),
|
20
23
|
rel_path, options.inspect, previous.inspect, current.inspect)
|
@@ -36,22 +39,25 @@ module Listen
|
|
36
39
|
previous = previous.reject { |entry, _| current.include? path + entry }
|
37
40
|
|
38
41
|
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
39
|
-
|
40
42
|
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
41
43
|
record.unset_path(rel_path)
|
42
44
|
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
43
|
-
|
44
45
|
rescue Errno::ENOTDIR
|
45
46
|
# TODO: path not tested
|
46
47
|
record.unset_path(rel_path)
|
47
48
|
_async_changes(snapshot, path, previous, options)
|
48
49
|
_change(snapshot, :file, rel_path, options)
|
49
50
|
rescue
|
50
|
-
Listen
|
51
|
-
format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
|
52
|
-
end
|
51
|
+
Listen.logger.warn { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") }
|
53
52
|
raise
|
54
53
|
end
|
54
|
+
# rubocop:enable Metrics/MethodLength
|
55
|
+
|
56
|
+
def self.ascendant_of?(base, other)
|
57
|
+
other.ascend do |ascendant|
|
58
|
+
break true if base == ascendant
|
59
|
+
end
|
60
|
+
end
|
55
61
|
|
56
62
|
def self._async_changes(snapshot, path, previous, options)
|
57
63
|
fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
|
@@ -80,8 +86,8 @@ module Listen
|
|
80
86
|
# https://github.com/jruby/jruby/issues/3840
|
81
87
|
exists = path.exist?
|
82
88
|
directory = path.directory?
|
83
|
-
|
84
|
-
|
89
|
+
exists && !directory and raise Errno::ENOTDIR, path.to_s
|
90
|
+
path.children
|
85
91
|
end
|
86
92
|
end
|
87
93
|
end
|
data/lib/listen/error.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Besides programming error exceptions like ArgumentError,
|
4
|
+
# all public interface exceptions should be declared here and inherit from Listen::Error.
|
5
|
+
module Listen
|
6
|
+
class Error < RuntimeError
|
7
|
+
class NotStarted < Error; end
|
8
|
+
class SymlinkLoop < Error; end
|
9
|
+
end
|
10
|
+
end
|
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,12 @@ 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
|
24
|
-
end
|
25
|
-
|
26
|
-
def timestamp
|
27
|
-
Time.now.to_f
|
28
|
-
end
|
29
|
-
|
30
|
-
def event_queue
|
31
|
-
@event_queue
|
28
|
+
@block&.call(*args)
|
32
29
|
end
|
33
30
|
|
34
31
|
def callable?
|
@@ -38,22 +35,6 @@ module Listen
|
|
38
35
|
def optimize_changes(changes)
|
39
36
|
@queue_optimizer.smoosh_changes(changes)
|
40
37
|
end
|
41
|
-
|
42
|
-
def min_delay_between_events
|
43
|
-
@min_delay_between_events
|
44
|
-
end
|
45
|
-
|
46
|
-
def stopped?
|
47
|
-
listener.state == :stopped
|
48
|
-
end
|
49
|
-
|
50
|
-
def paused?
|
51
|
-
listener.state == :paused
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
|
56
|
-
attr_reader :listener
|
57
38
|
end
|
58
39
|
end
|
59
40
|
end
|
data/lib/listen/event/loop.rb
CHANGED
@@ -1,55 +1,62 @@
|
|
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'
|
8
|
+
require 'listen/error'
|
5
9
|
|
6
10
|
module Listen
|
7
11
|
module Event
|
8
12
|
class Loop
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
include Listen::FSM
|
14
|
+
|
15
|
+
Error = ::Listen::Error
|
16
|
+
NotStarted = ::Listen::Error::NotStarted # for backward compatibility
|
17
|
+
|
18
|
+
start_state :pre_start
|
19
|
+
state :pre_start
|
20
|
+
state :starting
|
21
|
+
state :started
|
22
|
+
state :stopped
|
13
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
|
-
|
43
|
+
# @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS
|
44
|
+
def start
|
39
45
|
# TODO: use a Fiber instead?
|
40
|
-
|
41
|
-
|
42
|
-
|
46
|
+
return unless state == :pre_start
|
47
|
+
|
48
|
+
transition! :starting
|
49
|
+
|
50
|
+
@wait_thread = Listen::Thread.new("wait_thread") do
|
51
|
+
_process_changes
|
43
52
|
end
|
44
53
|
|
45
|
-
Listen
|
46
|
-
Timeout.timeout(5) { q.pop }
|
47
|
-
end
|
54
|
+
Listen.logger.debug("Waiting for processing to start...")
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
56
|
+
wait_for_state(:started, timeout: MAX_STARTUP_SECONDS) or
|
57
|
+
raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})"
|
58
|
+
|
59
|
+
Listen.logger.debug('Processing started.')
|
53
60
|
end
|
54
61
|
|
55
62
|
def pause
|
@@ -57,60 +64,30 @@ module Listen
|
|
57
64
|
# fail NotImplementedError
|
58
65
|
end
|
59
66
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
wait_thread.join
|
65
|
-
end
|
67
|
+
def stop
|
68
|
+
transition! :stopped
|
69
|
+
|
70
|
+
@wait_thread&.join
|
66
71
|
@wait_thread = nil
|
67
72
|
end
|
68
73
|
|
69
74
|
def stopped?
|
70
|
-
|
75
|
+
state == :stopped
|
71
76
|
end
|
72
77
|
|
73
78
|
private
|
74
79
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
attr_accessor :state
|
79
|
-
|
80
|
-
def _wait_for_changes(ready_queue, config)
|
81
|
-
processor = Event::Processor.new(config, @reasons)
|
80
|
+
def _process_changes
|
81
|
+
processor = Event::Processor.new(@config, @reasons)
|
82
82
|
|
83
|
-
|
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
|
83
|
+
transition! :started
|
99
84
|
|
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)
|
85
|
+
processor.loop_for(@config.min_delay_between_events)
|
109
86
|
end
|
110
87
|
|
111
88
|
def _wakeup(reason)
|
112
89
|
@reasons << reason
|
113
|
-
wait_thread.wakeup
|
90
|
+
@wait_thread.wakeup
|
114
91
|
end
|
115
92
|
end
|
116
93
|
end
|
@@ -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'
|
@@ -5,6 +7,8 @@ require 'forwardable'
|
|
5
7
|
module Listen
|
6
8
|
module Event
|
7
9
|
class Queue
|
10
|
+
extend Forwardable
|
11
|
+
|
8
12
|
class Config
|
9
13
|
def initialize(relative)
|
10
14
|
@relative = relative
|
@@ -15,9 +19,8 @@ module Listen
|
|
15
19
|
end
|
16
20
|
end
|
17
21
|
|
18
|
-
def initialize(config
|
22
|
+
def initialize(config)
|
19
23
|
@event_queue = ::Queue.new
|
20
|
-
@block = block
|
21
24
|
@config = config
|
22
25
|
end
|
23
26
|
|
@@ -27,28 +30,21 @@ module Listen
|
|
27
30
|
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
28
31
|
fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
|
29
32
|
|
30
|
-
dir =
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
def empty?
|
37
|
-
event_queue.empty?
|
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]
|
38
39
|
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
delegate empty?: :@event_queue
|
42
|
+
delegate pop: :@event_queue
|
43
|
+
delegate close: :@event_queue
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
|
-
attr_reader :event_queue
|
47
|
-
attr_reader :block
|
48
|
-
attr_reader :config
|
49
|
-
|
50
47
|
def _safe_relative_from_cwd(dir)
|
51
|
-
return dir unless config.relative?
|
52
48
|
dir.relative_path_from(Pathname.pwd)
|
53
49
|
rescue ArgumentError
|
54
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,8 +30,13 @@ 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
|
-
return unless
|
39
|
+
return unless inaccurate_mac_time?(lstat)
|
30
40
|
|
31
41
|
# Check if change happened within 1 second (maybe it's even
|
32
42
|
# too much, e.g. 0.3-0.5 could be sufficient).
|
@@ -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
|