188 lines
6.3 KiB
Ruby
188 lines
6.3 KiB
Ruby
# Phusion Passenger - https://www.phusionpassenger.com/
|
|
# Copyright (c) 2010-2025 Asynchronous B.V.
|
|
#
|
|
# "Passenger", "Phusion Passenger" and "Union Station" are registered
|
|
# trademarks of Asynchronous B.V.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
PhusionPassenger.require_passenger_lib 'native_support'
|
|
|
|
module PhusionPassenger
|
|
module Utils
|
|
|
|
# Watches changes on one or more files or directories. To use this class,
|
|
# construct an object, passing it file or directory names to watch, then
|
|
# call #wait_for_change. #wait_for_change waits until one of the following
|
|
# events has happened since the constructor was called:
|
|
#
|
|
# - One of the specified files has been renamed, deleted, or its access
|
|
# revoked. This will cause +true+ to be returned.
|
|
# - One of the specified directories has been modified, renamed, deleted,
|
|
# or its access revoked. This will cause +true+ to be returned.
|
|
# - +termination_pipe+ (as passed to the constructor) becomes readable.
|
|
# This will cause +nil+ to be returned.
|
|
# - The thread is interrupted. This will cause +nil+ to be returned.
|
|
#
|
|
# The constructor will attempt to stat and possibly also open all specified
|
|
# files/directories. If one of them cannot be statted or opened, then
|
|
# +false+ will be returned by #wait_for_change.
|
|
#
|
|
# #wait_for_change may only be called once. After calling it one should
|
|
# create a new object if one wishes to watch the filesystem again.
|
|
#
|
|
# Always call #close when a FileSystemWatcher object is no longer needed
|
|
# in order to free resources.
|
|
#
|
|
# This class tries to use kqueue for efficient filesystem watching on
|
|
# platforms that support it. On other platforms it'll fallback to stat
|
|
# polling instead.
|
|
|
|
if defined?(NativeSupport::FileSystemWatcher)
|
|
FileSystemWatcher = NativeSupport::FileSystemWatcher
|
|
|
|
FileSystemWatcher.class_eval do
|
|
def self.new(filenames, termination_pipe = nil)
|
|
# Default parameter values, type conversion and exception
|
|
# handling in C is too much of a pain.
|
|
filenames = filenames.map do |filename|
|
|
filename.to_s
|
|
end
|
|
return _new(filenames, termination_pipe)
|
|
end
|
|
|
|
def self.opens_files?
|
|
return true
|
|
end
|
|
end
|
|
else
|
|
class FileSystemWatcher
|
|
attr_accessor :poll_interval
|
|
|
|
def self.opens_files?
|
|
return false
|
|
end
|
|
|
|
def initialize(filenames, termination_pipe = nil)
|
|
@poll_interval = 3
|
|
@termination_pipe = termination_pipe
|
|
@dirs = []
|
|
@files = []
|
|
|
|
begin
|
|
filenames.each do |filename|
|
|
stat = File.stat(filename)
|
|
if stat.directory?
|
|
@dirs << DirInfo.new(filename, stat)
|
|
else
|
|
@files << FileInfo.new(filename, stat)
|
|
end
|
|
end
|
|
rescue Errno::EACCES, Errno::ENOENT
|
|
@dirs = @files = nil
|
|
end
|
|
end
|
|
|
|
def wait_for_change
|
|
if !@dirs
|
|
return false
|
|
end
|
|
|
|
while true
|
|
if changed?
|
|
return true
|
|
elsif select([@termination_pipe], nil, nil, @poll_interval)
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def close
|
|
end
|
|
|
|
private
|
|
class DirInfo
|
|
DOT = "."
|
|
DOTDOT = ".."
|
|
|
|
def initialize(filename, stat)
|
|
@filename = filename
|
|
@stat = stat
|
|
@subfiles = {}
|
|
Dir.foreach(filename) do |entry|
|
|
next if entry == DOT || entry == DOTDOT
|
|
subfilename = "#{filename}/#{entry}"
|
|
@subfiles[entry] = FileInfo.new(subfilename, File.stat(subfilename))
|
|
end
|
|
end
|
|
|
|
def changed?
|
|
new_stat = File.stat(@filename)
|
|
if @stat.ino != new_stat.ino || !new_stat.directory? || @stat.mtime != new_stat.mtime
|
|
return true
|
|
end
|
|
|
|
count = 0
|
|
Dir.foreach(@filename) do |entry|
|
|
next if entry == DOT || entry == DOTDOT
|
|
subfilename = "#{@filename}/#{entry}"
|
|
|
|
file_info = @subfiles[entry]
|
|
if !file_info || file_info.changed?(false)
|
|
return true
|
|
else
|
|
count += 1
|
|
end
|
|
end
|
|
|
|
return count != @subfiles.size
|
|
rescue Errno::EACCES, Errno::ENOENT
|
|
return true
|
|
end
|
|
end
|
|
|
|
class FileInfo
|
|
def initialize(filename, stat)
|
|
@filename = filename
|
|
@stat = stat
|
|
end
|
|
|
|
def changed?(check_mtime = true)
|
|
new_stat = File.stat(@filename)
|
|
if check_mtime
|
|
mtime_changed = @stat.mtime != new_stat.mtime || @stat.size != new_stat.size
|
|
else
|
|
mtime_changed = false
|
|
end
|
|
return @stat.ino != new_stat.ino || @stat.ftype != new_stat.ftype || mtime_changed
|
|
rescue Errno::EACCES, Errno::ENOENT
|
|
return true
|
|
end
|
|
end
|
|
|
|
def changed?
|
|
return @dirs.any? { |dir_info| dir_info.changed? } ||
|
|
@files.any? { |file_info| file_info.changed? }
|
|
end
|
|
end
|
|
end
|
|
|
|
end # module Utils
|
|
end # module PhusionPassenger
|