listen 2.10.1 → 3.0.0

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.
@@ -0,0 +1,122 @@
1
+ module Listen
2
+ module Event
3
+ class Processor
4
+ def initialize(config, reasons)
5
+ @config = config
6
+ @reasons = reasons
7
+ _reset_no_unprocessed_events
8
+ end
9
+
10
+ # TODO: implement this properly instead of checking the state at arbitrary
11
+ # points in time
12
+ def loop_for(latency)
13
+ @latency = latency
14
+
15
+ loop do
16
+ _wait_until_events
17
+ _wait_until_events_calm_down
18
+ _wait_until_no_longer_paused
19
+ _process_changes
20
+ end
21
+ rescue Stopped
22
+ Listen::Logger.debug('Processing stopped')
23
+ end
24
+
25
+ private
26
+
27
+ class Stopped < RuntimeError
28
+ end
29
+
30
+ def _wait_until_events_calm_down
31
+ loop do
32
+ now = _timestamp
33
+
34
+ # Assure there's at least latency between callbacks to allow
35
+ # for accumulating changes
36
+ diff = _deadline - now
37
+ break if diff <= 0
38
+
39
+ # give events a bit of time to accumulate so they can be
40
+ # compressed/optimized
41
+ _sleep(:waiting_until_latency, diff)
42
+ end
43
+ end
44
+
45
+ def _wait_until_no_longer_paused
46
+ # TODO: may not be a good idea?
47
+ _sleep(:waiting_for_unpause) while config.paused?
48
+ end
49
+
50
+ def _check_stopped
51
+ return unless config.stopped?
52
+
53
+ _flush_wakeup_reasons
54
+ raise Stopped
55
+ end
56
+
57
+ def _sleep(_local_reason, *args)
58
+ _check_stopped
59
+ sleep_duration = config.sleep(*args)
60
+ _check_stopped
61
+
62
+ _flush_wakeup_reasons do |reason|
63
+ next unless reason == :event
64
+ _remember_time_of_first_unprocessed_event unless config.paused?
65
+ end
66
+
67
+ sleep_duration
68
+ end
69
+
70
+ def _remember_time_of_first_unprocessed_event
71
+ @first_unprocessed_event_time ||= _timestamp
72
+ end
73
+
74
+ def _reset_no_unprocessed_events
75
+ @first_unprocessed_event_time = nil
76
+ end
77
+
78
+ def _deadline
79
+ @first_unprocessed_event_time + @latency
80
+ end
81
+
82
+ def _wait_until_events
83
+ # TODO: long sleep may not be a good idea?
84
+ _sleep(:waiting_for_events) while config.event_queue.empty?
85
+ @first_unprocessed_event_time ||= _timestamp
86
+ end
87
+
88
+ def _flush_wakeup_reasons
89
+ reasons = @reasons
90
+ until reasons.empty?
91
+ reason = reasons.pop
92
+ yield reason if block_given?
93
+ end
94
+ end
95
+
96
+ def _timestamp
97
+ config.timestamp
98
+ end
99
+
100
+ # for easier testing without sleep loop
101
+ def _process_changes
102
+ _reset_no_unprocessed_events
103
+
104
+ changes = []
105
+ changes << config.event_queue.pop until config.event_queue.empty?
106
+
107
+ callable = config.callable?
108
+ return unless callable
109
+
110
+ hash = config.optimize_changes(changes)
111
+ result = [hash[:modified], hash[:added], hash[:removed]]
112
+ return if result.all?(&:empty?)
113
+
114
+ block_start = _timestamp
115
+ config.call(*result)
116
+ Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
117
+ end
118
+
119
+ attr_reader :config
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,54 @@
1
+ module Listen
2
+ module Event
3
+ class Queue
4
+ class Config
5
+ def initialize(relative)
6
+ @relative = relative
7
+ end
8
+
9
+ def relative?
10
+ @relative
11
+ end
12
+ end
13
+
14
+ def initialize(config, &block)
15
+ @event_queue = Thread::Queue.new
16
+ @block = block
17
+ @config = config
18
+ end
19
+
20
+ def <<(args)
21
+ type, change, dir, path, options = *args
22
+ fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
23
+ fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
24
+ fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
25
+
26
+ dir = _safe_relative_from_cwd(dir)
27
+ event_queue.public_send(:<<, [type, change, dir, path, options])
28
+
29
+ block.call(args) if block
30
+ end
31
+
32
+ def empty?
33
+ event_queue.empty?
34
+ end
35
+
36
+ def pop
37
+ event_queue.pop
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :event_queue
43
+ attr_reader :block
44
+ attr_reader :config
45
+
46
+ def _safe_relative_from_cwd(dir)
47
+ return dir unless config.relative?
48
+ dir.relative_path_from(Pathname.pwd)
49
+ rescue ArgumentError
50
+ dir
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/listen/file.rb CHANGED
@@ -1,25 +1,25 @@
1
1
  module Listen
