diff --git a/.travis.yml b/.travis.yml index c2cc0a30..0a9b5039 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ matrix: os: - linux - osx +osx_image: xcode7.1 env: # TODO: 0.8 is enough on Linux, but 2 seems needed for Travis/OSX - LISTEN_TESTS_DEFAULT_LAG=2 diff --git a/lib/listen/adapter/darwin.rb b/lib/listen/adapter/darwin.rb index 8779c227..d4acf966 100644 --- a/lib/listen/adapter/darwin.rb +++ b/lib/listen/adapter/darwin.rb @@ -6,17 +6,34 @@ module Adapter # Adapter implementation for Mac OS X `FSEvents`. # class Darwin < Base - OS_REGEXP = /darwin(1.+)?$/i + OS_REGEXP = /darwin(?1\d+)/i # The default delay between checking for changes. DEFAULTS = { latency: 0.1 } + INCOMPATIBLE_GEM_VERSION = <<-EOS.gsub(/^ {8}/, '') + rb-fsevent > 0.9.4 no longer supports OS X 10.6 through 10.8. + + Please add the following to your Gemfile to avoid polling for changes: + require 'rbconfig' + if RbConfig::CONFIG['target_os'] =~ /darwin(1[0-3])/i + gem 'rb-fsevent', '<= 0.9.4' + end + EOS + + def self.usable? + require 'rb-fsevent' + darwin_version = RbConfig::CONFIG['target_os'][OS_REGEXP, :major_version] or return false + return true if darwin_version.to_i >= 13 # darwin13 is OS X 10.9 + return true if Gem::Version.new(FSEvent::VERSION) <= Gem::Version.new('0.9.4') + Kernel.warn INCOMPATIBLE_GEM_VERSION + false + end + private # NOTE: each directory gets a DIFFERENT callback! def _configure(dir, &callback) - require 'rb-fsevent' - opts = { latency: options.latency } @workers ||= ::Queue.new diff --git a/lib/listen/adapter/linux.rb b/lib/listen/adapter/linux.rb index 54564d5d..4e29caff 100644 --- a/lib/listen/adapter/linux.rb +++ b/lib/listen/adapter/linux.rb @@ -35,7 +35,9 @@ def _configure(directory, &callback) end def _run + Thread.current[:listen_blocking_read_thread] = true @worker.run + Thread.current[:listen_blocking_read_thread] = false end def _process_event(dir, event) @@ -99,7 +101,7 @@ def _dir_event?(event) end def _stop - @worker.close + @worker && @worker.close end end end diff --git a/lib/listen/backend.rb b/lib/listen/backend.rb index 21125407..a98f6031 100644 --- a/lib/listen/backend.rb +++ b/lib/listen/backend.rb @@ -2,6 +2,8 @@ require 'listen/adapter/base' require 'listen/adapter/config' +require 'forwardable' + # This class just aggregates configuration object to avoid Listener specs # from exploding with huge test setup blocks module Listen diff --git a/lib/listen/directory.rb b/lib/listen/directory.rb index b61db30f..9f9ecaab 100644 --- a/lib/listen/directory.rb +++ b/lib/listen/directory.rb @@ -12,7 +12,7 @@ def self.scan(snapshot, rel_path, options) # TODO: use children(with_directory: false) path = dir + rel_path - current = Set.new(path.children) + current = Set.new(_children(path)) Listen::Logger.debug do format('%s: %s(%s): %s -> %s', @@ -28,7 +28,7 @@ def self.scan(snapshot, rel_path, options) end rescue Errno::ENOENT # The directory changed meanwhile, so rescan it - current = Set.new(path.children) + current = Set.new(_children(path)) retry end @@ -72,5 +72,16 @@ def self._change(snapshot, type, path, options) opts.delete(:recursive) snapshot.invalidate(type, path, opts) end + + def self._children(path) + return path.children unless RUBY_ENGINE == 'jruby' + + # JRuby inconsistency workaround, see: + # https://github.com/jruby/jruby/issues/3840 + exists = path.exist? + directory = path.directory? + return path.children unless (exists && !directory) + raise Errno::ENOTDIR, path.to_s + end end end diff --git a/lib/listen/event/queue.rb b/lib/listen/event/queue.rb index 553d3ffe..a650720f 100644 --- a/lib/listen/event/queue.rb +++ b/lib/listen/event/queue.rb @@ -1,5 +1,7 @@ require 'thread' +require 'forwardable' + module Listen module Event class Queue diff --git a/lib/listen/internals/thread_pool.rb b/lib/listen/internals/thread_pool.rb index b1fe7567..e112d90a 100644 --- a/lib/listen/internals/thread_pool.rb +++ b/lib/listen/internals/thread_pool.rb @@ -13,8 +13,16 @@ def self.stop return if @threads.empty? # return to avoid using possibly stubbed Queue killed = Queue.new + # You can't kill a read on a descriptor in JRuby, so let's just + # ignore running threads (listen rb-inotify waiting for disk activity + # before closing) pray threads die faster than they are created... + limit = RUBY_ENGINE == 'jruby' ? [1] : [] + killed << @threads.pop.kill until @threads.empty? - killed.pop.join until killed.empty? + until killed.empty? + th = killed.pop + th.join(*limit) unless th[:listen_blocking_read_thread] + end end end end diff --git a/lib/listen/listener.rb b/lib/listen/listener.rb index 6acf4a63..80e61525 100644 --- a/lib/listen/listener.rb +++ b/lib/listen/listener.rb @@ -61,7 +61,7 @@ def initialize(*dirs, &block) default_state :initializing - state :initializing, to: :backend_started + state :initializing, to: [:backend_started, :stopped] state :backend_started, to: [:frontend_ready, :stopped] do backend.start diff --git a/lib/listen/record/entry.rb b/lib/listen/record/entry.rb index 1080977c..de37b9ac 100644 --- a/lib/listen/record/entry.rb +++ b/lib/listen/record/entry.rb @@ -13,7 +13,7 @@ def initialize(root, relative, name = nil) def children child_relative = _join - (Dir.entries(sys_path) - %w(. ..)).map do |name| + (_entries(sys_path) - %w(. ..)).map do |name| Entry.new(@root, child_relative, name) end end @@ -46,6 +46,17 @@ def _join args = [@relative, @name].compact args.empty? ? nil : ::File.join(*args) end + + def _entries(dir) + return Dir.entries(dir) unless RUBY_ENGINE == 'jruby' + + # JRuby inconsistency workaround, see: + # https://github.com/jruby/jruby/issues/3840 + exists = ::File.exist?(dir) + directory = ::File.directory?(dir) + return Dir.entries(dir) unless (exists && !directory) + raise Errno::ENOTDIR, dir + end end end end diff --git a/listen.gemspec b/listen.gemspec index 43f55698..ee1316ec 100644 --- a/listen.gemspec +++ b/listen.gemspec @@ -24,8 +24,8 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 1.9.3' - s.add_dependency 'rb-fsevent', '>= 0.9.3' - s.add_dependency 'rb-inotify', '>= 0.9.7' + s.add_dependency 'rb-fsevent', '~> 0.9', '>= 0.9.4' + s.add_dependency 'rb-inotify', '~> 0.9', '>= 0.9.7' s.add_development_dependency 'bundler', '>= 1.3.5' end diff --git a/spec/lib/listen/adapter/linux_spec.rb b/spec/lib/listen/adapter/linux_spec.rb index 148c6849..d77f0c0b 100644 --- a/spec/lib/listen/adapter/linux_spec.rb +++ b/spec/lib/listen/adapter/linux_spec.rb @@ -131,24 +131,37 @@ let(:adapter_options) { { events: [:recursive, :close_write] } } before do - events = [:recursive, :close_write] - allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) - - fake_notifier = double(:fake_notifier, new: fake_worker) - stub_const('INotify::Notifier', fake_notifier) - allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(adapter_options) - allow(config).to receive(:queue).and_return(queue) - allow(config).to receive(:silencer).and_return(silencer) + end - allow(subject).to receive(:require).with('rb-inotify') - subject.configure + context 'when configured' do + before do + events = [:recursive, :close_write] + allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) + + fake_notifier = double(:fake_notifier, new: fake_worker) + stub_const('INotify::Notifier', fake_notifier) + + allow(config).to receive(:queue).and_return(queue) + allow(config).to receive(:silencer).and_return(silencer) + + allow(subject).to receive(:require).with('rb-inotify') + subject.configure + end + + it 'stops the worker' do + expect(fake_worker).to receive(:close) + subject.stop + end end - it 'stops the worker' do - expect(fake_worker).to receive(:close) - subject.stop + context 'when not even initialized' do + it 'does not crash' do + expect do + subject.stop + end.to_not raise_error + end end end end diff --git a/spec/lib/listen/listener_spec.rb b/spec/lib/listen/listener_spec.rb index 98e34c93..05b4a92c 100644 --- a/spec/lib/listen/listener_spec.rb +++ b/spec/lib/listen/listener_spec.rb @@ -85,8 +85,8 @@ context 'with a block' do let(:myblock) { instance_double(Proc) } - let(:block) { proc { myblock.call } } - subject { described_class.new('dir1', &block) } + let(:real_block) { proc { myblock.call } } + subject { described_class.new('dir1', &real_block) } it 'passes the block to the event processor' do allow(Event::Config).to receive(:new) do |*_args, &some_block| @@ -175,6 +175,18 @@ subject.stop end end + + context 'when only initialized' do + before do + subject + end + + it 'terminates' do + allow(backend).to receive(:stop) + allow(processor).to receive(:teardown) + subject.stop + end + end end describe '#pause' do