From 785adabc4b8d892b6e06fca2f259e9c5147e9ca5 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Mon, 12 Oct 2015 20:41:14 +0200 Subject: 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. --- Gemfile | 1 + Gemfile.lock | 10 ++ activesupport/lib/active_support.rb | 1 + .../active_support/file_evented_update_checker.rb | 67 +++++++++++++ .../test/file_evented_update_checker_test.rb | 21 ++++ activesupport/test/file_update_checker_test.rb | 107 +------------------- ...le_update_checker_with_enumerable_test_cases.rb | 110 +++++++++++++++++++++ railties/lib/rails/application/configuration.rb | 2 +- .../rails/generators/rails/app/templates/Gemfile | 3 + 9 files changed, 218 insertions(+), 104 deletions(-) create mode 100644 activesupport/lib/active_support/file_evented_update_checker.rb create mode 100644 activesupport/test/file_evented_update_checker_test.rb create mode 100644 activesupport/test/file_update_checker_with_enumerable_test_cases.rb 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' -- cgit v1.2.3