2
2
  class File
3
- def self.change(record, dir, rel_path)
4
- path = dir + rel_path
3
+ def self.change(record, rel_path)
4
+ path = Pathname.new(record.root) + rel_path
5
5
  lstat = path.lstat
6
6
 
7
7
  data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
8
8
 
9
- record_data = record.file_data(dir, rel_path)
9
+ record_data = record.file_data(rel_path)
10
10
 
11
11
  if record_data.empty?
12
- record.async.update_file(dir, rel_path, data)
12
+ record.update_file(rel_path, data)
13
13
  return :added
14
14
  end
15
15
 
16
16
  if data[:mode] != record_data[:mode]
17
- record.async.update_file(dir, rel_path, data)
17
+ record.update_file(rel_path, data)
18
18
  return :modified
19
19
  end
20
20
 
21
21
  if data[:mtime] != record_data[:mtime]
22
- record.async.update_file(dir, rel_path, data)
22
+ record.update_file(rel_path, data)
23
23
  return :modified
24
24
  end
25
25
 
@@ -56,13 +56,13 @@ module Listen
56
56
  return if data[:mtime].to_i + 2 <= Time.now.to_f
57
57
 
58
58
  md5 = Digest::MD5.file(path).digest
59
- record.async.update_file(dir, rel_path, data.merge(md5: md5))
59
+ record.update_file(rel_path, data.merge(md5: md5))
60
60
  :modified if record_data[:md5] && md5 != record_data[:md5]
61
61
  rescue SystemCallError
62
- record.async.unset_path(dir, rel_path)
62
+ record.unset_path(rel_path)
63
63
  :removed
64
64
  rescue
65
- Celluloid::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
65
+ Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
66
66
  raise
67
67
  end
68
68
 
