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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -186
  3. data/CONTRIBUTING.md +45 -0
  4. data/{LICENSE → LICENSE.txt} +3 -1
  5. data/README.md +332 -181
  6. data/bin/listen +11 -0
  7. data/lib/listen/adapter/base.rb +129 -0
  8. data/lib/listen/adapter/bsd.rb +107 -0
  9. data/lib/listen/adapter/config.rb +25 -0
  10. data/lib/listen/adapter/darwin.rb +77 -0
  11. data/lib/listen/adapter/linux.rb +108 -0
  12. data/lib/listen/adapter/polling.rb +40 -0
  13. data/lib/listen/adapter/windows.rb +96 -0
  14. data/lib/listen/adapter.rb +32 -201
  15. data/lib/listen/backend.rb +40 -0
  16. data/lib/listen/change.rb +69 -0
  17. data/lib/listen/cli.rb +65 -0
  18. data/lib/listen/directory.rb +93 -0
  19. data/lib/listen/error.rb +11 -0
  20. data/lib/listen/event/config.rb +40 -0
  21. data/lib/listen/event/loop.rb +94 -0
  22. data/lib/listen/event/processor.rb +126 -0
  23. data/lib/listen/event/queue.rb +54 -0
  24. data/lib/listen/file.rb +95 -0
  25. data/lib/listen/fsm.rb +133 -0
  26. data/lib/listen/listener/config.rb +41 -0
  27. data/lib/listen/listener.rb +93 -160
  28. data/lib/listen/logger.rb +36 -0
  29. data/lib/listen/monotonic_time.rb +27 -0
  30. data/lib/listen/options.rb +26 -0
  31. data/lib/listen/queue_optimizer.rb +129 -0
  32. data/lib/listen/record/entry.rb +66 -0
  33. data/lib/listen/record/symlink_detector.rb +41 -0
  34. data/lib/listen/record.rb +123 -0
  35. data/lib/listen/silencer/controller.rb +50 -0
  36. data/lib/listen/silencer.rb +106 -0
  37. data/lib/listen/thread.rb +54 -0
  38. data/lib/listen/version.rb +3 -1
  39. data/lib/listen.rb +40 -32
  40. metadata +87 -38
  41. data/lib/listen/adapters/darwin.rb +0 -85
  42. data/lib/listen/adapters/linux.rb +0 -113
  43. data/lib/listen/adapters/polling.rb +0 -67
  44. data/lib/listen/adapters/windows.rb +0 -87
  45. data/lib/listen/dependency_manager.rb +0 -126
  46. data/lib/listen/directory_record.rb +0 -344
  47. data/lib/listen/multi_listener.rb +0 -121
  48. data/lib/listen/turnstile.rb +0 -28
@@ -1,211 +1,42 @@
1
- require 'rbconfig'
2
- require 'thread'
3
- require 'set'
4
- require 'fileutils'
1
+ # frozen_string_literal: true
5
2
 
6
- module Listen
7
- class Adapter
8
- attr_accessor :directories, :latency, :paused
9
-
10
- # The default delay between checking for changes.
11
- DEFAULT_LATENCY = 0.25
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
- # Blocks the main thread until the poll thread
112
- # runs the callback.
113
- #
114
- def wait_for_callback
115
- @turnstile.wait unless @paused
116
- end
117
-
118
- # Blocks the main thread until N changes are
119
- # detected.
120
- #
121
- def wait_for_changes(goal = 0)
122
- changes = 0
123
-
124
- loop do
125
- @mutex.synchronize { changes = @changed_dirs.size }
126
-
127
- return if @paused || @stop
128
- return if changes >= goal
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
- # Checks if the adapter is usable on the current OS.
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
- # Runs a tests to determine if the adapter can actually pick up
156
- # changes in a given directory and returns the result.
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
- @callback.call(changed_dirs, {})
197
- @turnstile.signal
198
- end
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
@@ -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