listen 0.5.3 → 3.7.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 +7 -0
- data/CHANGELOG.md +1 -186
- data/CONTRIBUTING.md +45 -0
- data/{LICENSE → LICENSE.txt} +3 -1
- data/README.md +332 -181
- data/bin/listen +11 -0
- data/lib/listen/adapter/base.rb +129 -0
- data/lib/listen/adapter/bsd.rb +107 -0
- data/lib/listen/adapter/config.rb +25 -0
- data/lib/listen/adapter/darwin.rb +77 -0
- data/lib/listen/adapter/linux.rb +108 -0
- data/lib/listen/adapter/polling.rb +40 -0
- data/lib/listen/adapter/windows.rb +96 -0
- data/lib/listen/adapter.rb +32 -201
- data/lib/listen/backend.rb +40 -0
- data/lib/listen/change.rb +69 -0
- data/lib/listen/cli.rb +65 -0
- data/lib/listen/directory.rb +93 -0
- data/lib/listen/error.rb +11 -0
- data/lib/listen/event/config.rb +40 -0
- data/lib/listen/event/loop.rb +94 -0
- data/lib/listen/event/processor.rb +126 -0
- data/lib/listen/event/queue.rb +54 -0
- data/lib/listen/file.rb +95 -0
- data/lib/listen/fsm.rb +133 -0
- data/lib/listen/listener/config.rb +41 -0
- data/lib/listen/listener.rb +93 -160
- data/lib/listen/logger.rb +36 -0
- data/lib/listen/monotonic_time.rb +27 -0
- data/lib/listen/options.rb +26 -0
- data/lib/listen/queue_optimizer.rb +129 -0
- data/lib/listen/record/entry.rb +66 -0
- data/lib/listen/record/symlink_detector.rb +41 -0
- data/lib/listen/record.rb +123 -0
- data/lib/listen/silencer/controller.rb +50 -0
- data/lib/listen/silencer.rb +106 -0
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- data/lib/listen.rb +40 -32
- metadata +87 -38
- data/lib/listen/adapters/darwin.rb +0 -85
- data/lib/listen/adapters/linux.rb +0 -113
- data/lib/listen/adapters/polling.rb +0 -67
- data/lib/listen/adapters/windows.rb +0 -87
- data/lib/listen/dependency_manager.rb +0 -126
- data/lib/listen/directory_record.rb +0 -344
- data/lib/listen/multi_listener.rb +0 -121
- data/lib/listen/turnstile.rb +0 -28
data/lib/listen/adapter.rb
CHANGED
@@ -1,211 +1,42 @@
|
|
1
|
-
|
2
|
-
require 'thread'
|
3
|
-
require 'set'
|
4
|
-
require 'fileutils'
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# The default warning message when there is a missing dependency.
|
14
|
-
MISSING_DEPENDENCY_MESSAGE = <<-EOS.gsub(/^\s*/, '')
|
15
|
-
For a better performance, it's recommended that you satisfy the missing dependency.
|
16
|
-
EOS
|
17
|
-
|
18
|
-
# The default warning message when falling back to polling adapter.
|
19
|
-
POLLING_FALLBACK_MESSAGE = <<-EOS.gsub(/^\s*/, '')
|
20
|
-
Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.
|
21
|
-
EOS
|
22
|
-
|
23
|
-
# Selects the appropriate adapter implementation for the
|
24
|
-
# current OS and initializes it.
|
25
|
-
#
|
26
|
-
# @param [String, Array<String>] directories the directories to watch
|
27
|
-
# @param [Hash] options the adapter options
|
28
|
-
# @option options [Boolean] force_polling to force polling or not
|
29
|
-
# @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
|
30
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
31
|
-
#
|
32
|
-
# @yield [changed_dirs, options] callback Callback called when a change happens
|
33
|
-
# @yieldparam [Array<String>] changed_dirs the changed directories
|
34
|
-
# @yieldparam [Hash] options callback options (like :recursive => true)
|
35
|
-
#
|
36
|
-
# @return [Listen::Adapter] the chosen adapter
|
37
|
-
#
|
38
|
-
def self.select_and_initialize(directories, options = {}, &callback)
|
39
|
-
return Adapters::Polling.new(directories, options, &callback) if options.delete(:force_polling)
|
40
|
-
|
41
|
-
warning = ''
|
42
|
-
|
43
|
-
begin
|
44
|
-
if Adapters::Darwin.usable_and_works?(directories, options)
|
45
|
-
return Adapters::Darwin.new(directories, options, &callback)
|
46
|
-
elsif Adapters::Linux.usable_and_works?(directories, options)
|
47
|
-
return Adapters::Linux.new(directories, options, &callback)
|
48
|
-
elsif Adapters::Windows.usable_and_works?(directories, options)
|
49
|
-
return Adapters::Windows.new(directories, options, &callback)
|
50
|
-
end
|
51
|
-
rescue DependencyManager::Error => e
|
52
|
-
warning += e.message + "\n" + MISSING_DEPENDENCY_MESSAGE
|
53
|
-
end
|
54
|
-
|
55
|
-
unless options[:polling_fallback_message] == false
|
56
|
-
warning += options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE
|
57
|
-
Kernel.warn "[Listen warning]:\n" + warning.gsub(/^(.*)/, ' \1')
|
58
|
-
end
|
59
|
-
|
60
|
-
Adapters::Polling.new(directories, options, &callback)
|
61
|
-
end
|
62
|
-
|
63
|
-
# Initializes the adapter.
|
64
|
-
#
|
65
|
-
# @param [String, Array<String>] directories the directories to watch
|
66
|
-
# @param [Hash] options the adapter options
|
67
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
68
|
-
# @option options [Boolean] report_changes whether or not to automatically report changes (run the callback)
|
69
|
-
#
|
70
|
-
# @yield [changed_dirs, options] callback Callback called when a change happens
|
71
|
-
# @yieldparam [Array<String>] changed_dirs the changed directories
|
72
|
-
# @yieldparam [Hash] options callback options (like :recursive => true)
|
73
|
-
#
|
74
|
-
# @return [Listen::Adapter] the adapter
|
75
|
-
#
|
76
|
-
def initialize(directories, options = {}, &callback)
|
77
|
-
@directories = Array(directories)
|
78
|
-
@callback = callback
|
79
|
-
@paused = false
|
80
|
-
@mutex = Mutex.new
|
81
|
-
@changed_dirs = Set.new
|
82
|
-
@turnstile = Turnstile.new
|
83
|
-
@latency ||= DEFAULT_LATENCY
|
84
|
-
@latency = options[:latency] if options[:latency]
|
85
|
-
@report_changes = options[:report_changes].nil? ? true : options[:report_changes]
|
86
|
-
end
|
87
|
-
|
88
|
-
# Starts the adapter.
|
89
|
-
#
|
90
|
-
# @param [Boolean] blocking whether or not to block the current thread after starting
|
91
|
-
#
|
92
|
-
def start(blocking = true)
|
93
|
-
@stop = false
|
94
|
-
end
|
95
|
-
|
96
|
-
# Stops the adapter.
|
97
|
-
#
|
98
|
-
def stop
|
99
|
-
@stop = true
|
100
|
-
@turnstile.signal # ensure no thread is blocked
|
101
|
-
end
|
102
|
-
|
103
|
-
# Returns whether the adapter is statred or not
|
104
|
-
#
|
105
|
-
# @return [Boolean] whether the adapter is started or not
|
106
|
-
#
|
107
|
-
def started?
|
108
|
-
@stop.nil? ? false : !@stop
|
109
|
-
end
|
3
|
+
require 'listen/adapter/base'
|
4
|
+
require 'listen/adapter/bsd'
|
5
|
+
require 'listen/adapter/darwin'
|
6
|
+
require 'listen/adapter/linux'
|
7
|
+
require 'listen/adapter/polling'
|
8
|
+
require 'listen/adapter/windows'
|
110
9
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
sleep(@latency)
|
10
|
+
module Listen
|
11
|
+
module Adapter
|
12
|
+
OPTIMIZED_ADAPTERS = [Darwin, Linux, BSD, Windows].freeze
|
13
|
+
POLLING_FALLBACK_MESSAGE = 'Listen will be polling for changes.'\
|
14
|
+
'Learn more at https://github.com/guard/listen#listen-adapters.'
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def select(options = {})
|
18
|
+
Listen.logger.debug 'Adapter: considering polling ...'
|
19
|
+
return Polling if options[:force_polling]
|
20
|
+
Listen.logger.debug 'Adapter: considering optimized backend...'
|
21
|
+
return _usable_adapter_class if _usable_adapter_class
|
22
|
+
Listen.logger.debug 'Adapter: falling back to polling...'
|
23
|
+
_warn_polling_fallback(options)
|
24
|
+
Polling
|
25
|
+
rescue
|
26
|
+
Listen.logger.warn format('Adapter: failed: %s:%s', $ERROR_POSITION.inspect,
|
27
|
+
$ERROR_POSITION * "\n")
|
28
|
+
raise
|
131
29
|
end
|
132
|
-
end
|
133
30
|
|
134
|
-
|
135
|
-
#
|
136
|
-
# @return [Boolean] whether usable or not
|
137
|
-
#
|
138
|
-
def self.usable?
|
139
|
-
load_depenencies
|
140
|
-
dependencies_loaded?
|
141
|
-
end
|
142
|
-
|
143
|
-
# Checks if the adapter is usable and works on the current OS.
|
144
|
-
#
|
145
|
-
# @param [String, Array<String>] directories the directories to watch
|
146
|
-
# @param [Hash] options the adapter options
|
147
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
148
|
-
#
|
149
|
-
# @return [Boolean] whether usable and work or not
|
150
|
-
#
|
151
|
-
def self.usable_and_works?(directories, options = {})
|
152
|
-
usable? && Array(directories).all? { |d| works?(d, options) }
|
153
|
-
end
|
31
|
+
private
|
154
32
|
|
155
|
-
|
156
|
-
|
157
|
-
#
|
158
|
-
# @note This test takes some time depending the adapter latency.
|
159
|
-
#
|
160
|
-
# @param [String, Pathname] directory the directory to watch
|
161
|
-
# @param [Hash] options the adapter options
|
162
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
163
|
-
#
|
164
|
-
# @return [Boolean] whether the adapter works or not
|
165
|
-
#
|
166
|
-
def self.works?(directory, options = {})
|
167
|
-
work = false
|
168
|
-
test_file = "#{directory}/.listen_test"
|
169
|
-
callback = lambda { |*| work = true }
|
170
|
-
adapter = self.new(directory, options, &callback)
|
171
|
-
adapter.start(false)
|
172
|
-
|
173
|
-
FileUtils.touch(test_file)
|
174
|
-
|
175
|
-
t = Thread.new { sleep(adapter.latency * 5); adapter.stop }
|
176
|
-
|
177
|
-
adapter.wait_for_callback
|
178
|
-
work
|
179
|
-
ensure
|
180
|
-
Thread.kill(t) if t
|
181
|
-
FileUtils.rm(test_file) if File.exists?(test_file)
|
182
|
-
adapter.stop if adapter && adapter.started?
|
183
|
-
end
|
184
|
-
|
185
|
-
# Runs the callback and passes it the changes if there are any.
|
186
|
-
#
|
187
|
-
def report_changes
|
188
|
-
changed_dirs = nil
|
189
|
-
|
190
|
-
@mutex.synchronize do
|
191
|
-
return if @changed_dirs.empty?
|
192
|
-
changed_dirs = @changed_dirs.to_a
|
193
|
-
@changed_dirs.clear
|
33
|
+
def _usable_adapter_class
|
34
|
+
OPTIMIZED_ADAPTERS.find(&:usable?)
|
194
35
|
end
|
195
36
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
private
|
201
|
-
|
202
|
-
# Polls changed directories and reports them back
|
203
|
-
# when there are changes.
|
204
|
-
#
|
205
|
-
def poll_changed_dirs
|
206
|
-
until @stop
|
207
|
-
sleep(@latency)
|
208
|
-
report_changes
|
37
|
+
def _warn_polling_fallback(options)
|
38
|
+
msg = options.fetch(:polling_fallback_message, POLLING_FALLBACK_MESSAGE)
|
39
|
+
Kernel.warn "[Listen warning]:\n #{msg}" if msg
|
209
40
|
end
|
210
41
|
end
|
211
42
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'listen/adapter'
|
4
|
+
require 'listen/adapter/base'
|
5
|
+
require 'listen/adapter/config'
|
6
|
+
|
7
|
+
require 'forwardable'
|
8
|
+
|
9
|
+
# This class just aggregates configuration object to avoid Listener specs
|
10
|
+
# from exploding with huge test setup blocks
|
11
|
+
module Listen
|
12
|
+
class Backend
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def initialize(directories, queue, silencer, config)
|
16
|
+
adapter_select_opts = config.adapter_select_options
|
17
|
+
|
18
|
+
adapter_class = Adapter.select(adapter_select_opts)
|
19
|
+
|
20
|
+
# Use default from adapter if possible
|
21
|
+
@min_delay_between_events = config.min_delay_between_events
|
22
|
+
@min_delay_between_events ||= adapter_class::DEFAULTS[:wait_for_delay]
|
23
|
+
@min_delay_between_events ||= 0.1
|
24
|
+
|
25
|
+
adapter_opts = config.adapter_instance_options(adapter_class)
|
26
|
+
|
27
|
+
aconfig = Adapter::Config.new(directories, queue, silencer, adapter_opts)
|
28
|
+
@adapter = adapter_class.new(aconfig)
|
29
|
+
end
|
30
|
+
|
31
|
+
delegate start: :adapter
|
32
|
+
delegate stop: :adapter
|
33
|
+
|
34
|
+
attr_reader :min_delay_between_events
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :adapter
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'listen/file'
|
4
|
+
require 'listen/directory'
|
5
|
+
|
6
|
+
module Listen
|
7
|
+
# TODO: rename to Snapshot
|
8
|
+
class Change
|
9
|
+
# TODO: test this class for coverage
|
10
|
+
class Config
|
11
|
+
def initialize(queue, silencer)
|
12
|
+
@queue = queue
|
13
|
+
@silencer = silencer
|
14
|
+
end
|
15
|
+
|
16
|
+
def silenced?(path, type)
|
17
|
+
@silencer.silenced?(Pathname(path), type)
|
18
|
+
end
|
19
|
+
|
20
|
+
def queue(*args)
|
21
|
+
@queue << args
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :record
|
26
|
+
|
27
|
+
def initialize(config, record)
|
28
|
+
@config = config
|
29
|
+
@record = record
|
30
|
+
end
|
31
|
+
|
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
|
36
|
+
def invalidate(type, rel_path, options)
|
37
|
+
watched_dir = Pathname.new(record.root)
|
38
|
+
|
39
|
+
change = options[:change]
|
40
|
+
cookie = options[:cookie]
|
41
|
+
|
42
|
+
if !cookie && @config.silenced?(rel_path, type)
|
43
|
+
Listen.logger.debug { "(silenced): #{rel_path.inspect}" }
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
path = watched_dir + rel_path
|
48
|
+
|
49
|
+
Listen.logger.debug do
|
50
|
+
log_details = options[:silence] && 'recording' || change || 'unknown'
|
51
|
+
"#{log_details}: #{type}:#{path} (#{options.inspect})"
|
52
|
+
end
|
53
|
+
|
54
|
+
if change
|
55
|
+
options = cookie ? { cookie: cookie } : {}
|
56
|
+
@config.queue(type, change, watched_dir, rel_path, options)
|
57
|
+
elsif type == :dir
|
58
|
+
# NOTE: POSSIBLE RECURSION
|
59
|
+
# TODO: fix - use a queue instead
|
60
|
+
Directory.scan(self, rel_path, options)
|
61
|
+
elsif (change = File.change(record, rel_path)) && !options[:silence]
|
62
|
+
@config.queue(:file, change, watched_dir, rel_path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# rubocop:enable Metrics/MethodLength
|
66
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
67
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
68
|
+
end
|
69
|
+
end
|
data/lib/listen/cli.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'listen'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module Listen
|
8
|
+
class CLI < Thor
|
9
|
+
default_task :start
|
10
|
+
|
11
|
+
desc 'start', 'Starts Listen'
|
12
|
+
|
13
|
+
class_option :verbose,
|
14
|
+
type: :boolean,
|
15
|
+
default: false,
|
16
|
+
aliases: '-v',
|
17
|
+
banner: 'Verbose'
|
18
|
+
|
19
|
+
class_option :directory,
|
20
|
+
type: :array,
|
21
|
+
default: '.',
|
22
|
+
aliases: '-d',
|
23
|
+
banner: 'The directory to listen to'
|
24
|
+
|
25
|
+
class_option :relative,
|
26
|
+
type: :boolean,
|
27
|
+
default: false,
|
28
|
+
aliases: '-r',
|
29
|
+
banner: 'Convert paths relative to current directory'
|
30
|
+
|
31
|
+
def start
|
32
|
+
Listen::Forwarder.new(options).start
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Forwarder
|
37
|
+
attr_reader :logger
|
38
|
+
|
39
|
+
def initialize(options)
|
40
|
+
@options = options
|
41
|
+
@logger = ::Logger.new(STDOUT, level: ::Logger::INFO)
|
42
|
+
@logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
logger.info 'Starting listen...'
|
47
|
+
|
48
|
+
directory = @options[:directory]
|
49
|
+
relative = @options[:relative]
|
50
|
+
callback = proc do |modified, added, removed|
|
51
|
+
if @options[:verbose]
|
52
|
+
logger.info "+ #{added}" unless added.empty?
|
53
|
+
logger.info "- #{removed}" unless removed.empty?
|
54
|
+
logger.info "> #{modified}" unless modified.empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
listener = Listen.to(directory, relative: relative, &callback)
|
59
|
+
|
60
|
+
listener.start
|
61
|
+
|
62
|
+
sleep 0.5 while listener.processing?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Listen
|
6
|
+
# TODO: refactor (turn it into a normal object, cache the stat, etc)
|
7
|
+
class Directory
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
9
|
+
def self.scan(snapshot, rel_path, options)
|
10
|
+
record = snapshot.record
|
11
|
+
dir = Pathname.new(record.root)
|
12
|
+
previous = record.dir_entries(rel_path)
|
13
|
+
|
14
|
+
record.add_dir(rel_path)
|
15
|
+
|
16
|
+
# TODO: use children(with_directory: false)
|
17
|
+
path = dir + rel_path
|
18
|
+
current = Set.new(_children(path))
|
19
|
+
|
20
|
+
Listen.logger.debug do
|
21
|
+
format('%s: %s(%s): %s -> %s',
|
22
|
+
(options[:silence] ? 'Recording' : 'Scanning'),
|
23
|
+
rel_path, options.inspect, previous.inspect, current.inspect)
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
current.each do |full_path|
|
28
|
+
type = ::File.lstat(full_path.to_s).directory? ? :dir : :file
|
29
|
+
item_rel_path = full_path.relative_path_from(dir).to_s
|
30
|
+
_change(snapshot, type, item_rel_path, options)
|
31
|
+
end
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
# The directory changed meanwhile, so rescan it
|
34
|
+
current = Set.new(_children(path))
|
35
|
+
retry
|
36
|
+
end
|
37
|
+
|
38
|
+
# TODO: this is not tested properly
|
39
|
+
previous = previous.reject { |entry, _| current.include?(path + entry) }
|
40
|
+
|
41
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
42
|
+
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
43
|
+
record.unset_path(rel_path)
|
44
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
45
|
+
rescue Errno::ENOTDIR
|
46
|
+
# TODO: path not tested
|
47
|
+
record.unset_path(rel_path)
|
48
|
+
_async_changes(snapshot, path, previous, options)
|
49
|
+
_change(snapshot, :file, rel_path, options)
|
50
|
+
rescue
|
51
|
+
Listen.logger.warn { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") }
|
52
|
+
raise
|
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
|
61
|
+
|
62
|
+
def self._async_changes(snapshot, path, previous, options)
|
63
|
+
fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
|
64
|
+
previous.each do |entry, data|
|
65
|
+
# TODO: this is a hack with insufficient testing
|
66
|
+
type = data.key?(:mtime) ? :file : :dir
|
67
|
+
rel_path_s = (path + entry).to_s
|
68
|
+
_change(snapshot, type, rel_path_s, options)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self._change(snapshot, type, path, options)
|
73
|
+
return snapshot.invalidate(type, path, options) if type == :dir
|
74
|
+
|
75
|
+
# Minor param cleanup for tests
|
76
|
+
# TODO: use a dedicated Event class
|
77
|
+
opts = options.dup
|
78
|
+
opts.delete(:recursive)
|
79
|
+
snapshot.invalidate(type, path, opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self._children(path)
|
83
|
+
return path.children unless RUBY_ENGINE == 'jruby'
|
84
|
+
|
85
|
+
# JRuby inconsistency workaround, see:
|
86
|
+
# https://github.com/jruby/jruby/issues/3840
|
87
|
+
exists = path.exist?
|
88
|
+
directory = path.directory?
|
89
|
+
exists && !directory and raise Errno::ENOTDIR, path.to_s
|
90
|
+
path.children
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/listen/error.rb
ADDED
@@ -0,0 +1,11 @@
|
|
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
|
+
class INotifyMaxWatchesExceeded < Error; end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Event
|
5
|
+
class Config
|
6
|
+
attr_reader :listener, :event_queue, :min_delay_between_events
|
7
|
+
|
8
|
+
def initialize(
|
9
|
+
listener,
|
10
|
+
event_queue,
|
11
|
+
queue_optimizer,
|
12
|
+
wait_for_delay,
|
13
|
+
&block
|
14
|
+
)
|
15
|
+
|
16
|
+
@listener = listener
|
17
|
+
@event_queue = event_queue
|
18
|
+
@queue_optimizer = queue_optimizer
|
19
|
+
@min_delay_between_events = wait_for_delay
|
20
|
+
@block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def sleep(seconds)
|
24
|
+
Kernel.sleep(seconds)
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(*args)
|
28
|
+
@block&.call(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def callable?
|
32
|
+
@block
|
33
|
+
end
|
34
|
+
|
35
|
+
def optimize_changes(changes)
|
36
|
+
@queue_optimizer.smoosh_changes(changes)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
require 'timeout'
|
6
|
+
require 'listen/event/processor'
|
7
|
+
require 'listen/thread'
|
8
|
+
require 'listen/error'
|
9
|
+
|
10
|
+
module Listen
|
11
|
+
module Event
|
12
|
+
class Loop
|
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
|
23
|
+
|
24
|
+
def initialize(config)
|
25
|
+
@config = config
|
26
|
+
@wait_thread = nil
|
27
|
+
@reasons = ::Queue.new
|
28
|
+
initialize_fsm
|
29
|
+
end
|
30
|
+
|
31
|
+
def wakeup_on_event
|
32
|
+
if started? && @wait_thread&.alive?
|
33
|
+
_wakeup(:event)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def started?
|
38
|
+
state == :started
|
39
|
+
end
|
40
|
+
|
41
|
+
MAX_STARTUP_SECONDS = 5.0
|
42
|
+
|
43
|
+
# @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS
|
44
|
+
def start
|
45
|
+
# TODO: use a Fiber instead?
|
46
|
+
return unless state == :pre_start
|
47
|
+
|
48
|
+
transition! :starting
|
49
|
+
|
50
|
+
@wait_thread = Listen::Thread.new("wait_thread") do
|
51
|
+
_process_changes
|
52
|
+
end
|
53
|
+
|
54
|
+
Listen.logger.debug("Waiting for processing to start...")
|
55
|
+
|
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.')
|
60
|
+
end
|
61
|
+
|
62
|
+
def pause
|
63
|
+
# TODO: works?
|
64
|
+
# fail NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop
|
68
|
+
transition! :stopped
|
69
|
+
|
70
|
+
@wait_thread&.join
|
71
|
+
@wait_thread = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def stopped?
|
75
|
+
state == :stopped
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def _process_changes
|
81
|
+
processor = Event::Processor.new(@config, @reasons)
|
82
|
+
|
83
|
+
transition! :started
|
84
|
+
|
85
|
+
processor.loop_for(@config.min_delay_between_events)
|
86
|
+
end
|
87
|
+
|
88
|
+
def _wakeup(reason)
|
89
|
+
@reasons << reason
|
90
|
+
@wait_thread.wakeup
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|