data/lib/listen/fsm.rb ADDED
@@ -0,0 +1,131 @@
1
+ # Code copied from https://github.com/celluloid/celluloid-fsm
2
+ module Listen
3
+ module FSM
4
+ DEFAULT_STATE = :default # Default state name unless one is explicitly set
5
+
6
+ # Included hook to extend class methods
7
+ def self.included(klass)
8
+ klass.send :extend, ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # Obtain or set the default state
13
+ # Passing a state name sets the default state
14
+ def default_state(new_default = nil)
15
+ if new_default
16
+ @default_state = new_default.to_sym
17
+ else
18
+ defined?(@default_state) ? @default_state : DEFAULT_STATE
19
+ end
20
+ end
21
+
22
+ # Obtain the valid states for this FSM
23
+ def states
24
+ @states ||= {}
25
+ end
26
+
27
+ # Declare an FSM state and optionally provide a callback block to fire
28
+ # Options:
29
+ # * to: a state or array of states this state can transition to
30
+ def state(*args, &block)
31
+ if args.last.is_a? Hash
32
+ # Stringify keys :/
33
+ options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
34
+ else
35
+ options = {}
36
+ end
37
+
38
+ args.each do |name|
39
+ name = name.to_sym
40
+ default_state name if options['default']
41
+ states[name] = State.new(name, options['to'], &block)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Be kind and call super if you must redefine initialize
47
+ def initialize
48
+ @state = self.class.default_state
49
+ end
50
+
51
+ # Obtain the current state of the FSM
52
+ attr_reader :state
53
+
54
+ def transition(state_name)
55
+ new_state = validate_and_sanitize_new_state(state_name)
56
+ return unless new_state
57
+ transition_with_callbacks!(new_state)
58
+ end
59
+
60
+ # Immediate state transition with no checks, or callbacks. "Dangerous!"
61
+ def transition!(state_name)
62
+ @state = state_name
63
+ end
64
+
65
+ protected
66
+
67
+ def validate_and_sanitize_new_state(state_name)
68
+ state_name = state_name.to_sym
69
+
70
+ return if current_state_name == state_name
71
+
72
+ if current_state && !current_state.valid_transition?(state_name)
73
+ valid = current_state.transitions.map(&:to_s).join(', ')
74
+ msg = "#{self.class} can't change state from '#{@state}'"\
75
+ " to '#{state_name}', only to: #{valid}"
76
+ fail ArgumentError, msg
77
+ end
78
+
79
+ new_state = states[state_name]
80
+
81
+ unless new_state
82
+ return if state_name == default_state
83
+ fail ArgumentError, "invalid state for #{self.class}: #{state_name}"
84
+ end
85
+
86
+ new_state
87
+ end
88
+
89
+ def transition_with_callbacks!(state_name)
90
+ transition! state_name.name
91
+ state_name.call(self)
92
+ end
93
+
94
+ def states
95
+ self.class.states
96
+ end
97
+
98
+ def default_state
99
+ self.class.default_state
100
+ end
101
+
102
+ def current_state
103
+ states[@state]
104
+ end
105
+
106
+ def current_state_name
107
+ current_state && current_state.name || ''
108
+ end
109
+
110
+ class State
111
+ attr_reader :name, :transitions
112
+
113
+ def initialize(name, transitions = nil, &block)
114
+ @name, @block = name, block
115
+ @transitions = nil
116
+ @transitions = Array(transitions).map(&:to_sym) if transitions
117
+ end
118
+
119
+ def call(obj)
120
+ obj.instance_eval(&@block) if @block
121
+ end
122
+
123
+ def valid_transition?(new_state)
124
+ # All transitions are allowed unless expressly
125
+ return true unless @transitions
126
+
127
+ @transitions.include? new_state.to_sym
128
+ end
129
+ end
130
+ end
131
+ end
@@ -1,7 +1,6 @@
1
1
  module Listen
2
2
  # @private api
3
3
  module Internals
4
- # Just a wrapper for tests to avoid interfereing with Celluloid's threads
5
4
  module ThreadPool
6
5
  def self.add(&block)
7
6
  Thread.new { block.call }.tap do |th|
@@ -11,6 +10,7 @@ module Listen
11
10
 
12
11
  def self.stop
13
12
  return unless @threads ||= nil
13
+ return if @threads.empty? # return to avoid using possibly stubbed Queue
14
14
 
15
15
  killed = Queue.new
16
16
  killed << @threads.pop.kill until @threads.empty?
@@ -1,31 +1,25 @@
1
- require 'pathname'
1
+ require 'English'
2
2
 
3
3
  require 'listen/version'
4
- require 'listen/adapter'
5
- require 'listen/change'
6
- require 'listen/record'
7
- require 'listen/silencer'
8
- require 'listen/queue_optimizer'
9
- require 'English'
10
4
 
11
- require 'listen/internals/logging'
5
+ require 'listen/backend'
12
6
 
13
- module Listen
14
- class Listener
15
- include Celluloid::FSM
16
- include QueueOptimizer
7
+ require 'listen/silencer'
8
+ require 'listen/silencer/controller'
17
9
 
18
- attr_accessor :block
10
+ require 'listen/queue_optimizer'
19
11
 
20
- attr_reader :silencer
12
+ require 'listen/fsm'
21
13
 
22
- # TODO: deprecate
23
- attr_reader :options, :directories
24
- attr_reader :registry, :supervisor
14
+ require 'listen/event/loop'
15
+ require 'listen/event/queue'
16
+ require 'listen/event/config'
25
17
 
26
- # TODO: deprecate
27
- # NOTE: these are VERY confusing (broadcast + recipient modes)
28
- attr_reader :host, :port
18
+ require 'listen/listener/config'
19
+
20
+ module Listen
21
+ class Listener
22
+ include Listen::FSM
29
23
 
