Skip to content

Commit

Permalink
Fix #54 - Add a dependency manager for managing platform specific gems.
Browse files Browse the repository at this point in the history
  • Loading branch information
Maher4Ever committed Jul 31, 2012
1 parent 38eec03 commit 2b7e352
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 41 deletions.
7 changes: 5 additions & 2 deletions Gemfile
Expand Up @@ -3,13 +3,16 @@ source :rubygems
gemspec gemspec


gem 'rake' gem 'rake'
gem 'wdm', :github => 'Maher4Ever/wdm'


group :development do group :development do
platform :ruby do platform :ruby do
gem 'coolline' gem 'coolline'
end end


gem 'rb-fsevent', '~> 0.9.1' if RUBY_PLATFORM =~ /darwin(1.+)?$/i
gem 'rb-inotify', '~> 0.8.8' if RUBY_PLATFORM =~ /linux/i
gem 'wdm', '~> 0.0.2' if RUBY_PLATFORM =~ /mswin|mingw/i

gem 'guard' gem 'guard'
gem 'guard-rspec' gem 'guard-rspec'
gem 'yard' gem 'yard'
Expand All @@ -21,4 +24,4 @@ end


group :test do group :test do
gem 'rspec' gem 'rspec'
end end
1 change: 0 additions & 1 deletion README.md
Expand Up @@ -251,7 +251,6 @@ want to force the use of the polling adapter, either use the `:force_polling` op
while initializing the listener or call the `force_polling` method on your listener while initializing the listener or call the `force_polling` method on your listener
before starting it. before starting it.


<a name="fallback"/>
## Polling fallback ## Polling fallback


When a OS-specific adapter doesn't work the Listen gem automatically falls back to the polling adapter. When a OS-specific adapter doesn't work the Listen gem automatically falls back to the polling adapter.
Expand Down
11 changes: 6 additions & 5 deletions lib/listen.rb
@@ -1,10 +1,11 @@
module Listen module Listen


autoload :Turnstile, 'listen/turnstile' autoload :Turnstile, 'listen/turnstile'
autoload :Listener, 'listen/listener' autoload :Listener, 'listen/listener'
autoload :MultiListener, 'listen/multi_listener' autoload :MultiListener, 'listen/multi_listener'
autoload :DirectoryRecord, 'listen/directory_record' autoload :DirectoryRecord, 'listen/directory_record'
autoload :Adapter, 'listen/adapter' autoload :DependencyManager, 'listen/dependency_manager'
autoload :Adapter, 'listen/adapter'


module Adapters module Adapters
autoload :Darwin, 'listen/adapters/darwin' autoload :Darwin, 'listen/adapters/darwin'
Expand Down
46 changes: 35 additions & 11 deletions lib/listen/adapter.rb
Expand Up @@ -10,8 +10,15 @@ class Adapter
# The default delay between checking for changes. # The default delay between checking for changes.
DEFAULT_LATENCY = 0.25 DEFAULT_LATENCY = 0.25


# The default warning message when there is a missing dependency.
MISSING_DEPENDENCY_MESSAGE = <<-EOS.gsub(/^\s*/, '')
For a better performance, it's recommended that you satisfy the missing dependency.
EOS

# The default warning message when falling back to polling adapter. # The default warning message when falling back to polling adapter.
POLLING_FALLBACK_MESSAGE = "WARNING: Listen has fallen back to polling, learn more at https://github.com/guard/listen#fallback." POLLING_FALLBACK_MESSAGE = <<-EOS.gsub(/^\s*/, '')
Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.
EOS


# Selects the appropriate adapter implementation for the # Selects the appropriate adapter implementation for the
# current OS and initializes it. # current OS and initializes it.
Expand All @@ -31,18 +38,26 @@ class Adapter
def self.select_and_initialize(directories, options = {}, &callback) def self.select_and_initialize(directories, options = {}, &callback)
return Adapters::Polling.new(directories, options, &callback) if options.delete(:force_polling) return Adapters::Polling.new(directories, options, &callback) if options.delete(:force_polling)


