class Listen::Listener

Attributes

block[RW]
directories[R]

TODO: deprecate

host[R]

TODO: deprecate NOTE: these are VERY confusing (broadcast + recipient modes)

options[R]

TODO: deprecate

port[R]

TODO: deprecate NOTE: these are VERY confusing (broadcast + recipient modes)

registry[R]
silencer[R]
supervisor[R]
wait_thread[R]

Public Class Methods

new(*args, &block) click to toggle source

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

async(type) click to toggle source
# File lib/listen/listener.rb, line 154
def async(type)
  proxy = sync(type)
  proxy ? proxy.async : nil
end
ignore(regexps) click to toggle source

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
ignore!(regexps) click to toggle source

Replace default ignore patterns with provided regexp

# File lib/listen/listener.rb, line 145
def ignore!(regexps)
  _reconfigure_silencer(ignore: [], ignore!: regexps)
end
listen?()

TODO: deprecate

Alias for: processing?
only(regexps) click to toggle source

Listen only to files and dirs matching regexp

# File lib/listen/listener.rb, line 150
def only(regexps)
  _reconfigure_silencer(only: regexps)
end
pause() click to toggle source

Stops invoking callbacks (messages pile up)

# File lib/listen/listener.rb, line 112
def pause
  transition :paused
end
paused()

TODO: deprecate

Alias for: paused?
paused=(value) click to toggle source

TODO: deprecate

# File lib/listen/listener.rb, line 129
def paused=(value)
  transition value ? :paused : :processing
end
paused?() click to toggle source
# File lib/listen/listener.rb, line 121
def paused?
  state == :paused
end
Also aliased as: paused
processing?() click to toggle source

processing means callbacks are called

# File lib/listen/listener.rb, line 117
def processing?
  state == :processing
end
Also aliased as: listen?
queue(type, change, dir, path, options = {}) click to toggle source
# 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
start() click to toggle source

Starts processing events and starts adapters or resumes invoking callbacks if paused

# File lib/listen/listener.rb, line 99
def start
  transition :processing
end
Also aliased as: unpause
stop() click to toggle source

Stops processing and terminates all actors

# File lib/listen/listener.rb, line 107
def stop
  transition :stopped
end
sync(type) click to toggle source
# File lib/listen/listener.rb, line 159
def sync(type)
  @registry[type]
end
unpause()

TODO: depreciate

Alias for: start

Private Instance Methods

_adapter_class() click to toggle source
# File lib/listen/listener.rb, line 271
def _adapter_class
  @adapter_class ||= Adapter.select(options)
end
_debug_level() click to toggle source
# 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
_init_actors() click to toggle source
# 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
_init_options(options = {}) click to toggle source
# 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
_init_tcp_options(target) click to toggle source
# 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
_process_changes() click to toggle source

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
_queue_raw_change(type, dir, rel_path, options) click to toggle source
# 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
_reconfigure_silencer(extra_options) click to toggle source
# 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
_silenced?(path, type) click to toggle source
# File lib/listen/listener.rb, line 261
def _silenced?(path, type)
  @silencer.silenced?(path, type)
end
_start_adapter() click to toggle source
# 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
_start_wait_thread() click to toggle source
# File lib/listen/listener.rb, line 326
def _start_wait_thread
  @wait_thread = Thread.new { _wait_for_changes }
end
_stop_wait_thread() click to toggle source
# 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
_wait_for_changes() click to toggle source
# 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
_wakeup_wait_thread() click to toggle source
# File lib/listen/listener.rb, line 330
def _wakeup_wait_thread
  wait_thread.wakeup if wait_thread && wait_thread.alive?
end