30
24
  # Initializes the directories listener.
31
25
  #
@@ -37,73 +31,69 @@ module Listen
37
31
  # @yieldparam [Array<String>] added the list of added files
38
32
  # @yieldparam [Array<String>] removed the list of removed files
39
33
  #
40
- def initialize(*args, &block)
41
- @options = _init_options(args.last.is_a?(Hash) ? args.pop : {})
42
-
43
- # Setup logging first
44
- if Celluloid.logger
45
- Celluloid.logger.level = _debug_level
46
- _info "Celluloid loglevel set to: #{Celluloid.logger.level}"
47
- _info "Listen version: #{Listen::VERSION}"
48
- end
49
-
50
- @silencer = Silencer.new
51
- _reconfigure_silencer({})
52
-
53
- @tcp_mode = nil
54
- if [:recipient, :broadcaster].include? args[1]
55
- target = args.shift
56
- @tcp_mode = args.shift
57
- _init_tcp_options(target)
58
- end
59
-
60
- @directories = args.flatten.map { |path| Pathname.new(path).realpath }
61
- @queue = Queue.new
62
- @block = block
63
- @registry = Celluloid::Registry.new
34
+ def initialize(*dirs, &block)
35
+ options = dirs.last.is_a?(Hash) ? dirs.pop : {}
64
36
 
65
- transition :stopped
37
+ @config = Config.new(options)
38
+
39
+ eq_config = Event::Queue::Config.new(@config.relative?)
40
+ queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
41
+
42
+ silencer = Silencer.new
43
+ rules = @config.silencer_rules
44
+ @silencer_controller = Silencer::Controller.new(silencer, rules)
45
+
46
+ @backend = Backend.new(dirs, queue, silencer, @config)
47
+
48
+ optimizer_config = QueueOptimizer::Config.new(@backend, silencer)
49
+
50
+ pconfig = Event::Config.new(
51
+ self,
52
+ queue,
53
+ QueueOptimizer.new(optimizer_config),
54
+ @backend.min_delay_between_events,
55
+ &block)
56
+
57
+ @processor = Event::Loop.new(pconfig)
58
+
59
+ super() # FSM
66
60
  end
67
61
 
68
62
  default_state :initializing
69
63
 
70
- state :initializing, to: :stopped
71
- state :paused, to: [:processing, :stopped]
64
+ state :initializing, to: :backend_started
72
65
 
73
- state :stopped, to: [:processing] do
74
- _stop_wait_thread
75
- if @supervisor
76
- @supervisor.terminate
77
- @supervisor = nil
78
- end
66
+ state :backend_started, to: [:frontend_ready] do
67
+ backend.start
79
68
  end
80
69
 
81
- state :processing, to: [:paused, :stopped] do
82
- if wait_thread # means - was paused
83
- _wakeup_wait_thread
84
- else
85
- @last_queue_event_time = nil
86
- _start_wait_thread
87
- _init_actors
70
+ state :frontend_ready, to: [:processing_events] do
71
+ processor.setup
72
+ end
88
73
 
89
- # Note: make sure building is finished before starting adapter (for
90
- # consistent results both in specs and normal usage)
91
- sync(:record).build
74
+ state :processing_events, to: [:paused, :stopped] do
75
+ processor.resume
76
+ end
77
+
78
+ state :paused, to: [:processing_events, :stopped] do
79
+ processor.pause
80
+ end
92
81
 
93
- _start_adapter
94
- end
82
+ state :stopped, to: [:backend_started] do
83
+ backend.stop # should be before processor.teardown to halt events ASAP
84
+ processor.teardown
95
85
  end
96
86
 
97
87
  # Starts processing events and starts adapters
98
88
  # or resumes invoking callbacks if paused
99
89
  def start
100
- transition :processing
90
+ transition :backend_started if state == :initializing
91
+ transition :frontend_ready if state == :backend_started
92
+ transition :processing_events if state == :frontend_ready
93
+ transition :processing_events if state == :paused
101
94
  end
102
95
 
103
- # TODO: depreciate
104
- alias_method :unpause, :start
105
-
106
- # Stops processing and terminates all actors
96
+ # Stops both listening for events and processing them
107
97
  def stop
