class Listen::Listener
Attributes
TODO: deprecate
TODO: deprecate NOTE: these are VERY confusing (broadcast + recipient modes)
TODO: deprecate
TODO: deprecate NOTE: these are VERY confusing (broadcast + recipient modes)
Public Class Methods
Initializes the directories listener.
@param [String] directory the directories to listen to @param [Hash] options the listen options (see Listen::Listener::Options)
@yield [modified, added, removed] the changed files @yieldparam [Array<String>] modified the list of modified files @yieldparam [Array<String>] added the list of added files @yieldparam [Array<String>] removed the list of removed files
# File lib/listen/listener.rb, line 40 def initialize(*args, &block) @options = _init_options(args.last.is_a?(Hash) ? args.pop : {}) # Setup logging first if Celluloid.logger Celluloid.logger.level = _debug_level _info "Celluloid loglevel set to: #{Celluloid.logger.level}" _info "Listen version: #{Listen::VERSION}" end @silencer = Silencer.new _reconfigure_silencer({}) @tcp_mode = nil if [:recipient, :broadcaster].include? args[1] target = args.shift @tcp_mode = args.shift _init_tcp_options(target) end @directories = args.flatten.map { |path| Pathname.new(path).realpath } @queue = Queue.new @block = block @registry = Celluloid::Registry.new transition :stopped end
Public Instance Methods
# File lib/listen/listener.rb, line 154 def async(type) proxy = sync(type) proxy ? proxy.async : nil end
Add files and dirs to ignore on top of defaults
(@see Listen::Silencer for default ignored files and dirs)
# File lib/listen/listener.rb, line 140 def ignore(regexps) _reconfigure_silencer(ignore: [options[:ignore], regexps]) end
Replace default ignore patterns with provided regexp
# File lib/listen/listener.rb, line 145 def ignore!(regexps) _reconfigure_silencer(ignore: [], ignore!: regexps) end
Listen only to files and dirs matching regexp
# File lib/listen/listener.rb, line 150 def only(regexps) _reconfigure_silencer(only: regexps) end
Stops invoking callbacks (messages pile up)
# File lib/listen/listener.rb, line 112 def pause transition :paused end
TODO: deprecate
# File lib/listen/listener.rb, line 129 def paused=(value) transition value ? :paused : :processing end
# File lib/listen/listener.rb, line 121 def paused? state == :paused end
processing means callbacks are called
# File lib/listen/listener.rb, line 117 def processing? state == :processing end
# File lib/listen/listener.rb, line 163 def queue(type, change, dir, path, options = {}) fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol) fail "Invalid path: #{path.inspect}" unless path.is_a?(String) if @options[:relative] dir = begin cwd = Pathname.pwd dir.relative_path_from(cwd) rescue ArgumentError dir end end @queue << [type, change, dir, path, options] @last_queue_event_time = Time.now.to_f _wakeup_wait_thread unless state == :paused return unless @tcp_mode == :broadcaster message = TCP::Message.new(type, change, dir, path, options) registry[:broadcaster].async.broadcast(message.payload) end
Starts processing events and starts adapters or resumes invoking callbacks if paused
# File lib/listen/listener.rb, line 99 def start transition :processing end
Stops processing and terminates all actors
# File lib/listen/listener.rb, line 107 def stop transition :stopped end
# File lib/listen/listener.rb, line 159 def sync(type) @registry[type] end
Private Instance Methods
# File lib/listen/listener.rb, line 271 def _adapter_class @adapter_class ||= Adapter.select(options) end
# File lib/listen/listener.rb, line 199 def _debug_level debugging = ENV['LISTEN_GEM_DEBUGGING'] || options[:debug] case debugging.to_s when /2/ Logger::DEBUG when /true|yes|1/i Logger::INFO else Logger::ERROR end end
# File lib/listen/listener.rb, line 211 def _init_actors options = [mq: self, directories: directories] @supervisor = Celluloid::SupervisionGroup.run!(registry) supervisor.add(Record, as: :record, args: self) supervisor.pool(Change, as: :change_pool, args: self) # TODO: broadcaster should be a separate plugin if @tcp_mode == :broadcaster require 'listen/tcp/broadcaster' supervisor.add(TCP::Broadcaster, as: :broadcaster, args: [@host, @port]) # TODO: should be auto started, because if it crashes # a new instance is spawned by supervisor, but it's 'start' isn't # called registry[:broadcaster].start elsif @tcp_mode == :recipient # TODO: adapter options should be configured in Listen.{on/to} options.first.merge!(host: @host, port: @port) end supervisor.add(_adapter_class, as: :adapter, args: options) end
# File lib/listen/listener.rb, line 190 def _init_options(options = {}) { debug: false, latency: nil, wait_for_delay: 0.1, force_polling: false, relative: false, polling_fallback_message: nil }.merge(options) end
# File lib/listen/listener.rb, line 297 def _init_tcp_options(target) # Handle TCP options here require 'listen/tcp' fail ArgumentError, 'missing host/port for TCP' unless target if @tcp_mode == :recipient @host = 'localhost' @options[:force_tcp] = true end if target.is_a? Fixnum @port = target else @host, port = target.split(':') @port = port.to_i end end
for easier testing without sleep loop
# File lib/listen/listener.rb, line 276 def _process_changes return if @queue.empty? @last_queue_event_time = nil changes = [] changes << @queue.pop until @queue.empty? return if block.nil? hash = _smoosh_changes(changes) result = [hash[:modified], hash[:added], hash[:removed]] block_start = Time.now.to_f # TODO: condition not tested, but too complex to test ATM block.call(*result) unless result.all?(&:empty?) _debug "Callback took #{Time.now.to_f - block_start} seconds" end
# File lib/listen/listener.rb, line 343 def _queue_raw_change(type, dir, rel_path, options) _debug { "raw queue: #{[type, dir, rel_path, options].inspect}" } unless (worker = async(:change_pool)) _warn 'Failed to allocate worker from change pool' return end worker.change(type, dir, rel_path, options) rescue RuntimeError _error_exception "_queue_raw_change exception %s:\n%s:\n" raise end
# File lib/listen/listener.rb, line 315 def _reconfigure_silencer(extra_options) @options.merge!(extra_options) # TODO: this should be directory specific rules = [:only, :ignore, :ignore!].map do |option| [option, @options[option]] if @options.key? option end @silencer.configure(Hash[rules.compact]) end
# File lib/listen/listener.rb, line 261 def _silenced?(path, type) @silencer.silenced?(path, type) end
# File lib/listen/listener.rb, line 265 def _start_adapter # Don't run async, because configuration has to finish first adapter = sync(:adapter) adapter.start end
# File lib/listen/listener.rb, line 326 def _start_wait_thread @wait_thread = Thread.new { _wait_for_changes } end
# File lib/listen/listener.rb, line 334 def _stop_wait_thread return unless wait_thread if wait_thread.alive? wait_thread.wakeup wait_thread.join end @wait_thread = nil end
# File lib/listen/listener.rb, line 235 def _wait_for_changes latency = options[:wait_for_delay] loop do break if state == :stopped if state == :paused || @queue.empty? sleep break if state == :stopped end # Assure there's at least latency between callbacks to allow # for accumulating changes now = Time.now.to_f diff = latency + (@last_queue_event_time || now) - now if diff > 0 sleep diff next end _process_changes unless state == :paused end rescue RuntimeError Kernel.warn _format_error('exception while processing events: %s %s') end
# File lib/listen/listener.rb, line 330 def _wakeup_wait_thread wait_thread.wakeup if wait_thread && wait_thread.alive? end