listen 3.2.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 +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 +13 -22
- data/lib/listen/internals/thread_pool.rb +0 -29
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
module Adapter
|
3
5
|
# Adapter implementation for Windows `wdm`.
|
4
6
|
#
|
5
7
|
class Windows < Base
|
6
|
-
OS_REGEXP = /mswin|mingw|cygwin/i
|
8
|
+
OS_REGEXP = /mswin|mingw|cygwin/i.freeze
|
7
9
|
|
8
10
|
BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
|
9
11
|
Please add the following to your Gemfile to avoid polling for changes:
|
@@ -15,8 +17,8 @@ module Listen
|
|
15
17
|
require 'wdm'
|
16
18
|
true
|
17
19
|
rescue LoadError
|
18
|
-
|
19
|
-
|
20
|
+
Listen.logger.debug format('wdm - load failed: %s:%s', $ERROR_INFO,
|
21
|
+
$ERROR_POSITION * "\n")
|
20
22
|
|
21
23
|
Kernel.warn BUNDLER_DECLARE_GEM
|
22
24
|
false
|
@@ -26,7 +28,7 @@ module Listen
|
|
26
28
|
|
27
29
|
def _configure(dir)
|
28
30
|
require 'wdm'
|
29
|
-
|
31
|
+
Listen.logger.debug 'wdm - starting...'
|
30
32
|
@worker ||= WDM::Monitor.new
|
31
33
|
@worker.watch_recursively(dir.to_s, :files) do |change|
|
32
34
|
yield([:file, change])
|
@@ -36,8 +38,7 @@ module Listen
|
|
36
38
|
yield([:dir, change])
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
@worker.watch_recursively(dir.to_s, *events) do |change|
|
41
|
+
@worker.watch_recursively(dir.to_s, :attributes, :last_write) do |change|
|
41
42
|
yield([:attr, change])
|
42
43
|
end
|
43
44
|
end
|
@@ -46,8 +47,9 @@ module Listen
|
|
46
47
|
@worker.run!
|
47
48
|
end
|
48
49
|
|
50
|
+
# rubocop:disable Metrics/MethodLength
|
49
51
|
def _process_event(dir, event)
|
50
|
-
|
52
|
+
Listen.logger.debug "wdm - callback: #{event.inspect}"
|
51
53
|
|
52
54
|
type, change = event
|
53
55
|
|
@@ -65,10 +67,11 @@ module Listen
|
|
65
67
|
_queue_change(:file, dir, rel_path, options)
|
66
68
|
end
|
67
69
|
when :dir
|
68
|
-
|
70
|
+
case change.type
|
71
|
+
when :removed
|
69
72
|
# TODO: check if watched dir?
|
70
73
|
_queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {})
|
71
|
-
|
74
|
+
when :added
|
72
75
|
_queue_change(:dir, dir, rel_path, {})
|
73
76
|
# do nothing - changed directory means either:
|
74
77
|
# - removed subdirs (handled above)
|
@@ -78,20 +81,15 @@ module Listen
|
|
78
81
|
# so what's left?
|
79
82
|
end
|
80
83
|
end
|
81
|
-
rescue
|
82
|
-
details = event.inspect
|
83
|
-
_log :error, format('wdm - callback (%s): %s:%s', details, $ERROR_INFO,
|
84
|
-
$ERROR_POSITION * "\n")
|
85
|
-
raise
|
86
84
|
end
|
85
|
+
# rubocop:enable Metrics/MethodLength
|
87
86
|
|
88
87
|
def _change(type)
|
89
88
|
{ modified: [:modified, :attrib], # TODO: is attrib really passed?
|
90
89
|
added: [:added, :renamed_new_file],
|
91
|
-
removed: [:removed, :renamed_old_file] }.
|
92
|
-
|
90
|
+
removed: [:removed, :renamed_old_file] }.find do |change, types|
|
91
|
+
types.include?(type) and break change
|
93
92
|
end
|
94
|
-
nil
|
95
93
|
end
|
96
94
|
end
|
97
95
|
end
|
data/lib/listen/backend.rb
CHANGED
data/lib/listen/change.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'listen/file'
|
2
4
|
require 'listen/directory'
|
3
5
|
|
@@ -28,49 +30,40 @@ module Listen
|
|
28
30
|
end
|
29
31
|
|
30
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
|
31
36
|
def invalidate(type, rel_path, options)
|
32
37
|
watched_dir = Pathname.new(record.root)
|
33
38
|
|
34
39
|
change = options[:change]
|
35
40
|
cookie = options[:cookie]
|
36
41
|
|
37
|
-
if !cookie && config.silenced?(rel_path, type)
|
38
|
-
Listen
|
42
|
+
if !cookie && @config.silenced?(rel_path, type)
|
43
|
+
Listen.logger.debug { "(silenced): #{rel_path.inspect}" }
|
39
44
|
return
|
40
45
|
end
|
41
46
|
|
42
47
|
path = watched_dir + rel_path
|
43
48
|
|
44
|
-
Listen
|
49
|
+
Listen.logger.debug do
|
45
50
|
log_details = options[:silence] && 'recording' || change || 'unknown'
|
46
51
|
"#{log_details}: #{type}:#{path} (#{options.inspect})"
|
47
52
|
end
|
48
53
|
|
49
54
|
if change
|
50
55
|
options = cookie ? { cookie: cookie } : {}
|
51
|
-
config.queue(type, change, watched_dir, rel_path, options)
|
56
|
+
@config.queue(type, change, watched_dir, rel_path, options)
|
52
57
|
elsif type == :dir
|
53
58
|
# NOTE: POSSIBLE RECURSION
|
54
59
|
# TODO: fix - use a queue instead
|
55
60
|
Directory.scan(self, rel_path, options)
|
56
|
-
|
57
|
-
|
58
|
-
return if !change || options[:silence]
|
59
|
-
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)
|
60
63
|
end
|
61
|
-
rescue RuntimeError => ex
|
62
|
-
msg = format(
|
63
|
-
'%s#%s crashed %s:%s',
|
64
|
-
self.class,
|
65
|
-
__method__,
|
66
|
-
exinspect,
|
67
|
-
ex.backtrace * "\n")
|
68
|
-
Listen::Logger.error(msg)
|
69
|
-
raise
|
70
64
|
end
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
attr_reader :config
|
65
|
+
# rubocop:enable Metrics/MethodLength
|
66
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
67
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
75
68
|
end
|
76
69
|
end
|
data/lib/listen/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thor'
|
2
4
|
require 'listen'
|
3
5
|
require 'logger'
|
@@ -33,15 +35,16 @@ module Listen
|
|
33
35
|
|
34
36
|
class Forwarder
|
35
37
|
attr_reader :logger
|
38
|
+
|
36
39
|
def initialize(options)
|
37
40
|
@options = options
|
38
|
-
@logger = ::Logger.new(STDOUT)
|
39
|
-
@logger.level = ::Logger::INFO
|
41
|
+
@logger = ::Logger.new(STDOUT, level: ::Logger::INFO)
|
40
42
|
@logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
41
43
|
end
|
42
44
|
|
43
45
|
def start
|
44
46
|
logger.info 'Starting listen...'
|
47
|
+
|
45
48
|
directory = @options[:directory]
|
46
49
|
relative = @options[:relative]
|
47
50
|
callback = proc do |modified, added, removed|
|
@@ -52,10 +55,7 @@ module Listen
|
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
55
|
-
listener = Listen.to(
|
56
|
-
directory,
|
57
|
-
relative: relative,
|
58
|
-
&callback)
|
58
|
+
listener = Listen.to(directory, relative: relative, &callback)
|
59
59
|
|
60
60
|
listener.start
|
61
61
|
|
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,19 @@ 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
55
|
|
56
56
|
def self.ascendant_of?(base, other)
|
57
57
|
other.ascend do |ascendant|
|
@@ -86,8 +86,8 @@ module Listen
|
|
86
86
|
# https://github.com/jruby/jruby/issues/3840
|
87
87
|
exists = path.exist?
|
88
88
|
directory = path.directory?
|
89
|
-
|
90
|
-
|
89
|
+
exists && !directory and raise Errno::ENOTDIR, path.to_s
|
90
|
+
path.children
|
91
91
|
end
|
92
92
|
end
|
93
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,14 @@ 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
|
+
@block&.call(*args)
|
28
29
|
end
|
29
30
|
|
30
|
-
attr_reader :event_queue
|
31
|
-
|
32
31
|
def callable?
|
33
32
|
@block
|
34
33
|
end
|
@@ -36,20 +35,6 @@ module Listen
|
|
36
35
|
def optimize_changes(changes)
|
37
36
|
@queue_optimizer.smoosh_changes(changes)
|
38
37
|
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
38
|
end
|
54
39
|
end
|
55
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
|