108
98
  transition :stopped
109
99
  end
@@ -115,257 +105,28 @@ module Listen
115
105
 
116
106
  # processing means callbacks are called
117
107
  def processing?
118
- state == :processing
108
+ state == :processing_events
119
109
  end
120
110
 
121
111
  def paused?
122
112
  state == :paused
123
113
  end
124
114
 
125
- # TODO: deprecate
126
- alias_method :listen?, :processing?
127
-
128
- # TODO: deprecate
129
- def paused=(value)
130
- transition value ? :paused : :processing
131
- end
132
-
133
- # TODO: deprecate
134
- alias_method :paused, :paused?
135
-
136
- # Add files and dirs to ignore on top of defaults
137
- #
138
- # (@see Listen::Silencer for default ignored files and dirs)
139
- #
140
115
  def ignore(regexps)
141
- _reconfigure_silencer(ignore: [options[:ignore], regexps])
116
+ @silencer_controller.append_ignores(regexps)
142
117
  end
143
118
 
144
- # Replace default ignore patterns with provided regexp
145
119
  def ignore!(regexps)
146
- _reconfigure_silencer(ignore: [], ignore!: regexps)
120
+ @silencer_controller.replace_with_bang_ignores(regexps)
147
121
  end
148
122
 
149
- # Listen only to files and dirs matching regexp
150
123
  def only(regexps)
151
- _reconfigure_silencer(only: regexps)
152
- end
153
-
154
- def async(type)
155
- proxy = sync(type)
156
- proxy ? proxy.async : nil
157
- end
158
-
159
- def sync(type)
160
- @registry[type]
161
- end
162
-
163
- def queue(type, change, dir, path, options = {})
164
- fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
165
- fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
166
- fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
167
- if @options[:relative]
168
- dir = begin
169
- cwd = Pathname.pwd
170
- dir.relative_path_from(cwd)
171
- rescue ArgumentError
172
- dir
173
- end
174
- end
175
- @queue << [type, change, dir, path, options]
176
-
177
- @last_queue_event_time = Time.now.to_f
178
- _wakeup_wait_thread unless state == :paused
179
-
180
- return unless @tcp_mode == :broadcaster
181
-
182
- message = TCP::Message.new(type, change, dir, path, options)
183
- registry[:broadcaster].async.broadcast(message.payload)
124
+ @silencer_controller.replace_with_only(regexps)
184
125
  end
185
126
 
186
127
  private
187
128
 
