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
data/lib/listen/listener.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'English'
|
2
4
|
|
3
5
|
require 'listen/version'
|
@@ -19,7 +21,6 @@ require 'listen/listener/config'
|
|
19
21
|
|
20
22
|
module Listen
|
21
23
|
class Listener
|
22
|
-
# TODO: move the state machine's methods private
|
23
24
|
include Listen::FSM
|
24
25
|
|
25
26
|
# Initializes the directories listener.
|
@@ -32,13 +33,14 @@ module Listen
|
|
32
33
|
# @yieldparam [Array<String>] added the list of added files
|
33
34
|
# @yieldparam [Array<String>] removed the list of removed files
|
34
35
|
#
|
36
|
+
# rubocop:disable Metrics/MethodLength
|
35
37
|
def initialize(*dirs, &block)
|
36
38
|
options = dirs.last.is_a?(Hash) ? dirs.pop : {}
|
37
39
|
|
38
40
|
@config = Config.new(options)
|
39
41
|
|
40
42
|
eq_config = Event::Queue::Config.new(@config.relative?)
|
41
|
-
queue = Event::Queue.new(eq_config)
|
43
|
+
queue = Event::Queue.new(eq_config)
|
42
44
|
|
43
45
|
silencer = Silencer.new
|
44
46
|
rules = @config.silencer_rules
|
@@ -57,41 +59,43 @@ module Listen
|
|
57
59
|
|
58
60
|
@processor = Event::Loop.new(pconfig)
|
59
61
|
|
60
|
-
|
62
|
+
initialize_fsm
|
61
63
|
end
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
62
65
|
|
63
|
-
|
66
|
+
start_state :initializing
|
64
67
|
|
65
68
|
state :initializing, to: [:backend_started, :stopped]
|
66
69
|
|
67
|
-
state :backend_started, to: [:
|
68
|
-
backend.start
|
69
|
-
end
|
70
|
-
|
71
|
-
state :frontend_ready, to: [:processing_events, :stopped] do
|
72
|
-
processor.setup
|
70
|
+
state :backend_started, to: [:processing_events, :stopped] do
|
71
|
+
@backend.start
|
73
72
|
end
|
74
73
|
|
75
74
|
state :processing_events, to: [:paused, :stopped] do
|
76
|
-
processor.
|
75
|
+
@processor.start
|
77
76
|
end
|
78
77
|
|
79
78
|
state :paused, to: [:processing_events, :stopped] do
|
80
|
-
processor.pause
|
79
|
+
@processor.pause
|
81
80
|
end
|
82
81
|
|
83
82
|
state :stopped, to: [:backend_started] do
|
84
|
-
backend.stop #
|
85
|
-
processor.
|
83
|
+
@backend.stop # halt events ASAP
|
84
|
+
@processor.stop
|
86
85
|
end
|
87
86
|
|
88
87
|
# Starts processing events and starts adapters
|
89
88
|
# or resumes invoking callbacks if paused
|
90
89
|
def start
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
case state
|
91
|
+
when :initializing
|
92
|
+
transition :backend_started
|
93
|
+
transition :processing_events
|
94
|
+
when :paused
|
95
|
+
transition :processing_events
|
96
|
+
else
|
97
|
+
raise ArgumentError, "cannot start from state #{state.inspect}"
|
98
|
+
end
|
95
99
|
end
|
96
100
|
|
97
101
|
# Stops both listening for events and processing them
|
@@ -113,6 +117,10 @@ module Listen
|
|
113
117
|
state == :paused
|
114
118
|
end
|
115
119
|
|
120
|
+
def stopped?
|
121
|
+
state == :stopped
|
122
|
+
end
|
123
|
+
|
116
124
|
def ignore(regexps)
|
117
125
|
@silencer_controller.append_ignores(regexps)
|
118
126
|
end
|
@@ -124,10 +132,5 @@ module Listen
|
|
124
132
|
def only(regexps)
|
125
133
|
@silencer_controller.replace_with_only(regexps)
|
126
134
|
end
|
127
|
-
|
128
|
-
private
|
129
|
-
|
130
|
-
attr_reader :processor
|
131
|
-
attr_reader :backend
|
132
135
|
end
|
133
136
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
class Listener
|
3
5
|
class Config
|
@@ -23,9 +25,7 @@ module Listen
|
|
23
25
|
@relative
|
24
26
|
end
|
25
27
|
|
26
|
-
attr_reader :min_delay_between_events
|
27
|
-
|
28
|
-
attr_reader :silencer_rules
|
28
|
+
attr_reader :min_delay_between_events, :silencer_rules
|
29
29
|
|
30
30
|
def adapter_instance_options(klass)
|
31
31
|
valid_keys = klass.const_get('DEFAULTS').keys
|
@@ -33,7 +33,7 @@ module Listen
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def adapter_select_options
|
36
|
-
valid_keys = %w
|
36
|
+
valid_keys = %w[force_polling polling_fallback_message].map(&:to_sym)
|
37
37
|
Hash[@options.select { |key, _| valid_keys.include?(key) }]
|
38
38
|
end
|
39
39
|
end
|
data/lib/listen/logger.rb
CHANGED
@@ -1,32 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
|
-
|
3
|
-
@logger ||= nil
|
4
|
-
end
|
4
|
+
@logger = nil
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
end
|
6
|
+
# Listen.logger will always be present.
|
7
|
+
# If you don't want logging, set Listen.logger = ::Logger.new('/dev/null', level: ::Logger::UNKNOWN)
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
class << self
|
10
|
+
attr_writer :logger
|
11
|
+
|
12
|
+
def logger
|
13
|
+
@logger ||= default_logger
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def default_logger
|
19
|
+
level =
|
20
|
+
case ENV['LISTEN_GEM_DEBUGGING'].to_s
|
21
|
+
when /debug|2/i
|
16
22
|
::Logger::DEBUG
|
17
|
-
when /true|yes|1/i
|
23
|
+
when /info|true|yes|1/i
|
18
24
|
::Logger::INFO
|
25
|
+
when /warn/i
|
26
|
+
::Logger::WARN
|
27
|
+
when /fatal/i
|
28
|
+
::Logger::FATAL
|
19
29
|
else
|
20
30
|
::Logger::ERROR
|
21
31
|
end
|
22
|
-
end
|
23
|
-
end
|
24
32
|
|
25
|
-
|
26
|
-
[:fatal, :error, :warn, :info, :debug].each do |meth|
|
27
|
-
define_singleton_method(meth) do |*args, &block|
|
28
|
-
Listen.logger.public_send(meth, *args, &block) if Listen.logger
|
29
|
-
end
|
33
|
+
::Logger.new(STDERR, level: level)
|
30
34
|
end
|
31
35
|
end
|
32
36
|
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
|
data/lib/listen/options.rb
CHANGED
@@ -1,23 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
class Options
|
3
5
|
def initialize(opts, defaults)
|
4
6
|
@options = {}
|
5
7
|
given_options = opts.dup
|
6
|
-
defaults.
|
8
|
+
defaults.each_key do |key|
|
7
9
|
@options[key] = given_options.delete(key) || defaults[key]
|
8
10
|
end
|
9
11
|
|
10
|
-
|
12
|
+
given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}"
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
# rubocop:disable Lint/MissingSuper
|
16
|
+
def respond_to_missing?(name, *_)
|
17
|
+
@options.has_key?(name)
|
15
18
|
end
|
16
19
|
|
17
20
|
def method_missing(name, *_)
|
18
|
-
|
19
|
-
|
20
|
-
fail NameError, msg
|
21
|
+
respond_to_missing?(name) or raise NameError, "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
|
22
|
+
@options[name]
|
21
23
|
end
|
24
|
+
# rubocop:enable Lint/MissingSuper
|
22
25
|
end
|
23
26
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
class QueueOptimizer
|
3
5
|
class Config
|
@@ -60,7 +62,7 @@ module Listen
|
|
60
62
|
actions << :added if actions.delete(:moved_to)
|
61
63
|
actions << :removed if actions.delete(:moved_from)
|
62
64
|
|
63
|
-
modified = actions.
|
65
|
+
modified = actions.find { |x| x == :modified }
|
64
66
|
_calculate_add_remove_difference(actions, path, modified)
|
65
67
|
end
|
66
68
|
|
@@ -89,10 +91,8 @@ module Listen
|
|
89
91
|
def _reinterpret_related_changes(cookies)
|
90
92
|
table = { moved_to: :added, moved_from: :removed }
|
91
93
|
cookies.flat_map do |_, changes|
|
92
|
-
|
93
|
-
|
94
|
-
to_dir, to_file = data
|
95
|
-
[[:modified, to_dir, to_file]]
|
94
|
+
if (editor_modified = editor_modified?(changes))
|
95
|
+
[[:modified, *editor_modified]]
|
96
96
|
else
|
97
97
|
not_silenced = changes.reject do |type, _, _, path, _|
|
98
98
|
config.silenced?(Pathname(path), type)
|
@@ -104,7 +104,7 @@ module Listen
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
-
def
|
107
|
+
def editor_modified?(changes)
|
108
108
|
return unless changes.size == 2
|
109
109
|
|
110
110
|
from_type = from = nil
|
@@ -116,17 +116,14 @@ module Listen
|
|
116
116
|
from_type, _from_change, _, from, = data
|
117
117
|
when :moved_to
|
118
118
|
to_type, _to_change, to_dir, to, = data
|
119
|
-
else
|
120
|
-
return nil
|
121
119
|
end
|
122
120
|
end
|
123
121
|
|
124
|
-
return unless from && to
|
125
|
-
|
126
122
|
# Expect an ignored moved_from and non-ignored moved_to
|
127
123
|
# to qualify as an "editor modify"
|
128
|
-
|
129
|
-
|
124
|
+
if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type)
|
125
|
+
[to_dir, to]
|
126
|
+
end
|
130
127
|
end
|
131
128
|
end
|
132
129
|
end
|
data/lib/listen/record.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thread'
|
2
4
|
require 'listen/record/entry'
|
3
5
|
require 'listen/record/symlink_detector'
|
@@ -8,14 +10,16 @@ module Listen
|
|
8
10
|
# TODO: deprecate
|
9
11
|
|
10
12
|
attr_reader :root
|
13
|
+
|
11
14
|
def initialize(directory)
|
12
15
|
@tree = _auto_hash
|
13
16
|
@root = directory.to_s
|
14
17
|
end
|
15
18
|
|
16
19
|
def add_dir(rel_path)
|
17
|
-
|
18
|
-
|
20
|
+
if ![nil, '', '.'].include?(rel_path)
|
21
|
+
@tree[rel_path] ||= {}
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
def update_file(rel_path, data)
|
@@ -31,30 +35,27 @@ module Listen
|
|
31
35
|
def file_data(rel_path)
|
32
36
|
dirname, basename = Pathname(rel_path).split.map(&:to_s)
|
33
37
|
if [nil, '', '.'].include? dirname
|
34
|
-
tree[basename] ||= {}
|
35
|
-
tree[basename].dup
|
38
|
+
@tree[basename] ||= {}
|
39
|
+
@tree[basename].dup
|
36
40
|
else
|
37
|
-
tree[dirname] ||= {}
|
38
|
-
tree[dirname][basename] ||= {}
|
39
|
-
tree[dirname][basename].dup
|
41
|
+
@tree[dirname] ||= {}
|
42
|
+
@tree[dirname][basename] ||= {}
|
43
|
+
@tree[dirname][basename].dup
|
40
44
|
end
|
41
45
|
end
|
42
46
|
|
43
47
|
def dir_entries(rel_path)
|
44
|
-
subtree =
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
48
|
+
subtree = if ['', '.'].include? rel_path.to_s
|
49
|
+
@tree
|
50
|
+
else
|
51
|
+
@tree[rel_path.to_s] ||= _auto_hash
|
52
|
+
@tree[rel_path.to_s]
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
subtree.each do |key, values|
|
55
|
+
subtree.transform_values do |values|
|
54
56
|
# only get data for file entries
|
55
|
-
|
57
|
+
values.key?(:mtime) ? values : {}
|
56
58
|
end
|
57
|
-
result
|
58
59
|
end
|
59
60
|
|
60
61
|
def build
|
@@ -71,29 +72,27 @@ module Listen
|
|
71
72
|
private
|
72
73
|
|
73
74
|
def _auto_hash
|
74
|
-
Hash.new { |h, k| h[k] =
|
75
|
+
Hash.new { |h, k| h[k] = {} }
|
75
76
|
end
|
76
77
|
|
77
|
-
attr_reader :tree
|
78
|
-
|
79
78
|
def _fast_update_file(dirname, basename, data)
|
80
|
-
if [nil, '', '.'].include?
|
81
|
-
tree[basename] = (tree[basename] || {}).merge(data)
|
79
|
+
if [nil, '', '.'].include?(dirname)
|
80
|
+
@tree[basename] = (@tree[basename] || {}).merge(data)
|
82
81
|
else
|
83
|
-
tree[dirname] ||= {}
|
84
|
-
tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
|
82
|
+
@tree[dirname] ||= {}
|
83
|
+
@tree[dirname][basename] = (@tree[dirname][basename] || {}).merge(data)
|
85
84
|
end
|
86
85
|
end
|
87
86
|
|
88
87
|
def _fast_unset_path(dirname, basename)
|
89
88
|
# this may need to be reworked to properly remove
|
90
89
|
# entries from a tree, without adding non-existing dirs to the record
|
91
|
-
if [nil, '', '.'].include?
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
tree[dirname].delete(basename)
|
90
|
+
if [nil, '', '.'].include?(dirname)
|
91
|
+
if @tree.key?(basename)
|
92
|
+
@tree.delete(basename)
|
93
|
+
end
|
94
|
+
elsif @tree.key?(dirname)
|
95
|
+
@tree[dirname].delete(basename)
|
97
96
|
end
|
98
97
|
end
|
99
98
|
|
data/lib/listen/record/entry.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
# @private api
|
3
5
|
class Record
|
@@ -15,14 +17,14 @@ module Listen
|
|
15
17
|
|
16
18
|
def children
|
17
19
|
child_relative = _join
|
18
|
-
(_entries(sys_path) - %w
|
20
|
+
(_entries(sys_path) - %w[. ..]).map do |name|
|
19
21
|
Entry.new(@root, child_relative, name)
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
25
|
def meta
|
24
26
|
lstat = ::File.lstat(sys_path)
|
25
|
-
{ mtime: lstat.mtime.to_f, mode: lstat.mode }
|
27
|
+
{ mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
|
26
28
|
end
|
27
29
|
|
28
30
|
# record hash is e.g.
|
@@ -1,23 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
4
|
+
require 'listen/error'
|
2
5
|
|
3
6
|
module Listen
|
4
7
|
# @private api
|
5
8
|
class Record
|
6
9
|
class SymlinkDetector
|
7
|
-
|
10
|
+
README_URL = 'https://github.com/guard/listen/blob/master/README.md'
|
8
11
|
|
9
|
-
SYMLINK_LOOP_ERROR = <<-EOS
|
12
|
+
SYMLINK_LOOP_ERROR = <<-EOS
|
10
13
|
** ERROR: directory is already being watched! **
|
11
14
|
|
12
15
|
Directory: %s
|
13
16
|
|
14
17
|
is already being watched through: %s
|
15
18
|
|
16
|
-
MORE INFO: #{
|
19
|
+
MORE INFO: #{README_URL}
|
17
20
|
EOS
|
18
21
|
|
19
|
-
|
20
|
-
end
|
22
|
+
Error = ::Listen::Error # for backward compatibility
|
21
23
|
|
22
24
|
def initialize
|
23
25
|
@real_dirs = Set.new
|
@@ -25,14 +27,14 @@ module Listen
|
|
25
27
|
|
26
28
|
def verify_unwatched!(entry)
|
27
29
|
real_path = entry.real_path
|
28
|
-
@real_dirs.add?(real_path)
|
30
|
+
@real_dirs.add?(real_path) or _fail(entry.sys_path, real_path)
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
33
35
|
def _fail(symlinked, real_path)
|
34
|
-
|
35
|
-
|
36
|
+
warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path))
|
37
|
+
raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks'
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|