if Adapters::Darwin.usable_and_works?(directories, options) warning = ''
Adapters::Darwin.new(directories, options, &callback)
elsif Adapters::Linux.usable_and_works?(directories, options) begin
Adapters::Linux.new(directories, options, &callback) if Adapters::Darwin.usable_and_works?(directories, options)
elsif Adapters::Windows.usable_and_works?(directories, options) return Adapters::Darwin.new(directories, options, &callback)
Adapters::Windows.new(directories, options, &callback) elsif Adapters::Linux.usable_and_works?(directories, options)
else return Adapters::Linux.new(directories, options, &callback)
unless options[:polling_fallback_message] == false elsif Adapters::Windows.usable_and_works?(directories, options)
Kernel.warn(options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE) return Adapters::Windows.new(directories, options, &callback)
end end
Adapters::Polling.new(directories, options, &callback) rescue DependencyManager::Error => e
warning += e.message + "\n" + MISSING_DEPENDENCY_MESSAGE
end end

unless options[:polling_fallback_message] == false
warning += options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE
Kernel.warn "[Listen warning]:\n" + warning.gsub(/^(.*)/, ' \1')
end

Adapters::Polling.new(directories, options, &callback)
end end


# Initializes the adapter. # Initializes the adapter.
Expand Down Expand Up @@ -116,6 +131,15 @@ def wait_for_changes(goal = 0)
end end
end end


# Checks if the adapter is usable on the current OS.
#
# @return [Boolean] whether usable or not
#
def self.usable?
load_depenencies
dependencies_loaded?
end

# Checks if the adapter is usable and works on the current OS. # Checks if the adapter is usable and works on the current OS.
# #
# @param [String, Array<String>] directories the directories to watch # @param [String, Array<String>] directories the directories to watch
Expand Down
10 changes: 5 additions & 5 deletions lib/listen/adapters/darwin.rb
Expand Up @@ -4,6 +4,10 @@ module Adapters
# Adapter implementation for Mac OS X `FSEvents`. # Adapter implementation for Mac OS X `FSEvents`.
# #
class Darwin < Adapter class Darwin < Adapter
extend DependencyManager

# Declare the adapter's dependencies
dependency 'rb-fsevent', '~> 0.9.1'


LAST_SEPARATOR_REGEX = /\/$/ LAST_SEPARATOR_REGEX = /\/$/


Expand Down Expand Up @@ -54,11 +58,7 @@ def stop
# #
def self.usable? def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /darwin(1.+)?$/i return false unless RbConfig::CONFIG['target_os'] =~ /darwin(1.+)?$/i

super
require 'rb-fsevent'
true
rescue LoadError
false
end end


private private
Expand Down
12 changes: 6 additions & 6 deletions lib/listen/adapters/linux.rb
Expand Up @@ -4,6 +4,10 @@ module Adapters
# Listener implementation for Linux `inotify`. # Listener implementation for Linux `inotify`.
# #
class Linux < Adapter class Linux < Adapter
extend DependencyManager

# Declare the adapter's dependencies
dependency 'rb-inotify', '~> 0.8.8'


# Watched inotify events # Watched inotify events
# #
Expand Down Expand Up @@ -59,17 +63,13 @@ def stop
@poll_thread.join if @poll_thread @poll_thread.join if @poll_thread
end end


# Check if the adapter is usable on the current OS. # Checks if the adapter is usable on the current OS.
# #
# @return [Boolean] whether usable or not # @return [Boolean] whether usable or not
# #
def self.usable? def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /linux/i return false unless RbConfig::CONFIG['target_os'] =~ /linux/i

super
require 'rb-inotify'
true
rescue LoadError
false
end end


