listen 0.5.3 → 3.7.1

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