aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXavier Noria <fxn@hashref.com>2015-10-12 20:41:14 +0200
committerXavier Noria <fxn@hashref.com>2015-11-08 22:49:49 -0800
commit785adabc4b8d892b6e06fca2f259e9c5147e9ca5 (patch)
tree3f6aa360466928f1ecbe59870944d879abb454c4
parent823d3a8de0ccfe2301e51ac1e06e106035edb8e6 (diff)
downloadrails-785adabc4b8d892b6e06fca2f259e9c5147e9ca5.tar.gz
rails-785adabc4b8d892b6e06fca2f259e9c5147e9ca5.tar.bz2
rails-785adabc4b8d892b6e06fca2f259e9c5147e9ca5.zip
implements an evented file update checker [Puneet Agarwal]
This is the implementation of the file update checker written by Puneet Agarwal for GSoC 2015 (except for the tiny version of the listen gem, which was 3.0.2 in the original patch). Puneet's branch became too out of sync with upstream. This is the final work in one single clean commit. Credit goes in the first line using a convention understood by the contrib app.
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock10
-rw-r--r--activesupport/lib/active_support.rb1
-rw-r--r--activesupport/lib/active_support/file_evented_update_checker.rb67
-rw-r--r--activesupport/test/file_evented_update_checker_test.rb21
-rw-r--r--activesupport/test/file_update_checker_test.rb107
-rw-r--r--activesupport/test/file_update_checker_with_enumerable_test_cases.rb110
-rw-r--r--railties/lib/rails/application/configuration.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile3
9 files changed, 218 insertions, 104 deletions
diff --git a/Gemfile b/Gemfile
index 260604f570..bc14498372 100644
--- a/Gemfile
+++ b/Gemfile
@@ -45,6 +45,7 @@ end
# Active Support.
gem 'dalli', '>= 2.2.1'
+gem 'listen', '~> 3.0.3'
# Active Job.
group :job do
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d58065000..b3225dedea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -212,6 +212,9 @@ GEM
delayed_job (>= 3.0, < 5)
erubis (2.7.0)
execjs (2.6.0)
+ ffi (1.9.10)
+ ffi (1.9.10-x64-mingw32)
+ ffi (1.9.10-x86-mingw32)
hitimes (1.2.3)
hitimes (1.2.3-x86-mingw32)
i18n (0.7.0)
@@ -219,6 +222,9 @@ GEM
kindlerb (0.1.1)
mustache
nokogiri
+ listen (3.0.3)
+ rb-fsevent (>= 0.9.3)
+ rb-inotify (>= 0.9)
loofah (2.0.3)
nokogiri (>= 1.5.9)
metaclass (0.0.4)
@@ -252,6 +258,9 @@ GEM
rails-html-sanitizer (1.0.2)
loofah (~> 2.0)
rake (10.4.2)
+ rb-fsevent (0.9.6)
+ rb-inotify (0.9.5)
+ ffi (>= 0.5.0)
rdoc (4.2.0)
redcarpet (3.2.3)
redis (3.2.1)
@@ -333,6 +342,7 @@ DEPENDENCIES
jquery-rails!
json
kindlerb (= 0.1.1)
+ listen (~> 3.0.3)
mail!
minitest (< 5.3.4)
mocha (~> 0.14)
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 63277a65b4..3a2a7d28cb 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -34,6 +34,7 @@ module ActiveSupport
autoload :Dependencies
autoload :DescendantsTracker
autoload :FileUpdateChecker
+ autoload :FileEventedUpdateChecker
autoload :LogSubscriber
autoload :Notifications
diff --git a/activesupport/lib/active_support/file_evented_update_checker.rb b/activesupport/lib/active_support/file_evented_update_checker.rb
new file mode 100644
index 0000000000..d45576bb00
--- /dev/null
+++ b/activesupport/lib/active_support/file_evented_update_checker.rb
@@ -0,0 +1,67 @@
+require 'listen'
+
+module ActiveSupport
+ class FileEventedUpdateChecker
+ attr_reader :listener
+ def initialize(files, directories={}, &block)
+ @files = files.map { |f| File.expand_path(f)}.to_set
+ @dirs = Hash.new
+ directories.each do |key,value|
+ @dirs[File.expand_path(key)] = Array(value) if !Array(value).empty?
+ end
+ @block = block
+ @modified = false
+ watch_dirs = base_directories
+ @listener = Listen.to(*watch_dirs,&method(:changed)) if !watch_dirs.empty?
+ @listener.start if @listener
+ end
+
+ def updated?
+ @modified
+ end
+
+ def execute
+ @block.call
+ ensure
+ @modified = false
+ end
+
+ def execute_if_updated
+ if updated?
+ execute
+ true
+ else
+ false
+ end
+ end
+
+ private
+
+ def watching?(file)
+ return true if @files.include?(file)
+ cfile = file
+ while !cfile.eql? "/"
+ cfile = File.expand_path("#{cfile}/..")
+ if !@dirs[cfile].nil? and file.end_with?(*(@dirs[cfile].map {|ext| ".#{ext.to_s}"}))
+ return true
+ end
+ end
+ false
+ end
+
+ def changed(modified, added, removed)
+ return if updated?
+ if (modified + added + removed).any? { |f| watching? f }
+ @modified = true
+ end
+ end
+
+ def base_directories
+ (@files.map { |f| existing_parent(File.expand_path("#{f}/..")) } + @dirs.keys.map {|dir| existing_parent(dir)}).uniq
+ end
+
+ def existing_parent(path)
+ File.exist?(path) ? path : existing_parent(File.expand_path("#{path}/.."))
+ end
+ end
+end
diff --git a/activesupport/test/file_evented_update_checker_test.rb b/activesupport/test/file_evented_update_checker_test.rb
new file mode 100644
index 0000000000..09087738dc
--- /dev/null
+++ b/activesupport/test/file_evented_update_checker_test.rb
@@ -0,0 +1,21 @@
+require 'abstract_unit'
+require 'fileutils'
+require 'thread'
+require 'file_update_checker_with_enumerable_test_cases'
+
+MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__)
+
+class FileEventedUpdateCheckerWithEnumerableTest < ActiveSupport::TestCase
+ include FileUpdateCheckerWithEnumerableTestCases
+ def build_new_watcher(files, dirs={}, &block)
+ ActiveSupport::FileEventedUpdateChecker.new(files, dirs, &block)
+ end
+
+ def test_modified_should_become_true_when_watched_file_is_updated
+ watcher = ActiveSupport::FileEventedUpdateChecker.new(FILES){ i += 1 }
+ assert_equal watcher.updated?, false
+ FileUtils.rm(FILES)
+ sleep 1
+ assert_equal watcher.updated?, true
+ end
+end
diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb
index bd1df0f858..c61193d133 100644
--- a/activesupport/test/file_update_checker_test.rb
+++ b/activesupport/test/file_update_checker_test.rb
@@ -1,112 +1,13 @@
require 'abstract_unit'
require 'fileutils'
require 'thread'
+require 'file_update_checker_with_enumerable_test_cases'
MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__)
class FileUpdateCheckerWithEnumerableTest < ActiveSupport::TestCase
- FILES = %w(1.txt 2.txt 3.txt)
-
- def setup
- FileUtils.mkdir_p("tmp_watcher")
- FileUtils.touch(FILES)
- end
-
- def teardown
- FileUtils.rm_rf("tmp_watcher")
- FileUtils.rm_rf(FILES)
- end
-
- def test_should_not_execute_the_block_if_no_paths_are_given
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([]){ i += 1 }
- checker.execute_if_updated
- assert_equal 0, i
- end
-
- def test_should_not_invoke_the_block_if_no_file_has_changed
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- 5.times { assert !checker.execute_if_updated }
- assert_equal 0, i
- end
-
- def test_should_invoke_the_block_if_a_file_has_changed
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- sleep(1)
- FileUtils.touch(FILES)
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_be_robust_enough_to_handle_deleted_files
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- FileUtils.rm(FILES)
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_be_robust_to_handle_files_with_wrong_modified_time
- i = 0
- now = Time.now
- time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
- File.utime time, time, FILES[2]
-
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
-
- sleep(1)
- FileUtils.touch(FILES[0..1])
-
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_cache_updated_result_until_execute
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- assert !checker.updated?
-
- sleep(1)
- FileUtils.touch(FILES)
-
- assert checker.updated?
- checker.execute
- assert !checker.updated?
- end
-
- def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 }
- FileUtils.cd "tmp_watcher" do
- FileUtils.touch(FILES)
- end
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 }
- FileUtils.cd "tmp_watcher" do
- FileUtils.touch(FILES)
- end
- assert !checker.execute_if_updated
- assert_equal 0, i
- end
-
- def test_should_not_block_if_a_strange_filename_used
- FileUtils.mkdir_p("tmp_watcher/valid,yetstrange,path,")
- FileUtils.touch(FILES.map { |file_name| "tmp_watcher/valid,yetstrange,path,/#{file_name}" })
-
- test = Thread.new do
- ActiveSupport::FileUpdateChecker.new([],"tmp_watcher/valid,yetstrange,path," => :txt) { i += 1 }
- Thread.exit
- end
- test.priority = -1
- test.join(5)
-
- assert !test.alive?
+ include FileUpdateCheckerWithEnumerableTestCases
+ def build_new_watcher(files, dirs={}, &block)
+ ActiveSupport::FileUpdateChecker.new(files, dirs, &block)
end
end
diff --git a/activesupport/test/file_update_checker_with_enumerable_test_cases.rb b/activesupport/test/file_update_checker_with_enumerable_test_cases.rb
new file mode 100644
index 0000000000..cd1f12d42f
--- /dev/null
+++ b/activesupport/test/file_update_checker_with_enumerable_test_cases.rb
@@ -0,0 +1,110 @@
+module FileUpdateCheckerWithEnumerableTestCases
+ FILES = %w(1.txt 2.txt 3.txt)
+
+ def setup
+ FileUtils.mkdir_p("tmp_watcher")
+ FileUtils.touch(FILES)
+ end
+
+ def teardown
+ FileUtils.rm_rf("tmp_watcher")
+ FileUtils.rm_rf(FILES)
+ end
+
+ def test_should_not_execute_the_block_if_no_paths_are_given
+ i = 0
+ checker = build_new_watcher([]){ i += 1}
+ checker.execute_if_updated
+ assert_equal 0, i
+ end
+
+ def test_should_not_invoke_the_block_if_no_file_has_changed
+ i = 0
+ checker = build_new_watcher(FILES){ i += 1 }
+ 5.times { assert !checker.execute_if_updated }
+ assert_equal 0, i
+ end
+
+ def test_should_invoke_the_block_if_a_file_has_changed
+ i = 0
+ checker = build_new_watcher(FILES){ i += 1 }
+ sleep(1)
+ FileUtils.touch(FILES)
+ sleep(1) #extra
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ def test_should_be_robust_enough_to_handle_deleted_files
+ i = 0
+ checker = build_new_watcher(FILES){ i += 1 }
+ FileUtils.rm(FILES)
+ sleep(1) #extra
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ def test_should_be_robust_to_handle_files_with_wrong_modified_time
+ i = 0
+ now = Time.now
+ time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
+ File.utime time, time, FILES[2]
+
+ checker = build_new_watcher(FILES){ i += 1 }
+
+ sleep(1)
+ FileUtils.touch(FILES[0..1])
+ sleep(1) #extra
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ def test_should_cache_updated_result_until_execute
+ i = 0
+ checker = build_new_watcher(FILES){ i += 1 }
+ assert !checker.updated?
+
+ sleep(1)
+ FileUtils.touch(FILES)
+ sleep(1) #extra
+ assert checker.updated?
+ checker.execute
+ assert !checker.updated?
+ end
+
+ def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
+ i = 0
+ checker = build_new_watcher([], "tmp_watcher" => [:txt]){ i += 1 }
+ FileUtils.cd "tmp_watcher" do
+ FileUtils.touch(FILES)
+ end
+ sleep(1) #extra
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob
+ i = 0
+ checker = build_new_watcher([], "tmp_watcher" => :rb){ i += 1 }
+ FileUtils.cd "tmp_watcher" do
+ FileUtils.touch(FILES)
+ end
+ sleep(1) #extra
+ assert !checker.execute_if_updated
+ assert_equal 0, i
+ end
+
+ def test_should_not_block_if_a_strange_filename_used
+ FileUtils.mkdir_p("tmp_watcher/valid,yetstrange,path,")
+ FileUtils.touch(FILES.map { |file_name| "tmp_watcher/valid,yetstrange,path,/#{file_name}" })
+
+ test = Thread.new do
+ build_new_watcher([],"tmp_watcher/valid,yetstrange,path," => :txt) { i += 1 }
+ Thread.exit
+ end
+ test.priority = -1
+ test.join(5)
+
+ assert !test.alive?
+ end
+end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 785671f70b..6d68eea220 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -44,7 +44,7 @@ module Rails
@railties_order = [:all]
@relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
@reload_classes_only_on_change = true
- @file_watcher = ActiveSupport::FileUpdateChecker
+ @file_watcher = (defined?(Listen) && Listen::Adapter.select()!=Listen::Adapter::Polling)? ActiveSupport::FileEventedUpdateChecker : ActiveSupport::FileUpdateChecker
@exceptions_app = nil
@autoflush_log = true
@log_formatter = ActiveSupport::Logger::SimpleFormatter.new
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 975be07622..87ef60288d 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -53,3 +53,6 @@ end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+
+# Uncomment this, to increase the performance
+# gem 'listen', '~> 3.0.3'