private private
Expand Down
1 change: 1 addition & 0 deletions lib/listen/adapters/polling.rb
Expand Up @@ -10,6 +10,7 @@ module Adapters
# file IO that the other implementations. # file IO that the other implementations.
# #
class Polling < Adapter class Polling < Adapter
extend DependencyManager


# Initialize the Adapter. See {Listen::Adapter#initialize} for more info. # Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
# #
Expand Down
10 changes: 5 additions & 5 deletions lib/listen/adapters/windows.rb
Expand Up @@ -6,6 +6,10 @@ module Adapters
# Adapter implementation for Windows `wdm`. # Adapter implementation for Windows `wdm`.
# #
class Windows < Adapter class Windows < Adapter
extend DependencyManager

# Declare the adapter's dependencies
dependency 'wdm', '~> 0.0.2'


# Initializes the Adapter. See {Listen::Adapter#initialize} for more info. # Initializes the Adapter. See {Listen::Adapter#initialize} for more info.
# #
Expand Down Expand Up @@ -54,11 +58,7 @@ def stop
# #
def self.usable? def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i return false unless RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i

super
require 'wdm'
true
rescue LoadError
false
end end


private private
Expand Down
126 changes: 126 additions & 0 deletions lib/listen/dependency_manager.rb
@@ -0,0 +1,126 @@
require 'set'

module Listen

# The dependency-manager offers a simple DSL which allows
# classes to declare their gem dependencies and load them when
# needed.
# It raises a user-friendly exception when the dependencies
# can't be loaded which has the install command in the message.
#
module DependencyManager

GEM_LOAD_MESSAGE = <<-EOS.gsub(/^ {6}/, '')
Missing dependency '%s' (version '%s')!
EOS

GEM_INSTALL_COMMAND = <<-EOS.gsub(/^ {6}/, '')
Please run the following to satisfy the dependency:
gem install --version '%s' %s
EOS

BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
Please add the following to your Gemfile to satisfy the dependency:
gem '%s', '%s'
EOS

Dependency = Struct.new(:name, :version)

# The error raised when a dependency can't be loaded.
class Error < StandardError; end

# A list of all loaded dependencies in the dependency manager.
@_loaded_dependencies = Set.new

# class methods
class << self

# Initializes the extended class.
#
# @param [Class] the class for which some dependencies must be managed
#
def extended(base)
base.class_eval do
@_dependencies = Set.new
end
end

# Adds a loaded dependency to a list so that it doesn't have
# to be loaded again by another classes.
#
# @param [Dependency] dependency
#
def add_loaded(dependency)
@_loaded_dependencies << dependency
end

# Returns whether the dependency is alread loaded or not.
#
# @param [Dependency] dependency
# @return [Boolean]
#
def already_loaded?(dependency)
@_loaded_dependencies.include?(dependency)
end

# Clears the list of loaded dependencies.
#
def clear_loaded
@_loaded_dependencies.clear
end
end

# Registers a new dependency.
#
# @param [String] name the name of the gem
# @param [String] version the version of the gem
#
def dependency(name, version)
@_dependencies << Dependency.new(name, version)
end

# Loads the registered dependencies.
#
# @raise DependencyManager::Error if the dependency can't be loaded.
#
def load_depenencies
@_dependencies.each do |dependency|
begin
next if DependencyManager.already_loaded?(dependency)
gem(dependency.name, dependency.version)
require(dependency.name)
DependencyManager.add_loaded(dependency)
@_dependencies.delete(dependency)
rescue Gem::LoadError
args = [dependency.name, dependency.version]
command = if running_under_bundler?
BUNDLER_DECLARE_GEM % args
else
GEM_INSTALL_COMMAND % args.reverse
end
message = GEM_LOAD_MESSAGE % args

raise Error.new(message + command)
end
end
end

# Returns whether all the dependencies has been loaded or not.
#
# @return [Boolean]
#
def dependencies_loaded?
@_dependencies.empty?
end

private

