listen 3.2.1 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +14 -23
- 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
|