listen 3.3.1 → 3.4.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 +113 -8
- data/bin/listen +3 -4
- data/lib/listen/adapter.rb +3 -3
- data/lib/listen/adapter/base.rb +6 -6
- data/lib/listen/adapter/bsd.rb +3 -4
- data/lib/listen/adapter/config.rb +1 -4
- data/lib/listen/adapter/darwin.rb +2 -2
- data/lib/listen/adapter/linux.rb +7 -5
- data/lib/listen/adapter/polling.rb +6 -5
- data/lib/listen/adapter/windows.rb +10 -14
- data/lib/listen/change.rb +10 -19
- data/lib/listen/cli.rb +3 -4
- data/lib/listen/directory.rb +5 -7
- data/lib/listen/error.rb +10 -0
- data/lib/listen/event/config.rb +4 -9
- data/lib/listen/event/loop.rb +5 -4
- data/lib/listen/event/processor.rb +13 -15
- data/lib/listen/event/queue.rb +9 -9
- data/lib/listen/file.rb +6 -0
- data/lib/listen/fsm.rb +5 -2
- data/lib/listen/listener.rb +2 -0
- data/lib/listen/listener/config.rb +2 -4
- data/lib/listen/logger.rb +13 -12
- data/lib/listen/monotonic_time.rb +27 -0
- data/lib/listen/options.rb +9 -8
- data/lib/listen/queue_optimizer.rb +7 -12
- data/lib/listen/record.rb +28 -43
- data/lib/listen/record/entry.rb +1 -1
- data/lib/listen/record/symlink_detector.rb +8 -8
- data/lib/listen/silencer.rb +10 -8
- data/lib/listen/thread.rb +14 -12
- data/lib/listen/version.rb +1 -1
- metadata +10 -3
data/lib/listen/change.rb
CHANGED
|
@@ -30,13 +30,16 @@ module Listen
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
|
|
33
|
+
# rubocop:disable Metrics/MethodLength
|
|
34
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
35
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
33
36
|
def invalidate(type, rel_path, options)
|
|
34
37
|
watched_dir = Pathname.new(record.root)
|
|
35
38
|
|
|
36
39
|
change = options[:change]
|
|
37
40
|
cookie = options[:cookie]
|
|
38
41
|
|
|
39
|
-
if !cookie && config.silenced?(rel_path, type)
|
|
42
|
+
if !cookie && @config.silenced?(rel_path, type)
|
|
40
43
|
Listen.logger.debug { "(silenced): #{rel_path.inspect}" }
|
|
41
44
|
return
|
|
42
45
|
end
|
|
@@ -50,29 +53,17 @@ module Listen
|
|
|
50
53
|
|
|
51
54
|
if change
|
|
52
55
|
options = cookie ? { cookie: cookie } : {}
|
|
53
|
-
config.queue(type, change, watched_dir, rel_path, options)
|
|
56
|
+
@config.queue(type, change, watched_dir, rel_path, options)
|
|
54
57
|
elsif type == :dir
|
|
55
58
|
# NOTE: POSSIBLE RECURSION
|
|
56
59
|
# TODO: fix - use a queue instead
|
|
57
60
|
Directory.scan(self, rel_path, options)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return if !change || options[:silence]
|
|
61
|
-
config.queue(:file, change, watched_dir, rel_path)
|
|
61
|
+
elsif (change = File.change(record, rel_path)) && !options[:silence]
|
|
62
|
+
@config.queue(:file, change, watched_dir, rel_path)
|
|
62
63
|
end
|
|
63
|
-
rescue RuntimeError => ex
|
|
64
|
-
msg = format(
|
|
65
|
-
'%s#%s crashed %s:%s',
|
|
66
|
-
self.class,
|
|
67
|
-
__method__,
|
|
68
|
-
exinspect,
|
|
69
|
-
ex.backtrace * "\n")
|
|
70
|
-
Listen.logger.error(msg)
|
|
71
|
-
raise
|
|
72
64
|
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
attr_reader :config
|
|
65
|
+
# rubocop:enable Metrics/MethodLength
|
|
66
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
67
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
77
68
|
end
|
|
78
69
|
end
|
data/lib/listen/cli.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Listen
|
|
|
35
35
|
|
|
36
36
|
class Forwarder
|
|
37
37
|
attr_reader :logger
|
|
38
|
+
|
|
38
39
|
def initialize(options)
|
|
39
40
|
@options = options
|
|
40
41
|
@logger = ::Logger.new(STDOUT, level: ::Logger::INFO)
|
|
@@ -43,6 +44,7 @@ module Listen
|
|
|
43
44
|
|
|
44
45
|
def start
|
|
45
46
|
logger.info 'Starting listen...'
|
|
47
|
+
|
|
46
48
|
directory = @options[:directory]
|
|
47
49
|
relative = @options[:relative]
|
|
48
50
|
callback = proc do |modified, added, removed|
|
|
@@ -53,10 +55,7 @@ module Listen
|
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
listener = Listen.to(
|
|
57
|
-
directory,
|
|
58
|
-
relative: relative,
|
|
59
|
-
&callback)
|
|
58
|
+
listener = Listen.to(directory, relative: relative, &callback)
|
|
60
59
|
|
|
61
60
|
listener.start
|
|
62
61
|
|
data/lib/listen/directory.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'set'
|
|
|
5
5
|
module Listen
|
|
6
6
|
# TODO: refactor (turn it into a normal object, cache the stat, etc)
|
|
7
7
|
class Directory
|
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
|
8
9
|
def self.scan(snapshot, rel_path, options)
|
|
9
10
|
record = snapshot.record
|
|
10
11
|
dir = Pathname.new(record.root)
|
|
@@ -38,22 +39,19 @@ module Listen
|
|
|
38
39
|
previous = previous.reject { |entry, _| current.include? path + entry }
|
|
39
40
|
|
|
40
41
|
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
|
41
|
-
|
|
42
42
|
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
|
43
43
|
record.unset_path(rel_path)
|
|
44
44
|
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
|
45
|
-
|
|
46
45
|
rescue Errno::ENOTDIR
|
|
47
46
|
# TODO: path not tested
|
|
48
47
|
record.unset_path(rel_path)
|
|
49
48
|
_async_changes(snapshot, path, previous, options)
|
|
50
49
|
_change(snapshot, :file, rel_path, options)
|
|
51
50
|
rescue
|
|
52
|
-
Listen.logger.warn
|
|
53
|
-
format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
|
|
54
|
-
end
|
|
51
|
+
Listen.logger.warn { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") }
|
|
55
52
|
raise
|
|
56
53
|
end
|
|
54
|
+
# rubocop:enable Metrics/MethodLength
|
|
57
55
|
|
|
58
56
|
def self.ascendant_of?(base, other)
|
|
59
57
|
other.ascend do |ascendant|
|
|
@@ -88,8 +86,8 @@ module Listen
|
|
|
88
86
|
# https://github.com/jruby/jruby/issues/3840
|
|
89
87
|
exists = path.exist?
|
|
90
88
|
directory = path.directory?
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
exists && !directory and raise Errno::ENOTDIR, path.to_s
|
|
90
|
+
path.children
|
|
93
91
|
end
|
|
94
92
|
end
|
|
95
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
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
module Listen
|
|
4
4
|
module Event
|
|
5
5
|
class Config
|
|
6
|
-
attr_reader :listener
|
|
7
|
-
attr_reader :event_queue
|
|
8
|
-
attr_reader :min_delay_between_events
|
|
6
|
+
attr_reader :listener, :event_queue, :min_delay_between_events
|
|
9
7
|
|
|
10
8
|
def initialize(
|
|
11
9
|
listener,
|
|
12
10
|
event_queue,
|
|
13
11
|
queue_optimizer,
|
|
14
12
|
wait_for_delay,
|
|
15
|
-
&block
|
|
13
|
+
&block
|
|
14
|
+
)
|
|
16
15
|
|
|
17
16
|
@listener = listener
|
|
18
17
|
@event_queue = event_queue
|
|
@@ -26,11 +25,7 @@ module Listen
|
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def call(*args)
|
|
29
|
-
@block
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def timestamp
|
|
33
|
-
Time.now.to_f
|
|
28
|
+
@block&.call(*args)
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
def callable?
|
data/lib/listen/event/loop.rb
CHANGED
|
@@ -5,15 +5,15 @@ require 'thread'
|
|
|
5
5
|
require 'timeout'
|
|
6
6
|
require 'listen/event/processor'
|
|
7
7
|
require 'listen/thread'
|
|
8
|
+
require 'listen/error'
|
|
8
9
|
|
|
9
10
|
module Listen
|
|
10
11
|
module Event
|
|
11
12
|
class Loop
|
|
12
13
|
include Listen::FSM
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
15
|
+
Error = ::Listen::Error
|
|
16
|
+
NotStarted = ::Listen::Error::NotStarted # for backward compatibility
|
|
17
17
|
|
|
18
18
|
start_state :pre_start
|
|
19
19
|
state :pre_start
|
|
@@ -40,6 +40,7 @@ module Listen
|
|
|
40
40
|
|
|
41
41
|
MAX_STARTUP_SECONDS = 5.0
|
|
42
42
|
|
|
43
|
+
# @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS
|
|
43
44
|
def start
|
|
44
45
|
# TODO: use a Fiber instead?
|
|
45
46
|
return unless state == :pre_start
|
|
@@ -52,7 +53,7 @@ module Listen
|
|
|
52
53
|
|
|
53
54
|
Listen.logger.debug("Waiting for processing to start...")
|
|
54
55
|
|
|
55
|
-
wait_for_state(:started, MAX_STARTUP_SECONDS) or
|
|
56
|
+
wait_for_state(:started, timeout: MAX_STARTUP_SECONDS) or
|
|
56
57
|
raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})"
|
|
57
58
|
|
|
58
59
|
Listen.logger.debug('Processing started.')
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'listen/monotonic_time'
|
|
4
|
+
|
|
3
5
|
module Listen
|
|
4
6
|
module Event
|
|
5
7
|
class Processor
|
|
@@ -33,7 +35,7 @@ module Listen
|
|
|
33
35
|
|
|
34
36
|
def _wait_until_events_calm_down
|
|
35
37
|
loop do
|
|
36
|
-
now =
|
|
38
|
+
now = MonotonicTime.now
|
|
37
39
|
|
|
38
40
|
# Assure there's at least latency between callbacks to allow
|
|
39
41
|
# for accumulating changes
|
|
@@ -51,10 +53,10 @@ module Listen
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def _check_stopped
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if @listener.stopped?
|
|
57
|
+
_flush_wakeup_reasons
|
|
58
|
+
raise Stopped
|
|
59
|
+
end
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
def _sleep(seconds)
|
|
@@ -70,22 +72,22 @@ module Listen
|
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def _remember_time_of_first_unprocessed_event
|
|
73
|
-
@
|
|
75
|
+
@_remember_time_of_first_unprocessed_event ||= MonotonicTime.now
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
def _reset_no_unprocessed_events
|
|
77
|
-
@
|
|
79
|
+
@_remember_time_of_first_unprocessed_event = nil
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
def _deadline
|
|
81
|
-
@
|
|
83
|
+
@_remember_time_of_first_unprocessed_event + @latency
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
# blocks until event is popped
|
|
85
87
|
# returns the event or `nil` when the event_queue is closed
|
|
86
88
|
def _wait_until_events
|
|
87
89
|
config.event_queue.pop.tap do |_event|
|
|
88
|
-
@
|
|
90
|
+
@_remember_time_of_first_unprocessed_event ||= MonotonicTime.now
|
|
89
91
|
end
|
|
90
92
|
end
|
|
91
93
|
|
|
@@ -96,10 +98,6 @@ module Listen
|
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
|
|
99
|
-
def _timestamp
|
|
100
|
-
config.timestamp
|
|
101
|
-
end
|
|
102
|
-
|
|
103
101
|
# for easier testing without sleep loop
|
|
104
102
|
def _process_changes(event)
|
|
105
103
|
_reset_no_unprocessed_events
|
|
@@ -113,13 +111,13 @@ module Listen
|
|
|
113
111
|
result = [hash[:modified], hash[:added], hash[:removed]]
|
|
114
112
|
return if result.all?(&:empty?)
|
|
115
113
|
|
|
116
|
-
block_start =
|
|
114
|
+
block_start = MonotonicTime.now
|
|
117
115
|
exception_note = " (exception)"
|
|
118
116
|
::Listen::Thread.rescue_and_log('_process_changes') do
|
|
119
117
|
config.call(*result)
|
|
120
118
|
exception_note = nil
|
|
121
119
|
end
|
|
122
|
-
Listen.logger.debug "Callback#{exception_note} took #{
|
|
120
|
+
Listen.logger.debug "Callback#{exception_note} took #{MonotonicTime.now - block_start} sec"
|
|
123
121
|
end
|
|
124
122
|
|
|
125
123
|
attr_reader :config
|
data/lib/listen/event/queue.rb
CHANGED
|
@@ -30,21 +30,21 @@ module Listen
|
|
|
30
30
|
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
|
31
31
|
fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
|
|
32
32
|
|
|
33
|
-
dir =
|
|
34
|
-
|
|
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]
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
delegate empty?:
|
|
38
|
-
delegate pop:
|
|
39
|
-
delegate close:
|
|
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 :config
|
|
45
|
-
|
|
46
47
|
def _safe_relative_from_cwd(dir)
|
|
47
|
-
return dir unless config.relative?
|
|
48
48
|
dir.relative_path_from(Pathname.pwd)
|
|
49
49
|
rescue ArgumentError
|
|
50
50
|
dir
|
data/lib/listen/file.rb
CHANGED
|
@@ -4,6 +4,9 @@ require 'digest/md5'
|
|
|
4
4
|
|
|
5
5
|
module Listen
|
|
6
6
|
class File
|
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
|
8
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
9
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
7
10
|
def self.change(record, rel_path)
|
|
8
11
|
path = Pathname.new(record.root) + rel_path
|
|
9
12
|
lstat = path.lstat
|
|
@@ -74,6 +77,9 @@ module Listen
|
|
|
74
77
|
Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
|
|
75
78
|
raise
|
|
76
79
|
end
|
|
80
|
+
# rubocop:enable Metrics/MethodLength
|
|
81
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
82
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
77
83
|
|
|
78
84
|
def self.inaccurate_mac_time?(stat)
|
|
79
85
|
# 'mac' means Modified/Accessed/Created
|
data/lib/listen/fsm.rb
CHANGED
|
@@ -53,6 +53,9 @@ module Listen
|
|
|
53
53
|
# if not already, waits for a state change (up to timeout seconds--`nil` means infinite)
|
|
54
54
|
# returns truthy iff the transition to one of the desired state has occurred
|
|
55
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
|
|
56
59
|
@mutex.synchronize do
|
|
57
60
|
if !wait_for_states.include?(@state)
|
|
58
61
|
@state_changed.wait(@mutex, timeout)
|
|
@@ -113,8 +116,8 @@ module Listen
|
|
|
113
116
|
@name = name
|
|
114
117
|
@block = block
|
|
115
118
|
@transitions = if transitions
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
Array(transitions).map(&:to_sym)
|
|
120
|
+
end
|
|
118
121
|
end
|
|
119
122
|
|
|
120
123
|
def call(obj)
|
data/lib/listen/listener.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Listen
|
|
|
33
33
|
# @yieldparam [Array<String>] added the list of added files
|
|
34
34
|
# @yieldparam [Array<String>] removed the list of removed files
|
|
35
35
|
#
|
|
36
|
+
# rubocop:disable Metrics/MethodLength
|
|
36
37
|
def initialize(*dirs, &block)
|
|
37
38
|
options = dirs.last.is_a?(Hash) ? dirs.pop : {}
|
|
38
39
|
|
|
@@ -60,6 +61,7 @@ module Listen
|
|
|
60
61
|
|
|
61
62
|
initialize_fsm
|
|
62
63
|
end
|
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
|
63
65
|
|
|
64
66
|
start_state :initializing
|
|
65
67
|
|
|
@@ -25,9 +25,7 @@ module Listen
|
|
|
25
25
|
@relative
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
attr_reader :min_delay_between_events
|
|
29
|
-
|
|
30
|
-
attr_reader :silencer_rules
|
|
28
|
+
attr_reader :min_delay_between_events, :silencer_rules
|
|
31
29
|
|
|
32
30
|
def adapter_instance_options(klass)
|
|
33
31
|
valid_keys = klass.const_get('DEFAULTS').keys
|
|
@@ -35,7 +33,7 @@ module Listen
|
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
def adapter_select_options
|
|
38
|
-
valid_keys = %w
|
|
36
|
+
valid_keys = %w[force_polling polling_fallback_message].map(&:to_sym)
|
|
39
37
|
Hash[@options.select { |key, _| valid_keys.include?(key) }]
|
|
40
38
|
end
|
|
41
39
|
end
|
data/lib/listen/logger.rb
CHANGED
|
@@ -16,18 +16,19 @@ module Listen
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
18
|
def default_logger
|
|
19
|
-
level =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
level =
|
|
20
|
+
case ENV['LISTEN_GEM_DEBUGGING'].to_s
|
|
21
|
+
when /debug|2/i
|
|
22
|
+
::Logger::DEBUG
|
|
23
|
+
when /info|true|yes|1/i
|
|
24
|
+
::Logger::INFO
|
|
25
|
+
when /warn/i
|
|
26
|
+
::Logger::WARN
|
|
27
|
+
when /fatal/i
|
|
28
|
+
::Logger::FATAL
|
|
29
|
+
else
|
|
30
|
+
::Logger::ERROR
|
|
31
|
+
end
|
|
31
32
|
|
|
32
33
|
::Logger.new(STDERR, level: level)
|
|
33
34
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listen
|
|
4
|
+
module MonotonicTime
|
|
5
|
+
class << self
|
|
6
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
|
7
|
+
|
|
8
|
+
def now
|
|
9
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
elsif defined?(Process::CLOCK_MONOTONIC_RAW)
|
|
13
|
+
|
|
14
|
+
def now
|
|
15
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
else
|
|
19
|
+
|
|
20
|
+
def now
|
|
21
|
+
Time.now.to_f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|