# Returns whether we are running under bundler or not
#
# @return [Boolean]
#
def running_under_bundler?
!!(File.exists?('Gemfile') && ENV['BUNDLE_GEMFILE'])
end
end
end
3 changes: 0 additions & 3 deletions listen.gemspec
Expand Up @@ -15,9 +15,6 @@ Gem::Specification.new do |s|
s.required_rubygems_version = '>= 1.3.6' s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = 'listen' s.rubyforge_project = 'listen'


s.add_dependency 'rb-fsevent', '~> 0.9.1'
s.add_dependency 'rb-inotify', '~> 0.8.8'

s.add_development_dependency 'bundler' s.add_development_dependency 'bundler'


s.files = Dir.glob('{lib}/**/*') + %w[CHANGELOG.md LICENSE README.md] s.files = Dir.glob('{lib}/**/*') + %w[CHANGELOG.md LICENSE README.md]
Expand Down
27 changes: 24 additions & 3 deletions spec/listen/adapter_spec.rb
Expand Up @@ -31,14 +31,27 @@
described_class.select_and_initialize('dir') described_class.select_and_initialize('dir')
end end


it "warns with the default polling fallback message" do it 'warns with the default polling fallback message' do
Kernel.should_receive(:warn).with(Listen::Adapter::POLLING_FALLBACK_MESSAGE) Kernel.should_receive(:warn).with(/#{Listen::Adapter::POLLING_FALLBACK_MESSAGE}/)
described_class.select_and_initialize('dir') described_class.select_and_initialize('dir')
end end


context 'when the dependencies of an adapter are not satisfied' do
before do
Listen::Adapters::Darwin.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
Listen::Adapters::Linux.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
Listen::Adapters::Windows.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
end

it 'invites the user to satisfy the dependencies of the adapter in the warning' do
Kernel.should_receive(:warn).with(/#{Listen::Adapter::MISSING_DEPENDENCY_MESSAGE}/)
described_class.select_and_initialize('dir')
end
end

context "with custom polling_fallback_message option" do context "with custom polling_fallback_message option" do
it "warns with the custom polling fallback message" do it "warns with the custom polling fallback message" do
Kernel.should_receive(:warn).with('custom') Kernel.should_receive(:warn).with(/custom/)
described_class.select_and_initialize('dir', :polling_fallback_message => 'custom') described_class.select_and_initialize('dir', :polling_fallback_message => 'custom')
end end
end end
Expand Down Expand Up @@ -102,6 +115,14 @@


[Listen::Adapters::Darwin, Listen::Adapters::Linux, Listen::Adapters::Windows].each do |adapter_class| [Listen::Adapters::Darwin, Listen::Adapters::Linux, Listen::Adapters::Windows].each do |adapter_class|
if adapter_class.usable? if adapter_class.usable?
describe '.usable?' do
it 'checks the dependencies' do
adapter_class.should_receive(:load_depenencies)
adapter_class.should_receive(:dependencies_loaded?)
adapter_class.usable?
end
end

describe '.usable_and_works?' do describe '.usable_and_works?' do
it 'checks if the adapter is usable' do it 'checks if the adapter is usable' do
adapter_class.stub(:works?) adapter_class.stub(:works?)
Expand Down

2 comments on commit 2b7e352

@prusswan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, how is the new dependency manager supposed to work with guard? I just realized that on Ubuntu, the rb-inotify, rb-fchange, rb-fsevent gems are no longer included, and a warning will be shown:

> [Listen warning]:
  Missing dependency 'rb-inotify' (version '~> 0.8.8')!
  Please add the following to your Gemfile to satisfy the dependency:
    gem 'rb-inotify', '~> 0.8.8'

  For a better performance, it's recommended that you satisfy the missing dependency.
  Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.

Note that this is not immediately noticeable for those who updated from older versions of guard, since those gems may still be present in Gemfile.lock

@thibaudgg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just add

gem 'rb-inotify', '~> 0.8.8'

before gem 'guard' and you'll be good to go.

Please sign in to comment.