diff --git a/README.md b/README.md index 77f6295a..cc59e02f 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Note: `:only` regexp patterns are evaluated only against relative **file** paths All the following options can be set through the `Listen.to` after the directory path(s) params. -```ruby +``` ruby ignore: [%r{/foo/bar}, /\.pid$/, /\.coffee$/] # Ignore a list of paths # default: See DEFAULT_IGNORED_FILES and DEFAULT_IGNORED_EXTENSIONS in Listen::Silencer @@ -187,7 +187,7 @@ This is the primary method of debugging. ### Custom Logger You can call `Listen.logger =` to set a custom `listen` logger for the process. For example: -``` +``` ruby Listen.logger = Rails.logger ``` @@ -197,7 +197,7 @@ If no custom logger is set, a default `listen` logger which logs to to `STDERR` The default logger defaults to the `error` logging level (severity). You can override the logging level by setting the environment variable `LISTEN_GEM_DEBUGGING=`. For ``, all standard `::Logger` levels are supported, with any mix of upper-/lower-case: -``` +``` ruby export LISTEN_GEM_DEBUGGING=debug # or 2 [deprecated] export LISTEN_GEM_DEBUGGING=info # or 1 or true or yes [deprecated] export LISTEN_GEM_DEBUGGING=warn @@ -210,9 +210,38 @@ Note: The alternate values `1`, `2`, `true` and `yes` shown above are deprecated ### Disabling Logging If you want to disable `listen` logging, set -``` +``` ruby Listen.logger = ::Logger.new('/dev/null') ``` + +### Adapter Warnings +If listen is having trouble with the underlying adapter, it will display warnings with `Kernel#warn` by default, +which in turn writes to STDERR. +Sometimes this is not desirable, for example in an environment where STDERR is ignored. +For these reasons, the behavior can be configured using `Listen.adapter_warn_behavior =`: +``` ruby +Listen.adapter_warn_behavior = :warn # default (true means the same) +Listen.adapter_warn_behavior = :log # send to logger.warn +Listen.adapter_warn_behavior = :silent # suppress all adapter warnings (nil or false mean the same) +``` +Also there are some cases where specific warnings are not helpful. +For example, if you are using the polling adapter--and expect to--you can suppress the warning about it +by providing a callable object like a lambda or proc that determines the behavior based on the `message`: +``` ruby +Listen.adapter_warn_behavior = ->(message) do + case message + when /Listen will be polling for changes/ + :silent + when /directory is already being watched/ + :log + else + :warn + end +end +``` +In cases where the `Listen` gem is embedded inside another service--such as `guard`--the above configuration +can be set in the environment variable `LISTEN_GEM_ADAPTER_WARN_BEHAVIOR=warn|log|silent`. + ## Listen Adapters The `Listen` gem has a set of adapters to notify it when there are changes. diff --git a/lib/listen/adapter.rb b/lib/listen/adapter.rb index f3083810..dce86236 100644 --- a/lib/listen/adapter.rb +++ b/lib/listen/adapter.rb @@ -36,7 +36,7 @@ def _usable_adapter_class def _warn_polling_fallback(options) msg = options.fetch(:polling_fallback_message, POLLING_FALLBACK_MESSAGE) - Kernel.warn "[Listen warning]:\n #{msg}" if msg + Listen.adapter_warn("[Listen warning]:\n #{msg}") if msg end end end diff --git a/lib/listen/adapter/bsd.rb b/lib/listen/adapter/bsd.rb index 04a6958d..aafa2ceb 100644 --- a/lib/listen/adapter/bsd.rb +++ b/lib/listen/adapter/bsd.rb @@ -34,7 +34,7 @@ def self.usable? require 'find' true rescue LoadError - Kernel.warn BUNDLER_DECLARE_GEM + Listen.adapter_warn(BUNDLER_DECLARE_GEM) false end diff --git a/lib/listen/adapter/darwin.rb b/lib/listen/adapter/darwin.rb index 6106a145..3a610c5d 100644 --- a/lib/listen/adapter/darwin.rb +++ b/lib/listen/adapter/darwin.rb @@ -30,7 +30,7 @@ def self.usable? require 'rb-fsevent' fsevent_version = Gem::Version.new(FSEvent::VERSION) return true if fsevent_version <= Gem::Version.new('0.9.4') - Kernel.warn INCOMPATIBLE_GEM_VERSION + Listen.adapter_warn(INCOMPATIBLE_GEM_VERSION) false end diff --git a/lib/listen/adapter/windows.rb b/lib/listen/adapter/windows.rb index 252cb798..26a64005 100644 --- a/lib/listen/adapter/windows.rb +++ b/lib/listen/adapter/windows.rb @@ -20,7 +20,7 @@ def self.usable? Listen.logger.debug format('wdm - load failed: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") - Kernel.warn BUNDLER_DECLARE_GEM + Listen.adapter_warn(BUNDLER_DECLARE_GEM) false end diff --git a/lib/listen/logger.rb b/lib/listen/logger.rb index e839c02d..9689252e 100644 --- a/lib/listen/logger.rb +++ b/lib/listen/logger.rb @@ -6,13 +6,27 @@ module Listen # Listen.logger will always be present. # If you don't want logging, set Listen.logger = ::Logger.new('/dev/null', level: ::Logger::UNKNOWN) + @adapter_warn_behavior = :warn + class << self attr_writer :logger + attr_accessor :adapter_warn_behavior def logger @logger ||= default_logger end + def adapter_warn(message) + case ENV['LISTEN_GEM_ADAPTER_WARN_BEHAVIOR']&.to_sym || adapter_warn_behavior_callback(message) + when :log + logger.warn(message) + when :silent, nil, false + # do nothing + else # :warn + warn(message) + end + end + private def default_logger @@ -32,5 +46,20 @@ def default_logger ::Logger.new(STDERR, level: level) end + + def adapter_warn_behavior_callback(message) + if adapter_warn_behavior.respond_to?(:call) + case behavior = adapter_warn_behavior.call(message) + when Symbol + behavior + when false, nil + :silent + else + :warn + end + else + adapter_warn_behavior + end + end end end diff --git a/lib/listen/record/symlink_detector.rb b/lib/listen/record/symlink_detector.rb index cce87739..f193043c 100644 --- a/lib/listen/record/symlink_detector.rb +++ b/lib/listen/record/symlink_detector.rb @@ -30,6 +30,12 @@ def verify_unwatched!(entry) @real_dirs.add?(real_path) or _fail(entry.sys_path, real_path) end + # Leaving this stub here since some warning work-arounds were referring to it. + # Deprecated. Will be removed in Listen v4.0. + def warn(message) + Listen.adapter_warn(message) + end + private def _fail(symlinked, real_path) diff --git a/lib/listen/version.rb b/lib/listen/version.rb index 06c30a8e..d235bf9f 100644 --- a/lib/listen/version.rb +++ b/lib/listen/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Listen - VERSION = '3.8.0' + VERSION = '3.9.0' end diff --git a/spec/lib/listen/adapter/darwin_spec.rb b/spec/lib/listen/adapter/darwin_spec.rb index 4c53e436..d9ff0cc8 100644 --- a/spec/lib/listen/adapter/darwin_spec.rb +++ b/spec/lib/listen/adapter/darwin_spec.rb @@ -35,7 +35,7 @@ context 'with rb-fsevent > 0.9.4' do before { stub_const('FSEvent::VERSION', '0.9.6') } it 'shows a warning and should not be usable' do - expect(Kernel).to receive(:warn) + expect(Listen).to receive(:adapter_warn) expect(subject).to_not be_usable end end diff --git a/spec/lib/listen/adapter_spec.rb b/spec/lib/listen/adapter_spec.rb index be19765b..b92c6bd4 100644 --- a/spec/lib/listen/adapter_spec.rb +++ b/spec/lib/listen/adapter_spec.rb @@ -49,7 +49,7 @@ it 'warns polling fallback with default message' do msg = described_class::POLLING_FALLBACK_MESSAGE - expect(Kernel).to receive(:warn).with("[Listen warning]:\n #{msg}") + expect(Listen).to receive(:adapter_warn).with("[Listen warning]:\n #{msg}") Listen::Adapter.select end @@ -60,7 +60,7 @@ it 'warns polling fallback with custom message if set' do expected_msg = "[Listen warning]:\n custom fallback message" - expect(Kernel).to receive(:warn).with(expected_msg) + expect(Listen).to receive(:adapter_warn).with(expected_msg) msg = 'custom fallback message' Listen::Adapter.select(polling_fallback_message: msg) end diff --git a/spec/lib/listen/logger_spec.rb b/spec/lib/listen/logger_spec.rb index d58c304f..3dfd472b 100644 --- a/spec/lib/listen/logger_spec.rb +++ b/spec/lib/listen/logger_spec.rb @@ -2,11 +2,7 @@ require 'listen/logger' -RSpec.describe 'Listen.logger' do - ENV_VARIABLE_NAME = 'LISTEN_GEM_DEBUGGING' - - let(:logger) { instance_double(::Logger, "logger") } - +RSpec.describe 'logger.rb' do around do |spec| orig_logger = Listen.instance_variable_get(:@logger) @@ -15,71 +11,201 @@ Listen.logger = orig_logger end - around do |spec| - orig_debugging_env_variable = ENV.fetch(ENV_VARIABLE_NAME, :not_set) + describe 'Listen.logger' do + ENV_VARIABLE_NAME = 'LISTEN_GEM_DEBUGGING' - spec.run + let(:logger) { instance_double(::Logger, "logger") } - if orig_debugging_env_variable == :not_set - ENV.delete(ENV_VARIABLE_NAME) - else - ENV[ENV_VARIABLE_NAME] = orig_debugging_env_variable + around do |spec| + orig_debugging_env_variable = ENV.fetch(ENV_VARIABLE_NAME, :not_set) + + spec.run + + if orig_debugging_env_variable == :not_set + ENV.delete(ENV_VARIABLE_NAME) + else + ENV[ENV_VARIABLE_NAME] = orig_debugging_env_variable + end end - end - describe 'logger=' do - it 'allows the logger to be set' do - Listen.logger = logger - expect(Listen.logger).to be(logger) + describe 'logger=' do + it 'allows the logger to be set' do + Listen.logger = logger + expect(Listen.logger).to be(logger) + end + + it 'allows nil to be set (implying default logger)' do + Listen.logger = nil + expect(Listen.logger).to be_kind_of(::Logger) + end end - it 'allows nil to be set (implying default logger)' do - Listen.logger = nil - expect(Listen.logger).to be_kind_of(::Logger) + describe 'logger' do + before do + Listen.instance_variable_set(:@logger, nil) + end + + it 'returns default logger if none set' do + expect(Listen.logger).to be_kind_of(::Logger) + end + + ['debug', 'DEBUG', '2', 'level2', '2 '].each do |env_value| + it "infers DEBUG level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do + ENV[ENV_VARIABLE_NAME] = env_value + expect(Listen.logger.level).to eq(::Logger::DEBUG) + end + end + + ['info', 'INFO', 'true', ' true', 'TRUE', 'TRUE ', 'yes', 'YES', ' yesss!', '1', 'level1'].each do |env_value| + it "infers INFO level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do + ENV[ENV_VARIABLE_NAME] = env_value + expect(Listen.logger.level).to eq(::Logger::INFO) + end + end + + ['warn', 'WARN', ' warn', 'warning'].each do |env_value| + it "infers WARN level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do + ENV[ENV_VARIABLE_NAME] = env_value + expect(Listen.logger.level).to eq(::Logger::WARN) + end + end + + ['error', 'ERROR', 'OTHER'].each do |env_value| + it "infers ERROR level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do + ENV[ENV_VARIABLE_NAME] = env_value + expect(Listen.logger.level).to eq(::Logger::ERROR) + end + end + + ['fatal', 'FATAL', ' fatal'].each do |env_value| + it "infers FATAL level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do + ENV[ENV_VARIABLE_NAME] = env_value + expect(Listen.logger.level).to eq(::Logger::FATAL) + end + end end end - describe 'logger' do - before do - Listen.instance_variable_set(:@logger, nil) + describe 'Listen.adapter_warn_behavior' do + subject { Listen.adapter_warn(message) } + + after do + Listen.adapter_warn_behavior = :warn end + let(:message) { "warning message" } + + it 'defaults to :warn' do + expect(Listen.adapter_warn_behavior).to eq(:warn) - it 'returns default logger if none set' do - expect(Listen.logger).to be_kind_of(::Logger) + expect(Listen).to receive(:warn).with(message) + + subject end - ['debug', 'DEBUG', '2', 'level2', '2 '].each do |env_value| - it "infers DEBUG level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do - ENV[ENV_VARIABLE_NAME] = env_value - expect(Listen.logger.level).to eq(::Logger::DEBUG) - end + it 'allows the adapter_warn_behavior to be set to :log' do + Listen.adapter_warn_behavior = :log + + expect(Listen.logger).to receive(:warn).with(message) + + subject end - ['info', 'INFO', 'true', ' true', 'TRUE', 'TRUE ', 'yes', 'YES', ' yesss!', '1', 'level1'].each do |env_value| - it "infers INFO level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do - ENV[ENV_VARIABLE_NAME] = env_value - expect(Listen.logger.level).to eq(::Logger::INFO) + [:silent, nil, false].each do |behavior| + it "allows the adapter_warn_behavior to be set to #{behavior} to silence the warnings" do + Listen.adapter_warn_behavior = behavior + + expect(Listen.logger).not_to receive(:warn) + expect(Listen).not_to receive(:warn) + + subject end end - ['warn', 'WARN', ' warn', 'warning'].each do |env_value| - it "infers WARN level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do - ENV[ENV_VARIABLE_NAME] = env_value - expect(Listen.logger.level).to eq(::Logger::WARN) + context "when LISTEN_GEM_ADAPTER_WARN_BEHAVIOR is set to 'log'" do + around do |spec| + orig_debugging_env_variable = ENV.fetch('LISTEN_GEM_ADAPTER_WARN_BEHAVIOR', :not_set) + + ENV['LISTEN_GEM_ADAPTER_WARN_BEHAVIOR'] = 'log' + + spec.run + + if orig_debugging_env_variable == :not_set + ENV.delete('LISTEN_GEM_ADAPTER_WARN_BEHAVIOR') + else + ENV['ENV_VARIABLE_NAME'] = orig_debugging_env_variable + end end - end - ['error', 'ERROR', 'OTHER'].each do |env_value| - it "infers ERROR level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do - ENV[ENV_VARIABLE_NAME] = env_value - expect(Listen.logger.level).to eq(::Logger::ERROR) + [:silent, nil, false, :warn].each do |behavior| + it "respects the environment variable over #{behavior.inspect}" do + Listen.adapter_warn_behavior = behavior + + expect(Listen.logger).to receive(:warn).with(message) + + subject + end + end + + it "respects the environment variable over a callable config" do + Listen.adapter_warn_behavior = ->(_message) { :warn } + + expect(Listen.logger).to receive(:warn).with(message) + + subject end end - ['fatal', 'FATAL', ' fatal'].each do |env_value| - it "infers FATAL level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do - ENV[ENV_VARIABLE_NAME] = env_value - expect(Listen.logger.level).to eq(::Logger::FATAL) + context 'when adapter_warn_behavior is set to a callable object like a proc' do + before do + Listen.adapter_warn_behavior = ->(message) do + case message + when /USE warn/ + :warn + when /USE log/ + :log + when /USE silent/ + :silent + when /USE false/ + false + when /USE nil/ + nil + else + true + end + end + end + + [true, :warn].each do |behavior| + context "when the message matches a #{behavior.inspect} pattern" do + let(:message) { "USE #{behavior.inspect}" } + it 'respects :warn' do + expect(Listen).to receive(:warn).with(message) + + subject + end + end + end + + context 'when the message matches a :silent pattern' do + let(:message) { "USE silent" } + it 'respects :silent' do + expect(Listen).not_to receive(:warn).with(message) + expect(Listen).not_to receive(:warn) + + subject + end + end + + [false, nil].each do |behavior| + context 'when the message matches a #{behavior} pattern' do + let(:message) { "USE #{behavior.inspect}" } + it 'respects :silent' do + expect(Listen).not_to receive(:warn).with(message) + expect(Listen).not_to receive(:warn) + + subject + end + end end end end