mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #25302 from schneems/schneems/evented-file-boot-at-check-time-master
EventedFileUpdateChecker boots once per process
This commit is contained in:
commit
30dd8b2cb0
2 changed files with 87 additions and 6 deletions
|
@ -3,6 +3,33 @@ require 'pathname'
|
||||||
require 'concurrent/atomic/atomic_boolean'
|
require 'concurrent/atomic/atomic_boolean'
|
||||||
|
|
||||||
module ActiveSupport
|
module ActiveSupport
|
||||||
|
# Allows you to "listen" to changes in a file system.
|
||||||
|
# The evented file updater does not hit disk when checking for updates
|
||||||
|
# instead it uses platform specific file system events to trigger a change
|
||||||
|
# in state.
|
||||||
|
#
|
||||||
|
# The file checker takes an array of files to watch or a hash specifying directories
|
||||||
|
# and file extensions to watch. It also takes a block that is called when
|
||||||
|
# EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
|
||||||
|
# is run and there have been changes to the file system.
|
||||||
|
#
|
||||||
|
# Note: Forking will cause the first call to `updated?` to return `true`.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" })
|
||||||
|
# checker.updated?
|
||||||
|
# # => false
|
||||||
|
# checker.execute_if_updated
|
||||||
|
# # => nil
|
||||||
|
#
|
||||||
|
# FileUtils.touch("/tmp/foo")
|
||||||
|
#
|
||||||
|
# checker.updated?
|
||||||
|
# # => true
|
||||||
|
# checker.execute_if_updated
|
||||||
|
# # => "changed"
|
||||||
|
#
|
||||||
class EventedFileUpdateChecker #:nodoc: all
|
class EventedFileUpdateChecker #:nodoc: all
|
||||||
def initialize(files, dirs = {}, &block)
|
def initialize(files, dirs = {}, &block)
|
||||||
@ph = PathHelper.new
|
@ph = PathHelper.new
|
||||||
|
@ -16,8 +43,10 @@ module ActiveSupport
|
||||||
@block = block
|
@block = block
|
||||||
@updated = Concurrent::AtomicBoolean.new(false)
|
@updated = Concurrent::AtomicBoolean.new(false)
|
||||||
@lcsp = @ph.longest_common_subpath(@dirs.keys)
|
@lcsp = @ph.longest_common_subpath(@dirs.keys)
|
||||||
|
@pid = Process.pid
|
||||||
|
@boot_mutex = Mutex.new
|
||||||
|
|
||||||
if (dtw = directories_to_watch).any?
|
if (@dtw = directories_to_watch).any?
|
||||||
# Loading listen triggers warnings. These are originated by a legit
|
# Loading listen triggers warnings. These are originated by a legit
|
||||||
# usage of attr_* macros for private attributes, but adds a lot of noise
|
# usage of attr_* macros for private attributes, but adds a lot of noise
|
||||||
# to our test suite. Thus, we lazy load it and disable warnings locally.
|
# to our test suite. Thus, we lazy load it and disable warnings locally.
|
||||||
|
@ -28,11 +57,18 @@ module ActiveSupport
|
||||||
raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
|
raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Listen.to(*dtw, &method(:changed)).start
|
|
||||||
end
|
end
|
||||||
|
boot!
|
||||||
end
|
end
|
||||||
|
|
||||||
def updated?
|
def updated?
|
||||||
|
@boot_mutex.synchronize do
|
||||||
|
if @pid != Process.pid
|
||||||
|
boot!
|
||||||
|
@pid = Process.pid
|
||||||
|
@updated.make_true
|
||||||
|
end
|
||||||
|
end
|
||||||
@updated.true?
|
@updated.true?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,6 +86,9 @@ module ActiveSupport
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def boot!
|
||||||
|
Listen.to(*@dtw, &method(:changed)).start
|
||||||
|
end
|
||||||
|
|
||||||
def changed(modified, added, removed)
|
def changed(modified, added, removed)
|
||||||
unless updated?
|
unless updated?
|
||||||
|
|
|
@ -11,7 +11,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_checker(files = [], dirs = {}, &block)
|
def new_checker(files = [], dirs = {}, &block)
|
||||||
ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do
|
ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c|
|
||||||
wait
|
wait
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -34,6 +34,48 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
|
||||||
super
|
super
|
||||||
wait
|
wait
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'notifies forked processes' do
|
||||||
|
FileUtils.touch(tmpfiles)
|
||||||
|
|
||||||
|
checker = new_checker(tmpfiles) { }
|
||||||
|
assert !checker.updated?
|
||||||
|
|
||||||
|
# Pipes used for flow controll across fork.
|
||||||
|
boot_reader, boot_writer = IO.pipe
|
||||||
|
touch_reader, touch_writer = IO.pipe
|
||||||
|
|
||||||
|
pid = fork do
|
||||||
|
assert checker.updated?
|
||||||
|
|
||||||
|
# Clear previous check value.
|
||||||
|
checker.execute
|
||||||
|
assert !checker.updated?
|
||||||
|
|
||||||
|
# Fork is booted, ready for file to be touched
|
||||||
|
# notify parent process.
|
||||||
|
boot_writer.write("booted")
|
||||||
|
|
||||||
|
# Wait for parent process to signal that file
|
||||||
|
# has been touched.
|
||||||
|
IO.select([touch_reader])
|
||||||
|
|
||||||
|
assert checker.updated?
|
||||||
|
end
|
||||||
|
|
||||||
|
assert pid
|
||||||
|
|
||||||
|
# Wait for fork to be booted before touching files.
|
||||||
|
IO.select([boot_reader])
|
||||||
|
touch(tmpfiles)
|
||||||
|
|
||||||
|
# Notify fork that files have been touched.
|
||||||
|
touch_writer.write("touched")
|
||||||
|
|
||||||
|
assert checker.updated?
|
||||||
|
|
||||||
|
Process.wait(pid)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
|
class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
|
||||||
|
|
Loading…
Reference in a new issue