listen 0.5.3 → 3.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|