listen 2.10.1 → 3.0.0

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