188
- include Internals::Logging
189
-
190
- def _init_options(options = {})
191
- {
192
- # Listener options
193
- debug: false,
194
- wait_for_delay: 0.1,
195
- relative: false,
196
-
197
- # Backend selecting options
198
- force_polling: false,
199
- polling_fallback_message: nil,
200
-
201
- }.merge(options)
202
- end
203
-
204
- def _debug_level
205
- debugging = ENV['LISTEN_GEM_DEBUGGING'] || options[:debug]
206
- case debugging.to_s
207
- when /2/
208
- Logger::DEBUG
209
- when /true|yes|1/i
210
- Logger::INFO
211
- else
212
- Logger::ERROR
213
- end
214
- end
215
-
216
- def _init_actors
217
- adapter_options = { mq: self, directories: directories }
218
-
219
- @supervisor = Celluloid::SupervisionGroup.run!(registry)
220
- supervisor.add(Record, as: :record, args: self)
221
- supervisor.pool(Change, as: :change_pool, args: self)
222
-
223
- # TODO: broadcaster should be a separate plugin
224
- if @tcp_mode == :broadcaster
225
- require 'listen/tcp/broadcaster'
226
-
227
- # TODO: pass a TCP::Config class to make sure host and port are properly
228
- # passed, even when nil
229
- supervisor.add(TCP::Broadcaster, as: :broadcaster, args: [@host, @port])
230
-
231
- # TODO: should be auto started, because if it crashes
232
- # a new instance is spawned by supervisor, but it's 'start' isn't
233
- # called
234
- registry[:broadcaster].start
235
- elsif @tcp_mode == :recipient
236
- # TODO: adapter options should be configured in Listen.{on/to}
237
- adapter_options.merge!(host: @host, port: @port)
238
- end
239
-
240
- # TODO: refactor
241
- valid_adapter_options = _adapter_class.const_get(:DEFAULTS).keys
242
- valid_adapter_options.each do |key|
243
- adapter_options.merge!(key => options[key]) if options.key?(key)
244
- end
245
-
246
- supervisor.add(_adapter_class, as: :adapter, args: [adapter_options])
247
- end
248
-
249
- def _wait_for_changes
250
- latency = options[:wait_for_delay]
251
-
252
- loop do
253
- break if state == :stopped
254
-
255
- if state == :paused || @queue.empty?
256
- sleep
257
- break if state == :stopped
258
- end
259
-
260
- # Assure there's at least latency between callbacks to allow
261
- # for accumulating changes
262
- now = Time.now.to_f
263
- diff = latency + (@last_queue_event_time || now) - now
264
- if diff > 0
265
- sleep diff
266
- next
267
- end
268
-
269
- _process_changes unless state == :paused
270
- end
271
- rescue RuntimeError
272
- Kernel.warn _format_error('exception while processing events: %s %s')
273
- end
274
-
275
- def _silenced?(path, type)
276
- @silencer.silenced?(path, type)
277
- end
278
-
279
- def _start_adapter
280
- # Don't run async, because configuration has to finish first
281
- adapter = sync(:adapter)
282
- adapter.start
283
- end
284
-
285
- def _adapter_class
286
- @adapter_class ||= Adapter.select(options)
287
- end
288
-
289
- # for easier testing without sleep loop
290
- def _process_changes
291
- return if @queue.empty?
292
-
293
- @last_queue_event_time = nil
294
-
295
- changes = []
296
- changes << @queue.pop until @queue.empty?
297
-
298
- return if block.nil?
299
-
300
- hash = _smoosh_changes(changes)
301
- result = [hash[:modified], hash[:added], hash[:removed]]
302
-
303
- block_start = Time.now.to_f
304
- # TODO: condition not tested, but too complex to test ATM
305
- block.call(*result) unless result.all?(&:empty?)
306
- _debug "Callback took #{Time.now.to_f - block_start} seconds"
307
- end
308
-
309
- attr_reader :wait_thread
310
-
311
- def _init_tcp_options(target)
312
- # Handle TCP options here
313
- require 'listen/tcp'
314
- fail ArgumentError, 'missing host/port for TCP' unless target
315
-
316
- if @tcp_mode == :recipient
317
- @host = 'localhost'
318
- @options[:force_tcp] = true
319
- end
320
-
321
- if target.is_a? Fixnum
322
- @port = target
323
- else
324
- @host, port = target.split(':')
325
- @port = port.to_i
326
- end
327
- end
328
-
329
- def _reconfigure_silencer(extra_options)
330
- @options.merge!(extra_options)
331
-
332
- # TODO: this should be directory specific
333
- rules = [:only, :ignore, :ignore!].map do |option|
334
- [option, @options[option]] if @options.key? option
335
- end
336
-
337
- @silencer.configure(Hash[rules.compact])
338
- end
339
-
340
- def _start_wait_thread
341
- @wait_thread = Thread.new { _wait_for_changes }
342
- end
343
-
344
- def _wakeup_wait_thread
345
- wait_thread.wakeup if wait_thread && wait_thread.alive?
346
- end
347
-
348
- def _stop_wait_thread
349
- return unless wait_thread
350
- if wait_thread.alive?
351
- wait_thread.wakeup
352
- wait_thread.join
353
- end
354
- @wait_thread = nil
355
- end
356
-
357
- def _queue_raw_change(type, dir, rel_path, options)
358
- _debug { "raw queue: #{[type, dir, rel_path, options].inspect}" }
359
-
360
- unless (worker = async(:change_pool))
361
- _warn 'Failed to allocate worker from change pool'
362
- return
363
- end
364
-
365
- worker.change(type, dir, rel_path, options)
366
- rescue RuntimeError
367
- _error_exception "_queue_raw_change exception %s:\n%s:\n"
368
- raise
369
- end
129
+ attr_reader :processor
130
+ attr_reader :backend
370
131
  end
371
132
  end